diff --git a/.github/workflows/product.yml b/.github/workflows/product.yml index 8700247d..0f683253 100644 --- a/.github/workflows/product.yml +++ b/.github/workflows/product.yml @@ -63,7 +63,7 @@ jobs: - name: Exact owned coverage run: ./scripts/check_coverage.sh - - name: Validate public JSON contract + - name: Validate public JSON contracts run: | npx --yes ajv-cli@5.0.0 compile \ --spec=draft2020 \ @@ -88,12 +88,69 @@ jobs: -s contracts/semantic-candidate.schema.json \ -d contracts/fixtures/semantic-candidate.invalid-state-truth-mismatch.json \ --invalid + npx --yes ajv-cli@5.0.0 compile \ + --spec=draft2020 \ + -s contracts/semantic-release.schema.json + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release.schema.json \ + -d contracts/fixtures/semantic-release.valid.json \ + --valid + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release.schema.json \ + -d contracts/fixtures/semantic-release.invalid-published-truth.json \ + --invalid + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release.schema.json \ + -d contracts/fixtures/semantic-release.invalid-duplicate-concept.json \ + --invalid + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release.schema.json \ + -d contracts/fixtures/semantic-release.invalid-digest.json \ + --invalid + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release.schema.json \ + -d contracts/fixtures/semantic-release.invalid-uppercase-digest.json \ + --invalid + npx --yes ajv-cli@5.0.0 compile \ + --spec=draft2020 \ + -s contracts/semantic-release-supersession.schema.json + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release-supersession.schema.json \ + -d contracts/fixtures/semantic-release-supersession.valid.json \ + --valid + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release-supersession.schema.json \ + -d contracts/fixtures/semantic-release-supersession.invalid-digest.json \ + --invalid + npx --yes ajv-cli@5.0.0 test \ + --spec=draft2020 \ + -s contracts/semantic-release-supersession.schema.json \ + -d contracts/fixtures/semantic-release-supersession.invalid-rationale.json \ + --invalid + python3 scripts/validate_semantic_release_supersession.py \ + contracts/fixtures/semantic-release-supersession.valid.json + if python3 scripts/validate_semantic_release_supersession.py \ + contracts/fixtures/semantic-release-supersession.invalid-self.json; then + echo "::error::self-supersession fixture unexpectedly passed semantic validation" + exit 1 + fi - name: Lockfile freshness run: | cargo generate-lockfile --locked git ls-files --error-unmatch Cargo.lock >/dev/null - test -z "$(git status --porcelain=v1 --untracked-files=all -- Cargo.lock)" + if ! test -z "$(git status --porcelain=v1 --untracked-files=all -- Cargo.lock)"; then + echo "::error::Cargo.lock changed while validating the declared dependency graph" + git diff -- Cargo.lock + exit 1 + fi - name: Clean working tree run: test -z "$(git status --porcelain=v1 --untracked-files=all)" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c5dd4993..18f7b009 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ ## Product responsibility -ConceptWeave owns the process that turns observed enterprise evidence into governed semantic-model releases. It does not own source-system truth or downstream catalog/query experiences. +ConceptWeave owns the process that turns observed enterprise evidence into governed semantic-model releases and the stable client contract used to inspect those releases. It does not own source-system truth, consuming-product authorization, physical query execution, or downstream catalog/search experiences. ```mermaid flowchart LR @@ -11,13 +11,15 @@ flowchart LR D --> V[Model Validation] V --> G[Governance & Publication] G --> P[Versioned semantic release] + P --> C[Client Consumption] - CO[contextual-orchestrator] -. proposal assistance .-> D + CO[contextual-orchestrator] -. proposal or optional alignment assistance .-> D + CO -. optional bounded matching assistance .-> C LW[LineageWeave] -. inferred/proposed lineage .-> O CG[context-graph-contracts] -. shared graph/provenance contracts .-> P - P --> SDP[semantic-data-portal] - P --> GRC[governance-risk-compliance] - P --> EA[enterprise-architecture-core] + C --> SDP[semantic-data-portal] + C --> GRC[governance-risk-compliance] + C --> EA[enterprise-architecture-core] ``` ## DDD context map @@ -27,18 +29,37 @@ flowchart LR | Source Observation | Supporting | immutable observations, parser receipts, evidence locations | source-system business truth | | Semantic Discovery | Core | candidate generation and evidence binding | publication authority | | Model Validation | Supporting | deterministic validation reports | human review decisions | -| Governance & Publication | Core | proposal lifecycle, review receipts, releases, supersession | catalog/search runtime | +| Governance & Publication | Core | proposal lifecycle, review receipts, releases, supersession authority | catalog/search runtime | +| Client Consumption | Supporting | release admission, compatibility, exact byte verification, diff/resolution, explicit immutable supersession validation, future match/query-plan contracts | generator internals, consumer authorization, publication authority, physical query execution | | Interoperability | Supporting | versioned import/export and ACL adapters | foreign product internals | -## Aggregate boundaries +The generation-to-client dependency crosses only versioned public release contracts. Client code may reuse public domain value types, but it must not import generator-private adapters, prompts, persistence tables, Source Observation internals, or orchestration state. + +## Aggregate and value-object boundaries ### SemanticCandidate Smallest consistency boundary for a single proposed semantic artifact and its evidence-bound publication state. It cannot jump directly from Draft to Published. -### SemanticModelRelease (planned) +### SemanticModelRelease + +Planned Governance & Publication aggregate for immutable publication. The current Client slice defines only the consumer-visible release contract: stable release identity, contract and ontology versions, truth/publication state, declared artifact digest identity, provenance references, and stable concept identifiers. Release construction is not publication authority. + +### ReleaseDigest + +Client value object for a declared canonical `sha256:<64 lowercase hex>` digest identity. It validates digest syntax only. Exact detached artifact bytes must be hashed and compared before integrity is claimed. + +### SemanticReleaseReference -Immutable publication aggregate containing approved candidate identities, release version, artifact digests, validation receipts, reviewer receipts, and supersession metadata. It will reference candidates rather than copy foreign source records. +Client value object that binds a stable semantic-release id to its exact artifact digest. It is an immutable coordinate for published release identity and avoids treating a mutable name or version number alone as sufficient supersession evidence. + +### ReleaseSupersession + +Client-visible immutable declaration naming an exact predecessor reference, exact successor reference, and nonblank rationale. It rejects self-supersession. Client validation requires both releases to pass normal Published + Authoritative compatibility admission and both id+digest references to match exactly. The declaration does not mutate either release and never infers replacement from version ordering, timestamps, diff size, or semantic similarity. Governance & Publication remains the authority that creates the eventual publication/supersession receipt; the Client only validates the consumer-visible contract. + +### SemanticReleaseClient + +A stateless domain service in Client Consumption. Its compatibility policy has one explicit current contract version and an explicit set of supported legacy versions; it never infers compatibility from semantic-version ordering. Unknown versions fail closed. Current and supported-legacy releases pass the same `Published` plus `Authoritative` gate before resolution, diff, artifact verification, or supersession validation. It performs no network, LLM, database, tenant-authorization, publication-decision, or physical-query work. ## Truth model @@ -46,32 +67,36 @@ Immutable publication aggregate containing approved candidate identities, releas - `inferred`: derived candidate; - `proposed`: submitted for governance; - `authoritative`: explicitly reviewed and published; -- `superseded`: formerly authoritative and replaced; +- `superseded`: formerly authoritative and replaced through explicit release evidence; - `rejected`: explicitly rejected. -Truth status and publication workflow are distinct. A source observation can be authoritative in its source domain without making an inferred semantic interpretation authoritative. +Truth status and publication workflow are distinct. A source observation can be authoritative in its source domain without making an inferred semantic interpretation authoritative. Client admission fails closed rather than coercing these states. Supersession preserves the immutable prior release rather than overwriting it. ## Integration boundaries -- `contextual-orchestrator`: LLM/model routing only. +- `contextual-orchestrator`: all production LLM/model routing; optional future matching/explanation is still proposal evidence. - `LineageWeave`: inferred/proposed lineage evidence only. - `semantic-data-portal`: published semantic artifact consumer/governance/catalog plane; it is not ConceptWeave's internal database. - `context-graph-contracts`: shared cross-product identifiers, truth/provenance/event contracts where adopted. - Keyverse: future identity/tenant authentication boundary. +- Consuming products: retain tenant/purpose authorization, business-domain truth, and physical data/query execution behind their own ACLs. No direct cross-service application-table SQL is permitted. -## Foundation directory structure +## Current directory structure ```text crates/ - conceptweave-domain/ # Core domain contract only -contracts/ # Versioned public schemas + conceptweave-domain/ # Core candidate/evidence lifecycle contracts + conceptweave-client/ # Offline release admission, compatibility, integrity and supersession validation +contracts/ # Versioned public JSON Schemas and fixtures docs/ - adr/ # Binding architecture decisions + adr/ # Proposed/accepted architecture decisions doctoring/ # Standards/research evidence scripts/ # Deterministic repository-quality helpers .github/workflows/ # CI evidence ``` Adapters and application services are added only when their bounded responsibility exists; generic `utils`, `helpers`, or `services` dumping grounds are prohibited. + +ADR 0005 remains Proposed while PR #5 is Draft and current-head checks/governance are incomplete; implementation on an unintegrated head is not sufficient to mark the architecture decision Accepted. diff --git a/CHANGELOG.md b/CHANGELOG.md index 8910d6fa..7bd0bb7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,22 @@ All notable changes to ConceptWeave are documented here. - Rust 1.98.0 `conceptweave-domain` foundation with evidence-bound semantic candidate contracts. - Fail-closed Draft -> Proposed -> Validated -> Reviewed -> Published lifecycle with explicit rejection and supersession. - Draft 2020-12 JSON Schema for the semantic-candidate public contract. -- Standards and research doctoring covering stable W3C ontology standards, 2026 RDF/SHACL work in progress, Apache Ossie, and recent LLM ontology-engineering research. +- Rust-first `conceptweave-client` supporting subdomain with deterministic offline semantic-release admission by contract version, publication state, truth status, provenance, stable concept identity, and declared SHA-256 digest identity. +- Deterministic offline semantic-release diff that first applies the same authoritative-use admission policy, then reports stable previous/current release identity and sorted added/removed concept identifiers without network or model calls. +- Exact offline SHA-256 verification of caller-supplied detached immutable semantic-artifact bytes through `verify_detached_artifact`, with typed digest-mismatch evidence and the same fail-closed authoritative-use admission gate. +- Explicit semantic-release compatibility policy that distinguishes the current contract version, caller-declared supported legacy versions, and unknown versions without inferring compatibility from version ordering; supported legacy releases still pass the same Published/Authoritative gate. +- Explicit immutable semantic-release supersession references that bind predecessor and successor release ids to their exact artifact digests, require a rationale, reject self-supersession, and validate both releases through the ordinary authoritative-use gate without inferring replacement from version order or timestamps. +- Draft 2020-12 `semantic-release` public JSON Schema with valid and fail-closed fixtures for non-authoritative publication, duplicate concept identifiers, and malformed digest identity. +- Standards and research doctoring covering stable W3C ontology standards, 2026 RDF/SHACL work in progress, Apache Ossie, and recent LLM ontology-engineering/matching research. +- Executable documentation/API contract preventing the retired `verify_serialized_artifact` name or manifest-self-digest semantics from drifting back into Client Consumption documentation. ### Security - Model-generated semantics remain non-authoritative until deterministic validation and authorized review. -- Unsafe Rust is forbidden in the core domain crate. +- Client authoritative-use admission rejects incompatible, unpublished, or non-authoritative releases without requiring a network/model call. +- Legacy compatibility is explicit opt-in policy; unknown versions remain fail-closed and the current version cannot also be configured as legacy. +- Release diff validates both compared releases through the same fail-closed authoritative-use gate so comparison cannot bypass contract-version, publication-state, or truth-status policy. +- Detached-artifact integrity verification first applies authoritative-use admission, then computes SHA-256 over the exact supplied detached semantic-artifact bytes and rejects any mismatch with the declared release digest. +- Digest syntax validation remains distinct from byte verification so a syntactically valid digest is never treated as proof that detached artifact content matches it. +- Supersession validation requires exact predecessor/successor id-and-digest references and leaves the prior published release immutable; a correction is not inferred from ordering, timestamps, or semantic similarity. +- Unsafe Rust is forbidden in the core domain and client crates. diff --git a/Cargo.lock b/Cargo.lock index 451324f0..35e09dda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,97 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "conceptweave-client" +version = "0.1.0" +dependencies = [ + "conceptweave-domain", + "sha2", +] + [[package]] name = "conceptweave-domain" version = "0.1.0" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" diff --git a/Cargo.toml b/Cargo.toml index 0eec8e8c..afb4f9c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/conceptweave-domain"] +members = ["crates/conceptweave-domain", "crates/conceptweave-client"] resolver = "2" [workspace.package] diff --git a/SECURITY.md b/SECURITY.md index 5d930ad0..474d3d88 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Trust boundaries -All source artifacts, generated candidate payloads, external ontology files, model outputs, and future web-retrieved content are untrusted input. +All source artifacts, generated candidate payloads, external ontology files, model outputs, future web-retrieved content, and semantic-release payloads received by a client are untrusted input. ## Required controls @@ -14,6 +14,11 @@ All source artifacts, generated candidate payloads, external ontology files, mod - outbound retrieval, when introduced, uses a reviewed SSRF/DNS-rebinding-safe CWL egress boundary; - no source-system writes from discovery or validation; - reviewed authorization required before publication; +- client authoritative-use admission must fail closed on unsupported contract versions, non-Published state, or non-Authoritative truth status; +- client admission does not substitute for consuming-product tenant/purpose authorization; +- declared release digest syntax is not an integrity claim: `SemanticReleaseClient::verify_detached_artifact` must hash the exact detached immutable semantic-artifact bytes and compare them with the declared digest before those supplied bytes are accepted as the referenced artifact; +- the release manifest's digest names detached artifact bytes rather than claiming a self-referential digest of the manifest bytes that contain that field; +- signature authenticity and provenance-chain verification remain separate controls until Governance & Publication defines a stable signing contract; - future tenant isolation applies to source snapshots, candidates, review receipts, releases, exports, and object storage; - published semantic truth is immutable: a published artifact must never be overwritten in place, including when an audit trail exists; corrections are issued as a new release that explicitly supersedes the prior release while retaining both releases and their provenance. @@ -24,11 +29,14 @@ All source artifacts, generated candidate payloads, external ontology files, mod 3. ontology import cycles or reasoning/resource exhaustion; 4. unsafe generated query/expression execution; 5. cross-tenant evidence exposure; -6. provenance stripping during export; -7. malicious or oversized schema/API artifacts; +6. provenance stripping during export or consumption; +7. malicious or oversized schema/API/release artifacts; 8. external-source SSRF or credential leakage; 9. model/provider compromise or unexpected retention; 10. governance bypass from Proposed/Validated directly to Published; -11. in-place mutation or overwrite of previously published semantic truth. +11. in-place mutation or overwrite of previously published semantic truth; +12. consumer use of an incompatible, unpublished, non-authoritative, stale, or superseded release; +13. false integrity claims caused by checking digest syntax without hashing the exact detached artifact bytes; +14. manifest/artifact scope confusion that validates bytes other than the semantic artifact named by the release digest. Security findings become tests before the related runtime capability can be marked release-ready. diff --git a/TEST_STRATEGY.md b/TEST_STRATEGY.md index 6d137795..d0382879 100644 --- a/TEST_STRATEGY.md +++ b/TEST_STRATEGY.md @@ -3,11 +3,31 @@ ## Foundation gates - Rust formatting and Clippy with warnings denied; -- unit tests for every domain lifecycle branch; +- unit/integration tests for every owned domain/client behavior branch; - owned production line/function/region and LLVM branch coverage target of 100%; -- JSON Schema syntax validation; +- Draft 2020-12 JSON Schema syntax and positive/negative fixture validation for public contracts; - lockfile freshness and clean-tree verification; -- public Rust documentation with `missing_docs` denied. +- public Rust documentation with `missing_docs` denied; +- every CI result is valid only for the unchanged exact PR head. + +## Current Client Consumption tests + +- authoritative + Published release admits offline for the exact current or explicitly supported legacy contract version; +- Reviewed/unpublished and Published/non-Authoritative releases fail closed; +- unsupported contract versions fail closed without version-order inference; +- release/contract/ontology identifiers reject blank values; +- provenance is required; +- concept identifiers reject blanks and duplicates; +- declared digest identity rejects unsupported algorithm, wrong length, uppercase and non-hex payloads; +- `SemanticReleaseClient::verify_detached_artifact` hashes exact detached immutable semantic-artifact bytes only after authoritative-use admission; +- exact bytes pass only when SHA-256 equals the declared artifact digest; changed/truncated/wrong bytes fail with typed mismatch evidence; +- release diff admits both releases and returns deterministic sorted concept changes; +- exact concept resolution is provider-independent and rejects blank identifiers; +- supersession validation binds distinct predecessor/successor release ids and digests, rejects self-supersession and blank rationale, and applies ordinary admission to both releases; +- public error messages identify the rejected admission/integrity/supersession invariant; +- JSON Schema fixtures mirror Published -> Authoritative, unique concepts, provenance and digest-shape constraints. + +Digest identity syntax and detached-byte integrity remain separate controls. The current verifier does not claim signature authenticity or provenance-chain trust. Those cases require a stable Governance & Publication signing contract before they become release gates. ## Future product test families @@ -19,6 +39,14 @@ Realistic PostgreSQL schema snapshots, OpenAPI/AsyncAPI fixtures, malformed cont Golden concept/type/taxonomy/relation sets; mapping precision/recall; multilingual labels; synonyms/homonyms; false friends; unrelated sources; cross-domain collisions; explicit no-answer cases. +### Client compatibility and alignment + +Malformed, partial, conflicting, stale and superseded releases beyond the currently implemented explicit compatibility/diff/supersession contracts; candidate-retrieval recall; OAEI-style matching precision/recall/F1; deterministic preprocessing ablations; abstention/ambiguity handling; LLM-call reduction against naive full-prompt baselines. Optional model calls use `contextual-orchestrator`; no model judge is sole truth. + +### Query-plan seam + +Golden semantic query plans preserve governed dimensions/measures/relations while physical execution and tenant/purpose authorization remain in the consuming product. Tests must prove no direct foreign application-table SQL or cross-tenant authorization bypass. + ### Semantic measures Exact deterministic calculations, grain correctness, join/cardinality safety, units, null semantics, time windows, currency/unit conversions through approved deterministic layers, and no LLM arithmetic authority. @@ -33,8 +61,8 @@ No bypass of Reviewed before Published, immutable published releases, rejection, ### Security -Prompt injection, malicious ontology/source content, SSRF, cross-tenant leakage, secret leakage, expression injection, resource exhaustion, replay, malformed source provenance, and hostile export values. +Prompt injection, malicious ontology/source/release content, SSRF, cross-tenant leakage, secret leakage, expression injection, resource exhaustion, replay, malformed source provenance, hostile export values, compatibility downgrade, stale/superseded use, and detached-artifact tampering. ### Evaluation -Model-backed evaluation must include deterministic fixtures and human-reviewed expert cases. Report extraction recall, semantic precision, structural validity, mapping accuracy, citation/provenance completeness, and abstention quality separately rather than collapsing them into one opaque score. +Model-backed evaluation must include deterministic fixtures and human-reviewed expert cases. Report extraction recall, semantic precision, structural validity, mapping accuracy, citation/provenance completeness, compatibility correctness, and abstention quality separately rather than collapsing them into one opaque score. diff --git a/contracts/fixtures/semantic-release-supersession.invalid-digest.json b/contracts/fixtures/semantic-release-supersession.invalid-digest.json new file mode 100644 index 00000000..fc8f4068 --- /dev/null +++ b/contracts/fixtures/semantic-release-supersession.invalid-digest.json @@ -0,0 +1,11 @@ +{ + "superseded": { + "release_id": "semantic_release_2026_09", + "artifact_digest": "sha256:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + }, + "successor": { + "release_id": "semantic_release_2026_10", + "artifact_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "rationale": "Correct the governed control taxonomy while preserving the prior release." +} diff --git a/contracts/fixtures/semantic-release-supersession.invalid-rationale.json b/contracts/fixtures/semantic-release-supersession.invalid-rationale.json new file mode 100644 index 00000000..1034f4d4 --- /dev/null +++ b/contracts/fixtures/semantic-release-supersession.invalid-rationale.json @@ -0,0 +1,11 @@ +{ + "superseded": { + "release_id": "semantic_release_2026_09", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "successor": { + "release_id": "semantic_release_2026_10", + "artifact_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "rationale": " " +} diff --git a/contracts/fixtures/semantic-release-supersession.invalid-self.json b/contracts/fixtures/semantic-release-supersession.invalid-self.json new file mode 100644 index 00000000..a31a1dc5 --- /dev/null +++ b/contracts/fixtures/semantic-release-supersession.invalid-self.json @@ -0,0 +1,11 @@ +{ + "superseded": { + "release_id": "semantic_release_same_identity", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "successor": { + "release_id": "semantic_release_same_identity", + "artifact_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "rationale": "Invalid fixture: one stable release identity cannot supersede itself." +} diff --git a/contracts/fixtures/semantic-release-supersession.valid.json b/contracts/fixtures/semantic-release-supersession.valid.json new file mode 100644 index 00000000..0ec6f668 --- /dev/null +++ b/contracts/fixtures/semantic-release-supersession.valid.json @@ -0,0 +1,11 @@ +{ + "superseded": { + "release_id": "semantic_release_2026_09", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "successor": { + "release_id": "semantic_release_2026_10", + "artifact_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + }, + "rationale": "Correct the governed control taxonomy while preserving the prior release." +} diff --git a/contracts/fixtures/semantic-release.invalid-digest.json b/contracts/fixtures/semantic-release.invalid-digest.json new file mode 100644 index 00000000..2e56c579 --- /dev/null +++ b/contracts/fixtures/semantic-release.invalid-digest.json @@ -0,0 +1,16 @@ +{ + "release_id": "semantic-release-grc-2026-09-01", + "contract_version": "1.0.0", + "ontology_version": "grc-ontology-2026-09", + "truth_status": "authoritative", + "publication_state": "published", + "artifact_digest": "sha256:not-a-digest", + "provenance": [ + { + "source_id": "snapshot:grc-schema-2026-09-01", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "location": "public.control_evidence.control_identifier" + } + ], + "concept_ids": ["control.evidence"] +} diff --git a/contracts/fixtures/semantic-release.invalid-duplicate-concept.json b/contracts/fixtures/semantic-release.invalid-duplicate-concept.json new file mode 100644 index 00000000..2d9945cc --- /dev/null +++ b/contracts/fixtures/semantic-release.invalid-duplicate-concept.json @@ -0,0 +1,16 @@ +{ + "release_id": "semantic-release-grc-2026-09-01", + "contract_version": "1.0.0", + "ontology_version": "grc-ontology-2026-09", + "truth_status": "authoritative", + "publication_state": "published", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "provenance": [ + { + "source_id": "snapshot:grc-schema-2026-09-01", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "location": "public.control_evidence.control_identifier" + } + ], + "concept_ids": ["control.evidence", "control.evidence"] +} diff --git a/contracts/fixtures/semantic-release.invalid-published-truth.json b/contracts/fixtures/semantic-release.invalid-published-truth.json new file mode 100644 index 00000000..828b9a6a --- /dev/null +++ b/contracts/fixtures/semantic-release.invalid-published-truth.json @@ -0,0 +1,16 @@ +{ + "release_id": "semantic-release-grc-2026-09-01", + "contract_version": "1.0.0", + "ontology_version": "grc-ontology-2026-09", + "truth_status": "proposed", + "publication_state": "published", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "provenance": [ + { + "source_id": "snapshot:grc-schema-2026-09-01", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "location": "public.control_evidence.control_identifier" + } + ], + "concept_ids": ["control.evidence"] +} diff --git a/contracts/fixtures/semantic-release.invalid-uppercase-digest.json b/contracts/fixtures/semantic-release.invalid-uppercase-digest.json new file mode 100644 index 00000000..1e9a1a04 --- /dev/null +++ b/contracts/fixtures/semantic-release.invalid-uppercase-digest.json @@ -0,0 +1,16 @@ +{ + "release_id": "semantic-release-grc-2026-09-01", + "contract_version": "1.0.0", + "ontology_version": "grc-ontology-2026-09", + "truth_status": "authoritative", + "publication_state": "published", + "artifact_digest": "sha256:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "provenance": [ + { + "source_id": "snapshot:grc-schema-2026-09-01", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "location": "public.control_evidence.control_identifier" + } + ], + "concept_ids": ["control.evidence", "control.owner"] +} diff --git a/contracts/fixtures/semantic-release.valid.json b/contracts/fixtures/semantic-release.valid.json new file mode 100644 index 00000000..59fc33df --- /dev/null +++ b/contracts/fixtures/semantic-release.valid.json @@ -0,0 +1,16 @@ +{ + "release_id": "semantic-release-grc-2026-09-01", + "contract_version": "1.0.0", + "ontology_version": "grc-ontology-2026-09", + "truth_status": "authoritative", + "publication_state": "published", + "artifact_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "provenance": [ + { + "source_id": "snapshot:grc-schema-2026-09-01", + "source_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "location": "public.control_evidence.control_identifier" + } + ], + "concept_ids": ["control.evidence", "control.owner"] +} diff --git a/contracts/semantic-release-supersession.rules.json b/contracts/semantic-release-supersession.rules.json new file mode 100644 index 00000000..4c5b2fc2 --- /dev/null +++ b/contracts/semantic-release-supersession.rules.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.contextualwisdomlab.org/conceptweave/semantic-release-supersession/1.0.0/semantic-rules", + "contract_version": "1.0.0", + "rules": [ + { + "id": "distinct_release_id", + "operator": "not_equal", + "left": "/superseded/release_id", + "right": "/successor/release_id" + } + ] +} diff --git a/contracts/semantic-release-supersession.schema.json b/contracts/semantic-release-supersession.schema.json new file mode 100644 index 00000000..59ac0a22 --- /dev/null +++ b/contracts/semantic-release-supersession.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.contextualwisdomlab.org/conceptweave/semantic-release-supersession/1.0.0", + "title": "ConceptWeave Semantic Release Supersession", + "type": "object", + "additionalProperties": false, + "required": ["superseded", "successor", "rationale"], + "properties": { + "superseded": {"$ref": "#/$defs/release_reference"}, + "successor": {"$ref": "#/$defs/release_reference"}, + "rationale": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + } + }, + "$defs": { + "release_reference": { + "type": "object", + "additionalProperties": false, + "required": ["release_id", "artifact_digest"], + "properties": { + "release_id": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "artifact_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + } + } + } + } +} diff --git a/contracts/semantic-release.schema.json b/contracts/semantic-release.schema.json new file mode 100644 index 00000000..a9f30803 --- /dev/null +++ b/contracts/semantic-release.schema.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.contextualwisdomlab.org/conceptweave/semantic-release/1.0.0", + "title": "ConceptWeave Semantic Release", + "type": "object", + "additionalProperties": false, + "required": [ + "release_id", + "contract_version", + "ontology_version", + "truth_status", + "publication_state", + "artifact_digest", + "provenance", + "concept_ids" + ], + "properties": { + "release_id": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "contract_version": { + "const": "1.0.0" + }, + "ontology_version": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "truth_status": { + "enum": [ + "observed", + "inferred", + "proposed", + "authoritative", + "superseded", + "rejected" + ] + }, + "publication_state": { + "enum": [ + "draft", + "proposed", + "validated", + "reviewed", + "published", + "superseded", + "rejected" + ] + }, + "artifact_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "provenance": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["source_id", "source_digest", "location"], + "properties": { + "source_id": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "source_digest": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "location": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + } + } + } + }, + "concept_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "publication_state": {"const": "published"} + }, + "required": ["publication_state"] + }, + "then": { + "properties": { + "truth_status": {"const": "authoritative"} + } + } + } + ] +} diff --git a/crates/conceptweave-client/Cargo.toml b/crates/conceptweave-client/Cargo.toml new file mode 100644 index 00000000..1b71255e --- /dev/null +++ b/crates/conceptweave-client/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "conceptweave-client" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true +description = "Offline semantic-release admission contracts for ConceptWeave consumers" + +[dependencies] +conceptweave-domain = { path = "../conceptweave-domain" } +sha2 = "0.10.9" diff --git a/crates/conceptweave-client/src/lib.rs b/crates/conceptweave-client/src/lib.rs new file mode 100644 index 00000000..0edc2dbe --- /dev/null +++ b/crates/conceptweave-client/src/lib.rs @@ -0,0 +1,662 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Offline ConceptWeave semantic-release client contracts. +//! +//! This crate is intentionally provider- and transport-independent. A consumer +//! can inspect release identity, provenance and governance state before making +//! authoritative use of a semantic release. Generator-private classes, source +//! database access and LLM orchestration stay outside this boundary. + +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; +use core::fmt; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; + +/// A validated content-digest identity carried by a semantic release. +/// +/// The current contract accepts only the canonical `sha256:<64 lowercase hex>` +/// shape. This value object validates digest identity syntax; exact detached +/// artifact bytes are cryptographically verified by +/// [`SemanticReleaseClient::verify_detached_artifact`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseDigest(String); + +impl ReleaseDigest { + /// Parses a release digest and rejects unsupported algorithms or malformed hex. + pub fn new(value: &str) -> Result { + let Some(hex) = value.strip_prefix("sha256:") else { + return Err(ReleaseContractError::InvalidDigest); + }; + if hex.len() != 64 { + return Err(ReleaseContractError::InvalidDigest); + } + if !hex + .bytes() + .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) + { + return Err(ReleaseContractError::InvalidDigest); + } + Ok(Self(value.to_owned())) + } + + /// Returns the canonical digest identity string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Stable identity and version metadata for one semantic release. +/// +/// Keeping these related identity fields in one value object prevents call sites +/// from relying on a long positional constructor and makes later compatibility +/// policy explicit without exposing mutable release internals. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseMetadata { + release_id: String, + contract_version: String, + ontology_version: String, +} + +impl ReleaseMetadata { + /// Creates validated stable release, contract, and ontology identities. + pub fn new( + release_id: impl Into, + contract_version: impl Into, + ontology_version: impl Into, + ) -> Result { + let release_id = release_id.into(); + let contract_version = contract_version.into(); + let ontology_version = ontology_version.into(); + require_non_blank(&release_id, "release_id")?; + require_non_blank(&contract_version, "contract_version")?; + require_non_blank(&ontology_version, "ontology_version")?; + Ok(Self { + release_id, + contract_version, + ontology_version, + }) + } + + /// Returns the stable semantic-release identity. + pub fn release_id(&self) -> &str { + &self.release_id + } + + /// Returns the client contract version encoded by the release. + pub fn contract_version(&self) -> &str { + &self.contract_version + } + + /// Returns the ontology/model version encoded by the release. + pub fn ontology_version(&self) -> &str { + &self.ontology_version + } +} + +/// Immutable client-visible metadata required to admit a semantic release. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticRelease { + metadata: ReleaseMetadata, + truth_status: TruthStatus, + publication_state: PublicationState, + artifact_digest: ReleaseDigest, + provenance: Vec, + concept_ids: Vec, +} + +impl SemanticRelease { + /// Constructs a structurally valid semantic release contract. + /// + /// Construction validates provenance and concept identity uniqueness while + /// [`ReleaseMetadata`] validates stable release/version identities. Whether + /// the release is safe for authoritative use is a separate client-policy + /// decision performed by [`SemanticReleaseClient::validate_for_authoritative_use`]. + pub fn new( + metadata: ReleaseMetadata, + truth_status: TruthStatus, + publication_state: PublicationState, + artifact_digest: ReleaseDigest, + provenance: Vec, + concept_ids: Vec, + ) -> Result { + if provenance.is_empty() { + return Err(ReleaseContractError::MissingProvenance); + } + + let mut unique_concepts = BTreeSet::new(); + for concept_id in &concept_ids { + require_non_blank(concept_id, "concept_id")?; + if !unique_concepts.insert(concept_id.as_str()) { + return Err(ReleaseContractError::DuplicateConceptId(concept_id.clone())); + } + } + + Ok(Self { + metadata, + truth_status, + publication_state, + artifact_digest, + provenance, + concept_ids, + }) + } + + /// Returns the stable semantic-release identity. + pub fn release_id(&self) -> &str { + self.metadata.release_id() + } + + /// Returns the client contract version encoded by this release. + pub fn contract_version(&self) -> &str { + self.metadata.contract_version() + } + + /// Returns the ontology/model version carried by this release. + pub fn ontology_version(&self) -> &str { + self.metadata.ontology_version() + } + + /// Returns the release truth status. + pub fn truth_status(&self) -> TruthStatus { + self.truth_status + } + + /// Returns the governance/publication state of this release. + pub fn publication_state(&self) -> PublicationState { + self.publication_state + } + + /// Returns the declared immutable artifact digest identity. + pub fn artifact_digest(&self) -> &ReleaseDigest { + &self.artifact_digest + } + + /// Returns immutable evidence/provenance references for this release. + pub fn provenance(&self) -> &[EvidenceReference] { + &self.provenance + } + + /// Returns stable concept identifiers carried by this release. + pub fn concept_ids(&self) -> &[String] { + &self.concept_ids + } +} + +/// Immutable reference to the exact bytes of one published semantic release. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticReleaseReference { + release_id: String, + artifact_digest: ReleaseDigest, +} + +impl SemanticReleaseReference { + /// Creates an exact release reference from a stable release id and canonical digest. + pub fn new( + release_id: impl Into, + artifact_digest: ReleaseDigest, + ) -> Result { + let release_id = release_id.into(); + require_non_blank(&release_id, "release_reference_id")?; + Ok(Self { + release_id, + artifact_digest, + }) + } + + /// Captures the immutable id-and-digest identity of a release contract. + #[must_use] + pub fn from_release(release: &SemanticRelease) -> Self { + Self { + release_id: release.release_id().to_owned(), + artifact_digest: release.artifact_digest().clone(), + } + } + + /// Returns the referenced stable semantic-release id. + pub fn release_id(&self) -> &str { + &self.release_id + } + + /// Returns the referenced immutable semantic-release artifact digest. + pub fn artifact_digest(&self) -> &ReleaseDigest { + &self.artifact_digest + } +} + +/// Explicit immutable declaration that one release is superseded by another release. +/// +/// Supersession is never inferred from semantic version ordering, timestamps, or diffs. Both +/// references are bound to exact release ids and artifact digests so a correction preserves the +/// prior published release while naming the precise steward-approved successor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReleaseSupersession { + superseded: SemanticReleaseReference, + successor: SemanticReleaseReference, + rationale: String, +} + +impl ReleaseSupersession { + /// Creates an explicit supersession declaration between two distinct release identities. + pub fn new( + superseded: SemanticReleaseReference, + successor: SemanticReleaseReference, + rationale: impl Into, + ) -> Result { + let rationale = rationale.into(); + require_non_blank(&rationale, "supersession_rationale")?; + if superseded.release_id() == successor.release_id() { + return Err(ReleaseContractError::SelfSupersession( + superseded.release_id().to_owned(), + )); + } + Ok(Self { + superseded, + successor, + rationale, + }) + } + + /// Returns the exact immutable release reference being superseded. + pub fn superseded(&self) -> &SemanticReleaseReference { + &self.superseded + } + + /// Returns the exact immutable successor release reference. + pub fn successor(&self) -> &SemanticReleaseReference { + &self.successor + } + + /// Returns the explicit steward-facing reason for supersession. + pub fn rationale(&self) -> &str { + &self.rationale + } +} + +/// Deterministic concept-level change between two admitted semantic releases. +/// +/// This value reports only public semantic-contract differences. It does not +/// authorize downstream queries, mutate either release, or infer business-domain +/// consequences for a consuming product. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticReleaseDiff { + previous_release_id: String, + current_release_id: String, + added_concept_ids: Vec, + removed_concept_ids: Vec, +} + +impl SemanticReleaseDiff { + /// Returns the stable identity of the earlier release. + pub fn previous_release_id(&self) -> &str { + &self.previous_release_id + } + + /// Returns the stable identity of the later release. + pub fn current_release_id(&self) -> &str { + &self.current_release_id + } + + /// Returns concept identities present only in the later release. + pub fn added_concept_ids(&self) -> &[String] { + &self.added_concept_ids + } + + /// Returns concept identities present only in the earlier release. + pub fn removed_concept_ids(&self) -> &[String] { + &self.removed_concept_ids + } +} + +/// Compatibility classification for one semantic-release contract version. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ContractVersionCompatibility { + /// The release uses the client's current contract version. + Current, + /// The release uses a legacy version explicitly admitted by client policy. + SupportedLegacy, + /// The release version is neither current nor explicitly supported legacy. + Unsupported, +} + +/// Offline admission policy for current and explicitly supported legacy contract versions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticReleaseClient { + supported_contract_version: String, + supported_legacy_contract_versions: BTreeSet, +} + +impl SemanticReleaseClient { + /// Creates a client pinned to one explicit current semantic-release contract version. + pub fn new( + supported_contract_version: impl Into, + ) -> Result { + Self::with_supported_legacy_contract_versions(supported_contract_version, Vec::new()) + } + + /// Creates a client with an explicit current version and explicit supported legacy versions. + /// + /// Legacy support is opt-in rather than inferred from version ordering. Blank versions and the + /// current version repeated as legacy are rejected so admission policy remains unambiguous. + pub fn with_supported_legacy_contract_versions( + supported_contract_version: impl Into, + supported_legacy_contract_versions: Vec, + ) -> Result { + let supported_contract_version = supported_contract_version.into(); + require_non_blank(&supported_contract_version, "supported_contract_version")?; + + let mut validated_legacy_versions = BTreeSet::new(); + for legacy_version in supported_legacy_contract_versions { + require_non_blank(&legacy_version, "supported_legacy_contract_version")?; + if legacy_version == supported_contract_version { + return Err(ReleaseContractError::CurrentContractVersionMarkedLegacy( + legacy_version, + )); + } + validated_legacy_versions.insert(legacy_version); + } + + Ok(Self { + supported_contract_version, + supported_legacy_contract_versions: validated_legacy_versions, + }) + } + + /// Returns the exact current semantic-release contract version this client accepts. + pub fn supported_contract_version(&self) -> &str { + &self.supported_contract_version + } + + /// Returns explicitly supported legacy contract versions in deterministic order. + pub fn supported_legacy_contract_versions(&self) -> &BTreeSet { + &self.supported_legacy_contract_versions + } + + /// Classifies a release against the client's explicit compatibility policy. + /// + /// Version ordering is deliberately not inferred. A version is compatible only when it equals + /// the current version or appears in the explicit legacy allow-set. + pub fn compatibility(&self, release: &SemanticRelease) -> ContractVersionCompatibility { + if release.contract_version() == self.supported_contract_version { + ContractVersionCompatibility::Current + } else if self + .supported_legacy_contract_versions + .contains(release.contract_version()) + { + ContractVersionCompatibility::SupportedLegacy + } else { + ContractVersionCompatibility::Unsupported + } + } + + fn validate_contract_compatibility( + &self, + release: &SemanticRelease, + ) -> Result<(), ReleaseContractError> { + if self.compatibility(release) == ContractVersionCompatibility::Unsupported { + return Err(ReleaseContractError::UnsupportedContractVersion { + expected: self.supported_contract_version.clone(), + actual: release.contract_version().to_string(), + }); + } + Ok(()) + } + + /// Fails closed unless a release is explicitly compatible, Published and Authoritative. + /// + /// This check is deterministic and performs no network or model calls. It is + /// suitable as an admission gate before a consuming product performs its own + /// tenant/purpose authorization and physical query planning. + pub fn validate_for_authoritative_use( + &self, + release: &SemanticRelease, + ) -> Result<(), ReleaseContractError> { + self.validate_contract_compatibility(release)?; + if release.publication_state != PublicationState::Published { + return Err(ReleaseContractError::ReleaseNotPublished { + actual: release.publication_state, + }); + } + if release.truth_status != TruthStatus::Authoritative { + return Err(ReleaseContractError::ReleaseNotAuthoritative { + actual: release.truth_status, + }); + } + Ok(()) + } + + /// Resolves one exact concept identifier from an admitted semantic release. + /// + /// Resolution is deliberately exact and deterministic: it performs no + /// case-folding, fuzzy matching, ontology inference, provider call, or + /// mutation. The release must first pass authoritative-use admission. + pub fn resolve_concept<'release>( + &self, + release: &'release SemanticRelease, + concept_id: &str, + ) -> Result, ReleaseContractError> { + require_non_blank(concept_id, "concept_id")?; + self.validate_for_authoritative_use(release)?; + Ok(release + .concept_ids() + .iter() + .map(String::as_str) + .find(|candidate| *candidate == concept_id)) + } + + /// Verifies the SHA-256 digest of exact detached semantic-artifact bytes. + /// + /// The release must first satisfy the same authoritative-use admission gate + /// as other Client operations. The caller supplies the exact detached bytes + /// whose identity is declared by [`SemanticRelease::artifact_digest`]; this + /// method performs no network access, parsing, provider call, or source-system read. + pub fn verify_detached_artifact( + &self, + release: &SemanticRelease, + artifact_bytes: &[u8], + ) -> Result<(), ReleaseContractError> { + self.validate_for_authoritative_use(release)?; + + const HEX: &[u8; 16] = b"0123456789abcdef"; + let digest = Sha256::digest(artifact_bytes); + let mut computed = String::with_capacity("sha256:".len() + digest.len() * 2); + computed.push_str("sha256:"); + for byte in digest { + let high_nibble = usize::from(byte >> 4); + let low_nibble = usize::from(byte & 0x0f); + computed.push(char::from(HEX[high_nibble])); + computed.push(char::from(HEX[low_nibble])); + } + + let declared = release.artifact_digest().as_str(); + if computed != declared { + return Err(ReleaseContractError::ArtifactDigestMismatch { + declared: declared.to_string(), + computed, + }); + } + Ok(()) + } + + /// Validates an explicit immutable supersession declaration between two governed releases. + /// + /// The successor must pass ordinary authoritative-use admission. The predecessor may either + /// still be the admitted Published+Authoritative release while a replacement is reviewed, or + /// already carry the governed Superseded+Superseded lifecycle state after publication of the + /// replacement. In both cases contract compatibility and exact id-and-digest binding remain + /// fail-closed; no version order, timestamp, diff, or ontology similarity is implicit evidence. + pub fn validate_supersession( + &self, + declaration: &ReleaseSupersession, + superseded: &SemanticRelease, + successor: &SemanticRelease, + ) -> Result<(), ReleaseContractError> { + if superseded.publication_state() == PublicationState::Superseded + && superseded.truth_status() == TruthStatus::Superseded + { + self.validate_contract_compatibility(superseded)?; + } else { + self.validate_for_authoritative_use(superseded)?; + } + self.validate_for_authoritative_use(successor)?; + + if declaration.superseded() != &SemanticReleaseReference::from_release(superseded) { + return Err(ReleaseContractError::SupersededReleaseReferenceMismatch); + } + if declaration.successor() != &SemanticReleaseReference::from_release(successor) { + return Err(ReleaseContractError::SuccessorReleaseReferenceMismatch); + } + Ok(()) + } + + /// Compares two admitted releases and reports deterministic concept changes. + /// + /// Both releases pass the same authoritative-use admission gate before any + /// difference is exposed. Reusing one stable release identity for conflicting + /// immutable content fails closed rather than being reported as ordinary + /// evolution. Concept identifiers are sorted deterministically for replay. + pub fn diff( + &self, + previous: &SemanticRelease, + current: &SemanticRelease, + ) -> Result { + self.validate_for_authoritative_use(previous)?; + self.validate_for_authoritative_use(current)?; + + if previous.release_id() == current.release_id() && previous != current { + return Err(ReleaseContractError::ConflictingReleaseIdentity( + previous.release_id().to_owned(), + )); + } + + let previous_concepts: BTreeSet<&str> = + previous.concept_ids().iter().map(String::as_str).collect(); + let current_concepts: BTreeSet<&str> = + current.concept_ids().iter().map(String::as_str).collect(); + + let added_concept_ids = current_concepts + .difference(&previous_concepts) + .map(|concept_id| (*concept_id).to_string()) + .collect(); + let removed_concept_ids = previous_concepts + .difference(¤t_concepts) + .map(|concept_id| (*concept_id).to_string()) + .collect(); + + Ok(SemanticReleaseDiff { + previous_release_id: previous.release_id().to_string(), + current_release_id: current.release_id().to_string(), + added_concept_ids, + removed_concept_ids, + }) + } +} + +fn require_non_blank(value: &str, field: &'static str) -> Result<(), ReleaseContractError> { + if value.trim().is_empty() { + return Err(ReleaseContractError::EmptyField(field)); + } + Ok(()) +} + +/// A deterministic semantic-release contract or admission failure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReleaseContractError { + /// A required stable identity or version field was blank. + EmptyField(&'static str), + /// The declared release digest is not canonical `sha256:<64 lowercase hex>`. + InvalidDigest, + /// Exact detached semantic-artifact bytes do not match the digest declared by the release. + ArtifactDigestMismatch { + /// Digest coordinate declared by the semantic release. + declared: String, + /// SHA-256 digest computed from the exact supplied bytes. + computed: String, + }, + /// The release carries no provenance evidence. + MissingProvenance, + /// The release repeats one semantic concept identity. + DuplicateConceptId(String), + /// The configured current contract version was also supplied as a legacy version. + CurrentContractVersionMarkedLegacy(String), + /// A release attempted to supersede the same stable release identity. + SelfSupersession(String), + /// One stable release identity was reused for conflicting immutable content. + ConflictingReleaseIdentity(String), + /// The declared superseded id-and-digest reference does not match the supplied release. + SupersededReleaseReferenceMismatch, + /// The declared successor id-and-digest reference does not match the supplied release. + SuccessorReleaseReferenceMismatch, + /// The release uses a contract version this client does not support. + UnsupportedContractVersion { + /// Current contract version required by the client when no explicit compatibility exists. + expected: String, + /// Contract version supplied by the release. + actual: String, + }, + /// The release has not crossed the governed Published boundary. + ReleaseNotPublished { + /// Actual release publication state. + actual: PublicationState, + }, + /// The release is Published but its truth status is not Authoritative. + ReleaseNotAuthoritative { + /// Actual release truth status. + actual: TruthStatus, + }, +} + +impl fmt::Display for ReleaseContractError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyField(field) => write!(formatter, "required field `{field}` is blank"), + Self::InvalidDigest => write!( + formatter, + "release digest must use sha256:<64 lowercase hex>" + ), + Self::ArtifactDigestMismatch { declared, computed } => write!( + formatter, + "semantic release artifact digest mismatch: declared `{declared}`, computed `{computed}`" + ), + Self::MissingProvenance => { + write!(formatter, "semantic releases require provenance evidence") + } + Self::DuplicateConceptId(concept_id) => write!( + formatter, + "semantic release contains duplicate concept id `{concept_id}`" + ), + Self::CurrentContractVersionMarkedLegacy(contract_version) => write!( + formatter, + "current semantic release contract version `{contract_version}` cannot also be marked legacy" + ), + Self::SelfSupersession(release_id) => write!( + formatter, + "semantic release `{release_id}` cannot supersede itself" + ), + Self::ConflictingReleaseIdentity(release_id) => write!( + formatter, + "semantic release `{release_id}` identifies conflicting immutable content" + ), + Self::SupersededReleaseReferenceMismatch => write!( + formatter, + "supersession predecessor reference does not match the exact supplied release" + ), + Self::SuccessorReleaseReferenceMismatch => write!( + formatter, + "supersession successor reference does not match the exact supplied release" + ), + Self::UnsupportedContractVersion { expected, actual } => write!( + formatter, + "semantic release contract version `{actual}` is unsupported; current version is `{expected}`" + ), + Self::ReleaseNotPublished { actual } => { + write!(formatter, "semantic release is {actual:?}, not Published") + } + Self::ReleaseNotAuthoritative { actual } => write!( + formatter, + "semantic release truth status is {actual:?}, not Authoritative" + ), + } + } +} + +impl std::error::Error for ReleaseContractError {} diff --git a/crates/conceptweave-client/tests/concept_resolution.rs b/crates/conceptweave-client/tests/concept_resolution.rs new file mode 100644 index 00000000..be5cb825 --- /dev/null +++ b/crates/conceptweave-client/tests/concept_resolution.rs @@ -0,0 +1,79 @@ +use conceptweave_client::{ + ReleaseContractError, ReleaseDigest, ReleaseMetadata, SemanticRelease, SemanticReleaseClient, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; + +fn evidence() -> EvidenceReference { + EvidenceReference::new( + "snapshot:grc-schema-2026-09-01", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "public.control_item.control_identifier", + ) + .unwrap() +} + +fn release(publication_state: PublicationState) -> SemanticRelease { + SemanticRelease::new( + ReleaseMetadata::new( + "semantic-release-grc-2026-09-01", + "1.0.0", + "grc-ontology-2026-09", + ) + .unwrap(), + TruthStatus::Authoritative, + publication_state, + ReleaseDigest::new( + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) + .unwrap(), + vec![evidence()], + vec![ + "control.internal_control".to_string(), + "evidence.control_evidence".to_string(), + ], + ) + .unwrap() +} + +#[test] +fn exact_concept_resolution_is_deterministic_and_does_not_fuzzy_match() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = release(PublicationState::Published); + + assert_eq!( + client.resolve_concept(&release, "control.internal_control"), + Ok(Some("control.internal_control")) + ); + assert_eq!( + client.resolve_concept(&release, "internal control"), + Ok(None) + ); + assert_eq!( + client.resolve_concept(&release, "CONTROL.INTERNAL_CONTROL"), + Ok(None) + ); +} + +#[test] +fn concept_resolution_reuses_authoritative_release_admission() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let reviewed = release(PublicationState::Reviewed); + + assert_eq!( + client.resolve_concept(&reviewed, "control.internal_control"), + Err(ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Reviewed, + }) + ); +} + +#[test] +fn concept_resolution_rejects_blank_identifiers() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = release(PublicationState::Published); + + assert_eq!( + client.resolve_concept(&release, " "), + Err(ReleaseContractError::EmptyField("concept_id")) + ); +} diff --git a/crates/conceptweave-client/tests/digest_canonicalization.rs b/crates/conceptweave-client/tests/digest_canonicalization.rs new file mode 100644 index 00000000..99cdc7bf --- /dev/null +++ b/crates/conceptweave-client/tests/digest_canonicalization.rs @@ -0,0 +1,11 @@ +use conceptweave_client::{ReleaseContractError, ReleaseDigest}; + +#[test] +fn uppercase_sha256_digest_identity_is_rejected() { + let uppercase_digest = format!("sha256:{}", "A".repeat(64)); + + assert_eq!( + ReleaseDigest::new(&uppercase_digest), + Err(ReleaseContractError::InvalidDigest) + ); +} diff --git a/crates/conceptweave-client/tests/documentation_contract.rs b/crates/conceptweave-client/tests/documentation_contract.rs new file mode 100644 index 00000000..cea0d1a3 --- /dev/null +++ b/crates/conceptweave-client/tests/documentation_contract.rs @@ -0,0 +1,45 @@ +//! Keep public Client Consumption documentation aligned with the Rust API. + +const PRD: &str = include_str!("../../../docs/PRD.md"); +const TRD: &str = include_str!("../../../docs/TRD.md"); +const UML: &str = include_str!("../../../docs/UML.md"); +const ADR: &str = include_str!("../../../docs/adr/0005-semantic-release-client-boundary.md"); +const GAP_BASELINE: &str = include_str!("../../../docs/product-technical-gap-baseline.md"); +const TEST_STRATEGY: &str = include_str!("../../../TEST_STRATEGY.md"); +const SECURITY: &str = include_str!("../../../SECURITY.md"); + +#[test] +fn retired_serialized_artifact_api_is_absent_from_public_docs() { + for (name, document) in [ + ("PRD", PRD), + ("TRD", TRD), + ("UML", UML), + ("ADR 0005", ADR), + ("gap baseline", GAP_BASELINE), + ("test strategy", TEST_STRATEGY), + ("security", SECURITY), + ] { + assert!( + !document.contains("verify_serialized_artifact"), + "{name} still names the retired serialized-artifact API" + ); + } +} + +#[test] +fn detached_artifact_integrity_is_documented_as_current_behavior() { + for (name, document) in [ + ("PRD", PRD), + ("TRD", TRD), + ("UML", UML), + ("ADR 0005", ADR), + ("gap baseline", GAP_BASELINE), + ("test strategy", TEST_STRATEGY), + ("security", SECURITY), + ] { + assert!( + document.contains("verify_detached_artifact"), + "{name} does not document the current detached-artifact integrity boundary" + ); + } +} diff --git a/crates/conceptweave-client/tests/error_messages.rs b/crates/conceptweave-client/tests/error_messages.rs new file mode 100644 index 00000000..b26f8586 --- /dev/null +++ b/crates/conceptweave-client/tests/error_messages.rs @@ -0,0 +1,82 @@ +use conceptweave_client::ReleaseContractError; +use conceptweave_domain::{PublicationState, TruthStatus}; + +#[test] +fn contract_errors_explain_the_failed_admission_invariant() { + let cases = [ + ( + ReleaseContractError::EmptyField("release_id"), + "required field `release_id` is blank".to_string(), + ), + ( + ReleaseContractError::InvalidDigest, + "release digest must use sha256:<64 lowercase hex>".to_string(), + ), + ( + ReleaseContractError::ArtifactDigestMismatch { + declared: "sha256:declared".to_string(), + computed: "sha256:computed".to_string(), + }, + "semantic release artifact digest mismatch: declared `sha256:declared`, computed `sha256:computed`" + .to_string(), + ), + ( + ReleaseContractError::MissingProvenance, + "semantic releases require provenance evidence".to_string(), + ), + ( + ReleaseContractError::DuplicateConceptId("concept.one".to_string()), + "semantic release contains duplicate concept id `concept.one`".to_string(), + ), + ( + ReleaseContractError::CurrentContractVersionMarkedLegacy("2.0.0".to_string()), + "current semantic release contract version `2.0.0` cannot also be marked legacy" + .to_string(), + ), + ( + ReleaseContractError::SelfSupersession("semantic_release_2026_09".to_string()), + "semantic release `semantic_release_2026_09` cannot supersede itself".to_string(), + ), + ( + ReleaseContractError::ConflictingReleaseIdentity( + "semantic_release_2026_09".to_string(), + ), + "semantic release `semantic_release_2026_09` identifies conflicting immutable content" + .to_string(), + ), + ( + ReleaseContractError::SupersededReleaseReferenceMismatch, + "supersession predecessor reference does not match the exact supplied release" + .to_string(), + ), + ( + ReleaseContractError::SuccessorReleaseReferenceMismatch, + "supersession successor reference does not match the exact supplied release" + .to_string(), + ), + ( + ReleaseContractError::UnsupportedContractVersion { + expected: "1.0.0".to_string(), + actual: "2.0.0".to_string(), + }, + "semantic release contract version `2.0.0` is unsupported; current version is `1.0.0`" + .to_string(), + ), + ( + ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Reviewed, + }, + "semantic release is Reviewed, not Published".to_string(), + ), + ( + ReleaseContractError::ReleaseNotAuthoritative { + actual: TruthStatus::Proposed, + }, + "semantic release truth status is Proposed, not Authoritative".to_string(), + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + } +} diff --git a/crates/conceptweave-client/tests/release_compatibility.rs b/crates/conceptweave-client/tests/release_compatibility.rs new file mode 100644 index 00000000..0926e17d --- /dev/null +++ b/crates/conceptweave-client/tests/release_compatibility.rs @@ -0,0 +1,118 @@ +use conceptweave_client::{ + ContractVersionCompatibility, ReleaseContractError, ReleaseDigest, ReleaseMetadata, + SemanticRelease, SemanticReleaseClient, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; + +fn evidence() -> EvidenceReference { + EvidenceReference::new( + "snapshot:grc-schema-2026-09-01", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "public.control_evidence.control_identifier", + ) + .expect("evidence fixture is valid") +} + +fn release(contract_version: &str) -> SemanticRelease { + SemanticRelease::new( + ReleaseMetadata::new( + format!("semantic_release_{contract_version}"), + contract_version, + "grc_ontology_2026_09", + ) + .expect("metadata fixture is valid"), + TruthStatus::Authoritative, + PublicationState::Published, + ReleaseDigest::new( + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) + .expect("digest fixture is valid"), + vec![evidence()], + vec!["control.evidence".to_owned()], + ) + .expect("release fixture is valid") +} + +#[test] +fn client_explicitly_distinguishes_current_supported_legacy_and_unknown_versions() { + let client = SemanticReleaseClient::with_supported_legacy_contract_versions( + "2.0.0", + vec!["1.1.0".to_owned(), "1.0.0".to_owned()], + ) + .expect("explicit compatibility policy is valid"); + + assert_eq!( + client + .supported_legacy_contract_versions() + .iter() + .map(String::as_str) + .collect::>(), + vec!["1.0.0", "1.1.0"] + ); + assert_eq!( + client.compatibility(&release("2.0.0")), + ContractVersionCompatibility::Current + ); + assert_eq!( + client.compatibility(&release("1.0.0")), + ContractVersionCompatibility::SupportedLegacy + ); + assert_eq!( + client.compatibility(&release("3.0.0")), + ContractVersionCompatibility::Unsupported + ); +} + +#[test] +fn supported_legacy_release_passes_the_same_authoritative_use_gate() { + let client = SemanticReleaseClient::with_supported_legacy_contract_versions( + "2.0.0", + vec!["1.0.0".to_owned()], + ) + .expect("explicit compatibility policy is valid"); + + assert_eq!( + client.validate_for_authoritative_use(&release("1.0.0")), + Ok(()) + ); +} + +#[test] +fn unknown_version_still_fails_closed_when_legacy_support_exists() { + let client = SemanticReleaseClient::with_supported_legacy_contract_versions( + "2.0.0", + vec!["1.0.0".to_owned()], + ) + .expect("explicit compatibility policy is valid"); + + assert_eq!( + client.validate_for_authoritative_use(&release("0.9.0")), + Err(ReleaseContractError::UnsupportedContractVersion { + expected: "2.0.0".to_owned(), + actual: "0.9.0".to_owned(), + }) + ); +} + +#[test] +fn compatibility_policy_rejects_blank_or_current_version_as_legacy() { + assert_eq!( + SemanticReleaseClient::with_supported_legacy_contract_versions( + "2.0.0", + vec![" ".to_owned()] + ), + Err(ReleaseContractError::EmptyField( + "supported_legacy_contract_version" + )) + ); + + assert_eq!( + SemanticReleaseClient::with_supported_legacy_contract_versions( + "2.0.0", + vec!["2.0.0".to_owned()] + ), + Err(ReleaseContractError::CurrentContractVersionMarkedLegacy( + "2.0.0".to_owned() + )) + ); +} diff --git a/crates/conceptweave-client/tests/release_diff.rs b/crates/conceptweave-client/tests/release_diff.rs new file mode 100644 index 00000000..7abf6d5e --- /dev/null +++ b/crates/conceptweave-client/tests/release_diff.rs @@ -0,0 +1,83 @@ +use conceptweave_client::{ + ReleaseContractError, ReleaseDigest, ReleaseMetadata, SemanticRelease, SemanticReleaseClient, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; + +fn evidence() -> EvidenceReference { + EvidenceReference::new( + "snapshot:grc-schema-2026-09-01", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "public.control_evidence.control_identifier", + ) + .unwrap() +} + +fn release(release_id: &str, concept_ids: &[&str], state: PublicationState) -> SemanticRelease { + SemanticRelease::new( + ReleaseMetadata::new(release_id, "1.0.0", "grc-ontology-2026-09").unwrap(), + if state == PublicationState::Published { + TruthStatus::Authoritative + } else { + TruthStatus::Inferred + }, + state, + ReleaseDigest::new( + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) + .unwrap(), + vec![evidence()], + concept_ids + .iter() + .map(|value| (*value).to_string()) + .collect(), + ) + .unwrap() +} + +#[test] +fn release_diff_reports_deterministic_added_and_removed_concepts() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let previous = release( + "semantic-release-grc-v1", + &["control.evidence", "control.owner"], + PublicationState::Published, + ); + let current = release( + "semantic-release-grc-v2", + &["control.effectiveness", "control.evidence"], + PublicationState::Published, + ); + + let diff = client.diff(&previous, ¤t).unwrap(); + + assert_eq!(diff.previous_release_id(), "semantic-release-grc-v1"); + assert_eq!(diff.current_release_id(), "semantic-release-grc-v2"); + assert_eq!(diff.added_concept_ids(), ["control.effectiveness"]); + assert_eq!(diff.removed_concept_ids(), ["control.owner"]); +} + +#[test] +fn release_diff_fails_closed_when_either_release_is_not_admissible() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let published = release( + "semantic-release-grc-published", + &["control.evidence"], + PublicationState::Published, + ); + let reviewed_previous = release( + "semantic-release-grc-reviewed-previous", + &["control.owner"], + PublicationState::Reviewed, + ); + let reviewed_current = release( + "semantic-release-grc-reviewed-current", + &["control.effectiveness"], + PublicationState::Reviewed, + ); + let expected = Err(ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Reviewed, + }); + + assert_eq!(client.diff(&reviewed_previous, &published), expected); + assert_eq!(client.diff(&published, &reviewed_current), expected); +} diff --git a/crates/conceptweave-client/tests/release_integrity.rs b/crates/conceptweave-client/tests/release_integrity.rs new file mode 100644 index 00000000..c70d8b5f --- /dev/null +++ b/crates/conceptweave-client/tests/release_integrity.rs @@ -0,0 +1,76 @@ +use conceptweave_client::{ + ReleaseContractError, ReleaseDigest, ReleaseMetadata, SemanticRelease, SemanticReleaseClient, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; + +const DETACHED_ARTIFACT_BYTES: &[u8] = b"conceptweave-semantic-model-artifact-v1"; +const DETACHED_ARTIFACT_DIGEST: &str = + "sha256:13b5f6c3d51da7bd481e8d267a135f0c7ef2a7a4e3987ceb6a1b610e215ccefd"; + +fn release_with_state(digest: &str, publication_state: PublicationState) -> SemanticRelease { + SemanticRelease::new( + ReleaseMetadata::new( + "semantic-release-grc-integrity-v1", + "1.0.0", + "grc-ontology-2026-09", + ) + .unwrap(), + TruthStatus::Authoritative, + publication_state, + ReleaseDigest::new(digest).unwrap(), + vec![ + EvidenceReference::new( + "snapshot:grc-schema-2026-09-01", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "public.control_evidence.control_identifier", + ) + .unwrap(), + ], + vec!["control.evidence".to_string()], + ) + .unwrap() +} + +fn published_release(digest: &str) -> SemanticRelease { + release_with_state(digest, PublicationState::Published) +} + +#[test] +fn detached_artifact_digest_verification_accepts_exact_bytes_offline() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = published_release(DETACHED_ARTIFACT_DIGEST); + + assert_eq!( + client.verify_detached_artifact(&release, DETACHED_ARTIFACT_BYTES), + Ok(()) + ); +} + +#[test] +fn detached_artifact_digest_verification_rejects_changed_bytes() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = published_release(DETACHED_ARTIFACT_DIGEST); + + let result = client.verify_detached_artifact( + &release, + b"conceptweave-semantic-model-artifact-v1-tampered", + ); + + assert!(matches!( + result, + Err(ReleaseContractError::ArtifactDigestMismatch { .. }) + )); +} + +#[test] +fn detached_artifact_digest_verification_rejects_unpublished_release() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = release_with_state(DETACHED_ARTIFACT_DIGEST, PublicationState::Proposed); + + assert_eq!( + client.verify_detached_artifact(&release, DETACHED_ARTIFACT_BYTES), + Err(ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Proposed, + }) + ); +} diff --git a/crates/conceptweave-client/tests/release_supersession.rs b/crates/conceptweave-client/tests/release_supersession.rs new file mode 100644 index 00000000..f76db023 --- /dev/null +++ b/crates/conceptweave-client/tests/release_supersession.rs @@ -0,0 +1,159 @@ +use conceptweave_client::{ + ReleaseContractError, ReleaseDigest, ReleaseMetadata, ReleaseSupersession, SemanticRelease, + SemanticReleaseClient, SemanticReleaseReference, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; + +fn evidence() -> EvidenceReference { + EvidenceReference::new( + "snapshot:grc-schema-2026-09-01", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "governance_core.control_evidence.control_identifier", + ) + .expect("evidence fixture is valid") +} + +fn digest(hex: char) -> ReleaseDigest { + ReleaseDigest::new(&format!("sha256:{}", hex.to_string().repeat(64))) + .expect("digest fixture is valid") +} + +fn release( + release_id: &str, + digest_hex: char, + publication_state: PublicationState, +) -> SemanticRelease { + SemanticRelease::new( + ReleaseMetadata::new(release_id, "2.0.0", format!("ontology_{release_id}")) + .expect("metadata fixture is valid"), + TruthStatus::Authoritative, + publication_state, + digest(digest_hex), + vec![evidence()], + vec!["control.evidence".to_owned()], + ) + .expect("release fixture is valid") +} + +#[test] +fn supersession_preserves_exact_immutable_release_references_and_rationale() { + let previous = release("semantic_release_2026_09", 'b', PublicationState::Published); + let successor = release("semantic_release_2026_10", 'c', PublicationState::Published); + let declaration = ReleaseSupersession::new( + SemanticReleaseReference::from_release(&previous), + SemanticReleaseReference::from_release(&successor), + "Correct the governed control taxonomy while preserving the prior release.", + ) + .expect("supersession declaration is valid"); + + assert_eq!( + declaration.superseded().release_id(), + "semantic_release_2026_09" + ); + assert_eq!( + declaration.superseded().artifact_digest(), + previous.artifact_digest() + ); + assert_eq!( + declaration.successor().release_id(), + "semantic_release_2026_10" + ); + assert_eq!( + declaration.successor().artifact_digest(), + successor.artifact_digest() + ); + assert_eq!( + declaration.rationale(), + "Correct the governed control taxonomy while preserving the prior release." + ); +} + +#[test] +fn supersession_rejects_blank_reference_fields_blank_rationale_and_self_supersession() { + assert_eq!( + SemanticReleaseReference::new(" ", digest('b')), + Err(ReleaseContractError::EmptyField("release_reference_id")) + ); + assert_eq!( + ReleaseSupersession::new( + SemanticReleaseReference::new("semantic_release_2026_09", digest('b')) + .expect("reference is valid"), + SemanticReleaseReference::new("semantic_release_2026_10", digest('c')) + .expect("reference is valid"), + "\t", + ), + Err(ReleaseContractError::EmptyField("supersession_rationale")) + ); + assert_eq!( + ReleaseSupersession::new( + SemanticReleaseReference::new("semantic_release_2026_09", digest('b')) + .expect("reference is valid"), + SemanticReleaseReference::new("semantic_release_2026_09", digest('c')) + .expect("reference is valid"), + "replacement", + ), + Err(ReleaseContractError::SelfSupersession( + "semantic_release_2026_09".to_owned() + )) + ); +} + +#[test] +fn client_accepts_only_an_explicit_supersession_bound_to_both_exact_release_identities() { + let client = SemanticReleaseClient::new("2.0.0").expect("client policy is valid"); + let previous = release("semantic_release_2026_09", 'b', PublicationState::Published); + let successor = release("semantic_release_2026_10", 'c', PublicationState::Published); + let declaration = ReleaseSupersession::new( + SemanticReleaseReference::from_release(&previous), + SemanticReleaseReference::from_release(&successor), + "superseded by steward-approved correction", + ) + .expect("supersession declaration is valid"); + + assert_eq!( + client.validate_supersession(&declaration, &previous, &successor), + Ok(()) + ); + + let wrong_previous_digest = + release("semantic_release_2026_09", 'd', PublicationState::Published); + assert_eq!( + client.validate_supersession(&declaration, &wrong_previous_digest, &successor), + Err(ReleaseContractError::SupersededReleaseReferenceMismatch) + ); + + let wrong_successor_digest = + release("semantic_release_2026_10", 'e', PublicationState::Published); + assert_eq!( + client.validate_supersession(&declaration, &previous, &wrong_successor_digest), + Err(ReleaseContractError::SuccessorReleaseReferenceMismatch) + ); +} + +#[test] +fn supersession_never_bypasses_either_authoritative_release_admission_gate() { + let client = SemanticReleaseClient::new("2.0.0").expect("client policy is valid"); + let reviewed_previous = release("semantic_release_2026_09", 'b', PublicationState::Reviewed); + let published_previous = release("semantic_release_2026_09", 'b', PublicationState::Published); + let published_successor = release("semantic_release_2026_10", 'c', PublicationState::Published); + let reviewed_successor = release("semantic_release_2026_10", 'c', PublicationState::Reviewed); + let declaration = ReleaseSupersession::new( + SemanticReleaseReference::from_release(&published_previous), + SemanticReleaseReference::from_release(&published_successor), + "attempted supersession", + ) + .expect("supersession declaration is structurally valid"); + + assert_eq!( + client.validate_supersession(&declaration, &reviewed_previous, &published_successor), + Err(ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Reviewed, + }) + ); + assert_eq!( + client.validate_supersession(&declaration, &published_previous, &reviewed_successor), + Err(ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Reviewed, + }) + ); +} diff --git a/crates/conceptweave-client/tests/release_validation.rs b/crates/conceptweave-client/tests/release_validation.rs new file mode 100644 index 00000000..4aa80ef3 --- /dev/null +++ b/crates/conceptweave-client/tests/release_validation.rs @@ -0,0 +1,194 @@ +use conceptweave_client::{ + ReleaseContractError, ReleaseDigest, ReleaseMetadata, SemanticRelease, SemanticReleaseClient, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; + +fn evidence() -> EvidenceReference { + EvidenceReference::new( + "snapshot:grc-schema-2026-09-01", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "public.control_evidence.control_identifier", + ) + .unwrap() +} + +fn digest() -> ReleaseDigest { + ReleaseDigest::new("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + .unwrap() +} + +fn metadata(contract_version: &str) -> ReleaseMetadata { + ReleaseMetadata::new( + "semantic-release-grc-2026-09-01", + contract_version, + "grc-ontology-2026-09", + ) + .unwrap() +} + +fn release( + contract_version: &str, + truth_status: TruthStatus, + publication_state: PublicationState, +) -> SemanticRelease { + SemanticRelease::new( + metadata(contract_version), + truth_status, + publication_state, + digest(), + vec![evidence()], + vec!["control.evidence".to_string(), "control.owner".to_string()], + ) + .unwrap() +} + +#[test] +fn authoritative_published_release_is_admitted_offline() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = release( + "1.0.0", + TruthStatus::Authoritative, + PublicationState::Published, + ); + + assert_eq!(client.supported_contract_version(), "1.0.0"); + assert_eq!(release.release_id(), "semantic-release-grc-2026-09-01"); + assert_eq!(release.contract_version(), "1.0.0"); + assert_eq!(release.ontology_version(), "grc-ontology-2026-09"); + assert_eq!(release.truth_status(), TruthStatus::Authoritative); + assert_eq!(release.publication_state(), PublicationState::Published); + assert_eq!( + release.artifact_digest().as_str(), + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ); + assert_eq!(release.provenance().len(), 1); + assert_eq!(release.concept_ids(), ["control.evidence", "control.owner"]); + assert_eq!(client.validate_for_authoritative_use(&release), Ok(())); +} + +#[test] +fn client_fails_closed_on_unpublished_or_non_authoritative_release() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + + let reviewed = release("1.0.0", TruthStatus::Inferred, PublicationState::Reviewed); + assert_eq!( + client.validate_for_authoritative_use(&reviewed), + Err(ReleaseContractError::ReleaseNotPublished { + actual: PublicationState::Reviewed, + }) + ); + + let wrong_truth = release("1.0.0", TruthStatus::Proposed, PublicationState::Published); + assert_eq!( + client.validate_for_authoritative_use(&wrong_truth), + Err(ReleaseContractError::ReleaseNotAuthoritative { + actual: TruthStatus::Proposed, + }) + ); +} + +#[test] +fn client_rejects_unsupported_contract_version_before_use() { + let client = SemanticReleaseClient::new("1.0.0").unwrap(); + let release = release( + "2.0.0", + TruthStatus::Authoritative, + PublicationState::Published, + ); + + assert_eq!( + client.validate_for_authoritative_use(&release), + Err(ReleaseContractError::UnsupportedContractVersion { + expected: "1.0.0".to_string(), + actual: "2.0.0".to_string(), + }) + ); +} + +#[test] +fn metadata_requires_non_blank_release_contract_and_ontology_versions() { + for (release_id, contract_version, ontology_version, expected_field) in [ + (" ", "1.0.0", "ontology-1", "release_id"), + ("release-1", " ", "ontology-1", "contract_version"), + ("release-1", "1.0.0", " ", "ontology_version"), + ] { + assert_eq!( + ReleaseMetadata::new(release_id, contract_version, ontology_version), + Err(ReleaseContractError::EmptyField(expected_field)) + ); + } + + let metadata = ReleaseMetadata::new("release-1", "1.0.0", "ontology-1").unwrap(); + assert_eq!(metadata.release_id(), "release-1"); + assert_eq!(metadata.contract_version(), "1.0.0"); + assert_eq!(metadata.ontology_version(), "ontology-1"); +} + +#[test] +fn release_requires_provenance_and_unique_non_blank_concepts() { + assert_eq!( + SemanticRelease::new( + metadata("1.0.0"), + TruthStatus::Authoritative, + PublicationState::Published, + digest(), + vec![], + vec!["concept.one".to_string()], + ), + Err(ReleaseContractError::MissingProvenance) + ); + + assert_eq!( + SemanticRelease::new( + metadata("1.0.0"), + TruthStatus::Authoritative, + PublicationState::Published, + digest(), + vec![evidence()], + vec![" ".to_string()], + ), + Err(ReleaseContractError::EmptyField("concept_id")) + ); + + assert_eq!( + SemanticRelease::new( + metadata("1.0.0"), + TruthStatus::Authoritative, + PublicationState::Published, + digest(), + vec![evidence()], + vec!["concept.one".to_string(), "concept.one".to_string()], + ), + Err(ReleaseContractError::DuplicateConceptId( + "concept.one".to_string() + )) + ); +} + +#[test] +fn digest_contract_rejects_non_sha256_and_malformed_hex() { + assert_eq!( + ReleaseDigest::new("md5:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + Err(ReleaseContractError::InvalidDigest) + ); + assert_eq!( + ReleaseDigest::new("sha256:abc"), + Err(ReleaseContractError::InvalidDigest) + ); + assert_eq!( + ReleaseDigest::new( + "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg" + ), + Err(ReleaseContractError::InvalidDigest) + ); +} + +#[test] +fn client_requires_non_blank_supported_contract_version() { + assert_eq!( + SemanticReleaseClient::new(" "), + Err(ReleaseContractError::EmptyField( + "supported_contract_version" + )) + ); +} diff --git a/crates/conceptweave-client/tests/review_contract_regressions.rs b/crates/conceptweave-client/tests/review_contract_regressions.rs new file mode 100644 index 00000000..4d1b03ef --- /dev/null +++ b/crates/conceptweave-client/tests/review_contract_regressions.rs @@ -0,0 +1,129 @@ +use conceptweave_client::{ + ReleaseDigest, ReleaseMetadata, ReleaseSupersession, SemanticRelease, SemanticReleaseClient, + SemanticReleaseReference, +}; +use conceptweave_domain::{EvidenceReference, PublicationState, TruthStatus}; +use std::{fs, path::PathBuf}; + +fn digest(hex: char) -> ReleaseDigest { + ReleaseDigest::new(&format!("sha256:{}", hex.to_string().repeat(64))) + .expect("digest fixture must be canonical") +} + +fn evidence() -> EvidenceReference { + EvidenceReference::new( + "snapshot:client-review-regression", + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "governance_core.control_evidence.control_identifier", + ) + .expect("evidence fixture must be valid") +} + +fn release( + release_id: &str, + digest_hex: char, + truth_status: TruthStatus, + publication_state: PublicationState, + concept_ids: &[&str], +) -> SemanticRelease { + SemanticRelease::new( + ReleaseMetadata::new(release_id, "1.0.0", "ontology_client_review") + .expect("metadata fixture must be valid"), + truth_status, + publication_state, + digest(digest_hex), + vec![evidence()], + concept_ids.iter().map(|value| (*value).to_owned()).collect(), + ) + .expect("release fixture must be structurally valid") +} + +fn repository_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(|path| path.parent()) + .expect("client crate must live below the repository root") + .to_path_buf() +} + +#[test] +fn diff_fails_closed_when_one_release_id_names_conflicting_immutable_content() { + let client = SemanticReleaseClient::new("1.0.0").expect("client policy must be valid"); + let previous = release( + "semantic_release_same_id", + 'b', + TruthStatus::Authoritative, + PublicationState::Published, + &["control.evidence"], + ); + let conflicting = release( + "semantic_release_same_id", + 'c', + TruthStatus::Authoritative, + PublicationState::Published, + &["control.owner"], + ); + + assert!( + client.diff(&previous, &conflicting).is_err(), + "one stable release id must not be treated as ordinary evolution when its immutable content conflicts" + ); +} + +#[test] +fn supersession_accepts_the_governed_superseded_predecessor_state() { + let client = SemanticReleaseClient::new("1.0.0").expect("client policy must be valid"); + let previous = release( + "semantic_release_previous", + 'b', + TruthStatus::Superseded, + PublicationState::Superseded, + &["control.evidence"], + ); + let successor = release( + "semantic_release_successor", + 'c', + TruthStatus::Authoritative, + PublicationState::Published, + &["control.evidence", "control.owner"], + ); + let declaration = ReleaseSupersession::new( + SemanticReleaseReference::from_release(&previous), + SemanticReleaseReference::from_release(&successor), + "steward-approved immutable correction", + ) + .expect("supersession declaration must be valid"); + + assert_eq!( + client.validate_supersession(&declaration, &previous, &successor), + Ok(()), + "supersession validation must accept the predecessor after Governance marks it Superseded" + ); +} + +#[test] +fn public_contract_and_coverage_gates_encode_the_reviewed_fail_closed_rules() { + let root = repository_root(); + let release_schema = fs::read_to_string(root.join("contracts/semantic-release.schema.json")) + .expect("semantic-release schema must exist"); + let product_workflow = fs::read_to_string(root.join(".github/workflows/product.yml")) + .expect("Product workflow must exist"); + let coverage_gate = fs::read_to_string(root.join("scripts/check_coverage.sh")) + .expect("coverage gate must exist"); + + assert!( + release_schema.contains("\"contract_version\"") + && release_schema.contains("\"const\": \"1.0.0\""), + "the versioned 1.0.0 schema must reject unknown contract_version values" + ); + assert!( + product_workflow.contains("semantic-release-supersession.invalid-self.json") + && product_workflow.contains("validate_semantic_release_supersession"), + "the public contract gate must exercise a language-neutral self-supersession negative fixture through an explicit semantic validator" + ); + assert!( + !coverage_gate.contains("select(.[6] == 0)") + && coverage_gate.contains(".data[0].totals.regions.percent == 100"), + "coverage must retain expansion regions and independently enforce LLVM total region coverage" + ); +} diff --git a/docs/CONTEXT_MAP.md b/docs/CONTEXT_MAP.md index 5ea47792..531338d6 100644 --- a/docs/CONTEXT_MAP.md +++ b/docs/CONTEXT_MAP.md @@ -5,12 +5,17 @@ - Source Observation -> Semantic Discovery: **Customer/Supplier**; Discovery consumes immutable observation contracts. - Semantic Discovery -> Model Validation: **Conformist to published candidate contract**; validation must not rewrite discovery evidence. - Model Validation -> Governance & Publication: **Customer/Supplier**; governance consumes deterministic validation receipts. -- Governance & Publication -> Interoperability: **Published Language**; adapters consume immutable release contracts. +- Governance & Publication -> Client Consumption: **Published Language**; clients consume immutable, versioned semantic-release contracts and never generator-private implementation. +- Governance & Publication -> Interoperability: **Published Language**; export adapters consume immutable release contracts. +- Client Consumption -> Interoperability: **Customer/Supplier** for versioned consumer bindings/adapters only; deterministic admission remains usable without an adapter or LLM. ## External relationships - contextual-orchestrator -> Semantic Discovery: **Anti-Corruption Layer**. Model/provider envelopes never enter the domain model directly. +- contextual-orchestrator -> future Model Alignment/Client Consumption assistance: **Anti-Corruption Layer**. Matching/explanation outputs remain candidate evidence and never grant authority. - LineageWeave -> Source Observation: **Anti-Corruption Layer**. Inferred/proposed lineage remains explicitly non-authoritative until ConceptWeave governance evaluates it. - context-graph-contracts <-> Interoperability: **Shared Kernel only for versioned public contracts**, kept minimal. -- semantic-data-portal <- Interoperability: **Published Language**. SDP consumes releases; ConceptWeave does not read SDP application tables. -- Keyverse -> future delivery layer: **Anti-Corruption Layer** for verified identity/tenant context. +- semantic-data-portal <- Client Consumption/Interoperability: **Published Language**. SDP consumes releases; ConceptWeave does not read SDP application tables. +- governance-risk-compliance <- Client Consumption: **Published Language + downstream ACL**. GRC validates/uses releases while retaining business truth, tenant/purpose authorization, and physical execution. +- enterprise-architecture-core <- Client Consumption: **Published Language + downstream ACL** under the same boundary. +- Keyverse -> future delivery/consumer authorization seams: **Anti-Corruption Layer** for verified identity/tenant context; ConceptWeave does not take ownership of downstream authorization policy. diff --git a/docs/PRD.md b/docs/PRD.md index 0e68c400..05aacb06 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -2,11 +2,11 @@ ## 1. Product statement -ConceptWeave converts heterogeneous enterprise evidence into a governed ontology and semantic layer without collapsing observed facts, model inference, and human-approved meaning into the same truth state. +ConceptWeave converts heterogeneous enterprise evidence into a governed ontology and semantic layer without collapsing observed facts, model inference, and human-approved meaning into the same truth state. It also exposes a stable consumer contract so downstream products can reject incompatible or non-authoritative releases without understanding generation internals. ## 2. Buyer problem -Enterprise teams repeatedly hand-build business glossaries, ontologies, metric definitions, semantic mappings, and data relationships from database schemas, API contracts, documents, and tribal knowledge. The work is slow, inconsistent across tools, difficult to audit, and unsafe to delegate entirely to an LLM because inferred semantics can be plausible but wrong. +Enterprise teams repeatedly hand-build business glossaries, ontologies, metric definitions, semantic mappings, and data relationships from database schemas, API contracts, documents, and tribal knowledge. The work is slow, inconsistent across tools, difficult to audit, and unsafe to delegate entirely to an LLM because inferred semantics can be plausible but wrong. Even after a model is published, consumers need a deterministic way to determine whether a release is compatible, governed, immutable, superseded by an explicit successor, and safe to use. ## 3. Primary buyers and users @@ -18,7 +18,7 @@ Enterprise teams repeatedly hand-build business glossaries, ontologies, metric d ## 4. Core job to be done -Given an enterprise source estate, produce a **reviewable semantic model proposal** in which every concept, relationship, constraint, dimension, measure, and physical mapping is linked to exact evidence and can be validated, rejected, reviewed, published, superseded, and reproduced. +Given an enterprise source estate, produce a **reviewable semantic model proposal** in which every concept, relationship, constraint, dimension, measure, and physical mapping is linked to exact evidence and can be validated, rejected, reviewed, published, superseded, reproduced, and then safely admitted by downstream clients through a stable public contract. ## 5. Functional requirements @@ -32,7 +32,7 @@ Produce candidates for concepts, taxonomies, non-taxonomic relations, semantic c ### FR-3 Evidence and provenance -The current v0.1 candidate contract requires every candidate to retain exact source identity, source digest, and source location through `EvidenceReference`. Issue #2 must add immutable Source Observation and proposal-receipt contracts that also retain observation time, parser/extractor revision, and discovery method before the first Generation release. Until those receipt contracts exist, the Rust `SemanticCandidate` and `contracts/semantic-candidate.schema.json` must not be described as already carrying those deferred coordinates. Unsupported candidates fail closed. +The current v0.1 candidate contract requires every candidate to retain exact source identity, source digest, and source location through `EvidenceReference`. Issue #2 must add immutable Source Observation and proposal-receipt contracts that also retain observation time, parser/extractor revision, and discovery method before the first Generation release. Until those receipt contracts exist on an integrated Generation head, the Rust `SemanticCandidate` and `contracts/semantic-candidate.schema.json` must not be described as already carrying those deferred coordinates. Unsupported candidates fail closed. ### FR-4 Deterministic validation @@ -44,7 +44,7 @@ A candidate cannot become authoritative solely because an LLM or automated extra ### FR-6 Publication -Publish versioned artifacts for ontology and semantic-layer consumers while retaining the exact input snapshot and proposal/review receipts that produced the release. +Publish versioned immutable artifacts for ontology and semantic-layer consumers while retaining the exact input snapshot and proposal/review receipts that produced the release. A correction must create a distinct successor release rather than overwrite a published artifact in place. Supersession authority belongs to Governance & Publication and must produce an explicit predecessor/successor receipt; version ordering or timestamps alone are never replacement evidence. ### FR-7 Interoperability @@ -52,20 +52,37 @@ Support stable adapters for `semantic-data-portal`, `LineageWeave`, `context-gra ### FR-8 LLM assistance -All LLM-backed induction uses `contextual-orchestrator`. Model output is untrusted proposal data and may not skip deterministic validation or review. +All LLM-backed induction uses `contextual-orchestrator`. Model output is untrusted proposal data and may not skip deterministic validation or review. Optional future client matching/explanation also routes through this boundary and cannot silently promote a correspondence to authority. -## 6. First vertical slice +### FR-9 Client consumption -Relational schema snapshot -> observed tables/columns/foreign keys -> concept/relation/dimension/measure/mapping candidates -> evidence-bound validation report -> reviewable proposal package. +A consuming product can inspect a versioned `semantic_release` offline and fail closed before authoritative use. The first Client slice requires stable release identity, contract and ontology versions, truth/publication state, declared SHA-256 digest identity, provenance references, and unique concept identifiers. Admission accepts only the explicit current contract version or an explicitly configured supported-legacy version, plus `Published` and `Authoritative` state. Compatibility is never inferred from version ordering; unknown versions remain unsupported. Consuming products retain their own tenant/purpose authorization and physical data/query execution. + +An admitted client can compare two releases deterministically without contacting a model/provider. Release diff applies the same authoritative-use admission policy to both inputs before returning stable previous/current release identity and sorted added/removed concept identifiers. Diff is semantic-contract evidence only; it does not authorize downstream data access, calculate business measures, mutate either release, or infer consuming-domain impact automatically. + +The digest value object validates canonical `sha256:<64 lowercase hex>` identity syntax. Cryptographic integrity is a separate operation: `SemanticReleaseClient::verify_detached_artifact` first applies authoritative-use admission, then hashes the exact caller-supplied detached immutable semantic-artifact bytes and requires an exact digest match. The release manifest declares the artifact digest; it is not defined as a self-digest of the manifest bytes that carry that field. Syntax validity alone is never integrity evidence. + +A client can also validate an explicit immutable supersession declaration. `SemanticReleaseReference` binds a release id to its exact artifact digest. `ReleaseSupersession` names distinct predecessor/successor references plus a nonblank rationale, rejects self-supersession, and `validate_supersession` requires both releases to pass ordinary authoritative-use admission and both id+digest coordinates to match exactly. This is consumer-side validation only; it does not grant publication authority or infer supersession from version order, time, diff, or semantic similarity. A language-neutral supersession/publication-receipt schema remains required before cross-language client completeness is claimed. + +## 6. First Generation ↔ Client vertical + +`relational schema snapshot -> observed tables/columns/foreign keys -> concept/relation/dimension/measure/mapping candidates -> evidence-bound validation -> steward review -> immutable semantic_release -> offline client admission/diff/integrity/supersession validation -> consuming-product ACL/query boundary`. + +`ContextualWisdomLab/governance-risk-compliance` is the first reference source/client scenario, not a special-case algorithm. A shared golden fixture must exercise both Generation and Client without copying GRC truth into ConceptWeave or giving ConceptWeave direct GRC application-table access. ## 7. Non-goals for v0.1 - replacing `semantic-data-portal` as the enterprise catalog; +- owning downstream tenant/purpose authorization or physical query execution; - arbitrary write access to source systems; - automatic publication without review; - treating vector similarity as semantic truth; - copying every external ontology into one CWL namespace; - building a generic LLM gateway or browser crawler; +- treating digest syntax validation alone as cryptographic integrity evidence; +- inferring backward compatibility merely because one version number is older; +- inferring supersession from version order, timestamps, semantic similarity, or diff size; +- overwriting a published semantic release in place; - claiming an emerging draft semantic-layer format is a stable standard. ## 8. Acceptance criteria for the first commercial candidate @@ -78,4 +95,10 @@ Relational schema snapshot -> observed tables/columns/foreign keys -> concept/re - cross-tenant access denial when tenancy is introduced; - malformed/hostile source contracts rejected with bounded resource use; - semantic-model release can be reproduced from source receipts and approved proposal receipts; -- buyer can inspect why each published artifact exists and which evidence supported it. +- consumer can validate release schema/version/governance state offline before authoritative use; +- current, explicitly supported legacy, and unknown contract versions have deterministic fail-closed compatibility outcomes; +- consumer can deterministically diff admitted releases without provider access or bypassing release admission; +- exact detached artifact digest verification succeeds only for matching bytes; +- corrections preserve the immutable predecessor and identify an explicit distinct successor by exact release id plus digest rather than version-order inference; +- a language-neutral supersession/publication receipt is validated before cross-language release consumption is called complete; +- buyer can inspect why each published artifact exists, which evidence supported it, and why/when it was explicitly superseded. diff --git a/docs/TRD.md b/docs/TRD.md index ad5e1415..5b22b931 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -10,15 +10,16 @@ ConceptWeave starts as a Rust-first modular monolith with explicit bounded conte 2. **Semantic Discovery** — evidence-bound candidate generation. 3. **Model Validation** — deterministic structural, ontology, constraint, and semantic-model validation. 4. **Governance & Publication** — review decisions, immutable releases, supersession. -5. **Interoperability** — import/export adapters and CWL anti-corruption layers. +5. **Client Consumption** — release admission, compatibility, diff, exact resolution, detached-artifact integrity and supersession validation; later match/align/explain/query-plan contracts. +6. **Interoperability** — import/export adapters and CWL anti-corruption layers. -The Core Domain is **Semantic Model Engineering**, represented by the discovery-to-publication lifecycle. Identity, LLM routing, outbound web access, observability, and catalog consumption are external/generic responsibilities. +The Core Domain is **Semantic Model Engineering**, represented by the discovery-to-publication lifecycle. Client Consumption is a supporting subdomain that protects downstream consumers from incompatible or non-governed releases. Identity, LLM routing, outbound web access, observability, catalog/search and consuming-product authorization are external/generic responsibilities. ## 3. Dependency direction `domain <- application <- ports/contracts <- adapters <- delivery` -Domain code must not import web frameworks, databases, provider SDKs, LLM SDKs, or another CWL product's internals. +Client Consumption depends only on versioned public release/domain contracts. Domain and client code must not import web frameworks, databases, provider SDKs, LLM SDKs, generator-private adapters, or another CWL product's internals. ## 4. Source observation contract @@ -37,22 +38,42 @@ Every observed source will eventually carry at least: The initial Rust and JSON contracts cover candidate kind, truth status, publication state, and source evidence. Later revisions add ontology IRIs, language-tagged labels, relation endpoints, cardinality, units, measure expressions, physical mappings, confidence/evaluation receipts, and temporal validity without breaking v0.1 consumers. -## 6. LLM boundary +## 6. Semantic-release client contract -LLM calls go through `contextual-orchestrator`. The application sends bounded evidence/context and receives structured proposals. LLM output is never a database command, publication decision, validation result, or source-system mutation. Deterministic checks must be able to reject the output without another model call. +The Rust Client Consumption slice and Draft 2020-12 JSON Schema define an immutable consumer-visible contract containing: -## 7. Standards strategy +- `release_id`; +- `contract_version`; +- `ontology_version`; +- truth and publication state; +- declared artifact digest identity; +- one or more provenance references; +- unique stable concept identifiers. + +`SemanticReleaseClient` performs deterministic offline authoritative-use admission. A release is accepted only when its contract version is the explicit current version or one of the caller's explicit supported-legacy versions and its state is both `Published` and `Authoritative`. Compatibility is never inferred from version ordering. Structural construction and client admission do not grant publication authority. + +`ReleaseDigest` validates canonical `sha256:<64 lowercase hex>` digest identity. `SemanticReleaseClient::verify_detached_artifact` then verifies cryptographic integrity of the exact detached immutable semantic-artifact bytes supplied by the caller, after applying the same authoritative-use admission gate. The release manifest declares the detached artifact digest; the digest is not specified as a self-referential hash of the manifest bytes containing that field. + +The current Client slice also provides deterministic release diff, exact concept resolution, and explicit immutable supersession validation. `ReleaseSupersession` binds predecessor and successor release ids to their exact artifact digests and never infers replacement from ordering, timestamps, semantic diff, or similarity. A language-neutral supersession/publication-receipt schema is still required before cross-language completeness is claimed. + +Remaining Issue #3 work includes signature/provenance-chain validation when Governance & Publication stabilizes a signing contract; relation/mapping/dimension/measure resolution; research-backed match/alignment/explanation; semantic query-plan contracts; GRC reference-client fixtures; and generated bindings after the language-neutral seam stabilizes. LLM-assisted client operations are optional and route only through `contextual-orchestrator`; admission, compatibility, diff, exact resolution, integrity and supersession validation remain deterministic and provider-independent. + +## 7. LLM boundary + +LLM calls go through `contextual-orchestrator`. The application sends bounded evidence/context and receives structured proposals. LLM output is never a database command, publication decision, validation result, source-system mutation, client authorization decision, or automatic authoritative alignment. Deterministic checks must be able to reject the output without another model call. + +## 8. Standards strategy Stable publication targets use stable recommendations first: RDF 1.1, OWL 2, SKOS, SHACL 1.0, JSON-LD 1.1, and PROV-O as applicable. RDF 1.2 and SHACL 1.2 are tracked as 2026 drafts/candidate work and are not silently treated as final standards. Apache Ossie (incubating; formerly OSI) is tracked as an emerging semantic-model exchange format for metrics, dimensions, relationships, and datasets. -## 8. Persistence +## 9. Persistence -No durable product database is claimed by the foundation slice. When persistence is introduced it must be PostgreSQL, 3NF by default, use descriptive two-or-more-word `snake_case` objects, preserve business/effective time separately from system-recorded time when facts vary over time, enforce tenant-scoped references, and use explicit migration ownership rather than runtime DDL races. +No durable product database is claimed by the current slices. When persistence is introduced it must be PostgreSQL, 3NF by default, use descriptive two-or-more-word `snake_case` objects, preserve business/effective time separately from system-recorded time when facts vary over time, enforce tenant-scoped references, and use explicit migration ownership rather than runtime DDL races. Published releases are immutable; correction creates a superseding release. Item-level UPSERT behavior must be explicit and idempotency-tested before any mutable pre-publication persistence is introduced. -## 9. Security +## 10. Security -Source artifacts are untrusted input. Adapters must enforce source size/type bounds, parser timeouts, archive/decompression limits, SSRF-safe outbound access where external retrieval exists, and prompt-injection isolation for LLM-assisted extraction. Credentials and raw secrets never become semantic evidence. +Source artifacts and release payloads are untrusted input. Adapters must enforce source size/type bounds, parser timeouts, archive/decompression limits, SSRF-safe outbound access where external retrieval exists, and prompt-injection isolation for LLM-assisted extraction. Credentials and raw secrets never become semantic evidence. Client admission validates governance/compatibility but does not replace consuming-product tenant/purpose authorization. Exact detached artifact integrity must be verified against the declared digest before bytes are trusted as the referenced semantic artifact. -## 10. Evaluation +## 11. Evaluation -Evaluation must separate extraction recall, semantic correctness, structural correctness, ontology consistency, mapping accuracy, measure correctness, and governance outcomes. Model-judge scores may supplement but never replace deterministic golden fixtures and human-reviewed expert cases. +Evaluation must separate extraction recall, semantic correctness, structural correctness, ontology consistency, mapping accuracy, measure correctness, release compatibility/admission correctness, and governance outcomes. Model-judge scores may supplement but never replace deterministic golden fixtures and human-reviewed expert cases. Client matching later uses OAEI-style precision/recall/F1 and candidate-retrieval recall; release admission and integrity use deterministic malformed/version/state/provenance/digest/tamper fixtures. diff --git a/docs/UBIQUITOUS_LANGUAGE.md b/docs/UBIQUITOUS_LANGUAGE.md index 0c1d0c25..c0d03a7d 100644 --- a/docs/UBIQUITOUS_LANGUAGE.md +++ b/docs/UBIQUITOUS_LANGUAGE.md @@ -4,15 +4,20 @@ | --- | --- | | Source Snapshot | Immutable revision of source evidence observed by ConceptWeave. | | Observation | Deterministically extracted fact from a Source Snapshot. | -| Evidence Reference | Stable source identity, digest, and location supporting a candidate. | +| Evidence Reference | Stable source identity, digest, and location supporting a candidate or release. | | Semantic Candidate | Evidence-bound proposal for a concept, relation, constraint, dimension, measure, or physical mapping. | | Semantic Model Proposal | Versioned collection of candidates presented for validation/review. | | Validation Report | Deterministic result describing structural or semantic contract validity; not a review decision. | | Review Decision | Authorized accept/reject decision over validated candidates or a model proposal. | -| Semantic Model Release | Immutable governed publication artifact. | +| Semantic Model Release | Immutable governed publication artifact consumed only through versioned public contracts. | +| Release Contract Version | Explicit version of the client-visible semantic-release schema/compatibility contract. | +| Release Digest | Declared `sha256:<64 hex>` digest identity. Syntax validation alone is not cryptographic verification of artifact bytes. | +| Client Admission | Deterministic decision that a release has a supported contract version and is Published + Authoritative; it does not grant downstream authorization. | +| Client Consumption | Supporting bounded context for release admission, compatibility, and future diff/match/resolve/explain/query-plan contracts. | | Truth Status | Epistemic classification: observed, inferred, proposed, authoritative, superseded, rejected. | | Publication State | Governance workflow state: draft, proposed, validated, reviewed, published, superseded, rejected. | | Physical Mapping | Mapping from a physical schema/API/event element to a semantic concept or field. | | Dimension | Governed categorical or temporal axis used to group/filter analytical facts. | | Measure | Governed calculation with explicit expression, grain, units, null semantics, and evidence. | | Semantic Steward | Authorized reviewer responsible for accepting or rejecting semantic meaning. | +| Consuming Product ACL | Downstream product boundary that retains tenant/purpose authorization and physical data/query execution after ConceptWeave client admission. | diff --git a/docs/UML.md b/docs/UML.md index a9559e7f..4b4f1c96 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -18,7 +18,7 @@ stateDiagram-v2 Superseded --> [*] ``` -## Foundation sequence +## Generation -> publication -> client sequence ```mermaid sequenceDiagram @@ -28,6 +28,8 @@ sequenceDiagram participant Validator participant Steward participant Publisher + participant Client + participant Consumer as Consuming Product ACL Source->>Observation: immutable snapshot Observation->>Discovery: observations + evidence refs @@ -36,5 +38,29 @@ sequenceDiagram Validator->>Steward: validated proposal Steward->>Publisher: reviewed acceptance Publisher-->>Source: no source mutation - Publisher-->>Steward: immutable release receipt + Publisher-->>Client: immutable versioned semantic_release + detached artifact digest + Client->>Client: validate contract version + Published + Authoritative + Client->>Client: verify_detached_artifact(exact bytes) + Client-->>Consumer: admitted public release contract + Consumer->>Consumer: tenant/purpose authorization + physical query planning/execution ``` + +## Client admission and integrity decision + +```mermaid +flowchart TD + R[Semantic release] --> V{Supported contract version?} + V -- no --> X1[Reject: incompatible] + V -- yes --> P{Publication state = Published?} + P -- no --> X2[Reject: not published] + P -- yes --> T{Truth status = Authoritative?} + T -- no --> X3[Reject: not authoritative] + T -- yes --> A[Admit for deterministic client operations] + A --> H[verify_detached_artifact: hash exact detached bytes] + H --> M{Digest equals declared artifact digest?} + M -- no --> X4[Reject: artifact digest mismatch] + M -- yes --> C[Integrity evidence established for supplied artifact bytes] + C --> D[Consuming product performs tenant/purpose authorization] +``` + +Client admission is not publication authority and is not downstream authorization. `ReleaseDigest` validates canonical digest identity syntax; `SemanticReleaseClient::verify_detached_artifact` separately proves whether the exact detached artifact bytes supplied by the caller match that declared identity. diff --git a/docs/adr/0005-semantic-release-client-boundary.md b/docs/adr/0005-semantic-release-client-boundary.md new file mode 100644 index 00000000..ea13ae20 --- /dev/null +++ b/docs/adr/0005-semantic-release-client-boundary.md @@ -0,0 +1,77 @@ +# ADR 0005 — Semantic-release client boundary + +- **Status:** Proposed +- **Date:** 2026-09-02 +- **Decision owners:** ConceptWeave Governance & Publication and Client Consumption bounded contexts +- **Related:** Issue #3, PR #5, `docs/product-technical-gap-baseline.md` + +## Context + +Issue #3 requires downstream CWL products to consume governed ConceptWeave releases without importing generation internals. The foundation separates candidate truth from publication state, but a buyer-facing workflow is incomplete until a consumer can reject incompatible or non-governed releases, verify exact detached artifact bytes, compare releases, and follow an explicit correction/supersession relation without guessing from version order or timestamps. + +The Client contract must remain useful offline. LLM/provider availability, generator prompts, persistence state, and foreign application databases are not prerequisites for deterministic release admission. Client-side validation also must not be confused with publication authority, tenant/purpose authorization, or cryptographic evidence that was not actually verified. + +This ADR remains **Proposed** while PR #5 is Draft and exact-head Product/security/review evidence is incomplete. Code on an unintegrated head is decision evidence, not grounds for premature Accepted status. + +## Decision + +Introduce **Client Consumption** as a Supporting Bounded Context and `conceptweave-client` as its Rust reference implementation. + +The versioned `semantic_release` contract carries stable release identity, explicit contract and ontology/model versions, truth/publication state, canonical declared artifact digest identity, provenance references, and unique stable concept identifiers. The v1 public JSON Schema is bound to `contract_version = 1.0.0`; unknown/future versions cannot validate as v1. `SemanticReleaseClient` admits authoritative use only when the release is explicitly compatible and both `Published` and `Authoritative`. Compatibility is never inferred from semantic-version ordering. + +`ReleaseDigest` accepts only canonical `sha256:<64 lowercase hex>`. `SemanticReleaseClient::verify_detached_artifact` separately hashes the caller-supplied detached immutable artifact bytes and requires an exact digest match after authoritative-use admission. Digest syntax and byte-integrity evidence remain distinct. + +`SemanticReleaseClient::diff` admits both releases through the authoritative-use gate and reports deterministic sorted concept additions/removals. If one stable `release_id` names conflicting immutable release content, diff fails closed instead of representing the conflict as ordinary evolution. Exact concept resolution remains deterministic and performs no fuzzy matching or model call. + +For corrections, `SemanticReleaseReference` binds one release id to its exact artifact digest. `ReleaseSupersession` names a distinct predecessor reference, exact successor reference, and nonblank rationale. `validate_supersession` accepts a predecessor that is still Published+Authoritative while replacement is being governed or one that has already moved to the governed Superseded+Superseded lifecycle state; the successor must pass ordinary Published+Authoritative admission. Both id-and-digest references must match exactly. Supersession is never inferred from version order, timestamp, semantic diff, or ontology similarity, and the prior immutable release is never overwritten. + +Draft 2020-12 JSON Schema cannot express sibling-field inequality. Therefore the public supersession seam includes `semantic-release-supersession.rules.json`, whose `distinct_release_id` rule is machine-readable and language-neutral, plus a deterministic reference validator and negative fixture. Structural schema validation and semantic cross-field validation are both required conformance steps. + +The generation-to-client seam is a versioned public contract. Client code may use public domain value types such as `TruthStatus`, `PublicationState`, and `EvidenceReference`, but may not import generator-private classes, prompts, provider payloads, persistence tables, Source Observation internals, or orchestration state. + +Consuming products retain tenant/purpose authorization, business-domain truth, and physical query execution. ConceptWeave returns governed semantic contracts/query plans; it does not become a foreign product's data plane. Any future LLM-assisted match/explain/ranking operation must use released `contextual-orchestrator`; deterministic admission, compatibility, integrity, supersession, publication-state and authorization checks remain outside model authority. + +## Consequences + +### Positive + +- consumers can fail closed before authoritative use without an LLM provider; +- stable public contracts prevent generator-private implementation leakage; +- truth/publication authority remains explicit across repository boundaries; +- digest syntax and actual byte verification cannot be conflated; +- version admission is explicit and fail-closed; +- corrections preserve immutable predecessor releases and exact successor identity; +- structural JSON conformance and cross-field semantic conformance are explicit rather than pretending JSON Schema can express unsupported invariants. + +### Costs and deferred work + +- signature/provenance-chain verification remains deferred until Governance & Publication defines a stable signing contract; +- typed relation/mapping/dimension/measure resolution, match/align/explain and semantic query-plan operations remain Issue #3 work; +- GRC-shaped reference-client fixtures remain required before buyer-facing integration readiness; +- this ADR cannot advance to Accepted until the stacked implementation is integrated and current-head deterministic/security/review evidence is terminal. + +## Alternatives rejected + +1. **Let consumers import generator internals.** Rejected because it couples downstream products to prompts/adapters/persistence and destroys the reuse boundary. +2. **Require an LLM call to decide release usability.** Rejected because compatibility, governance state, digest verification and explicit supersession are deterministic security/data-integrity controls. +3. **Treat a well-shaped digest string as proof of artifact integrity.** Rejected because syntax validation does not hash bytes. +4. **Infer compatibility or supersession from version ordering/timestamps.** Rejected because neither proves compatibility nor steward-approved replacement. +5. **Overwrite a published release in place when corrected.** Rejected because published semantic truth is immutable; correction creates a distinct successor and explicit supersession evidence. +6. **Pretend structural JSON Schema enforces self-supersession inequality.** Rejected because Draft 2020-12 has no general sibling-field inequality operator; the semantic rule must remain explicit. +7. **Move downstream authorization into ConceptWeave.** Rejected because tenant/purpose authorization belongs to each consuming product and its identity/control plane. + +## Verification evidence on the active branch + +- Predecessor `61776fbf5969ec4f8897f48b7bd410052f83ea9d` recorded a hosted Product RED for the missing public supersession contract. +- Test-only `6cf136b94b76e09c8ec1c15fee809fe5ca791dca` encodes five reviewed fail-closed regressions: immutable release-id conflict, governed Superseded predecessor admission, v1 schema version binding, self-supersession semantic conformance, and LLVM expansion-region/total-region coverage. +- `af1d123c...`, `1ac0758d...`, `e54a0b30...`, `663e52d6...`, and `d6690051...` apply the corresponding minimal schema, coverage, semantic-rule, Rust and edge-coverage repairs. +- `e0ef02ee99375ebef2ce2b815dc5340e45708b24` non-force adopts Foundation #14 as a two-parent merge and preserves the Client-specific public-contract gate while taking the Foundation repository-qualified PR concurrency/CI contract. +- The semantic reference validator was executed locally against its valid and self-supersession fixtures with return codes 0 and 1 respectively. This is focused local evidence, not hosted exact-head GREEN. + +## Follow-up / acceptance for Accepted status + +1. Obtain exact-head fmt/Clippy/tests/rustdoc/100% owned coverage and public structural+semantic contract validation on one unchanged final PR #5 head. +2. Integrate the Foundation prerequisite, retain the non-force ancestry, and rerun every then-required exact-head workflow. +3. Resolve each valid review thread only after the current implementation is verified; satisfy ordinary governance without self-approval or routine bypass. +4. Add provenance/signature verification only behind an explicit versioned Governance & Publication contract. +5. Prove the seam with an anonymized GRC-shaped reference-client fixture and no cross-service application-table access. diff --git a/docs/adr/README.md b/docs/adr/README.md index 291702a3..9b8cff1e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -3,3 +3,4 @@ - [ADR 0001 — Product and bounded-context boundary](0001-product-boundary.md) - [ADR 0002 — Evidence, truth, and publication lifecycle](0002-truth-publication-lifecycle.md) - [ADR 0003 — Standards and LLM engineering boundary](0003-standards-llm-boundary.md) +- [ADR 0005 — Semantic-release client boundary](0005-semantic-release-client-boundary.md) — Proposed diff --git a/scripts/check_coverage.sh b/scripts/check_coverage.sh index 2f691898..8d0c5860 100755 --- a/scripts/check_coverage.sh +++ b/scripts/check_coverage.sh @@ -2,7 +2,7 @@ set -euo pipefail coverage_toolchain="${COVERAGE_TOOLCHAIN:-nightly-2026-08-20}" -trap 'rm -f coverage.json source-branches.json' EXIT +trap 'rm -f coverage.json source-branches.json source-regions.json' EXIT cargo "+${coverage_toolchain}" llvm-cov \ --workspace \ @@ -21,6 +21,49 @@ jq -r ' | "COVERAGE_GAP file=\(.filename) lines=\(.summary.lines.percent) functions=\(.summary.functions.percent) regions=\(.summary.regions.percent)" ' coverage.json +jq ' + [ + .data[0].functions[] + | .filenames as $files + | .regions[] + | select(.[7] == 0 or .[7] == 1) + | { + file: $files[.[5]], + line_start: .[0], + column_start: .[1], + line_end: .[2], + column_end: .[3], + count: .[4] + } + | select(.file | contains("/tests/") | not) + ] + | sort_by(.file, .line_start, .column_start, .line_end, .column_end) + | group_by([.file, .line_start, .column_start, .line_end, .column_end]) + | map({ + file: .[0].file, + line_start: .[0].line_start, + column_start: .[0].column_start, + line_end: .[0].line_end, + column_end: .[0].column_end, + count: (map(.count) | add) + }) +' coverage.json > source-regions.json + +jq ' + { + count: length, + covered: ([.[] | select(.count > 0)] | length), + notcovered: ([.[] | select(.count == 0)] | length) + } + | .percent = (if .count == 0 then 100 else (.covered * 100 / .count) end) +' source-regions.json + +jq -r ' + .[] + | select(.count == 0) + | "REGION_GAP file=\(.file) start=\(.line_start):\(.column_start) end=\(.line_end):\(.column_end)" +' source-regions.json + jq ' [ .data[0].files[] @@ -70,4 +113,5 @@ jq -e ' .data[0].totals.regions.percent == 100 ' coverage.json >/dev/null +jq -e 'all(.[]; .count > 0)' source-regions.json >/dev/null jq -e 'all(.[]; .true_count > 0 and .false_count > 0)' source-branches.json >/dev/null diff --git a/scripts/validate_semantic_release_supersession.py b/scripts/validate_semantic_release_supersession.py new file mode 100644 index 00000000..a730c988 --- /dev/null +++ b/scripts/validate_semantic_release_supersession.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Apply language-neutral semantic rules to a supersession JSON contract.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +RULES_PATH = REPOSITORY_ROOT / "contracts" / "semantic-release-supersession.rules.json" + + +def _pointer(document: Any, pointer: str) -> Any: + value = document + for token in pointer.removeprefix("/").split("/"): + if not token: + continue + token = token.replace("~1", "/").replace("~0", "~") + if not isinstance(value, dict) or token not in value: + raise ValueError(f"missing semantic-rule coordinate: {pointer}") + value = value[token] + return value + + +def validate_semantic_release_supersession(document: Any, rules: Any) -> list[str]: + """Return deterministic semantic-rule violations for one public contract.""" + violations: list[str] = [] + for rule in rules.get("rules", []): + if rule.get("operator") != "not_equal": + violations.append(f"unsupported semantic rule operator: {rule.get('operator')!r}") + continue + left = _pointer(document, rule["left"]) + right = _pointer(document, rule["right"]) + if left == right: + violations.append(rule["id"]) + return violations + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + print("usage: validate_semantic_release_supersession.py CONTRACT.json", file=sys.stderr) + return 2 + + contract_path = Path(argv[1]) + try: + document = json.loads(contract_path.read_text(encoding="utf-8")) + rules = json.loads(RULES_PATH.read_text(encoding="utf-8")) + violations = validate_semantic_release_supersession(document, rules) + except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as error: + print(f"semantic supersession validation failed closed: {error}", file=sys.stderr) + return 2 + + if violations: + for violation in violations: + print(f"semantic supersession violation: {violation}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv))