diff --git a/README.md b/README.md index 0a95a527..2929d8bf 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![Security Audit](https://github.com/safal207/LiminalQAengineer/workflows/Security%20Audit/badge.svg)](https://github.com/safal207/LiminalQAengineer/actions/workflows/security-audit.yml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +Documentation: [English](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/docs/i18n/en/GETTING_STARTED.md) · [简体中文](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/docs/i18n/zh-CN/GETTING_STARTED.md) · [हिन्दी](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/docs/i18n/hi/GETTING_STARTED.md) · [Español](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/docs/i18n/es/GETTING_STARTED.md) · [العربية](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/docs/i18n/ar/GETTING_STARTED.md) + > **From raw test outcomes to actionable quality decisions.** --- @@ -169,6 +171,15 @@ Guidance → Co-Navigation → Inner Council → Reflection - **liminalqa-ingest** — REST API для приёма данных о прогонах - **limctl** — CLI для управления прогонами и генерации отчётов +Файловый адаптер [ContractGraph-QA interoperability v0.1](docs/CGQA_INTEROP.md) +принимает bounded evidence без подмены статусов и экспортирует только +неавторитетные candidates для независимого CGQA replay. + +Общий interop-hub также даёт consumer SDK для TypeScript/JavaScript, Go, +Java/JVM и .NET. Вместе с нативными runners на Python, Rust и Elixir это восемь +языковых экосистем с одним pinned contract и без смешения verdict authority: +[SDK matrix](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/sdks/README.md). + ## 🚀 Быстрый старт ### Docker-развертывание (рекомендуется) @@ -306,6 +317,9 @@ impl TestCase for LoginTest { - `limctl query` — выполнение би-временных запросов - `limctl collect` — сбор артефактов - `limctl init` — инициализация нового проекта +- `limctl import-cgqa` — строгая офлайн-проверка bounded evidence из ContractGraph-QA +- `limctl export-cgqa-candidates` — офлайн-экспорт неавторитетных candidate seeds +- `limctl cgqa-conformance` — 14 канонических взаимных golden/fail-closed vectors на Rust ## 🔗 Связанные проекты @@ -371,6 +385,7 @@ POST /query - **[Grant Evidence Package](docs/GRANT_EVIDENCE.md)** — reviewer-facing evidence matrix, product wedge, limitations, and roadmap - **[MVP-1 Quickstart](docs/MVP1_QUICKSTART.md)** — начало работы за 5 минут - **[Architecture](docs/ARCHITECTURE.md)** — подробная архитектура системы +- **[ContractGraph-QA Interop](docs/CGQA_INTEROP.md)** — взаимный evidence/candidate адаптер - **[Quickstart (Development)](docs/QUICKSTART.md)** — локальная разработка - **[Demo Guide](DEMO_GUIDE.md)** — руководство по демонстрации возможностей diff --git a/conformance/cgqa-liminalqa-v0.1/fixtures/cgqa-liminalqa-evidence-v0.1.json b/conformance/cgqa-liminalqa-v0.1/fixtures/cgqa-liminalqa-evidence-v0.1.json new file mode 100644 index 00000000..26274fe5 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/fixtures/cgqa-liminalqa-evidence-v0.1.json @@ -0,0 +1,112 @@ +{ + "adapter": { + "digest": { + "algorithm": "sha256", + "value": "b9c805a0c37fb2dbe75af45e4649cfd2a9bd5d57e2be0f4402628d0bc7a052eb" + }, + "id": "engagement-fixture-v1.3", + "version": "1.3.0" + }, + "artifacts": [ + { + "artifactId": "engagement-json", + "bytes": 1615, + "mediaType": "application/json", + "sha256": "d9ff43c6cc0232c0fc27548fa174f4f0f3d9997386eac7838304b3f8a0cbdf18" + } + ], + "assessment": { + "continuityVerdict": "not_computed", + "counts": { + "inconclusive": 1, + "not_found_within_bound": 1, + "violated": 1 + }, + "kind": "bounded_invariant_search", + "statusVocabulary": [ + "violated", + "not_found_within_bound", + "inconclusive" + ] + }, + "authority": { + "actionAuthorization": "not_evaluated", + "classification": "evidence_only", + "continuityVerdictOwner": "ltp", + "mayAuthorizeAction": false + }, + "bound": { + "exploredCandidates": 22, + "maxDepth": 4, + "replay": "forge test --match-test test_CaptureMultiInvariantEngagementResult -vvv", + "searchRunId": "local-engagement-search-001" + }, + "causalParents": [ + "manifest-engagement-fixture" + ], + "checks": [ + { + "exploredCandidates": 6, + "findingId": "CGQA-E-001-F01", + "invariantId": "terminal-state-bound", + "notes": "The repository-local fixture reaches the modeled terminal state through the shortest three-step advance path.", + "pathLength": 3, + "severity": "medium", + "status": "violated", + "title": "Terminal phase remains below the modeled violation threshold" + }, + { + "exploredCandidates": 8, + "invariantId": "phase-nonnegative", + "notes": "No negative phase was found within the declared local action corpus and maxDepth=4 bounded model.", + "severity": "low", + "status": "not_found_within_bound", + "title": "Phase never becomes negative" + }, + { + "exploredCandidates": 8, + "invariantId": "budget-sensitive-branch", + "notes": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check.", + "severity": "info", + "status": "inconclusive", + "title": "Budget-sensitive branch remains unresolved" + } + ], + "exportId": "cgqa-liminalqa-8bbf2e1385e7e321788766d4", + "identity": { + "attemptId": "attempt-001", + "operationId": "bounded-search-CGQA-E-001", + "traceId": "trace-CGQA-E-001" + }, + "limitations": [ + "A bounded search result is not proof that no violation exists outside the declared bound.", + "The evidence is bound only to the exact repository commit and subject declared here.", + "No request/outcome continuity verdict was computed; LTP remains the continuity verdict owner.", + "This artifact is evidence only and does not authorize an action." + ], + "producer": { + "name": "contractgraph-qa", + "version": "1.9.0" + }, + "profile": "org.contractgraph-qa.bounded-invariant-evidence.v0.1", + "schema": "org.contractgraph-qa.liminalqa-evidence.v0.1", + "subject": { + "commitSha": "a10862f40e2d4d59c122a61119fbb3c9c1ff6cab", + "contract": "AdapterFixtureMachine", + "network": "local-foundry-engagement", + "repository": "https://github.com/safal207/ContractGraph-QA", + "scopeId": "local-v1.3-engagement-fixture" + }, + "times": { + "observedAt": "2026-09-03T10:01:00Z", + "recordedAt": "2026-09-03T10:02:00Z", + "validAt": "2026-09-03T10:00:00Z" + }, + "verificationDebt": [ + { + "invariantId": "budget-sensitive-branch", + "reason": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check.", + "status": "inconclusive" + } + ] +} diff --git a/conformance/cgqa-liminalqa-v0.1/fixtures/liminalqa-cgqa-candidates-v0.1.json b/conformance/cgqa-liminalqa-v0.1/fixtures/liminalqa-cgqa-candidates-v0.1.json new file mode 100644 index 00000000..725303e7 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/fixtures/liminalqa-cgqa-candidates-v0.1.json @@ -0,0 +1,74 @@ +{ + "schema": "org.liminalqa.cgqa-candidates.v0.1", + "profile": "org.liminalqa.non-authoritative-candidate-seeds.v0.1", + "exportId": "liminal-candidates-2595f78d88288151b948a1da", + "producer": { + "name": "liminalqa", + "version": "0.1.0" + }, + "sourceEvidence": { + "schema": "org.contractgraph-qa.liminalqa-evidence.v0.1", + "exportId": "cgqa-liminalqa-8bbf2e1385e7e321788766d4", + "sha256": "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce" + }, + "subject": { + "repository": "https://github.com/safal207/ContractGraph-QA", + "commitSha": "a10862f40e2d4d59c122a61119fbb3c9c1ff6cab", + "contract": "AdapterFixtureMachine", + "network": "local-foundry-engagement", + "scopeId": "local-v1.3-engagement-fixture" + }, + "identity": { + "traceId": "trace-CGQA-E-001", + "operationId": "liminal-candidate-derivation-001", + "attemptId": "attempt-001" + }, + "derivedAt": "2026-09-03T10:03:00Z", + "authority": { + "classification": "non_authoritative_seed", + "mayAuthorizeAction": false, + "requiresCgqaVerification": true + }, + "candidates": [ + { + "candidateId": "liminal-candidate-7ab5731bafda6ff275addf49", + "invariantId": "terminal-state-bound", + "sourceStatus": "violated", + "kind": "replay_regression", + "priority": "medium", + "reason": "The repository-local fixture reaches the modeled terminal state through the shortest three-step advance path.", + "requiredChecks": [ + "exact_subject", + "independent_cgqa_replay", + "failing_path_integrity" + ] + }, + { + "candidateId": "liminal-candidate-497f6be066795a9547ec2e24", + "invariantId": "budget-sensitive-branch", + "sourceStatus": "inconclusive", + "kind": "verification_debt", + "priority": "low", + "reason": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check.", + "requiredChecks": [ + "exact_subject", + "reviewed_bound_change", + "independent_cgqa_replay" + ] + } + ], + "causalParents": [ + "cgqa-liminalqa-8bbf2e1385e7e321788766d4" + ], + "limitations": [ + "Candidates are hypotheses derived from bounded evidence, not verified findings.", + "ContractGraph-QA must independently replay each candidate against the exact commit.", + "Candidate export cannot authorize an action or compute an LTP continuity verdict." + ], + "verificationDebt": [ + { + "invariantId": "budget-sensitive-branch", + "reason": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check." + } + ] +} diff --git a/conformance/cgqa-liminalqa-v0.1/result.schema.json b/conformance/cgqa-liminalqa-v0.1/result.schema.json new file mode 100644 index 00000000..b03cf437 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/result.schema.json @@ -0,0 +1,345 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/safal207/ContractGraph-QA/main/contractgraph_qa/conformance/liminalqa-v0.1/result.schema.json", + "title": "ContractGraph-QA and LiminalQA interop conformance result v0.1", + "type": "object", + "required": [ + "schema", + "reportId", + "suiteId", + "suiteVersion", + "suiteSha256", + "implementation", + "status", + "counts", + "contractPins", + "results", + "authority", + "claimBoundary" + ], + "properties": { + "schema": { + "const": "org.contractgraph-qa.liminalqa-interop-conformance-result.v0.1" + }, + "reportId": { + "$ref": "#/$defs/safeId" + }, + "suiteId": { + "const": "cgqa-liminalqa-v0.1" + }, + "suiteVersion": { + "const": "0.1.0" + }, + "suiteSha256": { + "$ref": "#/$defs/sha256" + }, + "implementation": { + "type": "object", + "required": ["name", "version", "language"], + "properties": { + "name": { + "$ref": "#/$defs/nonBlank" + }, + "version": { + "$ref": "#/$defs/nonBlank" + }, + "language": { + "$ref": "#/$defs/nonBlank" + } + }, + "additionalProperties": false + }, + "status": { + "enum": ["PASS", "FAIL"] + }, + "counts": { + "type": "object", + "required": ["total", "passed", "failed"], + "properties": { + "total": { + "const": 14 + }, + "passed": { + "type": "integer", + "minimum": 0, + "maximum": 14 + }, + "failed": { + "type": "integer", + "minimum": 0, + "maximum": 14 + } + }, + "additionalProperties": false + }, + "contractPins": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/$defs/contractPin" + }, + "allOf": [ + { + "contains": { + "properties": { + "artifactSchema": { + "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" + } + }, + "required": ["artifactSchema"] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "artifactSchema": { + "const": "org.liminalqa.cgqa-candidates.v0.1" + } + }, + "required": ["artifactSchema"] + }, + "minContains": 1, + "maxContains": 1 + } + ] + }, + "results": { + "type": "array", + "minItems": 14, + "maxItems": 14, + "items": { + "$ref": "#/$defs/caseResult" + } + }, + "authority": { + "type": "object", + "required": ["classification", "mayAuthorizeAction"], + "properties": { + "classification": { + "const": "conformance_evidence_only" + }, + "mayAuthorizeAction": { + "const": false + } + }, + "additionalProperties": false + }, + "claimBoundary": { + "const": "Synthetic conformance verifies adapter behavior only for these pinned fixtures and mutations. It does not verify a production system, prove security or completeness, authorize an action, or replace independent replay against the exact subject." + } + }, + "allOf": [ + { + "if": { + "properties": { + "status": { + "const": "PASS" + } + }, + "required": ["status"] + }, + "then": { + "properties": { + "counts": { + "properties": { + "passed": { + "const": 14 + }, + "failed": { + "const": 0 + } + } + } + } + }, + "else": { + "properties": { + "counts": { + "properties": { + "passed": { + "maximum": 13 + }, + "failed": { + "minimum": 1 + } + } + } + } + } + } + ], + "additionalProperties": false, + "$defs": { + "nonBlank": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "safeId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "commitSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "contractPin": { + "type": "object", + "required": [ + "id", + "artifactSchema", + "artifactProfile", + "ownerRepository", + "producerCommit", + "schemaSha256", + "fixtureSha256" + ], + "properties": { + "id": { + "$ref": "#/$defs/safeId" + }, + "artifactSchema": { + "enum": [ + "org.contractgraph-qa.liminalqa-evidence.v0.1", + "org.liminalqa.cgqa-candidates.v0.1" + ] + }, + "artifactProfile": { + "$ref": "#/$defs/nonBlank" + }, + "ownerRepository": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "producerCommit": { + "$ref": "#/$defs/commitSha" + }, + "schemaSha256": { + "$ref": "#/$defs/sha256" + }, + "fixtureSha256": { + "$ref": "#/$defs/sha256" + } + }, + "allOf": [ + { + "if": { + "properties": { + "artifactSchema": { + "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" + } + }, + "required": ["artifactSchema"] + }, + "then": { + "properties": { + "id": { + "const": "cgqa-evidence" + }, + "artifactProfile": { + "const": "org.contractgraph-qa.bounded-invariant-evidence.v0.1" + }, + "ownerRepository": { + "const": "safal207/ContractGraph-QA" + }, + "producerCommit": { + "const": "bdf7ced074e3a7baf57cf89ac68be9674bd76a02" + }, + "schemaSha256": { + "const": "53b0b4a0b1f4d77de26b8be9dbb90006ea0bd30c5cd3960a2f3e7d44d9664184" + }, + "fixtureSha256": { + "const": "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce" + } + } + }, + "else": { + "properties": { + "id": { + "const": "liminal-candidates" + }, + "artifactProfile": { + "const": "org.liminalqa.non-authoritative-candidate-seeds.v0.1" + }, + "ownerRepository": { + "const": "safal207/LiminalQAengineer" + }, + "producerCommit": { + "const": "db9c85f678aafd6e28487e0679a9fb6c3ebfb0c3" + }, + "schemaSha256": { + "const": "896e32921d41925a976fef5d0ba561a08bd1f2265a08bc9ccf5065a3238a4f60" + }, + "fixtureSha256": { + "const": "60b794934959c30f9957d0e54de83d7760ac38b618b0676603d721daa8ef11d3" + } + } + } + } + ], + "additionalProperties": false + }, + "caseResult": { + "type": "object", + "required": [ + "id", + "contract", + "category", + "status", + "expectedSemantics", + "observedSemantics", + "inputSha256", + "diagnostic", + "sideEffectExecuted" + ], + "properties": { + "id": { + "$ref": "#/$defs/safeId" + }, + "contract": { + "$ref": "#/$defs/safeId" + }, + "category": { + "enum": [ + "golden", + "authority_escalation", + "semantic_mismatch", + "temporal_inversion", + "unknown_field", + "ambiguous_json", + "verification_weakening", + "unsafe_identifier" + ] + }, + "status": { + "enum": ["PASS", "FAIL"] + }, + "expectedSemantics": { + "enum": ["VALID_NON_AUTHORIZING", "INVALID_BLOCKED"] + }, + "observedSemantics": { + "enum": ["VALID_NON_AUTHORIZING", "INVALID_BLOCKED", "UNSAFE_ACCEPTED"] + }, + "inputSha256": { + "$ref": "#/$defs/sha256" + }, + "diagnostic": { + "$ref": "#/$defs/nonBlank" + }, + "sideEffectExecuted": { + "const": false + } + }, + "additionalProperties": false + } + } +} diff --git a/conformance/cgqa-liminalqa-v0.1/schemas/cgqa-liminalqa-evidence-v0.1.schema.json b/conformance/cgqa-liminalqa-v0.1/schemas/cgqa-liminalqa-evidence-v0.1.schema.json new file mode 100644 index 00000000..c0f36b54 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/schemas/cgqa-liminalqa-evidence-v0.1.schema.json @@ -0,0 +1,215 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/safal207/ContractGraph-QA/main/contractgraph_qa/schemas/cgqa-liminalqa-evidence-v0.1.schema.json", + "title": "ContractGraph-QA to LiminalQA bounded evidence v0.1", + "type": "object", + "required": [ + "schema", + "profile", + "exportId", + "producer", + "subject", + "identity", + "times", + "adapter", + "bound", + "assessment", + "checks", + "artifacts", + "causalParents", + "verificationDebt", + "limitations", + "authority" + ], + "properties": { + "schema": { "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" }, + "profile": { "const": "org.contractgraph-qa.bounded-invariant-evidence.v0.1" }, + "exportId": { "$ref": "#/$defs/safeId" }, + "producer": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "const": "contractgraph-qa" }, + "version": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "subject": { "$ref": "#/$defs/subject" }, + "identity": { "$ref": "#/$defs/identity" }, + "times": { + "type": "object", + "required": ["validAt", "observedAt", "recordedAt"], + "properties": { + "validAt": { "type": "string", "format": "date-time" }, + "observedAt": { "type": "string", "format": "date-time" }, + "recordedAt": { "type": "string", "format": "date-time" } + }, + "additionalProperties": false + }, + "adapter": { + "type": "object", + "required": ["id", "version", "digest"], + "properties": { + "id": { "$ref": "#/$defs/nonBlank" }, + "version": { "$ref": "#/$defs/nonBlank" }, + "digest": { + "type": "object", + "required": ["algorithm", "value"], + "properties": { + "algorithm": { "const": "sha256" }, + "value": { "$ref": "#/$defs/sha256" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "bound": { + "type": "object", + "required": ["searchRunId", "maxDepth", "exploredCandidates", "replay"], + "properties": { + "searchRunId": { "$ref": "#/$defs/safeId" }, + "maxDepth": { "type": "integer", "minimum": 1 }, + "exploredCandidates": { "type": "integer", "minimum": 0 }, + "replay": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "assessment": { + "type": "object", + "required": ["kind", "statusVocabulary", "counts", "continuityVerdict"], + "properties": { + "kind": { "const": "bounded_invariant_search" }, + "statusVocabulary": { + "const": ["violated", "not_found_within_bound", "inconclusive"] + }, + "counts": { + "type": "object", + "required": ["violated", "not_found_within_bound", "inconclusive"], + "properties": { + "violated": { "type": "integer", "minimum": 0 }, + "not_found_within_bound": { "type": "integer", "minimum": 0 }, + "inconclusive": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + "continuityVerdict": { "const": "not_computed" } + }, + "additionalProperties": false + }, + "checks": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/check" } + }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["artifactId", "mediaType", "sha256", "bytes"], + "properties": { + "artifactId": { "$ref": "#/$defs/safeId" }, + "mediaType": { "$ref": "#/$defs/nonBlank" }, + "sha256": { "$ref": "#/$defs/sha256" }, + "bytes": { "type": "integer", "minimum": 1 } + }, + "additionalProperties": false + } + }, + "causalParents": { + "type": "array", + "uniqueItems": true, + "items": { "$ref": "#/$defs/safeId" } + }, + "verificationDebt": { + "type": "array", + "items": { + "type": "object", + "required": ["invariantId", "status", "reason"], + "properties": { + "invariantId": { "$ref": "#/$defs/safeId" }, + "status": { "const": "inconclusive" }, + "reason": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + } + }, + "limitations": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/nonBlank" } + }, + "authority": { + "type": "object", + "required": [ + "classification", + "mayAuthorizeAction", + "actionAuthorization", + "continuityVerdictOwner" + ], + "properties": { + "classification": { "const": "evidence_only" }, + "mayAuthorizeAction": { "const": false }, + "actionAuthorization": { "const": "not_evaluated" }, + "continuityVerdictOwner": { "const": "ltp" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "nonBlank": { "type": "string", "minLength": 1, "pattern": ".*\\S.*" }, + "safeId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "subject": { + "type": "object", + "required": ["repository", "commitSha", "contract", "network", "scopeId"], + "properties": { + "repository": { "$ref": "#/$defs/nonBlank" }, + "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "contract": { "$ref": "#/$defs/nonBlank" }, + "network": { "$ref": "#/$defs/nonBlank" }, + "scopeId": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "required": ["traceId", "operationId", "attemptId"], + "properties": { + "traceId": { "$ref": "#/$defs/safeId" }, + "operationId": { "$ref": "#/$defs/safeId" }, + "attemptId": { "$ref": "#/$defs/safeId" } + }, + "additionalProperties": false + }, + "check": { + "type": "object", + "required": ["invariantId", "title", "severity", "status", "exploredCandidates", "notes"], + "properties": { + "invariantId": { "$ref": "#/$defs/safeId" }, + "title": { "$ref": "#/$defs/nonBlank" }, + "severity": { "enum": ["critical", "high", "medium", "low", "info"] }, + "status": { "enum": ["violated", "not_found_within_bound", "inconclusive"] }, + "exploredCandidates": { "type": "integer", "minimum": 0 }, + "notes": { "$ref": "#/$defs/nonBlank" }, + "findingId": { "$ref": "#/$defs/safeId" }, + "pathLength": { "type": "integer", "minimum": 1 } + }, + "allOf": [ + { + "if": { "properties": { "status": { "const": "violated" } }, "required": ["status"] }, + "then": { "required": ["findingId", "pathLength"] }, + "else": { "not": { "anyOf": [{ "required": ["findingId"] }, { "required": ["pathLength"] }] } } + } + ], + "additionalProperties": false + } + } +} diff --git a/conformance/cgqa-liminalqa-v0.1/schemas/liminalqa-cgqa-candidates-v0.1.schema.json b/conformance/cgqa-liminalqa-v0.1/schemas/liminalqa-cgqa-candidates-v0.1.schema.json new file mode 100644 index 00000000..61c4fa97 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/schemas/liminalqa-cgqa-candidates-v0.1.schema.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/safal207/LiminalQAengineer/main/schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json", + "title": "LiminalQA to ContractGraph-QA non-authoritative candidates v0.1", + "type": "object", + "required": [ + "schema", + "profile", + "exportId", + "producer", + "sourceEvidence", + "subject", + "identity", + "derivedAt", + "authority", + "candidates", + "causalParents", + "limitations", + "verificationDebt" + ], + "properties": { + "schema": { "const": "org.liminalqa.cgqa-candidates.v0.1" }, + "profile": { "const": "org.liminalqa.non-authoritative-candidate-seeds.v0.1" }, + "exportId": { "$ref": "#/$defs/safeId" }, + "producer": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "const": "liminalqa" }, + "version": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "sourceEvidence": { + "type": "object", + "required": ["schema", "exportId", "sha256"], + "properties": { + "schema": { "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" }, + "exportId": { "$ref": "#/$defs/safeId" }, + "sha256": { "$ref": "#/$defs/sha256" } + }, + "additionalProperties": false + }, + "subject": { "$ref": "#/$defs/subject" }, + "identity": { "$ref": "#/$defs/identity" }, + "derivedAt": { "type": "string", "format": "date-time" }, + "authority": { + "type": "object", + "required": ["classification", "mayAuthorizeAction", "requiresCgqaVerification"], + "properties": { + "classification": { "const": "non_authoritative_seed" }, + "mayAuthorizeAction": { "const": false }, + "requiresCgqaVerification": { "const": true } + }, + "additionalProperties": false + }, + "candidates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "candidateId", + "invariantId", + "sourceStatus", + "kind", + "priority", + "reason", + "requiredChecks" + ], + "properties": { + "candidateId": { "$ref": "#/$defs/safeId" }, + "invariantId": { "$ref": "#/$defs/safeId" }, + "sourceStatus": { "enum": ["violated", "inconclusive"] }, + "kind": { "enum": ["replay_regression", "verification_debt"] }, + "priority": { "enum": ["critical", "high", "medium", "low"] }, + "reason": { "$ref": "#/$defs/nonBlank" }, + "requiredChecks": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/nonBlank" } + } + }, + "allOf": [ + { + "if": { + "properties": { "sourceStatus": { "const": "violated" } }, + "required": ["sourceStatus"] + }, + "then": { "properties": { "kind": { "const": "replay_regression" } } }, + "else": { "properties": { "kind": { "const": "verification_debt" } } } + } + ], + "additionalProperties": false + } + }, + "causalParents": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/safeId" } + }, + "limitations": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/nonBlank" } + }, + "verificationDebt": { + "type": "array", + "items": { + "type": "object", + "required": ["invariantId", "reason"], + "properties": { + "invariantId": { "$ref": "#/$defs/safeId" }, + "reason": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "$defs": { + "nonBlank": { "type": "string", "minLength": 1, "pattern": ".*\\S.*" }, + "safeId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "subject": { + "type": "object", + "required": ["repository", "commitSha", "contract", "network", "scopeId"], + "properties": { + "repository": { "$ref": "#/$defs/nonBlank" }, + "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "contract": { "$ref": "#/$defs/nonBlank" }, + "network": { "$ref": "#/$defs/nonBlank" }, + "scopeId": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "required": ["traceId", "operationId", "attemptId"], + "properties": { + "traceId": { "$ref": "#/$defs/safeId" }, + "operationId": { "$ref": "#/$defs/safeId" }, + "attemptId": { "$ref": "#/$defs/safeId" } + }, + "additionalProperties": false + } + } +} diff --git a/conformance/cgqa-liminalqa-v0.1/suite.json b/conformance/cgqa-liminalqa-v0.1/suite.json new file mode 100644 index 00000000..14d3cb39 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/suite.json @@ -0,0 +1,216 @@ +{ + "schema": "org.contractgraph-qa.liminalqa-interop-conformance-suite.v0.1", + "suiteId": "cgqa-liminalqa-v0.1", + "version": "0.1.0", + "suiteSchema": { + "path": "suite.schema.json", + "sha256": "34acfc677802683c6c452a728ed533e92803a74d989b397d2d0fe549b1da93f9" + }, + "resultSchema": { + "path": "result.schema.json", + "sha256": "388d0aadbb8d30fb5aee223a89f29884b89a1b3303ac88dae8b21e91ab11b423" + }, + "contracts": [ + { + "id": "cgqa-evidence", + "artifactSchema": "org.contractgraph-qa.liminalqa-evidence.v0.1", + "artifactProfile": "org.contractgraph-qa.bounded-invariant-evidence.v0.1", + "ownerRepository": "safal207/ContractGraph-QA", + "producerCommit": "bdf7ced074e3a7baf57cf89ac68be9674bd76a02", + "schemaPath": "schemas/cgqa-liminalqa-evidence-v0.1.schema.json", + "schemaSha256": "53b0b4a0b1f4d77de26b8be9dbb90006ea0bd30c5cd3960a2f3e7d44d9664184", + "fixturePath": "fixtures/cgqa-liminalqa-evidence-v0.1.json", + "fixtureSha256": "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce" + }, + { + "id": "liminal-candidates", + "artifactSchema": "org.liminalqa.cgqa-candidates.v0.1", + "artifactProfile": "org.liminalqa.non-authoritative-candidate-seeds.v0.1", + "ownerRepository": "safal207/LiminalQAengineer", + "producerCommit": "db9c85f678aafd6e28487e0679a9fb6c3ebfb0c3", + "schemaPath": "schemas/liminalqa-cgqa-candidates-v0.1.schema.json", + "schemaSha256": "896e32921d41925a976fef5d0ba561a08bd1f2265a08bc9ccf5065a3238a4f60", + "fixturePath": "fixtures/liminalqa-cgqa-candidates-v0.1.json", + "fixtureSha256": "60b794934959c30f9957d0e54de83d7760ac38b618b0676603d721daa8ef11d3" + } + ], + "cases": [ + { + "id": "cgqa-evidence-golden", + "contract": "cgqa-evidence", + "category": "golden", + "description": "Accept the pinned CGQA evidence fixture only as non-authorizing evidence.", + "operation": { + "kind": "identity" + }, + "expectedInputSha256": "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce", + "expectedSemantics": "VALID_NON_AUTHORIZING" + }, + { + "id": "cgqa-evidence-authority-escalation", + "contract": "cgqa-evidence", + "category": "authority_escalation", + "description": "Block evidence that claims action authority.", + "operation": { + "kind": "replace", + "pointer": "/authority/mayAuthorizeAction", + "value": true + }, + "expectedInputSha256": "33eb3122738032c3ebc1043f5058bc7a9cc469c6ecff8ad0a602aaa3a80067ce", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "cgqa-evidence-count-mismatch", + "contract": "cgqa-evidence", + "category": "semantic_mismatch", + "description": "Block evidence whose declared finding counts disagree with its checks.", + "operation": { + "kind": "replace", + "pointer": "/assessment/counts/violated", + "value": 0 + }, + "expectedInputSha256": "3f348306ba20fdb780b662ec3aadbdf8d1a805a1d81cb2eb66103824e9f8b95f", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "cgqa-evidence-temporal-inversion", + "contract": "cgqa-evidence", + "category": "temporal_inversion", + "description": "Block evidence whose observation time precedes its valid time.", + "operation": { + "kind": "replace", + "pointer": "/times/observedAt", + "value": "2026-09-03T09:59:00Z" + }, + "expectedInputSha256": "dbc7d64eda4aeb497bf360e10335896b3f9b4316973306e37a8a21134cc85ba8", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "cgqa-evidence-unknown-authority-field", + "contract": "cgqa-evidence", + "category": "unknown_field", + "description": "Block evidence with an undeclared root authorization field.", + "operation": { + "kind": "add", + "pointer": "/authorization", + "value": "ALLOW" + }, + "expectedInputSha256": "49d7eab11be2a4fc5b90776a9822156573ef5753bff25e674629fdb8e742edea", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "cgqa-evidence-unsafe-causal-parent", + "contract": "cgqa-evidence", + "category": "unsafe_identifier", + "description": "Block path-like evidence identifiers that could be reinterpreted as traversal.", + "operation": { + "kind": "replace", + "pointer": "/causalParents/0", + "value": "../unsafe" + }, + "expectedInputSha256": "e50dfe383bbd2577b72dba043bbe129b6370b81ec8226d6fa9ed206bf6bf51af", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "cgqa-evidence-duplicate-schema-key", + "contract": "cgqa-evidence", + "category": "ambiguous_json", + "description": "Block ambiguous JSON containing a duplicate root schema key.", + "operation": { + "kind": "duplicate_root_key", + "key": "schema", + "value": "ambiguous" + }, + "expectedInputSha256": "6ff810788c268a93af16daa7a814cfe84616542951de92be8dbe79aabf3d41c9", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "liminal-candidates-golden", + "contract": "liminal-candidates", + "category": "golden", + "description": "Accept the pinned LiminalQA candidates only as non-authoritative CGQA seeds.", + "operation": { + "kind": "identity" + }, + "expectedInputSha256": "60b794934959c30f9957d0e54de83d7760ac38b618b0676603d721daa8ef11d3", + "expectedSemantics": "VALID_NON_AUTHORIZING" + }, + { + "id": "liminal-candidates-authority-escalation", + "contract": "liminal-candidates", + "category": "authority_escalation", + "description": "Block candidate seeds that claim action authority.", + "operation": { + "kind": "replace", + "pointer": "/authority/mayAuthorizeAction", + "value": true + }, + "expectedInputSha256": "261570efc9e6c13d46686a6f5941ee7d39db620c4603cf30a64e0f0baae3abff", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "liminal-candidates-unknown-authority-field", + "contract": "liminal-candidates", + "category": "unknown_field", + "description": "Block candidates with an undeclared root authorization field.", + "operation": { + "kind": "add", + "pointer": "/authorization", + "value": "ALLOW" + }, + "expectedInputSha256": "1a3841322a8dae89e793f92cedcf341c5fafa4a70e308426d76f674434291941", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "liminal-candidates-missing-independent-replay", + "contract": "liminal-candidates", + "category": "verification_weakening", + "description": "Block a candidate that omits the mandatory independent CGQA replay check.", + "operation": { + "kind": "remove", + "pointer": "/candidates/0/requiredChecks/1" + }, + "expectedInputSha256": "8030649160511f62065f4ba33d703fb4dcbf96bc25480d8fa9c7d4e85d715423", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "liminal-candidates-debt-mismatch", + "contract": "liminal-candidates", + "category": "semantic_mismatch", + "description": "Block candidates whose verification debt omits an inconclusive invariant.", + "operation": { + "kind": "remove", + "pointer": "/verificationDebt/0" + }, + "expectedInputSha256": "172a1567897dc4a78deaf2c9f50bc6634e59d2d671ab23d414ca8b4a089f8185", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "liminal-candidates-unsafe-causal-parent", + "contract": "liminal-candidates", + "category": "unsafe_identifier", + "description": "Block path-like causal identifiers that could be reinterpreted as traversal.", + "operation": { + "kind": "replace", + "pointer": "/causalParents/0", + "value": "../unsafe" + }, + "expectedInputSha256": "61b8e74f52248e50fc90e0765b6cc0449ea5b588cb6aa525d148dcb9ac447960", + "expectedSemantics": "INVALID_BLOCKED" + }, + { + "id": "liminal-candidates-duplicate-schema-key", + "contract": "liminal-candidates", + "category": "ambiguous_json", + "description": "Block ambiguous JSON containing a duplicate root schema key.", + "operation": { + "kind": "duplicate_root_key", + "key": "schema", + "value": "ambiguous" + }, + "expectedInputSha256": "9bf53f54b15a2eb09731c28dfffc5ba39f7c04b0d5fa4d076f300f8107ae2d40", + "expectedSemantics": "INVALID_BLOCKED" + } + ], + "claimBoundary": "Synthetic conformance verifies adapter behavior only for these pinned fixtures and mutations. It does not verify a production system, prove security or completeness, authorize an action, or replace independent replay against the exact subject." +} diff --git a/conformance/cgqa-liminalqa-v0.1/suite.schema.json b/conformance/cgqa-liminalqa-v0.1/suite.schema.json new file mode 100644 index 00000000..6ce65f32 --- /dev/null +++ b/conformance/cgqa-liminalqa-v0.1/suite.schema.json @@ -0,0 +1,330 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/safal207/ContractGraph-QA/main/contractgraph_qa/conformance/liminalqa-v0.1/suite.schema.json", + "title": "ContractGraph-QA and LiminalQA interop conformance suite v0.1", + "type": "object", + "required": [ + "schema", + "suiteId", + "version", + "suiteSchema", + "resultSchema", + "contracts", + "cases", + "claimBoundary" + ], + "properties": { + "schema": { + "const": "org.contractgraph-qa.liminalqa-interop-conformance-suite.v0.1" + }, + "suiteId": { + "$ref": "#/$defs/safeId" + }, + "version": { + "const": "0.1.0" + }, + "suiteSchema": { + "type": "object", + "required": ["path", "sha256"], + "properties": { + "path": { + "const": "suite.schema.json" + }, + "sha256": { + "$ref": "#/$defs/sha256" + } + }, + "additionalProperties": false + }, + "resultSchema": { + "type": "object", + "required": ["path", "sha256"], + "properties": { + "path": { + "const": "result.schema.json" + }, + "sha256": { + "$ref": "#/$defs/sha256" + } + }, + "additionalProperties": false + }, + "contracts": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/$defs/contract" + }, + "allOf": [ + { + "contains": { + "properties": { + "artifactSchema": { + "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" + } + }, + "required": ["artifactSchema"] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "artifactSchema": { + "const": "org.liminalqa.cgqa-candidates.v0.1" + } + }, + "required": ["artifactSchema"] + }, + "minContains": 1, + "maxContains": 1 + } + ] + }, + "cases": { + "type": "array", + "minItems": 14, + "maxItems": 14, + "items": { + "$ref": "#/$defs/case" + } + }, + "claimBoundary": { + "$ref": "#/$defs/nonBlank" + } + }, + "additionalProperties": false, + "$defs": { + "nonBlank": { + "type": "string", + "minLength": 1, + "pattern": ".*\\S.*" + }, + "safeId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "commitSha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "relativeJsonPath": { + "type": "string", + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+\\.json$" + }, + "contract": { + "type": "object", + "required": [ + "id", + "artifactSchema", + "artifactProfile", + "ownerRepository", + "producerCommit", + "schemaPath", + "schemaSha256", + "fixturePath", + "fixtureSha256" + ], + "properties": { + "id": { + "$ref": "#/$defs/safeId" + }, + "artifactSchema": { + "enum": [ + "org.contractgraph-qa.liminalqa-evidence.v0.1", + "org.liminalqa.cgqa-candidates.v0.1" + ] + }, + "artifactProfile": { + "$ref": "#/$defs/nonBlank" + }, + "ownerRepository": { + "type": "string", + "pattern": "^[^/\\s]+/[^/\\s]+$" + }, + "producerCommit": { + "$ref": "#/$defs/commitSha" + }, + "schemaPath": { + "$ref": "#/$defs/relativeJsonPath" + }, + "schemaSha256": { + "$ref": "#/$defs/sha256" + }, + "fixturePath": { + "$ref": "#/$defs/relativeJsonPath" + }, + "fixtureSha256": { + "$ref": "#/$defs/sha256" + } + }, + "allOf": [ + { + "if": { + "properties": { + "artifactSchema": { + "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" + } + }, + "required": ["artifactSchema"] + }, + "then": { + "properties": { + "id": { + "const": "cgqa-evidence" + }, + "artifactProfile": { + "const": "org.contractgraph-qa.bounded-invariant-evidence.v0.1" + }, + "ownerRepository": { + "const": "safal207/ContractGraph-QA" + }, + "producerCommit": { + "const": "bdf7ced074e3a7baf57cf89ac68be9674bd76a02" + }, + "schemaSha256": { + "const": "53b0b4a0b1f4d77de26b8be9dbb90006ea0bd30c5cd3960a2f3e7d44d9664184" + }, + "fixtureSha256": { + "const": "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce" + } + } + }, + "else": { + "properties": { + "id": { + "const": "liminal-candidates" + }, + "artifactProfile": { + "const": "org.liminalqa.non-authoritative-candidate-seeds.v0.1" + }, + "ownerRepository": { + "const": "safal207/LiminalQAengineer" + }, + "producerCommit": { + "const": "db9c85f678aafd6e28487e0679a9fb6c3ebfb0c3" + }, + "schemaSha256": { + "const": "896e32921d41925a976fef5d0ba561a08bd1f2265a08bc9ccf5065a3238a4f60" + }, + "fixtureSha256": { + "const": "60b794934959c30f9957d0e54de83d7760ac38b618b0676603d721daa8ef11d3" + } + } + } + } + ], + "additionalProperties": false + }, + "case": { + "type": "object", + "required": [ + "id", + "contract", + "category", + "description", + "operation", + "expectedInputSha256", + "expectedSemantics" + ], + "properties": { + "id": { + "$ref": "#/$defs/safeId" + }, + "contract": { + "$ref": "#/$defs/safeId" + }, + "category": { + "enum": [ + "golden", + "authority_escalation", + "semantic_mismatch", + "temporal_inversion", + "unknown_field", + "ambiguous_json", + "verification_weakening", + "unsafe_identifier" + ] + }, + "description": { + "$ref": "#/$defs/nonBlank" + }, + "operation": { + "$ref": "#/$defs/operation" + }, + "expectedInputSha256": { + "$ref": "#/$defs/sha256" + }, + "expectedSemantics": { + "enum": ["VALID_NON_AUTHORIZING", "INVALID_BLOCKED"] + } + }, + "additionalProperties": false + }, + "operation": { + "oneOf": [ + { + "type": "object", + "required": ["kind"], + "properties": { + "kind": { + "const": "identity" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["kind", "pointer", "value"], + "properties": { + "kind": { + "enum": ["replace", "add"] + }, + "pointer": { + "type": "string", + "pattern": "^/" + }, + "value": true + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["kind", "pointer"], + "properties": { + "kind": { + "const": "remove" + }, + "pointer": { + "type": "string", + "pattern": "^/" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["kind", "key", "value"], + "properties": { + "kind": { + "const": "duplicate_root_key" + }, + "key": { + "$ref": "#/$defs/nonBlank" + }, + "value": true + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/docs/CGQA_INTEROP.md b/docs/CGQA_INTEROP.md new file mode 100644 index 00000000..457708ac --- /dev/null +++ b/docs/CGQA_INTEROP.md @@ -0,0 +1,90 @@ +# ContractGraph-QA interoperability v0.1 + +LiminalQA can validate a ContractGraph-QA bounded-evidence artifact and derive +candidate seeds for another independent CGQA run. Both commands are local, +file-first operations: they do not open LIMINAL-DB and make no network request. + +## Import evidence + +```bash +cargo run --bin limctl -- import-cgqa \ + --input cgqa-evidence.json \ + --output liminal-import-receipt.json +``` + +The receipt says only that the strict v0.1 profile was accepted as bounded +evidence. It preserves all three producer statuses, including +`not_found_within_bound`; it does not turn that status into `pass`. + +## Export candidates + +```bash +cargo run --bin limctl -- export-cgqa-candidates \ + --input cgqa-evidence.json \ + --output liminal-candidates.json \ + --derived-at 2026-09-03T10:03:00Z \ + --operation-id liminal-candidate-derivation-001 \ + --attempt-id attempt-001 +``` + +Mapping is deliberately asymmetric: + +| CGQA source status | LiminalQA candidate | Meaning | +|---|---|---| +| `violated` | `replay_regression` | Replay and verify the observed failure independently | +| `inconclusive` | `verification_debt` | Review the bound and gather missing evidence | +| `not_found_within_bound` | none | Do not manufacture a passing or failing candidate | + +Every candidate export contains +`classification=non_authoritative_seed`, `mayAuthorizeAction=false`, and +`requiresCgqaVerification=true`. + +## Contracts and conformance + +- CGQA evidence is producer-owned by ContractGraph-QA. +- The consumer pin records the exact producer commit and schema SHA-256 in + [`schemas/interop/cgqa-liminalqa-evidence-v0.1.external-contract.json`](../schemas/interop/cgqa-liminalqa-evidence-v0.1.external-contract.json). +- LiminalQA candidate schema: + [`schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json`](../schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json) +- LiminalQA import receipt schema: + [`schemas/interop/liminalqa-cgqa-import-receipt-v0.1.schema.json`](../schemas/interop/liminalqa-cgqa-import-receipt-v0.1.schema.json) +- Golden CGQA fixture: + [`liminalqa-core/tests/fixtures/cgqa-liminalqa-evidence-v0.1.json`](../liminalqa-core/tests/fixtures/cgqa-liminalqa-evidence-v0.1.json) + +The Rust decoder denies unknown fields and validates exact subject identity, +time ordering, adapter digest, status counts, artifact metadata, verification +debt, and the evidence/authorization boundary. + +## Portable conformance runner + +LiminalQA vendors the canonical v0.1 suite byte-for-byte under +[`conformance/cgqa-liminalqa-v0.1`](../conformance/cgqa-liminalqa-v0.1). +Run all 14 golden and fail-closed vectors through the native Rust adapters: + +```bash +cargo run --bin limctl -- cgqa-conformance +``` + +To check a separately downloaded exact copy, pass its `suite.json`: + +```bash +cargo run --bin limctl -- cgqa-conformance \ + --suite path/to/cgqa-liminalqa-v0.1/suite.json +``` + +The runner rejects suite or asset drift before invoking an adapter. The +canonical `suite.json` SHA-256 is +`562e2f9ae699f001b9ccf1b2b9f6dd30c435d53d668b5fd9a04ca15ca1e4faac`. +Every result keeps `sideEffectExecuted=false`; `PASS` is bounded to the pinned +synthetic fixtures and mutations and is not production or security proof. + +Application integrations can validate this report with the shared +[TypeScript/JavaScript, Go, JVM, and .NET consumer SDKs](https://github.com/safal207/ContractGraph-QA/blob/3ff86db99ecb0eeae7fa4b517ac7c8a157a2441a/sdks/README.md). +Those packages pin the same 14 case digests and remain non-authorizing; the +Rust runner in this repository remains the native LiminalQA implementation. + +## Boundary + +LiminalQA candidates are hypotheses, not findings. They cannot authorize an +action and cannot compute an LTP continuity verdict. ContractGraph-QA remains +the independent oracle for replaying candidate paths against the exact subject. diff --git a/limctl/src/commands/cgqa_conformance_command.rs b/limctl/src/commands/cgqa_conformance_command.rs new file mode 100644 index 00000000..8a8a9245 --- /dev/null +++ b/limctl/src/commands/cgqa_conformance_command.rs @@ -0,0 +1,14 @@ +//! Offline execution of the pinned CGQA/LiminalQA conformance vectors. + +use anyhow::{anyhow, Result}; +use liminalqa_core::cgqa_conformance::run_cgqa_conformance_suite; +use std::path::Path; + +pub fn execute(suite: Option<&Path>) -> Result<()> { + let report = run_cgqa_conformance_suite(suite)?; + println!("{}", serde_json::to_string(&report)?); + if report["status"] != "PASS" { + return Err(anyhow!("one or more CGQA conformance vectors failed")); + } + Ok(()) +} diff --git a/limctl/src/commands/cgqa_interop_command.rs b/limctl/src/commands/cgqa_interop_command.rs new file mode 100644 index 00000000..11fb9fd6 --- /dev/null +++ b/limctl/src/commands/cgqa_interop_command.rs @@ -0,0 +1,127 @@ +//! File-first CGQA interop commands. These paths deliberately do not open LIMINAL-DB. + +use anyhow::{anyhow, Context, Result}; +use liminalqa_core::cgqa_interop::{export_candidates, import_receipt, CgqaEvidenceExport}; +use serde::Serialize; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; + +fn reject_parent_traversal(path: &Path, label: &str) -> Result<()> { + if path + .components() + .any(|component| component == Component::ParentDir) + { + return Err(anyhow!( + "{label} must not contain parent-directory traversal" + )); + } + Ok(()) +} + +fn read_input(path: &Path) -> Result<(PathBuf, Vec)> { + reject_parent_traversal(path, "input")?; + let metadata = fs::symlink_metadata(path) + .with_context(|| format!("failed to inspect input {}", path.display()))?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(anyhow!("input must be a regular non-symlink file")); + } + let canonical = fs::canonicalize(path) + .with_context(|| format!("failed to resolve input {}", path.display()))?; + let bytes = fs::read(&canonical) + .with_context(|| format!("failed to read input {}", canonical.display()))?; + Ok((canonical, bytes)) +} + +fn prepare_output(path: &Path, input: &Path, force: bool) -> Result { + reject_parent_traversal(path, "output")?; + let absolute = if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir()?.join(path) + }; + if absolute == input { + return Err(anyhow!("output must be distinct from input")); + } + if absolute.exists() { + let metadata = fs::symlink_metadata(&absolute)?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(anyhow!("output must be a regular non-symlink file")); + } + if fs::canonicalize(&absolute)? == input { + return Err(anyhow!("output must be distinct from input")); + } + if !force { + return Err(anyhow!( + "output already exists: {}; use --force to replace it", + absolute.display() + )); + } + } + Ok(absolute) +} + +fn write_json(path: &Path, value: &T) -> Result<()> { + let parent = path + .parent() + .ok_or_else(|| anyhow!("output must have a parent directory"))?; + fs::create_dir_all(parent)?; + let temporary = parent.join(format!( + ".{}.{}.tmp", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("liminal-interop"), + std::process::id() + )); + let result = (|| -> Result<()> { + let mut file = OpenOptions::new() + .create_new(true) + .write(true) + .open(&temporary) + .with_context(|| format!("failed to create {}", temporary.display()))?; + let mut payload = serde_json::to_vec_pretty(value)?; + payload.push(b'\n'); + file.write_all(&payload)?; + file.sync_all()?; + fs::rename(&temporary, path) + .with_context(|| format!("failed to publish {}", path.display()))?; + Ok(()) + })(); + if result.is_err() { + let _ = fs::remove_file(&temporary); + } + result +} + +pub fn execute_import(input: &Path, output: &Path, force: bool) -> Result<()> { + let (input_path, source_bytes) = read_input(input)?; + let evidence = CgqaEvidenceExport::from_json(&source_bytes)?; + let output_path = prepare_output(output, &input_path, force)?; + let receipt = import_receipt(&evidence, &source_bytes)?; + write_json(&output_path, &receipt)?; + println!("{}", output_path.display()); + Ok(()) +} + +pub fn execute_candidate_export( + input: &Path, + output: &Path, + derived_at: &str, + operation_id: &str, + attempt_id: &str, + force: bool, +) -> Result<()> { + let (input_path, source_bytes) = read_input(input)?; + let evidence = CgqaEvidenceExport::from_json(&source_bytes)?; + let output_path = prepare_output(output, &input_path, force)?; + let candidates = export_candidates( + &evidence, + &source_bytes, + derived_at, + operation_id, + attempt_id, + )?; + write_json(&output_path, &candidates)?; + println!("{}", output_path.display()); + Ok(()) +} diff --git a/limctl/src/commands/mod.rs b/limctl/src/commands/mod.rs index ff6395c5..7a469d81 100644 --- a/limctl/src/commands/mod.rs +++ b/limctl/src/commands/mod.rs @@ -1,5 +1,7 @@ //! CLI commands +pub mod cgqa_conformance_command; +pub mod cgqa_interop_command; pub mod collect_command; pub mod init_command; pub mod list_runs_command; diff --git a/limctl/src/main.rs b/limctl/src/main.rs index a33238f2..dc97655f 100644 --- a/limctl/src/main.rs +++ b/limctl/src/main.rs @@ -7,6 +7,9 @@ //! limctl query — Query LIMINAL-DB //! limctl list runs — List all runs //! limctl list tests — List tests for a run +//! limctl import-cgqa — Validate bounded CGQA evidence offline +//! limctl export-cgqa-candidates — Derive non-authoritative CGQA seeds offline +//! limctl cgqa-conformance — Run the pinned bidirectional interop suite use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; @@ -87,6 +90,55 @@ enum Commands { #[arg(default_value = ".")] directory: PathBuf, }, + + /// Validate bounded ContractGraph-QA evidence without opening LIMINAL-DB + ImportCgqa { + /// ContractGraph-QA LiminalQA Evidence v0.1 JSON + #[arg(long)] + input: PathBuf, + + /// Destination import receipt JSON + #[arg(long)] + output: PathBuf, + + /// Replace an existing regular output file + #[arg(long)] + force: bool, + }, + + /// Derive non-authoritative candidate seeds from bounded CGQA evidence + ExportCgqaCandidates { + /// ContractGraph-QA LiminalQA Evidence v0.1 JSON + #[arg(long)] + input: PathBuf, + + /// Destination candidate export JSON + #[arg(long)] + output: PathBuf, + + /// Derivation time as RFC 3339 with explicit offset + #[arg(long)] + derived_at: String, + + /// Identifier for this derivation operation + #[arg(long)] + operation_id: String, + + /// Identifier for this derivation attempt + #[arg(long)] + attempt_id: String, + + /// Replace an existing regular output file + #[arg(long)] + force: bool, + }, + + /// Run the pinned CGQA/LiminalQA golden and fail-closed vectors offline + CgqaConformance { + /// Optional path to an exact byte-for-byte copy of suite.json + #[arg(long)] + suite: Option, + }, } #[derive(Subcommand)] @@ -130,6 +182,38 @@ async fn main() -> Result<()> { .finish(); tracing::subscriber::set_global_default(subscriber)?; + // File-only interop must not create or open LIMINAL-DB as an incidental side effect. + match &cli.command { + Commands::ImportCgqa { + input, + output, + force, + } => { + return cgqa_interop_command::execute_import(input, output, *force); + } + Commands::ExportCgqaCandidates { + input, + output, + derived_at, + operation_id, + attempt_id, + force, + } => { + return cgqa_interop_command::execute_candidate_export( + input, + output, + derived_at, + operation_id, + attempt_id, + *force, + ); + } + Commands::CgqaConformance { suite } => { + return cgqa_conformance_command::execute(suite.as_deref()); + } + _ => {} + } + // Open database let db = LiminalDB::open(&cli.db_path) .context(format!("Failed to open database at {:?}", cli.db_path))?; @@ -166,6 +250,11 @@ async fn main() -> Result<()> { Commands::Init { directory } => { init_command::execute(&directory).await?; } + Commands::ImportCgqa { .. } + | Commands::ExportCgqaCandidates { .. } + | Commands::CgqaConformance { .. } => { + unreachable!("file-only interop commands returned before opening LIMINAL-DB") + } } Ok(()) diff --git a/liminalqa-core/src/cgqa_conformance.rs b/liminalqa-core/src/cgqa_conformance.rs new file mode 100644 index 00000000..0820de00 --- /dev/null +++ b/liminalqa-core/src/cgqa_conformance.rs @@ -0,0 +1,755 @@ +//! Native runner for the pinned ContractGraph-QA/LiminalQA conformance suite. +//! +//! The runner validates local bytes only. It does not open LIMINAL-DB, make a +//! network request, execute a candidate, or authorize a target-system action. + +use crate::cgqa_interop::{ + sha256_hex, CgqaCandidateExport, CgqaEvidenceExport, CGQA_EVIDENCE_PROFILE, + CGQA_EVIDENCE_SCHEMA, LIMINAL_CANDIDATE_PROFILE, LIMINAL_CANDIDATE_SCHEMA, +}; +use serde::Deserialize; +use serde_json::{json, Value}; +use std::collections::{BTreeMap, HashSet}; +use std::fs; +use std::path::{Component, Path, PathBuf}; +use thiserror::Error; + +pub const SUITE_SCHEMA: &str = "org.contractgraph-qa.liminalqa-interop-conformance-suite.v0.1"; +pub const RESULT_SCHEMA: &str = "org.contractgraph-qa.liminalqa-interop-conformance-result.v0.1"; +pub const SUITE_ID: &str = "cgqa-liminalqa-v0.1"; +pub const SUITE_VERSION: &str = "0.1.0"; +pub const SUITE_SHA256: &str = "562e2f9ae699f001b9ccf1b2b9f6dd30c435d53d668b5fd9a04ca15ca1e4faac"; +pub const SUITE_SCHEMA_SHA256: &str = + "34acfc677802683c6c452a728ed533e92803a74d989b397d2d0fe549b1da93f9"; +pub const RESULT_SCHEMA_SHA256: &str = + "388d0aadbb8d30fb5aee223a89f29884b89a1b3303ac88dae8b21e91ab11b423"; +pub const VALID_NON_AUTHORIZING: &str = "VALID_NON_AUTHORIZING"; +pub const INVALID_BLOCKED: &str = "INVALID_BLOCKED"; +pub const UNSAFE_ACCEPTED: &str = "UNSAFE_ACCEPTED"; +pub const CLAIM_BOUNDARY: &str = "Synthetic conformance verifies adapter behavior only for these pinned fixtures and mutations. It does not verify a production system, prove security or completeness, authorize an action, or replace independent replay against the exact subject."; + +const CGQA_SCHEMA_SHA256: &str = "53b0b4a0b1f4d77de26b8be9dbb90006ea0bd30c5cd3960a2f3e7d44d9664184"; +const CGQA_FIXTURE_SHA256: &str = + "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce"; +const LIMINAL_SCHEMA_SHA256: &str = + "896e32921d41925a976fef5d0ba561a08bd1f2265a08bc9ccf5065a3238a4f60"; +const LIMINAL_FIXTURE_SHA256: &str = + "60b794934959c30f9957d0e54de83d7760ac38b618b0676603d721daa8ef11d3"; + +#[derive(Debug, Error)] +pub enum CgqaConformanceError { + #[error("invalid CGQA conformance suite: {0}")] + Invalid(String), + #[error("CGQA conformance I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("invalid CGQA conformance JSON: {0}")] + Json(#[from] serde_json::Error), +} + +fn invalid(message: impl Into) -> CgqaConformanceError { + CgqaConformanceError::Invalid(message.into()) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct PinnedPath { + path: String, + sha256: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Contract { + id: String, + artifact_schema: String, + artifact_profile: String, + owner_repository: String, + producer_commit: String, + schema_path: String, + schema_sha256: String, + fixture_path: String, + fixture_sha256: String, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +enum Operation { + Identity, + Replace { pointer: String, value: Value }, + Add { pointer: String, value: Value }, + Remove { pointer: String }, + DuplicateRootKey { key: String, value: Value }, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Case { + id: String, + contract: String, + category: String, + description: String, + operation: Operation, + expected_input_sha256: String, + expected_semantics: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Suite { + schema: String, + suite_id: String, + version: String, + suite_schema: PinnedPath, + result_schema: PinnedPath, + contracts: Vec, + cases: Vec, + claim_boundary: String, +} + +enum AssetSource { + Embedded, + External(PathBuf), +} + +impl AssetSource { + fn read(&self, relative: &str) -> Result, CgqaConformanceError> { + match self { + Self::Embedded => embedded_asset(relative) + .map(|bytes| bytes.to_vec()) + .ok_or_else(|| invalid(format!("unsupported embedded suite asset: {relative}"))), + Self::External(root) => read_external_asset(root, relative), + } + } +} + +struct LoadedSuite { + suite: Suite, + suite_raw: Vec, + fixtures: BTreeMap>, +} + +fn embedded_asset(relative: &str) -> Option<&'static [u8]> { + match relative { + "suite.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/suite.json" + )), + "suite.schema.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/suite.schema.json" + )), + "result.schema.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/result.schema.json" + )), + "schemas/cgqa-liminalqa-evidence-v0.1.schema.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/schemas/cgqa-liminalqa-evidence-v0.1.schema.json" + )), + "schemas/liminalqa-cgqa-candidates-v0.1.schema.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/schemas/liminalqa-cgqa-candidates-v0.1.schema.json" + )), + "fixtures/cgqa-liminalqa-evidence-v0.1.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/fixtures/cgqa-liminalqa-evidence-v0.1.json" + )), + "fixtures/liminalqa-cgqa-candidates-v0.1.json" => Some(include_bytes!( + "../../conformance/cgqa-liminalqa-v0.1/fixtures/liminalqa-cgqa-candidates-v0.1.json" + )), + _ => None, + } +} + +fn safe_relative_path(value: &str) -> Result<&Path, CgqaConformanceError> { + let path = Path::new(value); + if path.is_absolute() + || path.extension().and_then(|extension| extension.to_str()) != Some("json") + || path + .components() + .any(|component| !matches!(component, Component::Normal(_))) + { + return Err(invalid(format!( + "suite asset path must be a traversal-free relative JSON path: {value}" + ))); + } + Ok(path) +} + +fn read_external_asset(root: &Path, relative: &str) -> Result, CgqaConformanceError> { + let relative_path = safe_relative_path(relative)?; + let candidate = root.join(relative_path); + let metadata = fs::symlink_metadata(&candidate)?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(invalid(format!( + "suite asset must be a regular non-symlink file: {relative}" + ))); + } + let resolved = fs::canonicalize(&candidate)?; + if !resolved.starts_with(root) { + return Err(invalid(format!( + "suite asset escapes suite root: {relative}" + ))); + } + Ok(fs::read(resolved)?) +} + +fn load_suite(path: Option<&Path>) -> Result { + let (source, suite_raw) = match path { + None => ( + AssetSource::Embedded, + embedded_asset("suite.json") + .expect("embedded v0.1 suite must exist") + .to_vec(), + ), + Some(path) => { + if path + .components() + .any(|component| component == Component::ParentDir) + { + return Err(invalid("suite path must not contain parent traversal")); + } + let metadata = fs::symlink_metadata(path)?; + if metadata.file_type().is_symlink() || !metadata.is_file() { + return Err(invalid("suite must be a regular non-symlink file")); + } + let resolved = fs::canonicalize(path)?; + let root = resolved + .parent() + .ok_or_else(|| invalid("suite must have a parent directory"))? + .to_path_buf(); + (AssetSource::External(root), fs::read(resolved)?) + } + }; + + if sha256_hex(&suite_raw) != SUITE_SHA256 { + return Err(invalid("suite digest does not match the v0.1 pin")); + } + let suite: Suite = serde_json::from_slice(&suite_raw)?; + validate_suite(&suite)?; + + let suite_schema = source.read(&suite.suite_schema.path)?; + if sha256_hex(&suite_schema) != suite.suite_schema.sha256 { + return Err(invalid("suite schema digest mismatch")); + } + serde_json::from_slice::(&suite_schema)?; + + let result_schema = source.read(&suite.result_schema.path)?; + if sha256_hex(&result_schema) != suite.result_schema.sha256 { + return Err(invalid("result schema digest mismatch")); + } + serde_json::from_slice::(&result_schema)?; + + let mut fixtures = BTreeMap::new(); + for contract in &suite.contracts { + let schema_raw = source.read(&contract.schema_path)?; + if sha256_hex(&schema_raw) != contract.schema_sha256 { + return Err(invalid(format!( + "contract {} schema digest mismatch", + contract.id + ))); + } + serde_json::from_slice::(&schema_raw)?; + + let fixture_raw = source.read(&contract.fixture_path)?; + if sha256_hex(&fixture_raw) != contract.fixture_sha256 { + return Err(invalid(format!( + "contract {} fixture digest mismatch", + contract.id + ))); + } + let fixture: Value = serde_json::from_slice(&fixture_raw)?; + if fixture.get("schema").and_then(Value::as_str) != Some(contract.artifact_schema.as_str()) + || fixture.get("profile").and_then(Value::as_str) + != Some(contract.artifact_profile.as_str()) + { + return Err(invalid(format!( + "contract {} fixture identity mismatch", + contract.id + ))); + } + fixtures.insert(contract.id.clone(), fixture_raw); + } + + Ok(LoadedSuite { + suite, + suite_raw, + fixtures, + }) +} + +fn valid_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn safe_id(value: &str) -> bool { + let mut chars = value.chars(); + let Some(first) = chars.next() else { + return false; + }; + value.len() <= 200 + && first.is_ascii_alphanumeric() + && chars.all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | ':' | '-') + }) +} + +fn validate_suite(suite: &Suite) -> Result<(), CgqaConformanceError> { + if suite.schema != SUITE_SCHEMA + || suite.suite_id != SUITE_ID + || suite.version != SUITE_VERSION + || suite.claim_boundary != CLAIM_BOUNDARY + { + return Err(invalid("suite identity or claim boundary is unsupported")); + } + if suite.suite_schema.path != "suite.schema.json" + || suite.suite_schema.sha256 != SUITE_SCHEMA_SHA256 + || suite.result_schema.path != "result.schema.json" + || suite.result_schema.sha256 != RESULT_SCHEMA_SHA256 + { + return Err(invalid("suite schema pins are unsupported")); + } + if suite.contracts.len() != 2 || suite.cases.len() != 14 { + return Err(invalid( + "v0.1 suite must contain two contracts and 14 cases", + )); + } + + let mut contract_ids = HashSet::new(); + for contract in &suite.contracts { + if !safe_id(&contract.id) + || !valid_sha256(&contract.schema_sha256) + || !valid_sha256(&contract.fixture_sha256) + || contract.producer_commit.len() != 40 + || !contract + .producer_commit + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + || !contract_ids.insert(contract.id.as_str()) + { + return Err(invalid( + "suite contains an invalid or duplicate contract pin", + )); + } + let expected = match contract.id.as_str() { + "cgqa-evidence" => ( + CGQA_EVIDENCE_SCHEMA, + CGQA_EVIDENCE_PROFILE, + "safal207/ContractGraph-QA", + "bdf7ced074e3a7baf57cf89ac68be9674bd76a02", + CGQA_SCHEMA_SHA256, + CGQA_FIXTURE_SHA256, + ), + "liminal-candidates" => ( + LIMINAL_CANDIDATE_SCHEMA, + LIMINAL_CANDIDATE_PROFILE, + "safal207/LiminalQAengineer", + "db9c85f678aafd6e28487e0679a9fb6c3ebfb0c3", + LIMINAL_SCHEMA_SHA256, + LIMINAL_FIXTURE_SHA256, + ), + _ => return Err(invalid("suite contains an unsupported contract")), + }; + if contract.artifact_schema != expected.0 + || contract.artifact_profile != expected.1 + || contract.owner_repository != expected.2 + || contract.producer_commit != expected.3 + || contract.schema_sha256 != expected.4 + || contract.fixture_sha256 != expected.5 + { + return Err(invalid(format!( + "contract {} does not match the v0.1 pin", + contract.id + ))); + } + safe_relative_path(&contract.schema_path)?; + safe_relative_path(&contract.fixture_path)?; + } + + let expected_categories: HashSet<&str> = [ + "golden", + "authority_escalation", + "semantic_mismatch", + "temporal_inversion", + "unknown_field", + "ambiguous_json", + "verification_weakening", + "unsafe_identifier", + ] + .into_iter() + .collect(); + let mut categories = HashSet::new(); + let mut case_ids = HashSet::new(); + let mut coverage: BTreeMap<&str, HashSet<&str>> = BTreeMap::new(); + for case in &suite.cases { + if !safe_id(&case.id) + || !contract_ids.contains(case.contract.as_str()) + || case.description.trim().is_empty() + || !valid_sha256(&case.expected_input_sha256) + || !matches!( + case.expected_semantics.as_str(), + VALID_NON_AUTHORIZING | INVALID_BLOCKED + ) + || !case_ids.insert(case.id.as_str()) + { + return Err(invalid("suite contains an invalid or duplicate case")); + } + categories.insert(case.category.as_str()); + coverage + .entry(case.contract.as_str()) + .or_default() + .insert(case.expected_semantics.as_str()); + } + if categories != expected_categories + || coverage.len() != suite.contracts.len() + || coverage.values().any(|outcomes| { + outcomes.len() != 2 + || !outcomes.contains(VALID_NON_AUTHORIZING) + || !outcomes.contains(INVALID_BLOCKED) + }) + { + return Err(invalid( + "suite cases do not cover every control and expected semantic", + )); + } + Ok(()) +} + +fn pointer_tokens(pointer: &str) -> Result, CgqaConformanceError> { + if !pointer.starts_with('/') { + return Err(invalid("operation pointer must be a JSON Pointer")); + } + pointer[1..] + .split('/') + .map(|token| { + let mut decoded = String::new(); + let mut characters = token.chars(); + while let Some(character) = characters.next() { + if character != '~' { + decoded.push(character); + continue; + } + match characters.next() { + Some('0') => decoded.push('~'), + Some('1') => decoded.push('/'), + _ => return Err(invalid("operation pointer contains an invalid escape")), + } + } + Ok(decoded) + }) + .collect() +} + +fn array_index(token: &str) -> Result { + if token.is_empty() || !token.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(invalid(format!( + "operation list pointer is invalid: {token}" + ))); + } + token + .parse::() + .map_err(|_| invalid(format!("operation list index is invalid: {token}"))) +} + +fn navigate_mut<'a>( + mut current: &'a mut Value, + tokens: &[String], +) -> Result<&'a mut Value, CgqaConformanceError> { + for token in tokens { + current = match current { + Value::Object(object) => object.get_mut(token).ok_or_else(|| { + invalid(format!("operation pointer component is absent: {token}")) + })?, + Value::Array(array) => { + let index = array_index(token)?; + array.get_mut(index).ok_or_else(|| { + invalid(format!("operation list index is out of range: {index}")) + })? + } + _ => return Err(invalid("operation pointer traverses a scalar value")), + }; + } + Ok(current) +} + +fn apply_operation( + base_raw: &[u8], + operation: &Operation, +) -> Result, CgqaConformanceError> { + if matches!(operation, Operation::Identity) { + return Ok(base_raw.to_vec()); + } + + let mut document: Value = serde_json::from_slice(base_raw)?; + if let Operation::DuplicateRootKey { key, value } = operation { + let object = document + .as_object() + .ok_or_else(|| invalid("duplicate_root_key requires an object fixture"))?; + if !object.contains_key(key) { + return Err(invalid(format!( + "duplicate_root_key target does not exist: {key}" + ))); + } + let start = base_raw + .iter() + .position(|byte| !byte.is_ascii_whitespace()) + .ok_or_else(|| invalid("case fixture is empty"))?; + if base_raw[start] != b'{' { + return Err(invalid("duplicate_root_key requires an object fixture")); + } + let mut result = Vec::new(); + result.push(b'{'); + result.extend(serde_json::to_vec(key)?); + result.push(b':'); + result.extend(serde_json::to_vec(value)?); + result.push(b','); + result.extend_from_slice(&base_raw[start + 1..]); + return Ok(result); + } + + let (kind, pointer, replacement) = match operation { + Operation::Replace { pointer, value } => ("replace", pointer, Some(value)), + Operation::Add { pointer, value } => ("add", pointer, Some(value)), + Operation::Remove { pointer } => ("remove", pointer, None), + Operation::Identity | Operation::DuplicateRootKey { .. } => unreachable!(), + }; + let tokens = pointer_tokens(pointer)?; + let (last, parents) = tokens + .split_last() + .ok_or_else(|| invalid("operation pointer must not target the document root"))?; + let container = navigate_mut(&mut document, parents)?; + match container { + Value::Object(object) => match kind { + "add" => { + if object.contains_key(last) { + return Err(invalid(format!("add target already exists: {last}"))); + } + object.insert( + last.clone(), + replacement.expect("add has a replacement value").clone(), + ); + } + "replace" => { + let target = object + .get_mut(last) + .ok_or_else(|| invalid(format!("operation target does not exist: {last}")))?; + *target = replacement + .expect("replace has a replacement value") + .clone(); + } + "remove" => { + if object.remove(last).is_none() { + return Err(invalid(format!("operation target does not exist: {last}"))); + } + } + _ => unreachable!(), + }, + Value::Array(array) => { + let index = array_index(last)?; + match kind { + "add" => { + if index > array.len() { + return Err(invalid(format!( + "operation list index is out of range: {index}" + ))); + } + array.insert( + index, + replacement.expect("add has a replacement value").clone(), + ); + } + "replace" => { + let target = array.get_mut(index).ok_or_else(|| { + invalid(format!("operation list index is out of range: {index}")) + })?; + *target = replacement + .expect("replace has a replacement value") + .clone(); + } + "remove" => { + if index >= array.len() { + return Err(invalid(format!( + "operation list index is out of range: {index}" + ))); + } + array.remove(index); + } + _ => unreachable!(), + } + } + _ => { + return Err(invalid( + "operation pointer parent must be an object or array", + )) + } + } + + let mut canonical = serde_json::to_vec(&document)?; + canonical.push(b'\n'); + Ok(canonical) +} + +fn observe(artifact_schema: &str, raw: &[u8]) -> (String, String) { + if artifact_schema == CGQA_EVIDENCE_SCHEMA { + return match CgqaEvidenceExport::from_json(raw) { + Ok(evidence) if evidence.authority.may_authorize_action => ( + UNSAFE_ACCEPTED.to_string(), + "adapter accepted evidence with action authority".to_string(), + ), + Ok(_) => ( + VALID_NON_AUTHORIZING.to_string(), + "profile accepted without action authority".to_string(), + ), + Err(error) => (INVALID_BLOCKED.to_string(), error.to_string()), + }; + } + if artifact_schema == LIMINAL_CANDIDATE_SCHEMA { + return match CgqaCandidateExport::from_json(raw) { + Ok(candidates) + if candidates.authority.may_authorize_action + || !candidates.authority.requires_cgqa_verification => + { + ( + UNSAFE_ACCEPTED.to_string(), + "adapter accepted candidate authority or weakened verification".to_string(), + ) + } + Ok(_) => ( + VALID_NON_AUTHORIZING.to_string(), + "profile accepted without action authority".to_string(), + ), + Err(error) => (INVALID_BLOCKED.to_string(), error.to_string()), + }; + } + ( + INVALID_BLOCKED.to_string(), + format!("unsupported artifact schema: {artifact_schema}"), + ) +} + +/// Run all pinned vectors through the native Rust adapter. +pub fn run_cgqa_conformance_suite(path: Option<&Path>) -> Result { + let loaded = load_suite(path)?; + let contracts: BTreeMap<&str, &Contract> = loaded + .suite + .contracts + .iter() + .map(|contract| (contract.id.as_str(), contract)) + .collect(); + let mut results = Vec::new(); + let mut passed = 0_u64; + for case in &loaded.suite.cases { + let contract = contracts + .get(case.contract.as_str()) + .ok_or_else(|| invalid(format!("unknown case contract: {}", case.contract)))?; + let fixture = loaded + .fixtures + .get(case.contract.as_str()) + .ok_or_else(|| invalid(format!("missing fixture for contract: {}", case.contract)))?; + let input = apply_operation(fixture, &case.operation)?; + let input_sha256 = sha256_hex(&input); + if input_sha256 != case.expected_input_sha256 { + return Err(invalid(format!( + "case {} mutation digest does not match the v0.1 pin", + case.id + ))); + } + let (observed, diagnostic) = observe(&contract.artifact_schema, &input); + let status = if observed == case.expected_semantics { + passed += 1; + "PASS" + } else { + "FAIL" + }; + results.push(json!({ + "id": case.id, + "contract": case.contract, + "category": case.category, + "status": status, + "expectedSemantics": case.expected_semantics, + "observedSemantics": observed, + "inputSha256": input_sha256, + "diagnostic": diagnostic, + "sideEffectExecuted": false + })); + } + + let total = results.len() as u64; + let contract_pins: Vec = loaded + .suite + .contracts + .iter() + .map(|contract| { + json!({ + "id": contract.id, + "artifactSchema": contract.artifact_schema, + "artifactProfile": contract.artifact_profile, + "ownerRepository": contract.owner_repository, + "producerCommit": contract.producer_commit, + "schemaSha256": contract.schema_sha256, + "fixtureSha256": contract.fixture_sha256 + }) + }) + .collect(); + let mut report = json!({ + "schema": RESULT_SCHEMA, + "suiteId": loaded.suite.suite_id, + "suiteVersion": loaded.suite.version, + "suiteSha256": sha256_hex(&loaded.suite_raw), + "implementation": { + "name": "liminalqa", + "version": env!("CARGO_PKG_VERSION"), + "language": "rust" + }, + "status": if passed == total { "PASS" } else { "FAIL" }, + "counts": {"total": total, "passed": passed, "failed": total - passed}, + "contractPins": contract_pins, + "results": results, + "authority": { + "classification": "conformance_evidence_only", + "mayAuthorizeAction": false + }, + "claimBoundary": loaded.suite.claim_boundary + }); + let report_digest = sha256_hex(&serde_json::to_vec(&report)?); + report.as_object_mut().expect("report is an object").insert( + "reportId".to_string(), + Value::String(format!( + "liminalqa-interop-conformance-{}", + &report_digest[..24] + )), + ); + Ok(report) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn embedded_suite_is_exact_and_all_vectors_pass() { + let report = run_cgqa_conformance_suite(None).unwrap(); + assert_eq!(report["suiteSha256"], SUITE_SHA256); + assert_eq!(report["status"], "PASS"); + assert_eq!(report["counts"]["total"], 14); + assert_eq!(report["counts"]["passed"], 14); + assert_eq!(report["counts"]["failed"], 0); + assert!(report["results"] + .as_array() + .expect("conformance results must be an array") + .iter() + .all(|result| result["sideEffectExecuted"] == false)); + assert_eq!(report["authority"]["mayAuthorizeAction"], false); + } + + #[test] + fn candidate_decoder_rejects_weakened_fresh_verification() { + let fixture = embedded_asset("fixtures/liminalqa-cgqa-candidates-v0.1.json") + .expect("candidate fixture must be embedded"); + let mut candidate: Value = serde_json::from_slice(fixture).unwrap(); + candidate["candidates"][0]["requiredChecks"] + .as_array_mut() + .expect("requiredChecks must be an array") + .retain(|check| check != "independent_cgqa_replay"); + let error = + CgqaCandidateExport::from_json(&serde_json::to_vec(&candidate).unwrap()).unwrap_err(); + assert!(error.to_string().contains("independent_cgqa_replay")); + } +} diff --git a/liminalqa-core/src/cgqa_interop.rs b/liminalqa-core/src/cgqa_interop.rs new file mode 100644 index 00000000..09414600 --- /dev/null +++ b/liminalqa-core/src/cgqa_interop.rs @@ -0,0 +1,1012 @@ +//! Strict ContractGraph-QA interoperability profiles. +//! +//! Imported CGQA records remain bounded evidence. Derived records are only +//! non-authoritative candidate seeds and can neither authorize an action nor +//! stand in for a fresh ContractGraph-QA verification run. + +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::HashSet; +use thiserror::Error; + +pub const CGQA_EVIDENCE_SCHEMA: &str = "org.contractgraph-qa.liminalqa-evidence.v0.1"; +pub const CGQA_EVIDENCE_PROFILE: &str = "org.contractgraph-qa.bounded-invariant-evidence.v0.1"; +pub const CGQA_EVIDENCE_SCHEMA_SHA256: &str = + "53b0b4a0b1f4d77de26b8be9dbb90006ea0bd30c5cd3960a2f3e7d44d9664184"; +pub const LIMINAL_IMPORT_SCHEMA: &str = "org.liminalqa.cgqa-import-receipt.v0.1"; +pub const LIMINAL_IMPORT_PROFILE: &str = "org.liminalqa.bounded-evidence-intake.v0.1"; +pub const LIMINAL_CANDIDATE_SCHEMA: &str = "org.liminalqa.cgqa-candidates.v0.1"; +pub const LIMINAL_CANDIDATE_PROFILE: &str = "org.liminalqa.non-authoritative-candidate-seeds.v0.1"; + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CgqaInteropError { + #[error("invalid CGQA interop profile: {0}")] + Invalid(String), + #[error("invalid CGQA interop JSON: {0}")] + Json(String), +} + +fn invalid(message: impl Into) -> CgqaInteropError { + CgqaInteropError::Invalid(message.into()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct Producer { + pub name: String, + pub version: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Subject { + pub repository: String, + pub commit_sha: String, + pub contract: String, + pub network: String, + pub scope_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Identity { + pub trace_id: String, + pub operation_id: String, + pub attempt_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EvidenceTimes { + pub valid_at: String, + pub observed_at: String, + pub recorded_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct Sha256Digest { + pub algorithm: String, + pub value: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct AdapterIdentity { + pub id: String, + pub version: String, + pub digest: Sha256Digest, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SearchBound { + pub search_run_id: String, + pub max_depth: u64, + pub explored_candidates: u64, + pub replay: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct StatusCounts { + pub violated: u64, + pub not_found_within_bound: u64, + pub inconclusive: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Assessment { + pub kind: String, + pub status_vocabulary: Vec, + pub counts: StatusCounts, + pub continuity_verdict: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct InvariantCheck { + pub invariant_id: String, + pub title: String, + pub severity: String, + pub status: String, + pub explored_candidates: u64, + pub notes: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub finding_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path_length: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EvidenceArtifact { + pub artifact_id: String, + pub media_type: String, + pub sha256: String, + pub bytes: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EvidenceDebt { + pub invariant_id: String, + pub status: String, + pub reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EvidenceAuthority { + pub classification: String, + pub may_authorize_action: bool, + pub action_authorization: String, + pub continuity_verdict_owner: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CgqaEvidenceExport { + pub schema: String, + pub profile: String, + pub export_id: String, + pub producer: Producer, + pub subject: Subject, + pub identity: Identity, + pub times: EvidenceTimes, + pub adapter: AdapterIdentity, + pub bound: SearchBound, + pub assessment: Assessment, + pub checks: Vec, + pub artifacts: Vec, + pub causal_parents: Vec, + pub verification_debt: Vec, + pub limitations: Vec, + pub authority: EvidenceAuthority, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SourceEvidenceRef { + pub schema: String, + pub export_id: String, + pub sha256: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CandidateAuthority { + pub classification: String, + pub may_authorize_action: bool, + pub requires_cgqa_verification: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Candidate { + pub candidate_id: String, + pub invariant_id: String, + pub source_status: String, + pub kind: String, + pub priority: String, + pub reason: String, + pub required_checks: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CandidateDebt { + pub invariant_id: String, + pub reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CgqaCandidateExport { + pub schema: String, + pub profile: String, + pub export_id: String, + pub producer: Producer, + pub source_evidence: SourceEvidenceRef, + pub subject: Subject, + pub identity: Identity, + pub derived_at: String, + pub authority: CandidateAuthority, + pub candidates: Vec, + pub causal_parents: Vec, + pub limitations: Vec, + pub verification_debt: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ImportSource { + pub schema: String, + pub export_id: String, + pub sha256: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CgqaImportReceipt { + pub schema: String, + pub profile: String, + pub receipt_id: String, + pub consumer: Producer, + pub source: ImportSource, + pub subject: Subject, + pub identity: Identity, + pub accepted_as: String, + pub may_authorize_action: bool, + pub status_counts: StatusCounts, + pub limitations: Vec, +} + +pub fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + digest + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::>() + .concat() +} + +fn non_blank(value: &str, field: &str) -> Result<(), CgqaInteropError> { + if value.trim().is_empty() { + return Err(invalid(format!("{field} must be a non-empty string"))); + } + Ok(()) +} + +fn safe_id(value: &str, field: &str) -> Result<(), CgqaInteropError> { + non_blank(value, field)?; + let mut chars = value.chars(); + let first = chars.next().expect("non-blank checked above"); + if value.len() > 200 + || !first.is_ascii_alphanumeric() + || !chars.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | ':' | '-')) + { + return Err(invalid(format!( + "{field} contains unsafe identifier characters" + ))); + } + Ok(()) +} + +fn valid_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn timestamp(value: &str, field: &str) -> Result, CgqaInteropError> { + DateTime::parse_from_rfc3339(value).map_err(|_| { + invalid(format!( + "{field} must be an RFC 3339 timestamp with explicit offset" + )) + }) +} + +fn validate_subject(subject: &Subject) -> Result<(), CgqaInteropError> { + non_blank(&subject.repository, "subject.repository")?; + non_blank(&subject.contract, "subject.contract")?; + non_blank(&subject.network, "subject.network")?; + non_blank(&subject.scope_id, "subject.scopeId")?; + if subject.commit_sha.len() != 40 + || !subject + .commit_sha + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(invalid( + "subject.commitSha must be a full lowercase 40-character commit SHA", + )); + } + Ok(()) +} + +fn validate_identity(identity: &Identity) -> Result<(), CgqaInteropError> { + safe_id(&identity.trace_id, "identity.traceId")?; + safe_id(&identity.operation_id, "identity.operationId")?; + safe_id(&identity.attempt_id, "identity.attemptId") +} + +fn ensure_source_matches( + evidence: &CgqaEvidenceExport, + source_bytes: &[u8], +) -> Result<(), CgqaInteropError> { + let decoded = CgqaEvidenceExport::from_json(source_bytes)?; + if decoded != *evidence { + return Err(invalid( + "source bytes do not encode the validated CGQA evidence", + )); + } + Ok(()) +} + +impl CgqaEvidenceExport { + pub fn from_json(bytes: &[u8]) -> Result { + let profile: Self = serde_json::from_slice(bytes) + .map_err(|error| CgqaInteropError::Json(error.to_string()))?; + profile.validate()?; + Ok(profile) + } + + pub fn validate(&self) -> Result<(), CgqaInteropError> { + if self.schema != CGQA_EVIDENCE_SCHEMA { + return Err(invalid("schema is unsupported")); + } + if self.profile != CGQA_EVIDENCE_PROFILE { + return Err(invalid("profile is unsupported")); + } + safe_id(&self.export_id, "exportId")?; + if self.producer.name != "contractgraph-qa" { + return Err(invalid("producer.name must be contractgraph-qa")); + } + non_blank(&self.producer.version, "producer.version")?; + validate_subject(&self.subject)?; + validate_identity(&self.identity)?; + + let valid_at = timestamp(&self.times.valid_at, "times.validAt")?; + let observed_at = timestamp(&self.times.observed_at, "times.observedAt")?; + let recorded_at = timestamp(&self.times.recorded_at, "times.recordedAt")?; + if !(valid_at <= observed_at && observed_at <= recorded_at) { + return Err(invalid( + "times must satisfy validAt <= observedAt <= recordedAt", + )); + } + + non_blank(&self.adapter.id, "adapter.id")?; + non_blank(&self.adapter.version, "adapter.version")?; + if self.adapter.digest.algorithm != "sha256" || !valid_sha256(&self.adapter.digest.value) { + return Err(invalid("adapter.digest must contain lowercase sha256")); + } + safe_id(&self.bound.search_run_id, "bound.searchRunId")?; + if self.bound.max_depth == 0 { + return Err(invalid("bound.maxDepth must be greater than zero")); + } + non_blank(&self.bound.replay, "bound.replay")?; + + if self.assessment.kind != "bounded_invariant_search" + || self.assessment.continuity_verdict != "not_computed" + { + return Err(invalid( + "assessment must be bounded_invariant_search with continuityVerdict=not_computed", + )); + } + let expected_vocabulary = vec![ + "violated".to_string(), + "not_found_within_bound".to_string(), + "inconclusive".to_string(), + ]; + if self.assessment.status_vocabulary != expected_vocabulary { + return Err(invalid( + "assessment.statusVocabulary must preserve the canonical CGQA statuses", + )); + } + if self.checks.is_empty() { + return Err(invalid("checks must be non-empty")); + } + + let mut seen = HashSet::new(); + let mut violated = 0_u64; + let mut not_found = 0_u64; + let mut inconclusive = 0_u64; + let mut explored = 0_u64; + for (index, check) in self.checks.iter().enumerate() { + safe_id(&check.invariant_id, &format!("checks[{index}].invariantId"))?; + if !seen.insert(&check.invariant_id) { + return Err(invalid(format!( + "duplicate invariant in checks: {}", + check.invariant_id + ))); + } + non_blank(&check.title, &format!("checks[{index}].title"))?; + if !matches!( + check.severity.as_str(), + "critical" | "high" | "medium" | "low" | "info" + ) { + return Err(invalid(format!("checks[{index}].severity is unsupported"))); + } + non_blank(&check.notes, &format!("checks[{index}].notes"))?; + explored = explored + .checked_add(check.explored_candidates) + .ok_or_else(|| invalid("exploredCandidates overflow"))?; + match check.status.as_str() { + "violated" => { + violated += 1; + let finding_id = check.finding_id.as_deref().ok_or_else(|| { + invalid(format!( + "checks[{index}] violated status requires findingId" + )) + })?; + safe_id(finding_id, &format!("checks[{index}].findingId"))?; + if check.path_length.unwrap_or(0) == 0 { + return Err(invalid(format!( + "checks[{index}] violated status requires positive pathLength" + ))); + } + } + "not_found_within_bound" => { + not_found += 1; + if check.finding_id.is_some() || check.path_length.is_some() { + return Err(invalid(format!( + "checks[{index}] not_found_within_bound must not carry finding fields" + ))); + } + } + "inconclusive" => { + inconclusive += 1; + if check.finding_id.is_some() || check.path_length.is_some() { + return Err(invalid(format!( + "checks[{index}] inconclusive must not carry finding fields" + ))); + } + } + _ => return Err(invalid(format!("checks[{index}].status is unsupported"))), + } + } + if self.assessment.counts.violated != violated + || self.assessment.counts.not_found_within_bound != not_found + || self.assessment.counts.inconclusive != inconclusive + { + return Err(invalid("assessment.counts does not match checks")); + } + if self.bound.explored_candidates != explored { + return Err(invalid("bound.exploredCandidates does not match checks")); + } + + if self.artifacts.is_empty() { + return Err(invalid("artifacts must be non-empty")); + } + let mut artifact_ids = HashSet::new(); + for (index, artifact) in self.artifacts.iter().enumerate() { + safe_id( + &artifact.artifact_id, + &format!("artifacts[{index}].artifactId"), + )?; + if !artifact_ids.insert(&artifact.artifact_id) { + return Err(invalid(format!( + "duplicate artifact id: {}", + artifact.artifact_id + ))); + } + non_blank( + &artifact.media_type, + &format!("artifacts[{index}].mediaType"), + )?; + if !valid_sha256(&artifact.sha256) || artifact.bytes == 0 { + return Err(invalid(format!( + "artifacts[{index}] must contain lowercase sha256 and positive bytes" + ))); + } + } + let mut parents = HashSet::new(); + for (index, parent) in self.causal_parents.iter().enumerate() { + safe_id(parent, &format!("causalParents[{index}]"))?; + if !parents.insert(parent) { + return Err(invalid("causalParents contains duplicates")); + } + } + + let expected_debt: HashSet<&str> = self + .checks + .iter() + .filter(|check| check.status == "inconclusive") + .map(|check| check.invariant_id.as_str()) + .collect(); + let mut actual_debt = HashSet::new(); + for (index, debt) in self.verification_debt.iter().enumerate() { + safe_id( + &debt.invariant_id, + &format!("verificationDebt[{index}].invariantId"), + )?; + if debt.status != "inconclusive" { + return Err(invalid(format!( + "verificationDebt[{index}].status must be inconclusive" + ))); + } + non_blank(&debt.reason, &format!("verificationDebt[{index}].reason"))?; + if !actual_debt.insert(debt.invariant_id.as_str()) { + return Err(invalid("verificationDebt contains duplicate invariants")); + } + } + if expected_debt != actual_debt { + return Err(invalid( + "verificationDebt must enumerate every and only inconclusive check", + )); + } + if self.limitations.is_empty() + || self + .limitations + .iter() + .any(|limitation| limitation.trim().is_empty()) + { + return Err(invalid("limitations must contain non-empty entries")); + } + if self.authority.classification != "evidence_only" + || self.authority.may_authorize_action + || self.authority.action_authorization != "not_evaluated" + || self.authority.continuity_verdict_owner != "ltp" + { + return Err(invalid( + "authority must remain evidence_only, non-authorizing, and LTP-owned for continuity", + )); + } + Ok(()) + } +} + +impl CgqaCandidateExport { + /// Decode and validate a LiminalQA candidate export at the trust boundary. + pub fn from_json(bytes: &[u8]) -> Result { + let profile: Self = serde_json::from_slice(bytes) + .map_err(|error| CgqaInteropError::Json(error.to_string()))?; + profile.validate()?; + Ok(profile) + } + + /// Validate the non-authorizing candidate contract without executing a candidate. + pub fn validate(&self) -> Result<(), CgqaInteropError> { + if self.schema != LIMINAL_CANDIDATE_SCHEMA { + return Err(invalid("candidate schema is unsupported")); + } + if self.profile != LIMINAL_CANDIDATE_PROFILE { + return Err(invalid("candidate profile is unsupported")); + } + safe_id(&self.export_id, "exportId")?; + if self.producer.name != "liminalqa" { + return Err(invalid("producer.name must be liminalqa")); + } + non_blank(&self.producer.version, "producer.version")?; + + if self.source_evidence.schema != CGQA_EVIDENCE_SCHEMA { + return Err(invalid("sourceEvidence.schema is unsupported")); + } + safe_id(&self.source_evidence.export_id, "sourceEvidence.exportId")?; + if !valid_sha256(&self.source_evidence.sha256) { + return Err(invalid("sourceEvidence.sha256 must be lowercase sha256")); + } + validate_subject(&self.subject)?; + validate_identity(&self.identity)?; + timestamp(&self.derived_at, "derivedAt")?; + + if self.authority.classification != "non_authoritative_seed" + || self.authority.may_authorize_action + || !self.authority.requires_cgqa_verification + { + return Err(invalid( + "candidate authority must remain non_authoritative_seed, non-authorizing, and require CGQA verification", + )); + } + + let mut candidate_ids = HashSet::new(); + let mut invariant_ids = HashSet::new(); + let mut expected_debt = HashSet::new(); + for (index, candidate) in self.candidates.iter().enumerate() { + safe_id( + &candidate.candidate_id, + &format!("candidates[{index}].candidateId"), + )?; + if !candidate_ids.insert(candidate.candidate_id.as_str()) { + return Err(invalid("candidates contains duplicate candidate ids")); + } + safe_id( + &candidate.invariant_id, + &format!("candidates[{index}].invariantId"), + )?; + if !invariant_ids.insert(candidate.invariant_id.as_str()) { + return Err(invalid("candidates contains duplicate invariants")); + } + non_blank(&candidate.reason, &format!("candidates[{index}].reason"))?; + if !matches!( + candidate.priority.as_str(), + "critical" | "high" | "medium" | "low" + ) { + return Err(invalid(format!( + "candidates[{index}].priority is unsupported" + ))); + } + + let expected_kind = match candidate.source_status.as_str() { + "violated" => "replay_regression", + "inconclusive" => { + expected_debt.insert(candidate.invariant_id.as_str()); + "verification_debt" + } + _ => { + return Err(invalid(format!( + "candidates[{index}].sourceStatus is unsupported" + ))) + } + }; + if candidate.kind != expected_kind { + return Err(invalid(format!( + "candidates[{index}].kind does not match sourceStatus" + ))); + } + + let mut required_checks = HashSet::new(); + for (check_index, check) in candidate.required_checks.iter().enumerate() { + non_blank( + check, + &format!("candidates[{index}].requiredChecks[{check_index}]"), + )?; + if !required_checks.insert(check.as_str()) { + return Err(invalid(format!( + "candidates[{index}].requiredChecks contains duplicates" + ))); + } + } + if !required_checks.contains("exact_subject") + || !required_checks.contains("independent_cgqa_replay") + { + return Err(invalid(format!( + "candidates[{index}] must require exact_subject and independent_cgqa_replay" + ))); + } + let status_specific_check = if candidate.source_status == "violated" { + "failing_path_integrity" + } else { + "reviewed_bound_change" + }; + if !required_checks.contains(status_specific_check) { + return Err(invalid(format!( + "candidates[{index}] is missing its status-specific verification check" + ))); + } + } + + let mut parents = HashSet::new(); + for (index, parent) in self.causal_parents.iter().enumerate() { + safe_id(parent, &format!("causalParents[{index}]"))?; + if !parents.insert(parent.as_str()) { + return Err(invalid("causalParents contains duplicates")); + } + } + if parents.is_empty() || !parents.contains(self.source_evidence.export_id.as_str()) { + return Err(invalid( + "causalParents must contain sourceEvidence.exportId", + )); + } + + if self.limitations.is_empty() + || self + .limitations + .iter() + .any(|limitation| limitation.trim().is_empty()) + { + return Err(invalid("limitations must contain non-empty entries")); + } + + let mut actual_debt = HashSet::new(); + for (index, debt) in self.verification_debt.iter().enumerate() { + safe_id( + &debt.invariant_id, + &format!("verificationDebt[{index}].invariantId"), + )?; + non_blank(&debt.reason, &format!("verificationDebt[{index}].reason"))?; + if !actual_debt.insert(debt.invariant_id.as_str()) { + return Err(invalid("verificationDebt contains duplicate invariants")); + } + } + if expected_debt != actual_debt { + return Err(invalid( + "verificationDebt must enumerate every and only inconclusive candidate", + )); + } + Ok(()) + } +} + +pub fn import_receipt( + evidence: &CgqaEvidenceExport, + source_bytes: &[u8], +) -> Result { + evidence.validate()?; + ensure_source_matches(evidence, source_bytes)?; + let source_sha = sha256_hex(source_bytes); + Ok(CgqaImportReceipt { + schema: LIMINAL_IMPORT_SCHEMA.to_string(), + profile: LIMINAL_IMPORT_PROFILE.to_string(), + receipt_id: format!("liminal-cgqa-import-{}", &source_sha[..24]), + consumer: Producer { + name: "liminalqa".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + source: ImportSource { + schema: evidence.schema.clone(), + export_id: evidence.export_id.clone(), + sha256: source_sha, + }, + subject: evidence.subject.clone(), + identity: evidence.identity.clone(), + accepted_as: "bounded_evidence".to_string(), + may_authorize_action: false, + status_counts: evidence.assessment.counts.clone(), + limitations: vec![ + "Import validates structure and semantic boundaries; it does not prove the underlying claim." + .to_string(), + "The receipt is not an action authorization or an LTP continuity verdict.".to_string(), + ], + }) +} + +fn candidate_priority(severity: &str) -> String { + match severity { + "critical" => "critical", + "high" => "high", + "medium" => "medium", + _ => "low", + } + .to_string() +} + +pub fn export_candidates( + evidence: &CgqaEvidenceExport, + source_bytes: &[u8], + derived_at: &str, + operation_id: &str, + attempt_id: &str, +) -> Result { + evidence.validate()?; + ensure_source_matches(evidence, source_bytes)?; + let derived = timestamp(derived_at, "derivedAt")?; + let recorded = timestamp(&evidence.times.recorded_at, "times.recordedAt")?; + if derived < recorded { + return Err(invalid( + "derivedAt must not precede source times.recordedAt", + )); + } + safe_id(operation_id, "identity.operationId")?; + safe_id(attempt_id, "identity.attemptId")?; + + let source_sha = sha256_hex(source_bytes); + let mut candidates = Vec::new(); + let mut debt = Vec::new(); + for check in &evidence.checks { + let (kind, required_checks) = match check.status.as_str() { + "violated" => ( + "replay_regression", + vec![ + "exact_subject".to_string(), + "independent_cgqa_replay".to_string(), + "failing_path_integrity".to_string(), + ], + ), + "inconclusive" => { + debt.push(CandidateDebt { + invariant_id: check.invariant_id.clone(), + reason: check.notes.clone(), + }); + ( + "verification_debt", + vec![ + "exact_subject".to_string(), + "reviewed_bound_change".to_string(), + "independent_cgqa_replay".to_string(), + ], + ) + } + "not_found_within_bound" => continue, + _ => unreachable!("validated evidence contains only canonical statuses"), + }; + let seed = format!( + "{}:{}:{}:{}", + evidence.export_id, check.invariant_id, check.status, kind + ); + candidates.push(Candidate { + candidate_id: format!("liminal-candidate-{}", &sha256_hex(seed.as_bytes())[..24]), + invariant_id: check.invariant_id.clone(), + source_status: check.status.clone(), + kind: kind.to_string(), + priority: candidate_priority(&check.severity), + reason: check.notes.clone(), + required_checks, + }); + } + + let export_seed = format!( + "{}:{}:{}:{}:{}", + evidence.export_id, source_sha, derived_at, operation_id, attempt_id + ); + Ok(CgqaCandidateExport { + schema: LIMINAL_CANDIDATE_SCHEMA.to_string(), + profile: LIMINAL_CANDIDATE_PROFILE.to_string(), + export_id: format!( + "liminal-candidates-{}", + &sha256_hex(export_seed.as_bytes())[..24] + ), + producer: Producer { + name: "liminalqa".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + source_evidence: SourceEvidenceRef { + schema: evidence.schema.clone(), + export_id: evidence.export_id.clone(), + sha256: source_sha, + }, + subject: evidence.subject.clone(), + identity: Identity { + trace_id: evidence.identity.trace_id.clone(), + operation_id: operation_id.to_string(), + attempt_id: attempt_id.to_string(), + }, + derived_at: derived_at.to_string(), + authority: CandidateAuthority { + classification: "non_authoritative_seed".to_string(), + may_authorize_action: false, + requires_cgqa_verification: true, + }, + candidates, + causal_parents: vec![evidence.export_id.clone()], + limitations: vec![ + "Candidates are hypotheses derived from bounded evidence, not verified findings." + .to_string(), + "ContractGraph-QA must independently replay each candidate against the exact commit." + .to_string(), + "Candidate export cannot authorize an action or compute an LTP continuity verdict." + .to_string(), + ], + verification_debt: debt, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture_bytes() -> &'static [u8] { + include_bytes!("../tests/fixtures/cgqa-liminalqa-evidence-v0.1.json") + } + + #[test] + fn imports_all_three_statuses_without_collapsing_not_found_to_pass() { + let evidence = CgqaEvidenceExport::from_json(fixture_bytes()).unwrap(); + assert_eq!( + evidence + .checks + .iter() + .map(|check| check.status.as_str()) + .collect::>(), + vec!["violated", "not_found_within_bound", "inconclusive"] + ); + let receipt = import_receipt(&evidence, fixture_bytes()).unwrap(); + assert_eq!(receipt.accepted_as, "bounded_evidence"); + assert!(!receipt.may_authorize_action); + } + + #[test] + fn rejects_evidence_that_claims_action_authority() { + let mut value: serde_json::Value = serde_json::from_slice(fixture_bytes()).unwrap(); + value["authority"]["mayAuthorizeAction"] = serde_json::Value::Bool(true); + let bytes = serde_json::to_vec(&value).unwrap(); + let error = CgqaEvidenceExport::from_json(&bytes).unwrap_err(); + assert!(error + .to_string() + .contains("authority must remain evidence_only")); + } + + #[test] + fn rejects_duplicate_debt_and_unknown_severity() { + let mut duplicate_debt: serde_json::Value = + serde_json::from_slice(fixture_bytes()).unwrap(); + let duplicate = duplicate_debt["verificationDebt"][0].clone(); + duplicate_debt["verificationDebt"] + .as_array_mut() + .expect("fixture verificationDebt must be an array") + .push(duplicate); + let error = CgqaEvidenceExport::from_json(&serde_json::to_vec(&duplicate_debt).unwrap()) + .unwrap_err(); + assert!(error.to_string().contains("duplicate invariants")); + + let mut unknown_severity: serde_json::Value = + serde_json::from_slice(fixture_bytes()).unwrap(); + unknown_severity["checks"][0]["severity"] = serde_json::Value::String("urgent".to_string()); + let error = CgqaEvidenceExport::from_json(&serde_json::to_vec(&unknown_severity).unwrap()) + .unwrap_err(); + assert!(error.to_string().contains("severity is unsupported")); + } + + #[test] + fn rejects_source_bytes_that_do_not_match_the_validated_evidence() { + let evidence = CgqaEvidenceExport::from_json(fixture_bytes()).unwrap(); + let mut other: serde_json::Value = serde_json::from_slice(fixture_bytes()).unwrap(); + other["exportId"] = serde_json::Value::String("cgqa-liminalqa-other".to_string()); + let other_bytes = serde_json::to_vec(&other).unwrap(); + + let import_error = import_receipt(&evidence, &other_bytes).unwrap_err(); + assert!(import_error + .to_string() + .contains("source bytes do not encode")); + + let export_error = export_candidates( + &evidence, + &other_bytes, + "2026-09-03T10:03:00Z", + "liminal-candidate-derivation-001", + "attempt-001", + ) + .unwrap_err(); + assert!(export_error + .to_string() + .contains("source bytes do not encode")); + } + + #[test] + fn rejects_unknown_fields_fail_closed() { + let mut value: serde_json::Value = serde_json::from_slice(fixture_bytes()).unwrap(); + value["authorization"] = serde_json::Value::String("ALLOW".to_string()); + let bytes = serde_json::to_vec(&value).unwrap(); + assert!(matches!( + CgqaEvidenceExport::from_json(&bytes), + Err(CgqaInteropError::Json(_)) + )); + } + + #[test] + fn candidate_export_is_deterministic_and_skips_not_found_status() { + let evidence = CgqaEvidenceExport::from_json(fixture_bytes()).unwrap(); + let first = export_candidates( + &evidence, + fixture_bytes(), + "2026-09-03T10:03:00Z", + "liminal-candidate-derivation-001", + "attempt-001", + ) + .unwrap(); + let second = export_candidates( + &evidence, + fixture_bytes(), + "2026-09-03T10:03:00Z", + "liminal-candidate-derivation-001", + "attempt-001", + ) + .unwrap(); + assert_eq!(first, second); + assert_eq!(first.candidates.len(), 2); + assert!(first + .candidates + .iter() + .all(|candidate| candidate.source_status != "not_found_within_bound")); + assert!(!first.authority.may_authorize_action); + assert!(first.authority.requires_cgqa_verification); + let expected: CgqaCandidateExport = serde_json::from_slice(include_bytes!( + "../tests/fixtures/liminalqa-cgqa-candidates-v0.1.json" + )) + .unwrap(); + assert_eq!(first, expected); + } + + #[test] + fn candidate_export_rejects_temporal_inversion() { + let evidence = CgqaEvidenceExport::from_json(fixture_bytes()).unwrap(); + let error = export_candidates( + &evidence, + fixture_bytes(), + "2026-09-03T09:59:59Z", + "liminal-candidate-derivation-001", + "attempt-001", + ) + .unwrap_err(); + assert!(error.to_string().contains("derivedAt must not precede")); + } + + #[test] + fn consumer_pin_names_the_exact_producer_schema_and_commit() { + let pin: serde_json::Value = serde_json::from_slice(include_bytes!( + "../../schemas/interop/cgqa-liminalqa-evidence-v0.1.external-contract.json" + )) + .unwrap(); + assert_eq!(pin["producerSchema"], CGQA_EVIDENCE_SCHEMA); + assert_eq!(pin["producerProfile"], CGQA_EVIDENCE_PROFILE); + assert_eq!(pin["schemaSha256"], CGQA_EVIDENCE_SCHEMA_SHA256); + assert_eq!( + pin["producerCommit"], + "bdf7ced074e3a7baf57cf89ac68be9674bd76a02" + ); + } +} diff --git a/liminalqa-core/src/lib.rs b/liminalqa-core/src/lib.rs index d555c0b5..bafd79a2 100644 --- a/liminalqa-core/src/lib.rs +++ b/liminalqa-core/src/lib.rs @@ -7,6 +7,8 @@ pub mod baseline; pub mod causality; +pub mod cgqa_conformance; +pub mod cgqa_interop; pub mod community; pub mod context; pub mod dashboard; diff --git a/liminalqa-core/tests/fixtures/cgqa-liminalqa-evidence-v0.1.json b/liminalqa-core/tests/fixtures/cgqa-liminalqa-evidence-v0.1.json new file mode 100644 index 00000000..26274fe5 --- /dev/null +++ b/liminalqa-core/tests/fixtures/cgqa-liminalqa-evidence-v0.1.json @@ -0,0 +1,112 @@ +{ + "adapter": { + "digest": { + "algorithm": "sha256", + "value": "b9c805a0c37fb2dbe75af45e4649cfd2a9bd5d57e2be0f4402628d0bc7a052eb" + }, + "id": "engagement-fixture-v1.3", + "version": "1.3.0" + }, + "artifacts": [ + { + "artifactId": "engagement-json", + "bytes": 1615, + "mediaType": "application/json", + "sha256": "d9ff43c6cc0232c0fc27548fa174f4f0f3d9997386eac7838304b3f8a0cbdf18" + } + ], + "assessment": { + "continuityVerdict": "not_computed", + "counts": { + "inconclusive": 1, + "not_found_within_bound": 1, + "violated": 1 + }, + "kind": "bounded_invariant_search", + "statusVocabulary": [ + "violated", + "not_found_within_bound", + "inconclusive" + ] + }, + "authority": { + "actionAuthorization": "not_evaluated", + "classification": "evidence_only", + "continuityVerdictOwner": "ltp", + "mayAuthorizeAction": false + }, + "bound": { + "exploredCandidates": 22, + "maxDepth": 4, + "replay": "forge test --match-test test_CaptureMultiInvariantEngagementResult -vvv", + "searchRunId": "local-engagement-search-001" + }, + "causalParents": [ + "manifest-engagement-fixture" + ], + "checks": [ + { + "exploredCandidates": 6, + "findingId": "CGQA-E-001-F01", + "invariantId": "terminal-state-bound", + "notes": "The repository-local fixture reaches the modeled terminal state through the shortest three-step advance path.", + "pathLength": 3, + "severity": "medium", + "status": "violated", + "title": "Terminal phase remains below the modeled violation threshold" + }, + { + "exploredCandidates": 8, + "invariantId": "phase-nonnegative", + "notes": "No negative phase was found within the declared local action corpus and maxDepth=4 bounded model.", + "severity": "low", + "status": "not_found_within_bound", + "title": "Phase never becomes negative" + }, + { + "exploredCandidates": 8, + "invariantId": "budget-sensitive-branch", + "notes": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check.", + "severity": "info", + "status": "inconclusive", + "title": "Budget-sensitive branch remains unresolved" + } + ], + "exportId": "cgqa-liminalqa-8bbf2e1385e7e321788766d4", + "identity": { + "attemptId": "attempt-001", + "operationId": "bounded-search-CGQA-E-001", + "traceId": "trace-CGQA-E-001" + }, + "limitations": [ + "A bounded search result is not proof that no violation exists outside the declared bound.", + "The evidence is bound only to the exact repository commit and subject declared here.", + "No request/outcome continuity verdict was computed; LTP remains the continuity verdict owner.", + "This artifact is evidence only and does not authorize an action." + ], + "producer": { + "name": "contractgraph-qa", + "version": "1.9.0" + }, + "profile": "org.contractgraph-qa.bounded-invariant-evidence.v0.1", + "schema": "org.contractgraph-qa.liminalqa-evidence.v0.1", + "subject": { + "commitSha": "a10862f40e2d4d59c122a61119fbb3c9c1ff6cab", + "contract": "AdapterFixtureMachine", + "network": "local-foundry-engagement", + "repository": "https://github.com/safal207/ContractGraph-QA", + "scopeId": "local-v1.3-engagement-fixture" + }, + "times": { + "observedAt": "2026-09-03T10:01:00Z", + "recordedAt": "2026-09-03T10:02:00Z", + "validAt": "2026-09-03T10:00:00Z" + }, + "verificationDebt": [ + { + "invariantId": "budget-sensitive-branch", + "reason": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check.", + "status": "inconclusive" + } + ] +} diff --git a/liminalqa-core/tests/fixtures/liminalqa-cgqa-candidates-v0.1.json b/liminalqa-core/tests/fixtures/liminalqa-cgqa-candidates-v0.1.json new file mode 100644 index 00000000..725303e7 --- /dev/null +++ b/liminalqa-core/tests/fixtures/liminalqa-cgqa-candidates-v0.1.json @@ -0,0 +1,74 @@ +{ + "schema": "org.liminalqa.cgqa-candidates.v0.1", + "profile": "org.liminalqa.non-authoritative-candidate-seeds.v0.1", + "exportId": "liminal-candidates-2595f78d88288151b948a1da", + "producer": { + "name": "liminalqa", + "version": "0.1.0" + }, + "sourceEvidence": { + "schema": "org.contractgraph-qa.liminalqa-evidence.v0.1", + "exportId": "cgqa-liminalqa-8bbf2e1385e7e321788766d4", + "sha256": "e1d5a14c5c1b75e2cfffaf87bf526fd61e141a0c5b7828de4f275e9792fda3ce" + }, + "subject": { + "repository": "https://github.com/safal207/ContractGraph-QA", + "commitSha": "a10862f40e2d4d59c122a61119fbb3c9c1ff6cab", + "contract": "AdapterFixtureMachine", + "network": "local-foundry-engagement", + "scopeId": "local-v1.3-engagement-fixture" + }, + "identity": { + "traceId": "trace-CGQA-E-001", + "operationId": "liminal-candidate-derivation-001", + "attemptId": "attempt-001" + }, + "derivedAt": "2026-09-03T10:03:00Z", + "authority": { + "classification": "non_authoritative_seed", + "mayAuthorizeAction": false, + "requiresCgqaVerification": true + }, + "candidates": [ + { + "candidateId": "liminal-candidate-7ab5731bafda6ff275addf49", + "invariantId": "terminal-state-bound", + "sourceStatus": "violated", + "kind": "replay_regression", + "priority": "medium", + "reason": "The repository-local fixture reaches the modeled terminal state through the shortest three-step advance path.", + "requiredChecks": [ + "exact_subject", + "independent_cgqa_replay", + "failing_path_integrity" + ] + }, + { + "candidateId": "liminal-candidate-497f6be066795a9547ec2e24", + "invariantId": "budget-sensitive-branch", + "sourceStatus": "inconclusive", + "kind": "verification_debt", + "priority": "low", + "reason": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check.", + "requiredChecks": [ + "exact_subject", + "reviewed_bound_change", + "independent_cgqa_replay" + ] + } + ], + "causalParents": [ + "cgqa-liminalqa-8bbf2e1385e7e321788766d4" + ], + "limitations": [ + "Candidates are hypotheses derived from bounded evidence, not verified findings.", + "ContractGraph-QA must independently replay each candidate against the exact commit.", + "Candidate export cannot authorize an action or compute an LTP continuity verdict." + ], + "verificationDebt": [ + { + "invariantId": "budget-sensitive-branch", + "reason": "The local evaluator intentionally returns inconclusive so unresolved evidence is not presented as a clean check." + } + ] +} diff --git a/schemas/interop/cgqa-liminalqa-evidence-v0.1.external-contract.json b/schemas/interop/cgqa-liminalqa-evidence-v0.1.external-contract.json new file mode 100644 index 00000000..5caf24d3 --- /dev/null +++ b/schemas/interop/cgqa-liminalqa-evidence-v0.1.external-contract.json @@ -0,0 +1,11 @@ +{ + "schema": "org.liminalqa.external-schema-pin.v0.1", + "consumer": "liminalqa-core", + "producerRepository": "https://github.com/safal207/ContractGraph-QA", + "producerCommit": "bdf7ced074e3a7baf57cf89ac68be9674bd76a02", + "producerSchemaPath": "contractgraph_qa/schemas/cgqa-liminalqa-evidence-v0.1.schema.json", + "producerSchema": "org.contractgraph-qa.liminalqa-evidence.v0.1", + "producerProfile": "org.contractgraph-qa.bounded-invariant-evidence.v0.1", + "schemaSha256": "53b0b4a0b1f4d77de26b8be9dbb90006ea0bd30c5cd3960a2f3e7d44d9664184", + "compatibility": "exact-version-only" +} diff --git a/schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json b/schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json new file mode 100644 index 00000000..61c4fa97 --- /dev/null +++ b/schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/safal207/LiminalQAengineer/main/schemas/interop/liminalqa-cgqa-candidates-v0.1.schema.json", + "title": "LiminalQA to ContractGraph-QA non-authoritative candidates v0.1", + "type": "object", + "required": [ + "schema", + "profile", + "exportId", + "producer", + "sourceEvidence", + "subject", + "identity", + "derivedAt", + "authority", + "candidates", + "causalParents", + "limitations", + "verificationDebt" + ], + "properties": { + "schema": { "const": "org.liminalqa.cgqa-candidates.v0.1" }, + "profile": { "const": "org.liminalqa.non-authoritative-candidate-seeds.v0.1" }, + "exportId": { "$ref": "#/$defs/safeId" }, + "producer": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "const": "liminalqa" }, + "version": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "sourceEvidence": { + "type": "object", + "required": ["schema", "exportId", "sha256"], + "properties": { + "schema": { "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" }, + "exportId": { "$ref": "#/$defs/safeId" }, + "sha256": { "$ref": "#/$defs/sha256" } + }, + "additionalProperties": false + }, + "subject": { "$ref": "#/$defs/subject" }, + "identity": { "$ref": "#/$defs/identity" }, + "derivedAt": { "type": "string", "format": "date-time" }, + "authority": { + "type": "object", + "required": ["classification", "mayAuthorizeAction", "requiresCgqaVerification"], + "properties": { + "classification": { "const": "non_authoritative_seed" }, + "mayAuthorizeAction": { "const": false }, + "requiresCgqaVerification": { "const": true } + }, + "additionalProperties": false + }, + "candidates": { + "type": "array", + "items": { + "type": "object", + "required": [ + "candidateId", + "invariantId", + "sourceStatus", + "kind", + "priority", + "reason", + "requiredChecks" + ], + "properties": { + "candidateId": { "$ref": "#/$defs/safeId" }, + "invariantId": { "$ref": "#/$defs/safeId" }, + "sourceStatus": { "enum": ["violated", "inconclusive"] }, + "kind": { "enum": ["replay_regression", "verification_debt"] }, + "priority": { "enum": ["critical", "high", "medium", "low"] }, + "reason": { "$ref": "#/$defs/nonBlank" }, + "requiredChecks": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/nonBlank" } + } + }, + "allOf": [ + { + "if": { + "properties": { "sourceStatus": { "const": "violated" } }, + "required": ["sourceStatus"] + }, + "then": { "properties": { "kind": { "const": "replay_regression" } } }, + "else": { "properties": { "kind": { "const": "verification_debt" } } } + } + ], + "additionalProperties": false + } + }, + "causalParents": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/safeId" } + }, + "limitations": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/nonBlank" } + }, + "verificationDebt": { + "type": "array", + "items": { + "type": "object", + "required": ["invariantId", "reason"], + "properties": { + "invariantId": { "$ref": "#/$defs/safeId" }, + "reason": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "$defs": { + "nonBlank": { "type": "string", "minLength": 1, "pattern": ".*\\S.*" }, + "safeId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "subject": { + "type": "object", + "required": ["repository", "commitSha", "contract", "network", "scopeId"], + "properties": { + "repository": { "$ref": "#/$defs/nonBlank" }, + "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "contract": { "$ref": "#/$defs/nonBlank" }, + "network": { "$ref": "#/$defs/nonBlank" }, + "scopeId": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "required": ["traceId", "operationId", "attemptId"], + "properties": { + "traceId": { "$ref": "#/$defs/safeId" }, + "operationId": { "$ref": "#/$defs/safeId" }, + "attemptId": { "$ref": "#/$defs/safeId" } + }, + "additionalProperties": false + } + } +} diff --git a/schemas/interop/liminalqa-cgqa-import-receipt-v0.1.schema.json b/schemas/interop/liminalqa-cgqa-import-receipt-v0.1.schema.json new file mode 100644 index 00000000..63a8f5d5 --- /dev/null +++ b/schemas/interop/liminalqa-cgqa-import-receipt-v0.1.schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/safal207/LiminalQAengineer/main/schemas/interop/liminalqa-cgqa-import-receipt-v0.1.schema.json", + "title": "LiminalQA CGQA bounded-evidence import receipt v0.1", + "type": "object", + "required": [ + "schema", + "profile", + "receiptId", + "consumer", + "source", + "subject", + "identity", + "acceptedAs", + "mayAuthorizeAction", + "statusCounts", + "limitations" + ], + "properties": { + "schema": { "const": "org.liminalqa.cgqa-import-receipt.v0.1" }, + "profile": { "const": "org.liminalqa.bounded-evidence-intake.v0.1" }, + "receiptId": { "$ref": "#/$defs/safeId" }, + "consumer": { + "type": "object", + "required": ["name", "version"], + "properties": { + "name": { "const": "liminalqa" }, + "version": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "source": { + "type": "object", + "required": ["schema", "exportId", "sha256"], + "properties": { + "schema": { "const": "org.contractgraph-qa.liminalqa-evidence.v0.1" }, + "exportId": { "$ref": "#/$defs/safeId" }, + "sha256": { "$ref": "#/$defs/sha256" } + }, + "additionalProperties": false + }, + "subject": { "$ref": "#/$defs/subject" }, + "identity": { "$ref": "#/$defs/identity" }, + "acceptedAs": { "const": "bounded_evidence" }, + "mayAuthorizeAction": { "const": false }, + "statusCounts": { + "type": "object", + "required": ["violated", "not_found_within_bound", "inconclusive"], + "properties": { + "violated": { "type": "integer", "minimum": 0 }, + "not_found_within_bound": { "type": "integer", "minimum": 0 }, + "inconclusive": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + "limitations": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/nonBlank" } + } + }, + "additionalProperties": false, + "$defs": { + "nonBlank": { "type": "string", "minLength": 1, "pattern": ".*\\S.*" }, + "safeId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" + }, + "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "subject": { + "type": "object", + "required": ["repository", "commitSha", "contract", "network", "scopeId"], + "properties": { + "repository": { "$ref": "#/$defs/nonBlank" }, + "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "contract": { "$ref": "#/$defs/nonBlank" }, + "network": { "$ref": "#/$defs/nonBlank" }, + "scopeId": { "$ref": "#/$defs/nonBlank" } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "required": ["traceId", "operationId", "attemptId"], + "properties": { + "traceId": { "$ref": "#/$defs/safeId" }, + "operationId": { "$ref": "#/$defs/safeId" }, + "attemptId": { "$ref": "#/$defs/safeId" } + }, + "additionalProperties": false + } + } +}