From 4ddf30e5459bd578ebb460b54f9a1e6a5df4a4e6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 20 May 2026 23:41:13 +0100 Subject: [PATCH] Add witness artifact bundle contract (topic_95053275-0a5a-4b05-806e-671dc5cff4d1) --- ...ultnet.witness-artifact-bundle.schema.json | 120 +++++++++ .../CultNetSchemaMessages.cs | 4 + .../CultNetSchemaRegistry.cs | 8 + .../CultWitnessArtifactBundle.cs | 254 ++++++++++++++++++ .../NetworkingTests.cs | 33 +++ 5 files changed, 419 insertions(+) create mode 100644 src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json create mode 100644 src/GameCult.Networking/CultWitnessArtifactBundle.cs diff --git a/src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json b/src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json new file mode 100644 index 00000000..a9cfbcef --- /dev/null +++ b/src/GameCult.Networking/Contracts/cultnet.witness-artifact-bundle.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.witness-artifact-bundle.schema.json", + "title": "CultNet Witness Artifact Bundle", + "type": "object", + "required": [ + "bundleId", + "witnessKind", + "capturedAt", + "subject", + "contracts", + "artifacts", + "timingWitnesses", + "provenance" + ], + "additionalProperties": false, + "properties": { + "bundleId": { "type": "string", "minLength": 1 }, + "witnessKind": { "type": "string", "minLength": 1 }, + "capturedAt": { "type": "string", "minLength": 1 }, + "subject": { "$ref": "#/$defs/subject_pin" }, + "contracts": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/contract_pin" } + }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/artifact_entry" } + }, + "timingWitnesses": { + "type": "array", + "items": { "$ref": "#/$defs/timing_entry" } + }, + "provenance": { "$ref": "#/$defs/provenance" } + }, + "$defs": { + "subject_pin": { + "type": "object", + "required": [ + "documentType", + "subjectId" + ], + "additionalProperties": false, + "properties": { + "documentType": { "type": "string", "minLength": 1 }, + "subjectId": { "type": "string", "minLength": 1 }, + "schemaVersion": { "type": "string", "minLength": 1 }, + "schemaId": { "type": "string", "minLength": 1 }, + "contentHash": { "type": "string", "minLength": 1 } + } + }, + "contract_pin": { + "type": "object", + "required": [ + "role", + "schemaId" + ], + "additionalProperties": false, + "properties": { + "role": { "type": "string", "minLength": 1 }, + "schemaId": { "type": "string", "minLength": 1 }, + "schemaVersion": { "type": "string", "minLength": 1 }, + "contentHash": { "type": "string", "minLength": 1 } + } + }, + "artifact_entry": { + "type": "object", + "required": [ + "role", + "uri", + "mediaType" + ], + "additionalProperties": false, + "properties": { + "role": { "type": "string", "minLength": 1 }, + "uri": { "type": "string", "minLength": 1 }, + "mediaType": { "type": "string", "minLength": 1 }, + "contentHash": { "type": "string", "minLength": 1 }, + "byteLength": { "type": "integer", "minimum": 0 }, + "producedAt": { "type": "string", "minLength": 1 } + } + }, + "timing_entry": { + "type": "object", + "required": [ + "stage", + "startedAt", + "completedAt", + "latencyMs" + ], + "additionalProperties": false, + "properties": { + "stage": { "type": "string", "minLength": 1 }, + "startedAt": { "type": "string", "minLength": 1 }, + "completedAt": { "type": "string", "minLength": 1 }, + "latencyMs": { "type": "number", "minimum": 0 }, + "witnessArtifactUri": { "type": "string", "minLength": 1 } + } + }, + "provenance": { + "type": "object", + "required": [ + "pipelineId", + "runId", + "runtimeId" + ], + "additionalProperties": false, + "properties": { + "pipelineId": { "type": "string", "minLength": 1 }, + "runId": { "type": "string", "minLength": 1 }, + "runtimeId": { "type": "string", "minLength": 1 }, + "agentId": { "type": "string", "minLength": 1 }, + "agentRole": { "type": "string", "minLength": 1 }, + "toolVersion": { "type": "string", "minLength": 1 } + } + } + } +} diff --git a/src/GameCult.Networking/CultNetSchemaMessages.cs b/src/GameCult.Networking/CultNetSchemaMessages.cs index fc21f5ee..18b6f9ef 100644 --- a/src/GameCult.Networking/CultNetSchemaMessages.cs +++ b/src/GameCult.Networking/CultNetSchemaMessages.cs @@ -116,6 +116,10 @@ namespace GameCult.Networking /// public const string SimulationConsensusCandidate = "cultnet.simulation_consensus_candidate.v0"; /// + /// witness artifact bundle document contract identifier. + /// + public const string WitnessArtifactBundle = "cultnet.witness_artifact_bundle.v0"; + /// /// CultMesh Verse catalog request contract identifier. /// public const string VerseCatalogRequest = "cultmesh.verse_catalog_request.v0"; diff --git a/src/GameCult.Networking/CultNetSchemaRegistry.cs b/src/GameCult.Networking/CultNetSchemaRegistry.cs index 025afa3f..10f216ec 100644 --- a/src/GameCult.Networking/CultNetSchemaRegistry.cs +++ b/src/GameCult.Networking/CultNetSchemaRegistry.cs @@ -375,6 +375,14 @@ namespace GameCult.Networking SchemaVersion = "ghostlight.agent_state.v0", DocumentType = "ghostlight.agent-state", WireContracts = new[] { CultNetWireContracts.SchemaV0 } + }, + new SchemaResourceSpec + { + FileName = "cultnet.witness-artifact-bundle.schema.json", + Kind = "document_payload", + SchemaVersion = CultNetSchemaVersions.WitnessArtifactBundle, + DocumentType = "cultnet.witness-artifact-bundle", + WireContracts = new[] { CultNetWireContracts.SchemaV0 } } }; } diff --git a/src/GameCult.Networking/CultWitnessArtifactBundle.cs b/src/GameCult.Networking/CultWitnessArtifactBundle.cs new file mode 100644 index 00000000..045ebfbb --- /dev/null +++ b/src/GameCult.Networking/CultWitnessArtifactBundle.cs @@ -0,0 +1,254 @@ +using System; +using GameCult.Caching; +using MessagePack; + +namespace GameCult.Networking +{ + /// + /// Portable manifest for one inspectable pipeline witness bundle. + /// + [MessagePackObject] + [CultDocument("cultnet.witness_artifact_bundle", CultNetSchemaVersions.WitnessArtifactBundle)] + public sealed class CultWitnessArtifactBundle + { + /// + /// Gets or sets the stable bundle identifier. + /// + [Key(0)] + [CultName] + public string BundleId { get; set; } = string.Empty; + + /// + /// Gets or sets the witness family or pipeline kind. + /// + [Key(1)] + public string WitnessKind { get; set; } = string.Empty; + + /// + /// Gets or sets when the manifest was captured. + /// + [Key(2)] + public string CapturedAt { get; set; } = string.Empty; + + /// + /// Gets or sets the pinned subject this witness bundle describes. + /// + [Key(3)] + public CultWitnessSubjectPin Subject { get; set; } = new CultWitnessSubjectPin(); + + /// + /// Gets or sets the schema and contract pins required to interpret the witness. + /// + [Key(4)] + public CultWitnessContractPin[] Contracts { get; set; } = Array.Empty(); + + /// + /// Gets or sets the inspectable artifact pointers carried by the bundle. + /// + [Key(5)] + public CultWitnessArtifactEntry[] Artifacts { get; set; } = Array.Empty(); + + /// + /// Gets or sets stage timing witnesses associated with the bundle. + /// + [Key(6)] + public CultWitnessTimingEntry[] TimingWitnesses { get; set; } = Array.Empty(); + + /// + /// Gets or sets provenance metadata for the bundle producer. + /// + [Key(7)] + public CultWitnessProvenance Provenance { get; set; } = new CultWitnessProvenance(); + } + + /// + /// Pins the document or payload instance the witness is about. + /// + [MessagePackObject] + public sealed class CultWitnessSubjectPin + { + /// + /// Gets or sets the subject document type or domain identifier. + /// + [Key(0)] + public string DocumentType { get; set; } = string.Empty; + + /// + /// Gets or sets the persisted record key or equivalent stable subject id. + /// + [Key(1)] + public string SubjectId { get; set; } = string.Empty; + + /// + /// Gets or sets the pinned subject schema version. + /// + [Key(2)] + public string? SchemaVersion { get; set; } + + /// + /// Gets or sets the pinned subject schema identifier when one exists. + /// + [Key(3)] + public string? SchemaId { get; set; } + + /// + /// Gets or sets the pinned subject content hash when one exists. + /// + [Key(4)] + public string? ContentHash { get; set; } + } + + /// + /// Pins one schema or contract dependency for the witness bundle. + /// + [MessagePackObject] + public sealed class CultWitnessContractPin + { + /// + /// Gets or sets the contract role inside the bundle. + /// + [Key(0)] + public string Role { get; set; } = string.Empty; + + /// + /// Gets or sets the stable schema identifier. + /// + [Key(1)] + public string SchemaId { get; set; } = string.Empty; + + /// + /// Gets or sets the pinned schema version. + /// + [Key(2)] + public string? SchemaVersion { get; set; } + + /// + /// Gets or sets the canonical schema content hash when one exists. + /// + [Key(3)] + public string? ContentHash { get; set; } + } + + /// + /// Points at one inspectable artifact emitted by the witnessed pipeline. + /// + [MessagePackObject] + public sealed class CultWitnessArtifactEntry + { + /// + /// Gets or sets the artifact role inside the witness bundle. + /// + [Key(0)] + public string Role { get; set; } = string.Empty; + + /// + /// Gets or sets the portable URI for the artifact. + /// + [Key(1)] + public string Uri { get; set; } = string.Empty; + + /// + /// Gets or sets the artifact media type. + /// + [Key(2)] + public string MediaType { get; set; } = string.Empty; + + /// + /// Gets or sets the artifact content hash when one exists. + /// + [Key(3)] + public string? ContentHash { get; set; } + + /// + /// Gets or sets the artifact size in bytes when known. + /// + [Key(4)] + public long? ByteLength { get; set; } + + /// + /// Gets or sets when the artifact was produced. + /// + [Key(5)] + public string? ProducedAt { get; set; } + } + + /// + /// Records timestamp and latency evidence for one stage in the witnessed pipeline. + /// + [MessagePackObject] + public sealed class CultWitnessTimingEntry + { + /// + /// Gets or sets the pipeline stage name. + /// + [Key(0)] + public string Stage { get; set; } = string.Empty; + + /// + /// Gets or sets when the stage started. + /// + [Key(1)] + public string StartedAt { get; set; } = string.Empty; + + /// + /// Gets or sets when the stage completed. + /// + [Key(2)] + public string CompletedAt { get; set; } = string.Empty; + + /// + /// Gets or sets the observed stage latency in milliseconds. + /// + [Key(3)] + public double LatencyMs { get; set; } + + /// + /// Gets or sets the URI for the timing witness artifact when one exists. + /// + [Key(4)] + public string? WitnessArtifactUri { get; set; } + } + + /// + /// Carries provenance metadata for the witness bundle producer. + /// + [MessagePackObject] + public sealed class CultWitnessProvenance + { + /// + /// Gets or sets the stable pipeline identifier. + /// + [Key(0)] + public string PipelineId { get; set; } = string.Empty; + + /// + /// Gets or sets the pipeline run identifier. + /// + [Key(1)] + public string RunId { get; set; } = string.Empty; + + /// + /// Gets or sets the runtime identifier that produced the bundle. + /// + [Key(2)] + public string RuntimeId { get; set; } = string.Empty; + + /// + /// Gets or sets the producing agent identifier when one exists. + /// + [Key(3)] + public string? AgentId { get; set; } + + /// + /// Gets or sets the producing agent role when one exists. + /// + [Key(4)] + public string? AgentRole { get; set; } + + /// + /// Gets or sets the pipeline or tool version when one exists. + /// + [Key(5)] + public string? ToolVersion { get; set; } + } +} diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs index 381ffaf0..25a5d4b6 100644 --- a/tests/GameCult.Networking.Tests/NetworkingTests.cs +++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs @@ -2286,6 +2286,39 @@ namespace GameCult.Networking.Tests Assert.That(ghostlight.Kind, Is.EqualTo("document_payload")); } + [Test] + public void CultWitnessArtifactBundle_Has_Stable_CacheContract() + { + var descriptor = new CultCache().Registry.GetRequired(); + + Assert.That(descriptor.SchemaName, Is.EqualTo("cultnet.witness_artifact_bundle")); + Assert.That(descriptor.SchemaVersion, Is.EqualTo(CultNetSchemaVersions.WitnessArtifactBundle)); + Assert.That(descriptor.NameMember, Is.EqualTo(nameof(CultWitnessArtifactBundle.BundleId))); + Assert.That(descriptor.CanonicalSchemaJson, Is.EqualTo( + "{\"schemaName\":\"cultnet.witness_artifact_bundle\",\"schemaVersion\":\"cultnet.witness_artifact_bundle.v0\",\"members\":[{\"slot\":0,\"name\":\"BundleId\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":true},{\"slot\":1,\"name\":\"WitnessKind\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":2,\"name\":\"CapturedAt\",\"type\":\"System.String\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":3,\"name\":\"Subject\",\"type\":\"GameCult.Networking.CultWitnessSubjectPin\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":4,\"name\":\"Contracts\",\"type\":\"GameCult.Networking.CultWitnessContractPin[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":5,\"name\":\"Artifacts\",\"type\":\"GameCult.Networking.CultWitnessArtifactEntry[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":6,\"name\":\"TimingWitnesses\",\"type\":\"GameCult.Networking.CultWitnessTimingEntry[]\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false},{\"slot\":7,\"name\":\"Provenance\",\"type\":\"GameCult.Networking.CultWitnessProvenance\",\"isReference\":false,\"many\":false,\"targetSchemaName\":null,\"indexAlias\":null,\"isName\":false}]}")); + } + + [Test] + public void CultNetSchemaRegistry_BuiltInCatalog_Advertises_WitnessArtifactBundle() + { + var response = CultNetSchemaRegistry.BuiltIn.CreateCatalogResponse( + new CultNetSchemaCatalogRequestMessage + { + MessageId = "catalog-witness", + IncludeSchemaJson = true + }); + var witnessBundle = Array.Find(response.Schemas, + schema => schema.SchemaVersion == CultNetSchemaVersions.WitnessArtifactBundle); + + Assert.That(witnessBundle, Is.Not.Null); + Assert.That(witnessBundle!.DocumentType, Is.EqualTo("cultnet.witness-artifact-bundle")); + Assert.That(witnessBundle.Kind, Is.EqualTo("document_payload")); + Assert.That(witnessBundle.WireContracts, Is.EqualTo([CultNetWireContracts.SchemaV0])); + Assert.That(witnessBundle.SchemaJson, Does.Contain("\"bundleId\"")); + Assert.That(witnessBundle.SchemaJson, Does.Contain("\"timingWitnesses\"")); + Assert.That(witnessBundle.SchemaJson, Does.Contain("\"witnessArtifactUri\"")); + } + [Test] public void Server_Separates_Connection_And_Auth_RateLimits() {