From 7d2fc2af5da334f1bcc5cf0bddb9b68b9544d1eb Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 4 Sep 2026 01:52:53 +0200 Subject: [PATCH 1/3] Add generated WORK-RECORDS Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400 --- WORK-RECORDS.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 WORK-RECORDS.md diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md new file mode 100644 index 0000000..5326a58 --- /dev/null +++ b/WORK-RECORDS.md @@ -0,0 +1,77 @@ +# Work Records — fluid-core + +> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record +> stage 3). Do not edit by hand — edit the source file/block listed for +> each record and re-run fix-consistency to refresh this index. Archived +> workplans are omitted; closed decisions/intakes/engagements stay listed +> so recently-resolved work is still visible. [auto] + +| Kind | ID | Status | Lane | Source | +| --- | --- | --- | --- | --- | +| workplan | FLUID-WP-0001 | ready | — | workplans/FLUID-WP-0001-statehub-bootstrap.md | +| workplan | FLUID-WP-0002 | active | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| workplan | FLUID-WP-0003 | active | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| workplan | FLUID-WP-0004 | active | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| workplan | FLUID-WP-0005 | active | — | workplans/FLUID-WP-0005-fluid-insight.md | +| workplan | FLUID-WP-0006 | active | — | workplans/FLUID-WP-0006-fluid-science.md | +| workplan | FLUID-WP-0007 | active | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| workplan | FLUID-WP-0008 | active | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0001-T01 | todo | — | workplans/FLUID-WP-0001-statehub-bootstrap.md | +| task | FLUID-WP-0001-T02 | todo | — | workplans/FLUID-WP-0001-statehub-bootstrap.md | +| task | FLUID-WP-0001-T03 | todo | — | workplans/FLUID-WP-0001-statehub-bootstrap.md | +| task | FLUID-WP-0002-T01 | done | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0002-T02 | todo | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0002-T03 | todo | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0002-T04 | todo | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0002-T05 | todo | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0002-T06 | todo | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0002-T07 | todo | — | workplans/FLUID-WP-0002-foundations-and-wire-contract.md | +| task | FLUID-WP-0003-T01 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T02 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T03 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T04 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T05 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T06 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T07 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T08 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T09 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0003-T10 | todo | — | workplans/FLUID-WP-0003-deterministic-data-plane.md | +| task | FLUID-WP-0004-T01 | todo | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| task | FLUID-WP-0004-T02 | todo | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| task | FLUID-WP-0004-T03 | todo | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| task | FLUID-WP-0004-T04 | todo | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| task | FLUID-WP-0004-T05 | todo | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| task | FLUID-WP-0004-T06 | todo | — | workplans/FLUID-WP-0004-publication-and-control-apis.md | +| task | FLUID-WP-0005-T01 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T02 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T03 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T04 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T05 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T06 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T07 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T08 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0005-T09 | todo | — | workplans/FLUID-WP-0005-fluid-insight.md | +| task | FLUID-WP-0006-T01 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T02 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T03 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T04 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T05 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T06 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T07 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T08 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0006-T09 | todo | — | workplans/FLUID-WP-0006-fluid-science.md | +| task | FLUID-WP-0007-T01 | todo | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| task | FLUID-WP-0007-T02 | todo | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| task | FLUID-WP-0007-T03 | todo | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| task | FLUID-WP-0007-T04 | todo | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| task | FLUID-WP-0007-T05 | todo | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| task | FLUID-WP-0007-T06 | todo | — | workplans/FLUID-WP-0007-conformance-and-self-validation.md | +| task | FLUID-WP-0008-T01 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T02 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T03 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T04 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T05 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T06 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T07 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T08 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | +| task | FLUID-WP-0008-T09 | todo | — | workplans/FLUID-WP-0008-fluid-telegram-handover.md | From fbbf56df7abe1600e79f6e639612c64d7ca76814 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 4 Sep 2026 01:55:47 +0200 Subject: [PATCH 2/3] Add FLUID wire contract schemas Nine JSON Schemas covering the five record types from the schema spec, plus backend requirements, feedback, the revision descriptor, routing policy and the telemetry envelope. These are the integration boundary: readable without any fluid-core code, so adapters may be written in any language. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400 --- schemas/backend-requirement.schema.json | 83 +++++++++ schemas/common.schema.json | 161 ++++++++++++++++ schemas/event.schema.json | 35 ++++ schemas/experiment.schema.json | 99 ++++++++++ schemas/feedback.schema.json | 40 ++++ schemas/hypothesis.schema.json | 228 +++++++++++++++++++++++ schemas/pressure.schema.json | 53 ++++++ schemas/revision-descriptor.schema.json | 124 +++++++++++++ schemas/revision.schema.json | 235 ++++++++++++++++++++++++ schemas/routing-policy.schema.json | 71 +++++++ schemas/telemetry-envelope.schema.json | 113 ++++++++++++ 11 files changed, 1242 insertions(+) create mode 100644 schemas/backend-requirement.schema.json create mode 100644 schemas/common.schema.json create mode 100644 schemas/event.schema.json create mode 100644 schemas/experiment.schema.json create mode 100644 schemas/feedback.schema.json create mode 100644 schemas/hypothesis.schema.json create mode 100644 schemas/pressure.schema.json create mode 100644 schemas/revision-descriptor.schema.json create mode 100644 schemas/revision.schema.json create mode 100644 schemas/routing-policy.schema.json create mode 100644 schemas/telemetry-envelope.schema.json diff --git a/schemas/backend-requirement.schema.json b/schemas/backend-requirement.schema.json new file mode 100644 index 0000000..697b17e --- /dev/null +++ b/schemas/backend-requirement.schema.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/backend-requirement.schema.json", + "title": "FLUID backend capability requirement", + "description": "Capability the interface needs but has no authority to build. FLUID escalates rather than crossing the boundary (FluidAPIStandards.md section 30).", + "type": "object", + "properties": { + "fluid_backend_requirement": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/backendRequirementId" }, + "originating_interface": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "originating_revision": { "oneOf": [{ "$ref": "common.schema.json#/$defs/revisionId" }, { "type": "null" }] }, + "originating_hypothesis": { "oneOf": [{ "$ref": "common.schema.json#/$defs/hypothesisId" }, { "type": "null" }] }, + "backend_service": { "type": "string", "minLength": 1 }, + + "capability": { + "type": "object", + "properties": { + "title": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 } + }, + "required": ["title", "description"], + "additionalProperties": false + }, + + "semantics": { + "type": "object", + "properties": { + "consistency": { "type": "string" }, + "required_fields": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": true + }, + + "quality": { + "type": "object", + "properties": { + "p95_latency_ms": { "type": "number", "minimum": 0 }, + "availability": { "$ref": "common.schema.json#/$defs/unitInterval" } + }, + "additionalProperties": true + }, + + "security": { + "type": "object", + "properties": { + "authorization_scope": { "type": "string" }, + "tenant_isolation": { "type": "string" } + }, + "additionalProperties": true + }, + + "expected_usage": { + "type": "object", + "properties": { + "requests_per_day": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": true + }, + + "urgency": { "enum": ["LOW", "MEDIUM", "HIGH"] }, + + "disposition": { + "type": "object", + "description": "The backend decides. Repeated OUT_OF_SCOPE feeds boundary learning (ArchitectureBlueprint.md section 22).", + "properties": { + "state": { "enum": ["OPEN", "ACCEPTED", "PLANNED", "AVAILABLE", "PARTIALLY_AVAILABLE", "OUT_OF_SCOPE", "REJECTED", "SUPERSEDED"] }, + "reason": { "type": ["string", "null"] }, + "target_ref": { "type": ["string", "null"] } + }, + "required": ["state"], + "additionalProperties": false + } + }, + "required": ["schema_version", "id", "originating_interface", "backend_service", "capability", "disposition"], + "additionalProperties": false + } + }, + "required": ["fluid_backend_requirement"], + "additionalProperties": false +} diff --git a/schemas/common.schema.json b/schemas/common.schema.json new file mode 100644 index 0000000..06e67fe --- /dev/null +++ b/schemas/common.schema.json @@ -0,0 +1,161 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/common.schema.json", + "title": "FLUID common definitions", + "description": "Shared identifier, reference and measurement types. Identifier prefixes follow FluidHypothesisRevisionSchema.md section 16.", + "$defs": { + "schemaVersion": { + "type": "string", + "const": "0.1" + }, + "interfaceId": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "description": "Stable identifier of the owning interface." + }, + "hypothesisId": { "type": "string", "pattern": "^H-[0-9A-Za-z_-]+$" }, + "revisionId": { "type": "string", "pattern": "^R-[0-9A-Za-z_-]+$" }, + "experimentId": { "type": "string", "pattern": "^E-[0-9A-Za-z_-]+$" }, + "pressureId": { "type": "string", "pattern": "^P-[0-9A-Za-z_-]+$" }, + "backendRequirementId": { "type": "string", "pattern": "^BR-[0-9A-Za-z_-]+$" }, + "decisionId": { "type": "string", "pattern": "^D-[0-9A-Za-z_-]+$" }, + "eventId": { "type": "string", "pattern": "^EV-[0-9A-Za-z_-]+$" }, + "feedbackId": { "type": "string", "pattern": "^F-[0-9A-Za-z_-]+$" }, + "cohortId": { "type": "string", "minLength": 1 }, + + "timestamp": { "type": "string", "format": "date-time" }, + + "evidenceRef": { + "type": "string", + "minLength": 1, + "description": "Opaque reference into the evidence store, conventionally ':' such as 'telemetry:invoice-pattern-2026w36'." + }, + "artifactRef": { + "type": "string", + "minLength": 1, + "description": "Opaque reference into the artifact store." + }, + "digest": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "Content address of an artifact." + }, + + "actor": { + "type": "object", + "description": "Who or what performed an action. FLUID distinguishes these because trust levels differ (ArchitectureBlueprint.md section 47).", + "properties": { + "type": { "enum": ["human", "daimon", "policy", "system", "consumer"] }, + "id": { "type": "string", "minLength": 1 }, + "model_or_agent": { "type": "string" } + }, + "required": ["type", "id"], + "additionalProperties": false + }, + + "observationWindow": { + "type": "object", + "properties": { + "start": { "$ref": "common.schema.json#/$defs/timestamp" }, + "end": { + "oneOf": [ + { "$ref": "common.schema.json#/$defs/timestamp" }, + { "type": "null" } + ], + "description": "Null while the window is still open." + } + }, + "required": ["start"], + "additionalProperties": false + }, + + "unitInterval": { "type": "number", "minimum": 0, "maximum": 1 }, + + "pressureClass": { + "enum": [ + "natural_usage", + "successful_but_inefficient_usage", + "recoverable_misunderstanding", + "repeated_expectation_mismatch", + "poor_discoverability", + "missing_interface_capability", + "missing_backend_capability", + "out_of_scope_demand", + "prohibited_demand", + "implementation_failure" + ], + "description": "FluidAPIStandards.md section 13." + }, + + "adaptationClass": { + "enum": ["presentation", "contract", "composition", "implementation", "requirement_escalation"], + "description": "FluidAPIStandards.md section 16." + }, + + "guardrail": { + "type": "object", + "description": "A constraint that must not regress beyond threshold. Guardrails are evaluated deterministically.", + "properties": { + "metric": { "type": "string", "minLength": 1 }, + "operator": { "enum": ["<", "<=", "==", "!=", ">=", ">"] }, + "threshold": { "type": ["number", "boolean", "string"] } + }, + "required": ["metric", "operator", "threshold"], + "additionalProperties": false + }, + + "expectedOutcome": { + "type": "object", + "properties": { + "metric": { "type": "string", "minLength": 1 }, + "cohort": { "type": "string" }, + "baseline": { "type": "number" }, + "target": { "type": "number" }, + "direction": { "enum": ["lower", "higher", "unchanged"] } + }, + "required": ["metric", "target", "direction"], + "additionalProperties": false + }, + + "complexityDelta": { + "type": "object", + "description": "Standard dimensions, implementation-defined scoring (FluidAPIStandards.md section 22).", + "properties": { + "surface_area": { "type": "number" }, + "concept_count": { "type": "number" }, + "operation_count": { "type": "number" }, + "parameter_dimensionality": { "type": "number" }, + "semantic_overlap": { "type": "number" }, + "alternative_path_count": { "type": "number" }, + "dependency_count": { "type": "number" }, + "backend_composition_count": { "type": "number" }, + "exception_count": { "type": "number" }, + "active_revision_count": { "type": "number" }, + "cognitive_load": { "type": "number" } + }, + "additionalProperties": false + }, + + "fitnessDimensions": { + "type": "object", + "description": "FluidAPIStandards.md section 20. No universal scalar is defined on purpose.", + "properties": { + "client_utility": { "type": "number" }, + "correctness": { "type": "number" }, + "reliability": { "type": "number" }, + "availability": { "type": "number" }, + "performance": { "type": "number" }, + "discoverability": { "type": "number" }, + "simplicity": { "type": "number" }, + "compatibility": { "type": "number" }, + "security": { "type": "number" }, + "maintainability": { "type": "number" }, + "operational_cost": { "type": "number" }, + "implementation_cost": { "type": "number" }, + "resource_consumption": { "type": "number" } + }, + "additionalProperties": false + } + } +} diff --git a/schemas/event.schema.json b/schemas/event.schema.json new file mode 100644 index 0000000..cf2a3a3 --- /dev/null +++ b/schemas/event.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/event.schema.json", + "title": "FLUID audit event", + "description": "Append-only record of a material lifecycle transition. FLUID evolution is itself a system behavior that must remain reconstructable (FluidHypothesisRevisionSchema.md section 15).", + "type": "object", + "properties": { + "fluid_event": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/eventId" }, + "occurred_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "entity_type": { "enum": ["pressure", "hypothesis", "revision", "experiment", "backend_requirement", "intent", "decision", "routing_policy"] }, + "entity_id": { "type": "string", "minLength": 1 }, + "event_type": { "type": "string", "minLength": 1 }, + "actor": { "$ref": "common.schema.json#/$defs/actor" }, + "inputs": { + "type": "array", + "items": { "type": "string" }, + "description": "Identifiers of the records this transition drew on." + }, + "reason": { "type": "string" }, + "evidence_refs": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/evidenceRef" } + } + }, + "required": ["schema_version", "id", "occurred_at", "entity_type", "entity_id", "event_type", "actor"], + "additionalProperties": false + } + }, + "required": ["fluid_event"], + "additionalProperties": false +} diff --git a/schemas/experiment.schema.json b/schemas/experiment.schema.json new file mode 100644 index 0000000..5e673d8 --- /dev/null +++ b/schemas/experiment.schema.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/experiment.schema.json", + "title": "FLUID experiment record", + "description": "Connects hypotheses to revisions under bounded conditions. Experiments must be interruptible (ArchitectureBlueprint.md section 16).", + "type": "object", + "properties": { + "fluid_experiment": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/experimentId" }, + "interface_id": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "hypothesis_refs": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/hypothesisId" }, + "minItems": 1, + "description": "An experiment without a hypothesis measures nothing in particular." + }, + "mechanism": { + "enum": ["sandbox", "shadow", "synthetic", "replay", "canary", "opt_in", "cohort", "tenant", "percentage"] + }, + "control_revision": { "$ref": "common.schema.json#/$defs/revisionId" }, + "candidate_revisions": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/revisionId" }, + "minItems": 1 + }, + "cohorts": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/cohortId" } }, + "allocation": { + "type": "object", + "description": "Shares by revision id or by the reserved key 'control'. Allocation is deterministic and enacted by the router, never by this record.", + "additionalProperties": { "$ref": "common.schema.json#/$defs/unitInterval" }, + "minProperties": 2 + }, + "start_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "planned_end_at": { "oneOf": [{ "$ref": "common.schema.json#/$defs/timestamp" }, { "type": "null" }] }, + "max_duration_hours": { "type": "number", "exclusiveMinimum": 0 }, + + "metrics": { + "type": "object", + "description": "Primary metrics and guardrails are kept distinct so a scalar can never hide a regression (ArchitectureBlueprint.md section 48.5).", + "properties": { + "primary": { "type": "array", "items": { "type": "string" }, "minItems": 1 }, + "guardrails": { "type": "array", "items": { "type": "string" } }, + "secondary": { "type": "array", "items": { "type": "string" } }, + "learning": { "type": "array", "items": { "type": "string" } } + }, + "required": ["primary"], + "additionalProperties": false + }, + + "start_conditions": { "type": "array", "items": { "type": "string" } }, + "stop_conditions": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Every experiment has a stop condition (ArchitectureBlueprint.md section 55, invariant 7)." + }, + + "amendments": { + "type": "array", + "description": "Success criteria may not be changed after results are visible without recording the amendment (ArchitectureBlueprint.md section 18).", + "items": { + "type": "object", + "properties": { + "at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "actor": { "$ref": "common.schema.json#/$defs/actor" }, + "change": { "type": "string", "minLength": 1 }, + "reason": { "type": "string", "minLength": 1 } + }, + "required": ["at", "actor", "change", "reason"], + "additionalProperties": false + } + }, + + "result": { + "type": "object", + "properties": { + "state": { "enum": ["PLANNED", "RUNNING", "STOPPED", "COMPLETED", "ABANDONED"] }, + "preferred_revision": { "oneOf": [{ "$ref": "common.schema.json#/$defs/revisionId" }, { "type": "null" }] }, + "stopped_reason": { "type": ["string", "null"] }, + "evidence_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/evidenceRef" } } + }, + "required": ["state"], + "additionalProperties": false + } + }, + "required": [ + "schema_version", "id", "interface_id", "hypothesis_refs", + "control_revision", "candidate_revisions", "allocation", + "metrics", "stop_conditions", "result" + ], + "additionalProperties": false + } + }, + "required": ["fluid_experiment"], + "additionalProperties": false +} diff --git a/schemas/feedback.schema.json b/schemas/feedback.schema.json new file mode 100644 index 0000000..8e7b31d --- /dev/null +++ b/schemas/feedback.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/feedback.schema.json", + "title": "FLUID explicit consumer feedback", + "description": "Structured feedback from a consumer. Treated as evidence and never as authority to change anything (FluidAPIStandards.md section 15).", + "type": "object", + "properties": { + "fluid_feedback": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/feedbackId" }, + "interface_id": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "received_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "cohort": { "$ref": "common.schema.json#/$defs/cohortId" }, + "revision": { "$ref": "common.schema.json#/$defs/revisionId" }, + "goal": { "type": "string" }, + "attempt": { "type": "string" }, + "attempted_operations": { "type": "array", "items": { "type": "string" } }, + "outcome": { "type": "string" }, + "missing_capability": { "type": "string" }, + "workaround": { "type": ["string", "array"], "items": { "type": "string" } }, + "impact": { + "type": "object", + "properties": { + "description": { "type": "string" }, + "calls": { "type": "integer", "minimum": 0 }, + "latency_ms": { "type": "number", "minimum": 0 } + }, + "additionalProperties": false + }, + "confidence": { "$ref": "common.schema.json#/$defs/unitInterval" } + }, + "required": ["schema_version", "id", "interface_id", "received_at", "goal"], + "additionalProperties": false + } + }, + "required": ["fluid_feedback"], + "additionalProperties": false +} diff --git a/schemas/hypothesis.schema.json b/schemas/hypothesis.schema.json new file mode 100644 index 0000000..e29181e --- /dev/null +++ b/schemas/hypothesis.schema.json @@ -0,0 +1,228 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/hypothesis.schema.json", + "title": "FLUID hypothesis record", + "description": "Why an interface change should be explored. Observation, explanation, prediction, intervention and result stay separate on purpose (FluidHypothesisRevisionSchema.md section 18).", + "type": "object", + "properties": { + "fluid_hypothesis": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/hypothesisId" }, + "interface_id": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "state": { + "enum": ["DRAFT", "READY", "PRIORITIZED", "DESIGNING", "EXPERIMENTING", "EVALUATING", "ACCEPTED", "REJECTED", "SUPERSEDED", "DEFERRED"] + }, + "created_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "created_by": { "$ref": "common.schema.json#/$defs/actor" }, + "title": { "type": "string", "minLength": 1 }, + + "observation": { + "type": "object", + "description": "What was seen. Not what it means.", + "properties": { + "summary": { "type": "string", "minLength": 1 }, + "evidence_refs": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/evidenceRef" }, + "minItems": 1 + }, + "affected_cohorts": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/cohortId" } }, + "observation_window": { "$ref": "common.schema.json#/$defs/observationWindow" } + }, + "required": ["summary", "evidence_refs"], + "additionalProperties": false + }, + + "pressure": { + "type": "object", + "properties": { + "classes": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/pressureClass" }, "minItems": 1 }, + "pressure_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/pressureId" } }, + "severity": { "$ref": "common.schema.json#/$defs/unitInterval" }, + "confidence": { "$ref": "common.schema.json#/$defs/unitInterval" } + }, + "required": ["classes"], + "additionalProperties": false + }, + + "explanation": { + "type": "object", + "description": "What we think explains the observation.", + "properties": { + "claim": { "type": "string", "minLength": 1 }, + "reach": { + "type": "object", + "description": "Explanatory reach is a prioritization signal, never a correctness claim (FluidAPIStandards.md section 18).", + "properties": { + "score": { "type": "number" }, + "explains": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/pressureId" } }, + "notes": { "type": "string" } + }, + "additionalProperties": false + } + }, + "required": ["claim"], + "additionalProperties": false + }, + + "proposed_adaptation": { + "type": "object", + "description": "What we propose to change.", + "properties": { + "class": { "$ref": "common.schema.json#/$defs/adaptationClass" }, + "summary": { "type": "string", "minLength": 1 }, + "candidate_contract": { + "type": "object", + "description": "Shape is protocol-specific and deliberately unconstrained.", + "additionalProperties": true + }, + "implementation_scope": { "enum": ["interface_only", "interface_and_backend", "documentation_only"] } + }, + "required": ["class", "summary"], + "additionalProperties": false + }, + + "backend_requirements": { + "type": "object", + "properties": { + "required": { "type": "boolean" }, + "requirement_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/backendRequirementId" } } + }, + "required": ["required"], + "additionalProperties": false + }, + + "expected_outcomes": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/expectedOutcome" }, + "minItems": 1, + "description": "A hypothesis must be falsifiable (FluidAPIStandards.md section 17)." + }, + "guardrails": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/guardrail" } + }, + + "fitness_dimensions": { + "type": "object", + "properties": { + "expected": { "$ref": "common.schema.json#/$defs/fitnessDimensions" } + }, + "additionalProperties": false + }, + + "complexity": { + "type": "object", + "properties": { + "expected_delta": { "$ref": "common.schema.json#/$defs/complexityDelta" }, + "score": { "type": "number" } + }, + "required": ["expected_delta"], + "additionalProperties": false + }, + + "risk": { + "type": "object", + "properties": { + "level": { "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] }, + "reasons": { "type": "array", "items": { "type": "string" } } + }, + "required": ["level"], + "additionalProperties": false + }, + + "economics": { + "type": "object", + "description": "A hypothesis may be valuable but not yet worth exploring (ArchitectureBlueprint.md section 29).", + "properties": { + "estimated_experiment_cost": { "type": "number", "minimum": 0 }, + "estimated_implementation_cost": { "type": "number", "minimum": 0 }, + "currency": { "type": "string" }, + "expected_value_class": { "enum": ["LOW", "MEDIUM", "HIGH"] } + }, + "additionalProperties": false + }, + + "learning_value": { + "type": "object", + "properties": { + "score": { "type": "number" }, + "notes": { "type": "string" } + }, + "additionalProperties": false + }, + + "priority": { + "type": "object", + "properties": { + "score": { "type": "number" }, + "decided_by": { "type": "string" } + }, + "additionalProperties": false + }, + + "competition": { + "type": "object", + "description": "Competing hypotheses are legitimate (FluidAPIStandards.md section 19).", + "properties": { + "group_id": { "type": "string", "minLength": 1 }, + "alternatives": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/hypothesisId" } } + }, + "required": ["group_id"], + "additionalProperties": false + }, + + "success_criteria": { + "type": "object", + "properties": { "expression": { "type": "string", "minLength": 1 } }, + "required": ["expression"], + "additionalProperties": false + }, + "failure_criteria": { + "type": "object", + "properties": { "expression": { "type": "string", "minLength": 1 } }, + "required": ["expression"], + "additionalProperties": false + }, + + "candidate_revision_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/revisionId" } }, + "experiment_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/experimentId" } }, + + "outcome": { + "type": "object", + "description": "What happened afterwards. Null until evaluation completes.", + "properties": { + "status": { + "oneOf": [ + { "enum": ["CONFIRMED", "REFUTED", "INCONCLUSIVE"] }, + { "type": "null" } + ] + }, + "summary": { "type": ["string", "null"] }, + "evidence_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/evidenceRef" } } + }, + "additionalProperties": false + }, + + "audit": { + "type": "object", + "properties": { + "decision_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/decisionId" } }, + "immutable_event_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/eventId" } } + }, + "additionalProperties": false + } + }, + "required": [ + "schema_version", "id", "interface_id", "state", "title", + "observation", "pressure", "explanation", "proposed_adaptation", + "expected_outcomes", "guardrails", "complexity", "risk", "success_criteria" + ], + "additionalProperties": false + } + }, + "required": ["fluid_hypothesis"], + "additionalProperties": false +} diff --git a/schemas/pressure.schema.json b/schemas/pressure.schema.json new file mode 100644 index 0000000..5d5a20f --- /dev/null +++ b/schemas/pressure.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/pressure.schema.json", + "title": "FLUID pressure record", + "description": "Evidence that the interface differs materially from consumer needs. Pressure is evidence, not truth (FluidAPIStandards.md section 13).", + "type": "object", + "properties": { + "fluid_pressure": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/pressureId" }, + "interface_id": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "class": { "$ref": "common.schema.json#/$defs/pressureClass" }, + "first_seen": { "$ref": "common.schema.json#/$defs/timestamp" }, + "last_seen": { "$ref": "common.schema.json#/$defs/timestamp" }, + "affected_cohorts": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/cohortId" } + }, + "frequency": { + "type": "object", + "properties": { + "observations": { "type": "integer", "minimum": 0 }, + "independent_consumers": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + "severity": { "$ref": "common.schema.json#/$defs/unitInterval" }, + "confidence": { "$ref": "common.schema.json#/$defs/unitInterval" }, + "summary": { "type": "string", "minLength": 1 }, + "evidence_refs": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/evidenceRef" }, + "minItems": 1, + "description": "A pressure record without evidence references is not auditable and must be rejected." + }, + "status": { + "enum": ["OPEN", "ANALYZING", "EXPLAINED", "ADDRESSED", "DISMISSED", "OUT_OF_SCOPE"], + "description": "ArchitectureBlueprint.md section 9. Pressure may remain unresolved on purpose." + }, + "linked_hypotheses": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/hypothesisId" } + } + }, + "required": ["schema_version", "id", "interface_id", "class", "first_seen", "last_seen", "summary", "evidence_refs", "status"], + "additionalProperties": false + } + }, + "required": ["fluid_pressure"], + "additionalProperties": false +} diff --git a/schemas/revision-descriptor.schema.json b/schemas/revision-descriptor.schema.json new file mode 100644 index 0000000..3400eac --- /dev/null +++ b/schemas/revision-descriptor.schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/revision-descriptor.schema.json", + "title": "FLUID revision descriptor", + "description": "The runtime-facing projection of a revision, deliberately small enough to be consumed deterministically by infrastructure (ArchitectureBlueprint.md section 36). This is a wire artifact: the gateway must be able to read it without any fluid-core library.", + "type": "object", + "properties": { + "revision": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/revisionId" }, + "interface": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "state": { + "enum": ["created", "verified", "experiment", "candidate", "stable", "deprecated", "retired"], + "description": "The router refuses to route to created, failed or retired revisions (ArchitectureBlueprint.md section 5.3)." + }, + + "contract": { + "type": "object", + "properties": { + "type": { "enum": ["openapi", "graphql", "protobuf", "asyncapi", "jsonschema", "custom"] }, + "digest": { "$ref": "common.schema.json#/$defs/digest" }, + "source": { "type": "string", "description": "Where the contract artifact can be fetched." } + }, + "required": ["type", "digest"], + "additionalProperties": false + }, + + "runtime": { + "type": "object", + "description": "Out-of-process attachment: the adapter is an upstream reached over the network, so it may be implemented in any language.", + "properties": { + "upstream": { + "type": "string", + "format": "uri", + "description": "Base URL of the adapter process serving this revision." + }, + "image": { "type": "string" }, + "digest": { "$ref": "common.schema.json#/$defs/digest" }, + "timeout_ms": { "type": "integer", "exclusiveMinimum": 0, "default": 5000 }, + "retry": { + "type": "object", + "properties": { + "max_attempts": { "type": "integer", "minimum": 1, "default": 1 }, + "backoff_ms": { "type": "integer", "minimum": 0 }, + "retry_on": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + }, + "circuit_breaker": { + "type": "object", + "properties": { + "failure_threshold": { "type": "integer", "minimum": 1 }, + "reset_after_ms": { "type": "integer", "exclusiveMinimum": 0 } + }, + "additionalProperties": false + } + }, + "required": ["upstream"], + "additionalProperties": false + }, + + "intent": { + "type": "object", + "description": "Which interface evolution intent governs this revision. Required: a revision with no governing intent cannot be audited.", + "properties": { + "version": { "type": "string", "minLength": 1 }, + "digest": { "$ref": "common.schema.json#/$defs/digest" } + }, + "required": ["version"], + "additionalProperties": false + }, + + "routing": { + "type": "object", + "properties": { + "eligible_cohorts": { + "type": "array", + "items": { "$ref": "common.schema.json#/$defs/cohortId" }, + "description": "Empty or absent means all cohorts are eligible." + }, + "max_traffic_share": { + "$ref": "common.schema.json#/$defs/unitInterval", + "description": "Ceiling the router enforces regardless of what a routing policy asks for." + } + }, + "additionalProperties": false + }, + + "policy": { + "type": "object", + "properties": { + "compatibility": { + "enum": ["backward_compatible", "forward_compatible", "additive", "behavioral_change", "breaking", "migration_only", "internal_only"] + }, + "security_check": { "enum": ["pending", "passed", "failed"] }, + "policy_check": { "enum": ["pending", "passed", "failed"] }, + "rollback_to": { "oneOf": [{ "$ref": "common.schema.json#/$defs/revisionId" }, { "type": "null" }] } + }, + "required": ["compatibility", "security_check"], + "additionalProperties": false + }, + + "signature": { + "type": "object", + "description": "The router accepts only signed published descriptors (ArchitectureBlueprint.md section 35). The signature covers the canonical form of this document with the signature member removed.", + "properties": { + "algorithm": { "enum": ["ed25519"] }, + "key_id": { "type": "string", "minLength": 1 }, + "value": { "type": "string", "contentEncoding": "base64" }, + "signed_at": { "$ref": "common.schema.json#/$defs/timestamp" } + }, + "required": ["algorithm", "key_id", "value"], + "additionalProperties": false + } + }, + "required": ["schema_version", "id", "interface", "state", "contract", "runtime", "intent", "policy"], + "additionalProperties": false + } + }, + "required": ["revision"], + "additionalProperties": false +} diff --git a/schemas/revision.schema.json b/schemas/revision.schema.json new file mode 100644 index 0000000..9a9d7ab --- /dev/null +++ b/schemas/revision.schema.json @@ -0,0 +1,235 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/revision.schema.json", + "title": "FLUID revision record", + "description": "What deterministic interface state was built, verified, exposed and measured. Published revisions should be immutable; corrections create successors (FluidAPIStandards.md section 8).", + "type": "object", + "properties": { + "fluid_revision": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "$ref": "common.schema.json#/$defs/revisionId" }, + "interface_id": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "revision_number": { "type": "integer", "minimum": 0 }, + "parent_revision": { + "oneOf": [ + { "$ref": "common.schema.json#/$defs/revisionId" }, + { "type": "null" } + ], + "description": "Null marks genesis." + }, + "state": { + "enum": ["CREATED", "VERIFIED", "EXPERIMENT", "CANDIDATE", "STABLE", "DEPRECATED", "RETIRED"] + }, + "created_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "created_by": { "$ref": "common.schema.json#/$defs/actor" }, + + "contract": { + "type": "object", + "properties": { + "type": { "enum": ["openapi", "graphql", "protobuf", "asyncapi", "jsonschema", "custom"] }, + "version": { "type": "string" }, + "artifact_ref": { "$ref": "common.schema.json#/$defs/artifactRef" }, + "digest": { "$ref": "common.schema.json#/$defs/digest" } + }, + "required": ["type", "artifact_ref", "digest"], + "additionalProperties": false + }, + + "implementation": { + "type": "object", + "properties": { + "artifact_ref": { "$ref": "common.schema.json#/$defs/artifactRef" }, + "source_ref": { "type": "string" }, + "build_ref": { "type": "string" }, + "digest": { "$ref": "common.schema.json#/$defs/digest" } + }, + "required": ["artifact_ref", "digest"], + "additionalProperties": false + }, + + "interface_evolution_intent": { + "type": "object", + "description": "Every revision is governed by a specific intent version, so audit can ask whether the change was valid under the intent that existed at the time (ArchitectureBlueprint.md section 27).", + "properties": { + "version": { "type": "string", "minLength": 1 }, + "artifact_ref": { "$ref": "common.schema.json#/$defs/artifactRef" } + }, + "required": ["version"], + "additionalProperties": false + }, + + "originating_hypotheses": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/hypothesisId" } }, + "backend_requirements": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/backendRequirementId" } }, + + "compatibility": { + "type": "object", + "properties": { + "class": { + "enum": ["BACKWARD_COMPATIBLE", "FORWARD_COMPATIBLE", "ADDITIVE", "BEHAVIORAL_CHANGE", "BREAKING", "MIGRATION_ONLY", "INTERNAL_ONLY"] + }, + "breaking_changes": { "type": "array", "items": { "type": "string" } }, + "deprecations": { "type": "array", "items": { "type": "string" } }, + "supersedes": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/revisionId" } }, + "compatibility_evidence_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/evidenceRef" } } + }, + "required": ["class"], + "additionalProperties": false + }, + + "adaptation_classes": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/adaptationClass" } }, + + "verification": { + "type": "object", + "description": "The primary safety barrier between adaptive generation and deterministic runtime (ArchitectureBlueprint.md section 15).", + "properties": { + "status": { "enum": ["PENDING", "PASSED", "FAILED"] }, + "test_refs": { "type": "array", "items": { "type": "string" } }, + "security_check": { "enum": ["PENDING", "PASSED", "FAILED"] }, + "policy_check": { "enum": ["PENDING", "PASSED", "FAILED"] } + }, + "required": ["status"], + "additionalProperties": false + }, + + "complexity": { + "type": "object", + "properties": { + "before": { "$ref": "common.schema.json#/$defs/complexityDelta" }, + "after": { "$ref": "common.schema.json#/$defs/complexityDelta" }, + "delta_score": { "type": "number" }, + "budget_status": { "enum": ["WITHIN_BUDGET", "AT_BUDGET", "OVER_BUDGET"] } + }, + "additionalProperties": false + }, + + "deployment": { + "type": "object", + "properties": { + "environments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "started_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "routing_policy_ref": { "type": "string" } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "current_exposure": { + "type": "object", + "properties": { + "traffic_share": { "$ref": "common.schema.json#/$defs/unitInterval" }, + "cohorts": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/cohortId" } } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "fitness": { + "type": "object", + "properties": { + "baseline_revision": { "oneOf": [{ "$ref": "common.schema.json#/$defs/revisionId" }, { "type": "null" }] }, + "measurement_window": { "$ref": "common.schema.json#/$defs/observationWindow" }, + "metrics": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "baseline": { "type": ["number", "null"] }, + "current": { "type": ["number", "null"] }, + "target": { "type": ["number", "null"] }, + "guardrail": { "type": ["number", "null"] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + + "adoption": { + "type": "object", + "description": "Adoption is evidence, not proof of quality (FluidAPIStandards.md section 32).", + "properties": { + "eligible_consumers": { "type": ["integer", "null"], "minimum": 0 }, + "active_consumers": { "type": ["integer", "null"], "minimum": 0 }, + "adoption_rate": { "oneOf": [{ "$ref": "common.schema.json#/$defs/unitInterval" }, { "type": "null" }] }, + "retained_adoption_rate": { "oneOf": [{ "$ref": "common.schema.json#/$defs/unitInterval" }, { "type": "null" }] } + }, + "additionalProperties": false + }, + + "economics": { + "type": "object", + "properties": { + "build_cost": { "type": "number", "minimum": 0 }, + "experiment_cost_to_date": { "type": "number", "minimum": 0 }, + "currency": { "type": "string" } + }, + "additionalProperties": false + }, + + "promotion": { + "type": "object", + "properties": { + "eligible": { "type": "boolean" }, + "recommended_state": { + "oneOf": [ + { "enum": ["CREATED", "VERIFIED", "EXPERIMENT", "CANDIDATE", "STABLE", "DEPRECATED", "RETIRED"] }, + { "type": "null" } + ] + }, + "recommendation_reason": { "type": ["string", "null"] }, + "authorized_by": { "oneOf": [{ "$ref": "common.schema.json#/$defs/actor" }, { "type": "null" }] }, + "authorized_at": { "oneOf": [{ "$ref": "common.schema.json#/$defs/timestamp" }, { "type": "null" }] } + }, + "additionalProperties": false + }, + + "rollback": { + "type": "object", + "properties": { + "supported": { "type": "boolean" }, + "target_revision": { "oneOf": [{ "$ref": "common.schema.json#/$defs/revisionId" }, { "type": "null" }] }, + "procedure_ref": { "type": "string" } + }, + "required": ["supported"], + "additionalProperties": false + }, + + "provenance": { + "type": "object", + "properties": { + "decision_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/decisionId" } }, + "experiment_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/experimentId" } }, + "telemetry_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/evidenceRef" } } + }, + "additionalProperties": false + }, + + "audit": { + "type": "object", + "properties": { + "immutable_event_refs": { "type": "array", "items": { "$ref": "common.schema.json#/$defs/eventId" } } + }, + "additionalProperties": false + } + }, + "required": [ + "schema_version", "id", "interface_id", "revision_number", "parent_revision", + "state", "contract", "implementation", "interface_evolution_intent", + "compatibility", "verification", "rollback", "provenance" + ], + "additionalProperties": false + } + }, + "required": ["fluid_revision"], + "additionalProperties": false +} diff --git a/schemas/routing-policy.schema.json b/schemas/routing-policy.schema.json new file mode 100644 index 0000000..7717197 --- /dev/null +++ b/schemas/routing-policy.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/routing-policy.schema.json", + "title": "FLUID routing policy", + "description": "Deterministic routing written by the experiment controller and consumed by the revision router. The controller never touches traffic itself; this separation keeps experimental intent out of the runtime decision mechanism (ArchitectureBlueprint.md section 17).", + "type": "object", + "properties": { + "routing_policy": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "type": "string", "minLength": 1 }, + "interface": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "generation": { + "type": "integer", + "minimum": 0, + "description": "Monotonic. The router ignores a policy older than the one it holds." + }, + "issued_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "issued_by": { "$ref": "common.schema.json#/$defs/actor" }, + + "default_revision": { + "$ref": "common.schema.json#/$defs/revisionId", + "description": "Where a request lands when no earlier resolution step matched." + }, + + "rules": { + "type": "array", + "description": "Evaluated in order; the first matching rule wins. Matching must be a pure function of the request and its cohort assignment.", + "items": { + "type": "object", + "properties": { + "cohort": { "$ref": "common.schema.json#/$defs/cohortId" }, + "tenant": { "type": "string" }, + "experiment": { "$ref": "common.schema.json#/$defs/experimentId" }, + "allocation": { + "type": "object", + "description": "Revision id to traffic share. Shares must sum to 1. Assignment of a given consumer must be stable across requests.", + "propertyNames": { "$ref": "common.schema.json#/$defs/revisionId" }, + "additionalProperties": { "$ref": "common.schema.json#/$defs/unitInterval" }, + "minProperties": 1 + }, + "sticky_by": { + "enum": ["consumer_id", "tenant", "correlation_id", "none"], + "default": "consumer_id", + "description": "What keeps a long-lived consumer on one side of an experiment." + } + }, + "required": ["allocation"], + "additionalProperties": false + } + }, + + "signature": { + "type": "object", + "properties": { + "algorithm": { "enum": ["ed25519"] }, + "key_id": { "type": "string", "minLength": 1 }, + "value": { "type": "string", "contentEncoding": "base64" } + }, + "required": ["algorithm", "key_id", "value"], + "additionalProperties": false + } + }, + "required": ["schema_version", "interface", "generation", "default_revision", "rules"], + "additionalProperties": false + } + }, + "required": ["routing_policy"], + "additionalProperties": false +} diff --git a/schemas/telemetry-envelope.schema.json b/schemas/telemetry-envelope.schema.json new file mode 100644 index 0000000..2a55b7f --- /dev/null +++ b/schemas/telemetry-envelope.schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://fluid.dev/schemas/telemetry-envelope.schema.json", + "title": "FLUID telemetry envelope", + "description": "The normalized interaction event. Designed for interface learning, not unrestricted behavioral capture: raw payload capture is never the default and redaction happens before an event reaches the store (ArchitectureBlueprint.md section 6.2).", + "type": "object", + "properties": { + "fluid_telemetry": { + "type": "object", + "properties": { + "schema_version": { "$ref": "common.schema.json#/$defs/schemaVersion" }, + "id": { "type": "string", "minLength": 1 }, + "occurred_at": { "$ref": "common.schema.json#/$defs/timestamp" }, + "interface_id": { "$ref": "common.schema.json#/$defs/interfaceId" }, + "kind": { + "enum": ["request", "error", "sequence", "adoption", "feedback"], + "description": "ArchitectureBlueprint.md section 6.1." + }, + + "correlation_id": { + "type": "string", + "description": "Ties an event to the response the consumer saw, and to the other events in its call chain." + }, + "consumer_ref": { + "type": "string", + "description": "Pseudonymous and stable. Never a raw end-user identifier." + }, + "cohort": { "$ref": "common.schema.json#/$defs/cohortId" }, + "revision": { "$ref": "common.schema.json#/$defs/revisionId" }, + "experiment": { "oneOf": [{ "$ref": "common.schema.json#/$defs/experimentId" }, { "type": "null" }] }, + + "resolution": { + "type": "object", + "description": "Why this revision was chosen. Revision resolution must be deterministic and auditable (ArchitectureBlueprint.md section 5.2).", + "properties": { + "reason": { + "enum": ["explicit_revision", "bound_contract", "experiment_assignment", "cohort_rule", "stable_default"] + }, + "policy_generation": { "type": "integer", "minimum": 0 } + }, + "required": ["reason"], + "additionalProperties": false + }, + + "request": { + "type": "object", + "properties": { + "operation": { "type": "string" }, + "route": { "type": "string" }, + "method": { "type": "string" }, + "status": { "type": "integer" }, + "latency_ms": { "type": "number", "minimum": 0 }, + "request_bytes": { "type": "integer", "minimum": 0 }, + "response_bytes": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + + "error": { + "type": "object", + "properties": { + "class": { + "enum": ["validation", "unknown_path", "unknown_field", "unsupported_parameter", "authorization", "backend_failure", "timeout", "policy_rejection"] + }, + "detail": { "type": "string", "description": "Redacted. Must not carry backend internals or consumer data." }, + "field": { "type": "string" } + }, + "required": ["class"], + "additionalProperties": false + }, + + "sequence": { + "type": "object", + "description": "Interaction topology is often more informative than error counts (ArchitectureBlueprint.md section 6.4).", + "properties": { + "chain_id": { "type": "string" }, + "position": { "type": "integer", "minimum": 0 }, + "pattern": { "type": "string" }, + "repeat_count": { "type": "integer", "minimum": 0 } + }, + "additionalProperties": false + }, + + "adoption": { + "type": "object", + "properties": { + "event": { "enum": ["first_use", "continued_use", "migration", "reversion", "deprecation_response"] }, + "from_revision": { "oneOf": [{ "$ref": "common.schema.json#/$defs/revisionId" }, { "type": "null" }] } + }, + "required": ["event"], + "additionalProperties": false + }, + + "feedback_ref": { "$ref": "common.schema.json#/$defs/feedbackId" }, + + "redaction": { + "type": "object", + "description": "What the privacy filter removed. Recorded so analysis knows what it cannot see.", + "properties": { + "applied": { "type": "boolean" }, + "rules": { "type": "array", "items": { "type": "string" } } + }, + "required": ["applied"], + "additionalProperties": false + } + }, + "required": ["schema_version", "id", "occurred_at", "interface_id", "kind"], + "additionalProperties": false + } + }, + "required": ["fluid_telemetry"], + "additionalProperties": false +} From 76912adef8c82ca9d839db97cf77b285cde6bd66 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 4 Sep 2026 02:03:12 +0200 Subject: [PATCH 3/3] Generate contract types, pin fixtures to spec, add build and ADRs Completes FLUID-WP-0002. The record types in internal/contract are generated from schemas/ by a dependency-free generator; fixtures transcribed from the spec's worked examples validate against those schemas and round-trip through the generated types with DisallowUnknownFields, so a spec change that misses the schemas fails CI rather than drifting silently. Adds identifier prefix helpers, Makefile, GitHub Actions, and five ADRs recording the Go choice, out-of-process attachment, the wire contract as boundary, the evidence store, and revision identity. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400 --- .github/workflows/ci.yml | 42 ++ Makefile | 54 ++ conformance/fixtures/README.md | 28 + conformance/fixtures/backend-requirement.yaml | 37 ++ conformance/fixtures/event.yaml | 24 + conformance/fixtures/experiment.yaml | 41 ++ conformance/fixtures/feedback.yaml | 22 + conformance/fixtures/hypothesis.yaml | 156 ++++++ .../fixtures/json/backend-requirement.json | 38 ++ conformance/fixtures/json/event.json | 22 + conformance/fixtures/json/experiment.json | 44 ++ conformance/fixtures/json/feedback.json | 26 + conformance/fixtures/json/hypothesis.json | 164 ++++++ conformance/fixtures/json/pressure.json | 25 + .../fixtures/json/revision-descriptor.json | 35 ++ conformance/fixtures/json/revision.json | 150 ++++++ conformance/fixtures/json/routing-policy.json | 25 + .../fixtures/json/telemetry-envelope.json | 34 ++ conformance/fixtures/pressure.yaml | 29 + conformance/fixtures/revision-descriptor.yaml | 32 ++ conformance/fixtures/revision.yaml | 134 +++++ conformance/fixtures/routing-policy.yaml | 20 + conformance/fixtures/telemetry-envelope.yaml | 32 ++ conformance/validate_schemas.py | 193 +++++++ .../adr/0001-go-as-implementation-language.md | 46 ++ docs/adr/0002-out-of-process-attachment.md | 43 ++ docs/adr/0003-wire-contract-as-boundary.md | 43 ++ .../0004-evidence-store-sqlite-to-postgres.md | 37 ++ docs/adr/0005-revision-identity.md | 47 ++ internal/contract/backend_requirement_gen.go | 100 ++++ internal/contract/common_gen.go | 207 ++++++++ internal/contract/contract_test.go | 203 +++++++ internal/contract/event_gen.go | 48 ++ internal/contract/experiment_gen.go | 108 ++++ internal/contract/feedback_gen.go | 33 ++ internal/contract/hypothesis_gen.go | 254 +++++++++ internal/contract/ids.go | 91 ++++ internal/contract/pressure_gen.go | 58 ++ internal/contract/revision_descriptor_gen.go | 215 ++++++++ internal/contract/revision_gen.go | 314 +++++++++++ internal/contract/routing_policy_gen.go | 82 +++ internal/contract/telemetry_envelope_gen.go | 172 ++++++ tools/schemagen/main.go | 495 ++++++++++++++++++ ...D-WP-0002-foundations-and-wire-contract.md | 14 +- 44 files changed, 4010 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Makefile create mode 100644 conformance/fixtures/README.md create mode 100644 conformance/fixtures/backend-requirement.yaml create mode 100644 conformance/fixtures/event.yaml create mode 100644 conformance/fixtures/experiment.yaml create mode 100644 conformance/fixtures/feedback.yaml create mode 100644 conformance/fixtures/hypothesis.yaml create mode 100644 conformance/fixtures/json/backend-requirement.json create mode 100644 conformance/fixtures/json/event.json create mode 100644 conformance/fixtures/json/experiment.json create mode 100644 conformance/fixtures/json/feedback.json create mode 100644 conformance/fixtures/json/hypothesis.json create mode 100644 conformance/fixtures/json/pressure.json create mode 100644 conformance/fixtures/json/revision-descriptor.json create mode 100644 conformance/fixtures/json/revision.json create mode 100644 conformance/fixtures/json/routing-policy.json create mode 100644 conformance/fixtures/json/telemetry-envelope.json create mode 100644 conformance/fixtures/pressure.yaml create mode 100644 conformance/fixtures/revision-descriptor.yaml create mode 100644 conformance/fixtures/revision.yaml create mode 100644 conformance/fixtures/routing-policy.yaml create mode 100644 conformance/fixtures/telemetry-envelope.yaml create mode 100755 conformance/validate_schemas.py create mode 100644 docs/adr/0001-go-as-implementation-language.md create mode 100644 docs/adr/0002-out-of-process-attachment.md create mode 100644 docs/adr/0003-wire-contract-as-boundary.md create mode 100644 docs/adr/0004-evidence-store-sqlite-to-postgres.md create mode 100644 docs/adr/0005-revision-identity.md create mode 100644 internal/contract/backend_requirement_gen.go create mode 100644 internal/contract/common_gen.go create mode 100644 internal/contract/contract_test.go create mode 100644 internal/contract/event_gen.go create mode 100644 internal/contract/experiment_gen.go create mode 100644 internal/contract/feedback_gen.go create mode 100644 internal/contract/hypothesis_gen.go create mode 100644 internal/contract/ids.go create mode 100644 internal/contract/pressure_gen.go create mode 100644 internal/contract/revision_descriptor_gen.go create mode 100644 internal/contract/revision_gen.go create mode 100644 internal/contract/routing_policy_gen.go create mode 100644 internal/contract/telemetry_envelope_gen.go create mode 100644 tools/schemagen/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cf956f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install schema validation dependencies + run: pip install --quiet jsonschema pyyaml + + # The spec-derived fixtures are validated first: if the specification and + # the schemas have diverged, nothing downstream is worth building. + - name: Validate schemas against spec fixtures + run: make validate + + # Generated types must match schemas/. A stale internal/contract means a + # schema change landed without regeneration. + - name: Check generated code is current + run: make check-generated + + - name: Build + run: make build + + - name: Vet + run: make vet + + - name: Test + run: make test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d445bb --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# FLUID core — build, generation and conformance. +# +# The record types in internal/contract are generated from schemas/. Never edit +# them by hand: run `make generate`. + +GO ?= go +PYTHON ?= python3 +PKGS := ./... + +.PHONY: all +all: generate build vet test validate + +.PHONY: generate +generate: ## Regenerate Go types from schemas/ + $(GO) run ./tools/schemagen + gofmt -w internal/contract + +.PHONY: build +build: + $(GO) build $(PKGS) + +.PHONY: vet +vet: + $(GO) vet $(PKGS) + +.PHONY: test +test: validate + $(GO) test $(PKGS) + +.PHONY: validate +validate: ## Validate spec-derived fixtures against the schemas + $(PYTHON) conformance/validate_schemas.py + +.PHONY: check-generated +check-generated: ## Fail if generated code is stale relative to schemas/ + @$(MAKE) --no-print-directory generate + @if ! git diff --quiet -- internal/contract; then \ + echo "internal/contract is stale — run 'make generate' and commit the result"; \ + git --no-pager diff --stat -- internal/contract; \ + exit 1; \ + fi + +.PHONY: conformance +conformance: ## Full conformance suite (grows through FLUID-WP-0007) + $(GO) test ./conformance/... $(PKGS) + +.PHONY: clean +clean: + rm -rf bin internal/contract/*.broken + +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' diff --git a/conformance/fixtures/README.md b/conformance/fixtures/README.md new file mode 100644 index 0000000..7e82221 --- /dev/null +++ b/conformance/fixtures/README.md @@ -0,0 +1,28 @@ +# Schema fixtures + +Every file here is transcribed from a worked example in `spec/`. They exist so +that a change to the specification which is not reflected in `schemas/` fails +the build. + +Two deviations from the spec text are deliberate and are the only ones allowed: + +1. **Placeholder digests.** The spec writes `sha256:`. Fixtures use a + syntactically valid digest, because the schema constrains the format. +2. **Placeholder artifact paths.** The spec writes `contractRef: ...`. Fixtures + use a plausible reference. + +Anything else that differs from the spec is a bug in the fixture or in the +schema, not a licence to relax the schema. + +| Fixture | Source | +|---|---| +| `hypothesis.yaml` | `FluidHypothesisRevisionSchema.md` §3 | +| `revision.yaml` | `FluidHypothesisRevisionSchema.md` §8 | +| `experiment.yaml` | `FluidHypothesisRevisionSchema.md` §12 | +| `pressure.yaml` | `FluidHypothesisRevisionSchema.md` §13 | +| `backend-requirement.yaml` | `FluidHypothesisRevisionSchema.md` §14 | +| `event.yaml` | `FluidHypothesisRevisionSchema.md` §15 | +| `revision-descriptor.yaml` | `ArchitectureBlueprint.md` §36 | +| `routing-policy.yaml` | `ArchitectureBlueprint.md` §17 | +| `feedback.yaml` | `FluidAPIStandards.md` §15, `ArchitectureBlueprint.md` §38 | +| `telemetry-envelope.yaml` | `ArchitectureBlueprint.md` §6.1 | diff --git a/conformance/fixtures/backend-requirement.yaml b/conformance/fixtures/backend-requirement.yaml new file mode 100644 index 0000000..1df6b76 --- /dev/null +++ b/conformance/fixtures/backend-requirement.yaml @@ -0,0 +1,37 @@ +fluid_backend_requirement: + schema_version: "0.1" + + id: "BR-0041" + originating_interface: "customer-context-api" + originating_revision: "R-0081" + originating_hypothesis: "H-0212" + + backend_service: "payments-service" + + capability: + title: "Historical payment-state query" + description: > + Retrieve payment state as of a specified timestamp. + + semantics: + consistency: "snapshot" + required_fields: + - "payment_id" + - "state" + - "effective_at" + + quality: + p95_latency_ms: 250 + availability: 0.999 + + security: + authorization_scope: "payments.read" + tenant_isolation: "required" + + expected_usage: + requests_per_day: 120000 + + disposition: + state: "PLANNED" + reason: null + target_ref: "payments-service/roadmap#412" diff --git a/conformance/fixtures/event.yaml b/conformance/fixtures/event.yaml new file mode 100644 index 0000000..51e0c8e --- /dev/null +++ b/conformance/fixtures/event.yaml @@ -0,0 +1,24 @@ +fluid_event: + schema_version: "0.1" + + id: "EV-990281" + occurred_at: "2026-09-09T11:31:02Z" + + entity_type: "revision" + entity_id: "R-000221" + + event_type: "PROMOTION_RECOMMENDED" + + actor: + type: "daimon" + id: "fluid-daimon/customer-finance-api" + + inputs: + - "H-000184" + - "E-000093" + + reason: > + Candidate met primary success target and violated no guardrails. + + evidence_refs: + - "metrics:E-000093/window-4" diff --git a/conformance/fixtures/experiment.yaml b/conformance/fixtures/experiment.yaml new file mode 100644 index 0000000..0de15a6 --- /dev/null +++ b/conformance/fixtures/experiment.yaml @@ -0,0 +1,41 @@ +fluid_experiment: + schema_version: "0.1" + + id: "E-000093" + interface_id: "customer-finance-api" + + hypothesis_refs: + - "H-000184" + + control_revision: "R-000220" + candidate_revisions: + - "R-000221" + + cohorts: + - "partner-integrations" + - "coding-agents" + + allocation: + control: 0.72 + candidate: 0.28 + + start_at: "2026-09-05T08:00:00Z" + planned_end_at: "2026-10-05T08:00:00Z" + + metrics: + primary: + - "requests_per_completed_task" + guardrails: + - "p95_latency_ms" + - "error_rate" + - "authorization_scope_change" + + stop_conditions: + - "hard_guardrail_violation" + - "security_failure" + - "manual_stop" + + result: + state: "RUNNING" + preferred_revision: null + evidence_refs: [] diff --git a/conformance/fixtures/feedback.yaml b/conformance/fixtures/feedback.yaml new file mode 100644 index 0000000..25986ca --- /dev/null +++ b/conformance/fixtures/feedback.yaml @@ -0,0 +1,22 @@ +fluid_feedback: + schema_version: "0.1" + + id: "F-9821" + interface_id: "customer-finance-api" + received_at: "2026-08-28T09:12:00Z" + cohort: "coding-agents" + revision: "R-000220" + + goal: "produce monthly account statement" + missing_capability: "consolidated-period-summary" + attempted_operations: + - "list-transactions" + - "get-balances" + outcome: "capability unavailable" + workaround: + - "aggregate-locally" + - "infer-closing-balance" + confidence: 0.94 + impact: + calls: 18 + latency_ms: 2810 diff --git a/conformance/fixtures/hypothesis.yaml b/conformance/fixtures/hypothesis.yaml new file mode 100644 index 0000000..aae29d4 --- /dev/null +++ b/conformance/fixtures/hypothesis.yaml @@ -0,0 +1,156 @@ +fluid_hypothesis: + schema_version: "0.1" + + id: "H-000184" + interface_id: "customer-finance-api" + + state: "PRIORITIZED" + + created_at: "2026-09-04T12:00:00Z" + created_by: + type: "daimon" + id: "fluid-daimon/customer-finance-api" + + title: "Provide direct latest-invoice capability" + + observation: + summary: > + A significant consumer cohort retrieves invoice collections, + sorts them by issue date, and discards all but the first item. + evidence_refs: + - "telemetry:invoice-pattern-2026w36" + - "feedback:F-9821" + affected_cohorts: + - "partner-integrations" + - "coding-agents" + observation_window: + start: "2026-08-15T00:00:00Z" + end: "2026-09-01T00:00:00Z" + + pressure: + classes: + - "successful_but_inefficient_usage" + - "repeated_expectation_mismatch" + severity: 0.61 + confidence: 0.88 + + explanation: + claim: > + Consumers treat "latest invoice" as a domain-level concept + and the existing generic collection interface does not present + that concept directly. + reach: + score: 0.42 + explains: + - "P-1831" + - "P-1834" + notes: > + Medium reach: explains several related request patterns + but not broader financial-context pressure. + + proposed_adaptation: + class: "contract" + summary: > + Add an explicit latest-invoice retrieval capability. + candidate_contract: + method: "GET" + path: "/customers/{id}/invoices/latest" + implementation_scope: "interface_only" + + backend_requirements: + required: false + requirement_refs: [] + + expected_outcomes: + - metric: "requests_per_completed_task" + cohort: "all" + baseline: 2.7 + target: 1.2 + direction: "lower" + - metric: "client_side_sort_workarounds" + cohort: "all" + baseline: 1.0 + target: 0.2 + direction: "lower" + + guardrails: + - metric: "p95_latency_ms" + operator: "<=" + threshold: 315 + - metric: "authorization_scope_change" + operator: "==" + threshold: false + - metric: "backend_query_count_delta" + operator: "<=" + threshold: 0 + + fitness_dimensions: + expected: + client_utility: 0.70 + correctness: 0.00 + reliability: 0.00 + performance: 0.10 + discoverability: 0.65 + simplicity: -0.15 + compatibility: 0.00 + security: 0.00 + maintainability: -0.05 + operational_cost: 0.00 + + complexity: + expected_delta: + surface_area: 1 + concept_count: 0 + operation_count: 1 + semantic_overlap: 0.1 + cognitive_load: -0.2 + score: 0.18 + + risk: + level: "LOW" + reasons: + - "read-only operation" + - "no authorization expansion" + + economics: + estimated_experiment_cost: 18.00 + estimated_implementation_cost: 40.00 + currency: "EUR" + expected_value_class: "MEDIUM" + + learning_value: + score: 0.56 + notes: > + Useful signal for whether first-class convenience resources + improve agent and partner integration behavior. + + priority: + score: 0.71 + decided_by: "optimization-policy-v3" + + success_criteria: + expression: > + requests_per_completed_task <= 1.2 + AND no hard guardrail violation + + failure_criteria: + expression: > + any hard guardrail violation + OR adoption below 5% after 30 days when equally discoverable + + candidate_revision_refs: + - "R-000221" + + experiment_refs: + - "E-000093" + + outcome: + status: null + summary: null + evidence_refs: [] + + audit: + decision_refs: + - "D-2811" + immutable_event_refs: + - "EV-990218" diff --git a/conformance/fixtures/json/backend-requirement.json b/conformance/fixtures/json/backend-requirement.json new file mode 100644 index 0000000..5c2478a --- /dev/null +++ b/conformance/fixtures/json/backend-requirement.json @@ -0,0 +1,38 @@ +{ + "fluid_backend_requirement": { + "backend_service": "payments-service", + "capability": { + "description": "Retrieve payment state as of a specified timestamp.\n", + "title": "Historical payment-state query" + }, + "disposition": { + "reason": null, + "state": "PLANNED", + "target_ref": "payments-service/roadmap#412" + }, + "expected_usage": { + "requests_per_day": 120000 + }, + "id": "BR-0041", + "originating_hypothesis": "H-0212", + "originating_interface": "customer-context-api", + "originating_revision": "R-0081", + "quality": { + "availability": 0.999, + "p95_latency_ms": 250 + }, + "schema_version": "0.1", + "security": { + "authorization_scope": "payments.read", + "tenant_isolation": "required" + }, + "semantics": { + "consistency": "snapshot", + "required_fields": [ + "payment_id", + "state", + "effective_at" + ] + } + } +} diff --git a/conformance/fixtures/json/event.json b/conformance/fixtures/json/event.json new file mode 100644 index 0000000..0fbd04f --- /dev/null +++ b/conformance/fixtures/json/event.json @@ -0,0 +1,22 @@ +{ + "fluid_event": { + "actor": { + "id": "fluid-daimon/customer-finance-api", + "type": "daimon" + }, + "entity_id": "R-000221", + "entity_type": "revision", + "event_type": "PROMOTION_RECOMMENDED", + "evidence_refs": [ + "metrics:E-000093/window-4" + ], + "id": "EV-990281", + "inputs": [ + "H-000184", + "E-000093" + ], + "occurred_at": "2026-09-09T11:31:02Z", + "reason": "Candidate met primary success target and violated no guardrails.\n", + "schema_version": "0.1" + } +} diff --git a/conformance/fixtures/json/experiment.json b/conformance/fixtures/json/experiment.json new file mode 100644 index 0000000..ddd4919 --- /dev/null +++ b/conformance/fixtures/json/experiment.json @@ -0,0 +1,44 @@ +{ + "fluid_experiment": { + "allocation": { + "candidate": 0.28, + "control": 0.72 + }, + "candidate_revisions": [ + "R-000221" + ], + "cohorts": [ + "partner-integrations", + "coding-agents" + ], + "control_revision": "R-000220", + "hypothesis_refs": [ + "H-000184" + ], + "id": "E-000093", + "interface_id": "customer-finance-api", + "metrics": { + "guardrails": [ + "p95_latency_ms", + "error_rate", + "authorization_scope_change" + ], + "primary": [ + "requests_per_completed_task" + ] + }, + "planned_end_at": "2026-10-05T08:00:00Z", + "result": { + "evidence_refs": [], + "preferred_revision": null, + "state": "RUNNING" + }, + "schema_version": "0.1", + "start_at": "2026-09-05T08:00:00Z", + "stop_conditions": [ + "hard_guardrail_violation", + "security_failure", + "manual_stop" + ] + } +} diff --git a/conformance/fixtures/json/feedback.json b/conformance/fixtures/json/feedback.json new file mode 100644 index 0000000..2b3a2f4 --- /dev/null +++ b/conformance/fixtures/json/feedback.json @@ -0,0 +1,26 @@ +{ + "fluid_feedback": { + "attempted_operations": [ + "list-transactions", + "get-balances" + ], + "cohort": "coding-agents", + "confidence": 0.94, + "goal": "produce monthly account statement", + "id": "F-9821", + "impact": { + "calls": 18, + "latency_ms": 2810 + }, + "interface_id": "customer-finance-api", + "missing_capability": "consolidated-period-summary", + "outcome": "capability unavailable", + "received_at": "2026-08-28T09:12:00Z", + "revision": "R-000220", + "schema_version": "0.1", + "workaround": [ + "aggregate-locally", + "infer-closing-balance" + ] + } +} diff --git a/conformance/fixtures/json/hypothesis.json b/conformance/fixtures/json/hypothesis.json new file mode 100644 index 0000000..2d4826d --- /dev/null +++ b/conformance/fixtures/json/hypothesis.json @@ -0,0 +1,164 @@ +{ + "fluid_hypothesis": { + "audit": { + "decision_refs": [ + "D-2811" + ], + "immutable_event_refs": [ + "EV-990218" + ] + }, + "backend_requirements": { + "required": false, + "requirement_refs": [] + }, + "candidate_revision_refs": [ + "R-000221" + ], + "complexity": { + "expected_delta": { + "cognitive_load": -0.2, + "concept_count": 0, + "operation_count": 1, + "semantic_overlap": 0.1, + "surface_area": 1 + }, + "score": 0.18 + }, + "created_at": "2026-09-04T12:00:00Z", + "created_by": { + "id": "fluid-daimon/customer-finance-api", + "type": "daimon" + }, + "economics": { + "currency": "EUR", + "estimated_experiment_cost": 18.0, + "estimated_implementation_cost": 40.0, + "expected_value_class": "MEDIUM" + }, + "expected_outcomes": [ + { + "baseline": 2.7, + "cohort": "all", + "direction": "lower", + "metric": "requests_per_completed_task", + "target": 1.2 + }, + { + "baseline": 1.0, + "cohort": "all", + "direction": "lower", + "metric": "client_side_sort_workarounds", + "target": 0.2 + } + ], + "experiment_refs": [ + "E-000093" + ], + "explanation": { + "claim": "Consumers treat \"latest invoice\" as a domain-level concept and the existing generic collection interface does not present that concept directly.\n", + "reach": { + "explains": [ + "P-1831", + "P-1834" + ], + "notes": "Medium reach: explains several related request patterns but not broader financial-context pressure.\n", + "score": 0.42 + } + }, + "failure_criteria": { + "expression": "any hard guardrail violation OR adoption below 5% after 30 days when equally discoverable\n" + }, + "fitness_dimensions": { + "expected": { + "client_utility": 0.7, + "compatibility": 0.0, + "correctness": 0.0, + "discoverability": 0.65, + "maintainability": -0.05, + "operational_cost": 0.0, + "performance": 0.1, + "reliability": 0.0, + "security": 0.0, + "simplicity": -0.15 + } + }, + "guardrails": [ + { + "metric": "p95_latency_ms", + "operator": "<=", + "threshold": 315 + }, + { + "metric": "authorization_scope_change", + "operator": "==", + "threshold": false + }, + { + "metric": "backend_query_count_delta", + "operator": "<=", + "threshold": 0 + } + ], + "id": "H-000184", + "interface_id": "customer-finance-api", + "learning_value": { + "notes": "Useful signal for whether first-class convenience resources improve agent and partner integration behavior.\n", + "score": 0.56 + }, + "observation": { + "affected_cohorts": [ + "partner-integrations", + "coding-agents" + ], + "evidence_refs": [ + "telemetry:invoice-pattern-2026w36", + "feedback:F-9821" + ], + "observation_window": { + "end": "2026-09-01T00:00:00Z", + "start": "2026-08-15T00:00:00Z" + }, + "summary": "A significant consumer cohort retrieves invoice collections, sorts them by issue date, and discards all but the first item.\n" + }, + "outcome": { + "evidence_refs": [], + "status": null, + "summary": null + }, + "pressure": { + "classes": [ + "successful_but_inefficient_usage", + "repeated_expectation_mismatch" + ], + "confidence": 0.88, + "severity": 0.61 + }, + "priority": { + "decided_by": "optimization-policy-v3", + "score": 0.71 + }, + "proposed_adaptation": { + "candidate_contract": { + "method": "GET", + "path": "/customers/{id}/invoices/latest" + }, + "class": "contract", + "implementation_scope": "interface_only", + "summary": "Add an explicit latest-invoice retrieval capability.\n" + }, + "risk": { + "level": "LOW", + "reasons": [ + "read-only operation", + "no authorization expansion" + ] + }, + "schema_version": "0.1", + "state": "PRIORITIZED", + "success_criteria": { + "expression": "requests_per_completed_task <= 1.2 AND no hard guardrail violation\n" + }, + "title": "Provide direct latest-invoice capability" + } +} diff --git a/conformance/fixtures/json/pressure.json b/conformance/fixtures/json/pressure.json new file mode 100644 index 0000000..9f90354 --- /dev/null +++ b/conformance/fixtures/json/pressure.json @@ -0,0 +1,25 @@ +{ + "fluid_pressure": { + "affected_cohorts": [ + "partner-integrations" + ], + "class": "successful_but_inefficient_usage", + "evidence_refs": [ + "telemetry:query-pattern-712" + ], + "first_seen": "2026-08-15T10:11:00Z", + "frequency": { + "independent_consumers": 83, + "observations": 1740 + }, + "id": "P-1831", + "interface_id": "customer-finance-api", + "last_seen": "2026-09-01T12:15:00Z", + "linked_hypotheses": [ + "H-000184" + ], + "schema_version": "0.1", + "status": "OPEN", + "summary": "Consumers repeatedly retrieve complete invoice collections to determine the latest invoice.\n" + } +} diff --git a/conformance/fixtures/json/revision-descriptor.json b/conformance/fixtures/json/revision-descriptor.json new file mode 100644 index 0000000..81b908c --- /dev/null +++ b/conformance/fixtures/json/revision-descriptor.json @@ -0,0 +1,35 @@ +{ + "revision": { + "contract": { + "digest": "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41", + "source": "artifact:openapi/customer-finance-api/r22", + "type": "openapi" + }, + "id": "R-000221", + "intent": { + "version": "IEI-7" + }, + "interface": "customer-finance-api", + "policy": { + "compatibility": "additive", + "policy_check": "passed", + "rollback_to": "R-000220", + "security_check": "passed" + }, + "routing": { + "eligible_cohorts": [ + "partner-integrations", + "coding-agents" + ], + "max_traffic_share": 0.3 + }, + "runtime": { + "digest": "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41", + "image": "registry/fluid/customer-finance:R-42", + "timeout_ms": 5000, + "upstream": "http://adapter-r22.fluid-runtime.svc:8080" + }, + "schema_version": "0.1", + "state": "candidate" + } +} diff --git a/conformance/fixtures/json/revision.json b/conformance/fixtures/json/revision.json new file mode 100644 index 0000000..501a722 --- /dev/null +++ b/conformance/fixtures/json/revision.json @@ -0,0 +1,150 @@ +{ + "fluid_revision": { + "adaptation_classes": [ + "contract" + ], + "adoption": { + "active_consumers": 119, + "adoption_rate": 0.289, + "eligible_consumers": 412, + "retained_adoption_rate": null + }, + "audit": { + "immutable_event_refs": [ + "EV-990221", + "EV-990244", + "EV-990281" + ] + }, + "backend_requirements": [], + "compatibility": { + "breaking_changes": [], + "class": "BACKWARD_COMPATIBLE", + "compatibility_evidence_refs": [ + "test:compat-suite-775" + ], + "deprecations": [], + "supersedes": [] + }, + "complexity": { + "after": { + "operation_count": 40, + "surface_area": 42 + }, + "before": { + "operation_count": 39, + "surface_area": 41 + }, + "budget_status": "WITHIN_BUDGET", + "delta_score": 0.18 + }, + "contract": { + "artifact_ref": "artifact:openapi/customer-finance-api/r22", + "digest": "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41", + "type": "openapi", + "version": "3.1" + }, + "created_at": "2026-09-04T13:10:00Z", + "created_by": { + "id": "fluid-daimon/customer-finance-api", + "type": "daimon" + }, + "deployment": { + "current_exposure": { + "cohorts": [ + "partner-integrations", + "coding-agents" + ], + "traffic_share": 0.28 + }, + "environments": [ + { + "name": "production", + "routing_policy_ref": "route-policy:rp-118", + "started_at": "2026-09-05T08:00:00Z" + } + ] + }, + "economics": { + "build_cost": 38.2, + "currency": "EUR", + "experiment_cost_to_date": 12.4 + }, + "fitness": { + "baseline_revision": "R-000220", + "measurement_window": { + "end": null, + "start": "2026-09-05T08:00:00Z" + }, + "metrics": { + "error_rate": { + "baseline": 0.008, + "current": 0.007 + }, + "p95_latency_ms": { + "baseline": 300, + "current": 302, + "guardrail": 315 + }, + "requests_per_completed_task": { + "baseline": 2.7, + "current": 1.15, + "target": 1.2 + } + } + }, + "id": "R-000221", + "implementation": { + "artifact_ref": "artifact:adapter/customer-finance-api/r22", + "build_ref": "build:8871", + "digest": "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41", + "source_ref": "git:9f4c2ab" + }, + "interface_evolution_intent": { + "artifact_ref": "artifact:intent/customer-finance-api/IEI-7", + "version": "IEI-7" + }, + "interface_id": "customer-finance-api", + "originating_hypotheses": [ + "H-000184" + ], + "parent_revision": "R-000220", + "promotion": { + "authorized_at": null, + "authorized_by": null, + "eligible": true, + "recommendation_reason": "Target fitness reached with no guardrail violations.\n", + "recommended_state": "STABLE" + }, + "provenance": { + "decision_refs": [ + "D-2811" + ], + "experiment_refs": [ + "E-000093" + ], + "telemetry_refs": [ + "telemetry:invoice-pattern-2026w36" + ] + }, + "revision_number": 22, + "rollback": { + "procedure_ref": "runbook:rollback-r22", + "supported": true, + "target_revision": "R-000220" + }, + "schema_version": "0.1", + "state": "CANDIDATE", + "verification": { + "policy_check": "PASSED", + "security_check": "PASSED", + "status": "PASSED", + "test_refs": [ + "test:unit-12081", + "test:contract-8821", + "test:security-610", + "test:compat-suite-775" + ] + } + } +} diff --git a/conformance/fixtures/json/routing-policy.json b/conformance/fixtures/json/routing-policy.json new file mode 100644 index 0000000..91e71e2 --- /dev/null +++ b/conformance/fixtures/json/routing-policy.json @@ -0,0 +1,25 @@ +{ + "routing_policy": { + "default_revision": "R-000220", + "generation": 118, + "id": "rp-118", + "interface": "customer-finance-api", + "issued_at": "2026-09-05T08:00:00Z", + "issued_by": { + "id": "fluid-experiment-controller", + "type": "system" + }, + "rules": [ + { + "allocation": { + "R-000220": 0.8, + "R-000221": 0.2 + }, + "cohort": "coding-agents", + "experiment": "E-000093", + "sticky_by": "consumer_id" + } + ], + "schema_version": "0.1" + } +} diff --git a/conformance/fixtures/json/telemetry-envelope.json b/conformance/fixtures/json/telemetry-envelope.json new file mode 100644 index 0000000..6d6967d --- /dev/null +++ b/conformance/fixtures/json/telemetry-envelope.json @@ -0,0 +1,34 @@ +{ + "fluid_telemetry": { + "cohort": "coding-agents", + "consumer_ref": "psu-4d1e9c", + "correlation_id": "c-7f21ab90", + "experiment": "E-000093", + "id": "tl-01J8Z3XK7QW2", + "interface_id": "customer-finance-api", + "kind": "request", + "occurred_at": "2026-09-05T08:14:22Z", + "redaction": { + "applied": true, + "rules": [ + "drop-path-parameters", + "pseudonymize-consumer" + ] + }, + "request": { + "latency_ms": 118.4, + "method": "GET", + "operation": "getLatestInvoice", + "request_bytes": 0, + "response_bytes": 812, + "route": "/customers/{id}/invoices/latest", + "status": 200 + }, + "resolution": { + "policy_generation": 118, + "reason": "experiment_assignment" + }, + "revision": "R-000221", + "schema_version": "0.1" + } +} diff --git a/conformance/fixtures/pressure.yaml b/conformance/fixtures/pressure.yaml new file mode 100644 index 0000000..472c0ae --- /dev/null +++ b/conformance/fixtures/pressure.yaml @@ -0,0 +1,29 @@ +fluid_pressure: + schema_version: "0.1" + + id: "P-1831" + interface_id: "customer-finance-api" + + class: "successful_but_inefficient_usage" + + first_seen: "2026-08-15T10:11:00Z" + last_seen: "2026-09-01T12:15:00Z" + + affected_cohorts: + - "partner-integrations" + + frequency: + observations: 1740 + independent_consumers: 83 + + summary: > + Consumers repeatedly retrieve complete invoice collections + to determine the latest invoice. + + evidence_refs: + - "telemetry:query-pattern-712" + + status: "OPEN" + + linked_hypotheses: + - "H-000184" diff --git a/conformance/fixtures/revision-descriptor.yaml b/conformance/fixtures/revision-descriptor.yaml new file mode 100644 index 0000000..111d4c2 --- /dev/null +++ b/conformance/fixtures/revision-descriptor.yaml @@ -0,0 +1,32 @@ +revision: + schema_version: "0.1" + + id: "R-000221" + interface: "customer-finance-api" + state: "candidate" + + contract: + type: "openapi" + digest: "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41" + source: "artifact:openapi/customer-finance-api/r22" + + runtime: + upstream: "http://adapter-r22.fluid-runtime.svc:8080" + image: "registry/fluid/customer-finance:R-42" + digest: "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41" + timeout_ms: 5000 + + intent: + version: "IEI-7" + + routing: + eligible_cohorts: + - "partner-integrations" + - "coding-agents" + max_traffic_share: 0.30 + + policy: + compatibility: "additive" + security_check: "passed" + policy_check: "passed" + rollback_to: "R-000220" diff --git a/conformance/fixtures/revision.yaml b/conformance/fixtures/revision.yaml new file mode 100644 index 0000000..c1bf796 --- /dev/null +++ b/conformance/fixtures/revision.yaml @@ -0,0 +1,134 @@ +fluid_revision: + schema_version: "0.1" + + id: "R-000221" + interface_id: "customer-finance-api" + + revision_number: 22 + parent_revision: "R-000220" + + state: "CANDIDATE" + + created_at: "2026-09-04T13:10:00Z" + created_by: + type: "daimon" + id: "fluid-daimon/customer-finance-api" + + contract: + type: "openapi" + version: "3.1" + artifact_ref: "artifact:openapi/customer-finance-api/r22" + digest: "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41" + + implementation: + artifact_ref: "artifact:adapter/customer-finance-api/r22" + source_ref: "git:9f4c2ab" + build_ref: "build:8871" + digest: "sha256:3b1f9c2e7a4d8b6051e2c9f0a7d4b83e15c6f2a9d0b7e4c1836a5f2d9c0b7e41" + + interface_evolution_intent: + version: "IEI-7" + artifact_ref: "artifact:intent/customer-finance-api/IEI-7" + + originating_hypotheses: + - "H-000184" + + backend_requirements: [] + + compatibility: + class: "BACKWARD_COMPATIBLE" + breaking_changes: [] + deprecations: [] + supersedes: [] + compatibility_evidence_refs: + - "test:compat-suite-775" + + adaptation_classes: + - "contract" + + verification: + status: "PASSED" + test_refs: + - "test:unit-12081" + - "test:contract-8821" + - "test:security-610" + - "test:compat-suite-775" + security_check: "PASSED" + policy_check: "PASSED" + + complexity: + before: + surface_area: 41 + operation_count: 39 + after: + surface_area: 42 + operation_count: 40 + delta_score: 0.18 + budget_status: "WITHIN_BUDGET" + + deployment: + environments: + - name: "production" + started_at: "2026-09-05T08:00:00Z" + routing_policy_ref: "route-policy:rp-118" + current_exposure: + traffic_share: 0.28 + cohorts: + - "partner-integrations" + - "coding-agents" + + fitness: + baseline_revision: "R-000220" + measurement_window: + start: "2026-09-05T08:00:00Z" + end: null + metrics: + requests_per_completed_task: + baseline: 2.7 + current: 1.15 + target: 1.2 + p95_latency_ms: + baseline: 300 + current: 302 + guardrail: 315 + error_rate: + baseline: 0.008 + current: 0.007 + + adoption: + eligible_consumers: 412 + active_consumers: 119 + adoption_rate: 0.289 + retained_adoption_rate: null + + economics: + build_cost: 38.20 + experiment_cost_to_date: 12.40 + currency: "EUR" + + promotion: + eligible: true + recommended_state: "STABLE" + recommendation_reason: > + Target fitness reached with no guardrail violations. + authorized_by: null + authorized_at: null + + rollback: + supported: true + target_revision: "R-000220" + procedure_ref: "runbook:rollback-r22" + + provenance: + decision_refs: + - "D-2811" + experiment_refs: + - "E-000093" + telemetry_refs: + - "telemetry:invoice-pattern-2026w36" + + audit: + immutable_event_refs: + - "EV-990221" + - "EV-990244" + - "EV-990281" diff --git a/conformance/fixtures/routing-policy.yaml b/conformance/fixtures/routing-policy.yaml new file mode 100644 index 0000000..dc40783 --- /dev/null +++ b/conformance/fixtures/routing-policy.yaml @@ -0,0 +1,20 @@ +routing_policy: + schema_version: "0.1" + + id: "rp-118" + interface: "customer-finance-api" + generation: 118 + issued_at: "2026-09-05T08:00:00Z" + issued_by: + type: "system" + id: "fluid-experiment-controller" + + default_revision: "R-000220" + + rules: + - cohort: "coding-agents" + experiment: "E-000093" + allocation: + R-000220: 0.80 + R-000221: 0.20 + sticky_by: "consumer_id" diff --git a/conformance/fixtures/telemetry-envelope.yaml b/conformance/fixtures/telemetry-envelope.yaml new file mode 100644 index 0000000..8550496 --- /dev/null +++ b/conformance/fixtures/telemetry-envelope.yaml @@ -0,0 +1,32 @@ +fluid_telemetry: + schema_version: "0.1" + + id: "tl-01J8Z3XK7QW2" + occurred_at: "2026-09-05T08:14:22Z" + interface_id: "customer-finance-api" + kind: "request" + + correlation_id: "c-7f21ab90" + consumer_ref: "psu-4d1e9c" + cohort: "coding-agents" + revision: "R-000221" + experiment: "E-000093" + + resolution: + reason: "experiment_assignment" + policy_generation: 118 + + request: + operation: "getLatestInvoice" + route: "/customers/{id}/invoices/latest" + method: "GET" + status: 200 + latency_ms: 118.4 + request_bytes: 0 + response_bytes: 812 + + redaction: + applied: true + rules: + - "drop-path-parameters" + - "pseudonymize-consumer" diff --git a/conformance/validate_schemas.py b/conformance/validate_schemas.py new file mode 100755 index 0000000..1a1236b --- /dev/null +++ b/conformance/validate_schemas.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Validate the schema fixtures against the FLUID wire-contract schemas. + +Every fixture is transcribed from a worked example in spec/. If the +specification moves and the schemas do not, this fails the build. +""" +import json +import pathlib +import sys + +import yaml +from jsonschema import Draft202012Validator, RefResolver + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SCHEMAS = ROOT / "schemas" +FIXTURES = ROOT / "conformance" / "fixtures" + +# fixture stem -> schema stem +PAIRS = { + "pressure": "pressure", + "hypothesis": "hypothesis", + "revision": "revision", + "experiment": "experiment", + "event": "event", + "feedback": "feedback", + "backend-requirement": "backend-requirement", + "revision-descriptor": "revision-descriptor", + "routing-policy": "routing-policy", + "telemetry-envelope": "telemetry-envelope", +} + + +def load_store(): + """Map every schema by both its $id and its bare filename. + + Schemas cross-reference each other as "common.schema.json#/$defs/...", + a relative reference, so the bare filename has to resolve too. + """ + store = {} + for path in SCHEMAS.glob("*.schema.json"): + schema = json.loads(path.read_text()) + store[path.name] = schema + if "$id" in schema: + store[schema["$id"]] = schema + return store + + +# Instances that MUST be rejected. Without these the suite could pass +# vacuously if reference resolution silently degraded to "accept anything". +NEGATIVE = [ + ( + "pressure", + "pressure without evidence references is not auditable", + {"fluid_pressure": { + "schema_version": "0.1", "id": "P-1", "interface_id": "x", + "class": "natural_usage", "first_seen": "2026-01-01T00:00:00Z", + "last_seen": "2026-01-01T00:00:00Z", "summary": "s", + "evidence_refs": [], "status": "OPEN"}}, + ), + ( + "pressure", + "pressure class outside the standard taxonomy", + {"fluid_pressure": { + "schema_version": "0.1", "id": "P-1", "interface_id": "x", + "class": "vibes", "first_seen": "2026-01-01T00:00:00Z", + "last_seen": "2026-01-01T00:00:00Z", "summary": "s", + "evidence_refs": ["telemetry:1"], "status": "OPEN"}}, + ), + ( + "hypothesis", + "hypothesis with no falsifiable expected outcome", + {"fluid_hypothesis": { + "schema_version": "0.1", "id": "H-1", "interface_id": "x", + "state": "DRAFT", "title": "t", + "observation": {"summary": "s", "evidence_refs": ["telemetry:1"]}, + "pressure": {"classes": ["natural_usage"]}, + "explanation": {"claim": "c"}, + "proposed_adaptation": {"class": "contract", "summary": "s"}, + "expected_outcomes": [], + "guardrails": [], + "complexity": {"expected_delta": {}}, + "risk": {"level": "LOW"}, + "success_criteria": {"expression": "e"}}}, + ), + ( + "experiment", + "experiment with no stop condition is not interruptible by policy", + {"fluid_experiment": { + "schema_version": "0.1", "id": "E-1", "interface_id": "x", + "hypothesis_refs": ["H-1"], "control_revision": "R-1", + "candidate_revisions": ["R-2"], + "allocation": {"control": 0.9, "candidate": 0.1}, + "metrics": {"primary": ["m"]}, + "stop_conditions": [], + "result": {"state": "PLANNED"}}}, + ), + ( + "revision-descriptor", + "descriptor with no governing intent cannot be audited", + {"revision": { + "schema_version": "0.1", "id": "R-1", "interface": "x", + "state": "stable", + "contract": {"type": "openapi", "digest": "sha256:" + "0" * 64}, + "runtime": {"upstream": "http://a:8080"}, + "policy": {"compatibility": "additive", "security_check": "passed"}}}, + ), + ( + "revision-descriptor", + "unknown revision state", + {"revision": { + "schema_version": "0.1", "id": "R-1", "interface": "x", + "state": "probably-fine", + "contract": {"type": "openapi", "digest": "sha256:" + "0" * 64}, + "runtime": {"upstream": "http://a:8080"}, + "intent": {"version": "IEI-1"}, + "policy": {"compatibility": "additive", "security_check": "passed"}}}, + ), +] + + +def run_negative(store): + failures = 0 + for schema_stem, why, instance in NEGATIVE: + schema = json.loads((SCHEMAS / f"{schema_stem}.schema.json").read_text()) + resolver = RefResolver(base_uri="", referrer=schema, store=store) + validator = Draft202012Validator(schema, resolver=resolver) + if validator.is_valid(instance): + print(f"FAIL rejected-case accepted: {why}") + failures += 1 + else: + print(f"ok rejects: {why}") + return failures + + +def main() -> int: + store = load_store() + failures = 0 + + for fixture_stem, schema_stem in sorted(PAIRS.items()): + fixture_path = FIXTURES / f"{fixture_stem}.yaml" + schema_path = SCHEMAS / f"{schema_stem}.schema.json" + + if not fixture_path.exists(): + print(f"MISSING {fixture_path.relative_to(ROOT)}") + failures += 1 + continue + + schema = json.loads(schema_path.read_text()) + instance = yaml.safe_load(fixture_path.read_text()) + + resolver = RefResolver(base_uri="", referrer=schema, store=store) + validator = Draft202012Validator(schema, resolver=resolver) + + errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path)) + if errors: + failures += 1 + print(f"FAIL {fixture_stem}") + for err in errors: + where = "/".join(str(p) for p in err.absolute_path) or "" + print(f" {where}: {err.message}") + else: + print(f"ok {fixture_stem}") + + # A schema that no fixture exercises is a schema nothing protects. + unexercised = { + p.stem.removesuffix(".schema") + for p in SCHEMAS.glob("*.schema.json") + } - set(PAIRS.values()) - {"common"} + for stem in sorted(unexercised): + print(f"WARN {stem}.schema.json has no fixture") + + # Emit JSON copies so the Go round-trip test can read fixtures without a + # YAML dependency (the build stays free of third-party Go modules). + json_dir = FIXTURES / "json" + json_dir.mkdir(exist_ok=True) + for fixture_stem in PAIRS: + instance = yaml.safe_load((FIXTURES / f"{fixture_stem}.yaml").read_text()) + (json_dir / f"{fixture_stem}.json").write_text( + json.dumps(instance, indent=2, sort_keys=True) + "\n" + ) + + print() + failures += run_negative(store) + + if failures: + print(f"\n{failures} check(s) failed") + return 1 + print(f"\nAll {len(PAIRS)} fixtures validate; all {len(NEGATIVE)} rejected cases refused") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/adr/0001-go-as-implementation-language.md b/docs/adr/0001-go-as-implementation-language.md new file mode 100644 index 0000000..cc10a9f --- /dev/null +++ b/docs/adr/0001-go-as-implementation-language.md @@ -0,0 +1,46 @@ +# ADR-0001 — Go as the implementation language + +**Status:** accepted · **Date:** 2026-09-04 + +## Context + +fluid-core had no implementation. Three stacks were credible: Go, TypeScript +and Python. The specification (`ArchitectureBlueprint.md` §42–43) points at a +long-running gateway, a router, an append-only store and, later, a Kubernetes +controller with custom resources. + +Python is the obvious first reach — pydantic maps almost literally onto the +record schemas, and it matches existing tooling in sibling repositories. +TypeScript is attractive because the first consumers are platform SDKs with +strong TypeScript clients. + +## Decision + +fluid-core is written in Go. + +## Rationale + +The always-on component is infrastructure: proxy, router, registry, evidence +store. That is Go's centre of mass. A single static binary is also the most +credible form of "drop this in front of any API" — the promise ADR-0002 makes. + +Blueprint §43 maps 1:1 onto controller-runtime, so the Kubernetes path stays +open at no extra cost. + +Go is the lowest-entropy of the three to debug, which matters under a per-task +budget policy. + +## What we gave up + +Record modeling is more verbose in Go than in Python. This is paid once, behind +generated types (ADR-0003), rather than continuously. + +The generative Daimon (Blueprint Phase D) would be easier in Python. ADR-0002 +makes that a non-issue: the Daimon is a separate process speaking the wire +contract, so it may be written in whatever suits it. + +## Consequences + +- No third-party Go modules are used where the standard library suffices; the + build stays offline-capable. +- Python remains a build-time dependency for schema validation only. diff --git a/docs/adr/0002-out-of-process-attachment.md b/docs/adr/0002-out-of-process-attachment.md new file mode 100644 index 0000000..40e2d4d --- /dev/null +++ b/docs/adr/0002-out-of-process-attachment.md @@ -0,0 +1,43 @@ +# ADR-0002 — Out-of-process attachment + +**Status:** accepted · **Date:** 2026-09-04 + +## Context + +A FLUID interface has to be able to wrap an existing API. The question is +whether fluid-core is a library that API imports, or a process that sits in +front of it. + +Blueprint §42 already answers this implicitly: `gateway`, `revision-router` and +`adapter-r41`/`adapter-r42` are separate deployments. That is process-level +integration, not library-level. + +## Decision + +fluid-core attaches out of process. The gateway and router run in front of +adapter processes reached over the network. The target API contributes no code, +imports no library, and may be written in any stack. + +An in-process SDK is explicitly deferred, not rejected. It would buy richer +signal — call-sequence correlation and structured consumer feedback that a +proxy cannot see from outside — at the cost of SDK versioning across languages. + +## Rationale + +Minimal conformance (`FluidAPIStandards.md` §36) requires a deterministic +contract, revision identity, telemetry, declared intent, an evidence link, +responsibility boundaries and deterministic security. Every one of those is +observable from the proxy. Nothing in the conformance core needs to be inside +the target process. + +The language-agnostic promise is only credible if the framework never asks for +an import. Making the SDK optional from the start, rather than retrofitting the +proxy later, is what keeps that true. + +## Consequences + +- The revision descriptor carries `runtime.upstream`: the adapter's address. +- Interaction topology (Blueprint §6.4) is reconstructed from correlation IDs + observed at the gateway, not reported by the consumer. +- Explicit feedback (`FluidAPIStandards.md` §15) arrives over the wire as its + own endpoint rather than through an SDK call. diff --git a/docs/adr/0003-wire-contract-as-boundary.md b/docs/adr/0003-wire-contract-as-boundary.md new file mode 100644 index 0000000..ef34eec --- /dev/null +++ b/docs/adr/0003-wire-contract-as-boundary.md @@ -0,0 +1,43 @@ +# ADR-0003 — The wire contract is the boundary + +**Status:** accepted · **Date:** 2026-09-04 + +## Context + +ADR-0002 puts fluid-core in a separate process from everything it serves. +Something has to define how those processes agree. + +## Decision + +The boundary is a set of on-the-wire artifacts, not a Go API: + +1. the revision descriptor (Blueprint §36); +2. the routing policy (Blueprint §17); +3. the telemetry envelope; +4. the control APIs (Blueprint §44). + +These live in `schemas/` as JSON Schema. Go types in `internal/contract` are +**generated** from those schemas and are never hand-written. + +## Rationale + +Field names and semantics are the interop surface — the schema specification +says so directly in its §1. Generating from the schema means the specification +cannot drift from the implementation without failing the build. + +It also makes the Go choice reversible where it matters. A future high-throughput +gateway in another language, a Python Daimon, a TypeScript adapter: none of them +need anything from this repository except the schemas. + +## Enforcement + +- `make check-generated` fails CI when `internal/contract` is stale. +- `conformance/validate_schemas.py` validates fixtures transcribed from the + spec's own worked examples, so a spec change that the schemas miss fails too. +- The Go round-trip test decodes those fixtures with `DisallowUnknownFields`, + catching any field the generated types have no home for. + +## Consequence + +Any change that would leak a Go type across this boundary is a design failure, +not a convenience. If a consumer needs something, it goes in a schema first. diff --git a/docs/adr/0004-evidence-store-sqlite-to-postgres.md b/docs/adr/0004-evidence-store-sqlite-to-postgres.md new file mode 100644 index 0000000..90016c1 --- /dev/null +++ b/docs/adr/0004-evidence-store-sqlite-to-postgres.md @@ -0,0 +1,37 @@ +# ADR-0004 — Append-only evidence store, SQLite then Postgres + +**Status:** accepted · **Date:** 2026-09-04 + +## Context + +Blueprint §26 requires an evidence store favouring append-only history, with +mutable summaries derived from immutable events. §45 suggests a relational +starting model and explicitly argues it is simpler than a graph database for a +first implementation. + +## Decision + +One relational schema, two backends. SQLite for development, single-node +deployments and the CI conformance suite; PostgreSQL for anything shared. The +event table is append-only: no `UPDATE`, no `DELETE`, enforced by trigger rather +than by convention. + +Summary tables are derived views, rebuildable from events at any time. + +## Rationale + +The first real workload — publishing hall-of-helix entries to a Telegram channel +— produces a handful of events per day. Requiring Postgres to run the framework +at that scale would be an operational tax with no return. + +Keeping one schema across both means the CI suite exercises the same statements +production runs, which is where divergence usually hides. + +## Consequences + +- Portable SQL only; no backend-specific features in the core path. +- Blueprint invariant 8 (every promotion is auditable) is a storage property, + not an application convention: rewriting history has to be blocked at the + database. +- Evidence-store failure must not stop the data plane (Blueprint §34.6). The + gateway serves from cached published configuration and buffers telemetry. diff --git a/docs/adr/0005-revision-identity.md b/docs/adr/0005-revision-identity.md new file mode 100644 index 0000000..efc40a2 --- /dev/null +++ b/docs/adr/0005-revision-identity.md @@ -0,0 +1,47 @@ +# ADR-0005 — Revision identity + +**Status:** accepted · **Date:** 2026-09-04 + +## Context + +Blueprint §54 lists this as deliberately open: "Should revision numbering be +global, per interface, semantic, or content-addressed?" It asks for the answer +to come from implementation experience rather than premature standardization. + +An answer is nonetheless needed before anything can be published, so this ADR +picks the smallest one that does not foreclose the others. + +## Decision + +Two identifiers, with different jobs. + +**A human-facing revision id, sequential per interface.** `R-000221` with a +`revision_number` of 22, both scoped to one interface. This is what appears in +audit trails, CLI output and conversation. + +**A content address for every artifact the revision names.** The contract and +the implementation each carry a `sha256:` digest. A revision is reproducibly +associated with its artifacts through those digests, per Blueprint §25. + +Identity is therefore human-sequential; equality is content-addressed. + +## Rationale + +Global numbering was rejected: it couples unrelated interfaces and makes the +number meaningless as a lineage signal. + +Purely content-addressed identity was rejected as the primary handle. Digests +are correct but unreadable, and Blueprint §16 of the schema spec is explicit +that human-readable prefixes are recommended for operational tooling. A framework +whose central concept cannot be said out loud will not get used carefully. + +Semantic versioning was rejected because compatibility is already declared +separately, in `compatibility.class`. Encoding it a second time in the +identifier invites the two to disagree. + +## What stays open + +Whether `revision_number` should be dense (no gaps) is unresolved. Failed +candidates currently consume a number. That is defensible — the attempt is part +of the history — but it may prove noisy. Revisit after the first real interface +has produced enough failed candidates to tell. diff --git a/internal/contract/backend_requirement_gen.go b/internal/contract/backend_requirement_gen.go new file mode 100644 index 0000000..9ee21f9 --- /dev/null +++ b/internal/contract/backend_requirement_gen.go @@ -0,0 +1,100 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidBackendRequirementCapability struct { + Description string `json:"description" yaml:"description"` + Title string `json:"title" yaml:"title"` +} + +type FluidBackendRequirementDispositionState string + +const ( + FluidBackendRequirementDispositionStateOPEN FluidBackendRequirementDispositionState = "OPEN" + FluidBackendRequirementDispositionStateACCEPTED FluidBackendRequirementDispositionState = "ACCEPTED" + FluidBackendRequirementDispositionStatePLANNED FluidBackendRequirementDispositionState = "PLANNED" + FluidBackendRequirementDispositionStateAVAILABLE FluidBackendRequirementDispositionState = "AVAILABLE" + FluidBackendRequirementDispositionStatePARTIALLYAVAILABLE FluidBackendRequirementDispositionState = "PARTIALLY_AVAILABLE" + FluidBackendRequirementDispositionStateOUTOFSCOPE FluidBackendRequirementDispositionState = "OUT_OF_SCOPE" + FluidBackendRequirementDispositionStateREJECTED FluidBackendRequirementDispositionState = "REJECTED" + FluidBackendRequirementDispositionStateSUPERSEDED FluidBackendRequirementDispositionState = "SUPERSEDED" +) + +// Valid reports whether v is a defined FluidBackendRequirementDispositionState. +func (v FluidBackendRequirementDispositionState) Valid() bool { + switch v { + case FluidBackendRequirementDispositionStateOPEN, FluidBackendRequirementDispositionStateACCEPTED, FluidBackendRequirementDispositionStatePLANNED, FluidBackendRequirementDispositionStateAVAILABLE, FluidBackendRequirementDispositionStatePARTIALLYAVAILABLE, FluidBackendRequirementDispositionStateOUTOFSCOPE, FluidBackendRequirementDispositionStateREJECTED, FluidBackendRequirementDispositionStateSUPERSEDED: + return true + } + return false +} + +// The backend decides. Repeated OUT_OF_SCOPE feeds boundary learning +// (ArchitectureBlueprint.md section 22). +type FluidBackendRequirementDisposition struct { + Reason *string `json:"reason,omitempty" yaml:"reason,omitempty"` + State FluidBackendRequirementDispositionState `json:"state" yaml:"state"` + TargetRef *string `json:"target_ref,omitempty" yaml:"target_ref,omitempty"` +} + +type FluidBackendRequirementExpectedUsage struct { + RequestsPerDay *int64 `json:"requests_per_day,omitempty" yaml:"requests_per_day,omitempty"` +} + +type FluidBackendRequirementQuality struct { + Availability *UnitInterval `json:"availability,omitempty" yaml:"availability,omitempty"` + P95LatencyMS *float64 `json:"p95_latency_ms,omitempty" yaml:"p95_latency_ms,omitempty"` +} + +type FluidBackendRequirementSecurity struct { + AuthorizationScope string `json:"authorization_scope,omitempty" yaml:"authorization_scope,omitempty"` + TenantIsolation string `json:"tenant_isolation,omitempty" yaml:"tenant_isolation,omitempty"` +} + +type FluidBackendRequirementSemantics struct { + Consistency string `json:"consistency,omitempty" yaml:"consistency,omitempty"` + RequiredFields []string `json:"required_fields,omitempty" yaml:"required_fields,omitempty"` +} + +type FluidBackendRequirementUrgency string + +const ( + FluidBackendRequirementUrgencyLOW FluidBackendRequirementUrgency = "LOW" + FluidBackendRequirementUrgencyMEDIUM FluidBackendRequirementUrgency = "MEDIUM" + FluidBackendRequirementUrgencyHIGH FluidBackendRequirementUrgency = "HIGH" +) + +// Valid reports whether v is a defined FluidBackendRequirementUrgency. +func (v FluidBackendRequirementUrgency) Valid() bool { + switch v { + case FluidBackendRequirementUrgencyLOW, FluidBackendRequirementUrgencyMEDIUM, FluidBackendRequirementUrgencyHIGH: + return true + } + return false +} + +type FluidBackendRequirement struct { + BackendService string `json:"backend_service" yaml:"backend_service"` + Capability FluidBackendRequirementCapability `json:"capability" yaml:"capability"` + + // The backend decides. Repeated OUT_OF_SCOPE feeds boundary learning + // (ArchitectureBlueprint.md section 22). + Disposition FluidBackendRequirementDisposition `json:"disposition" yaml:"disposition"` + ExpectedUsage *FluidBackendRequirementExpectedUsage `json:"expected_usage,omitempty" yaml:"expected_usage,omitempty"` + ID BackendRequirementID `json:"id" yaml:"id"` + OriginatingHypothesis *HypothesisID `json:"originating_hypothesis,omitempty" yaml:"originating_hypothesis,omitempty"` + OriginatingInterface InterfaceID `json:"originating_interface" yaml:"originating_interface"` + OriginatingRevision *RevisionID `json:"originating_revision,omitempty" yaml:"originating_revision,omitempty"` + Quality *FluidBackendRequirementQuality `json:"quality,omitempty" yaml:"quality,omitempty"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + Security *FluidBackendRequirementSecurity `json:"security,omitempty" yaml:"security,omitempty"` + Semantics *FluidBackendRequirementSemantics `json:"semantics,omitempty" yaml:"semantics,omitempty"` + Urgency *FluidBackendRequirementUrgency `json:"urgency,omitempty" yaml:"urgency,omitempty"` +} + +// Capability the interface needs but has no authority to build. FLUID escalates +// rather than crossing the boundary (FluidAPIStandards.md section 30). +type BackendRequirementDocument struct { + FluidBackendRequirement FluidBackendRequirement `json:"fluid_backend_requirement" yaml:"fluid_backend_requirement"` +} diff --git a/internal/contract/common_gen.go b/internal/contract/common_gen.go new file mode 100644 index 0000000..7eba741 --- /dev/null +++ b/internal/contract/common_gen.go @@ -0,0 +1,207 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +import "time" + +type ActorType string + +const ( + ActorTypeHuman ActorType = "human" + ActorTypeDaimon ActorType = "daimon" + ActorTypePolicy ActorType = "policy" + ActorTypeSystem ActorType = "system" + ActorTypeConsumer ActorType = "consumer" +) + +// Valid reports whether v is a defined ActorType. +func (v ActorType) Valid() bool { + switch v { + case ActorTypeHuman, ActorTypeDaimon, ActorTypePolicy, ActorTypeSystem, ActorTypeConsumer: + return true + } + return false +} + +// Who or what performed an action. FLUID distinguishes these because trust levels +// differ (ArchitectureBlueprint.md section 47). +type Actor struct { + ID string `json:"id" yaml:"id"` + ModelOrAgent string `json:"model_or_agent,omitempty" yaml:"model_or_agent,omitempty"` + Type ActorType `json:"type" yaml:"type"` +} + +// FluidAPIStandards.md section 16. +type AdaptationClass string + +const ( + AdaptationClassPresentation AdaptationClass = "presentation" + AdaptationClassContract AdaptationClass = "contract" + AdaptationClassComposition AdaptationClass = "composition" + AdaptationClassImplementation AdaptationClass = "implementation" + AdaptationClassRequirementEscalation AdaptationClass = "requirement_escalation" +) + +// Valid reports whether v is a defined AdaptationClass. +func (v AdaptationClass) Valid() bool { + switch v { + case AdaptationClassPresentation, AdaptationClassContract, AdaptationClassComposition, AdaptationClassImplementation, AdaptationClassRequirementEscalation: + return true + } + return false +} + +// Opaque reference into the artifact store. +type ArtifactRef string + +type BackendRequirementID string + +type CohortID string + +// Standard dimensions, implementation-defined scoring (FluidAPIStandards.md section +// 22). +type ComplexityDelta struct { + ActiveRevisionCount *float64 `json:"active_revision_count,omitempty" yaml:"active_revision_count,omitempty"` + AlternativePathCount *float64 `json:"alternative_path_count,omitempty" yaml:"alternative_path_count,omitempty"` + BackendCompositionCount *float64 `json:"backend_composition_count,omitempty" yaml:"backend_composition_count,omitempty"` + CognitiveLoad *float64 `json:"cognitive_load,omitempty" yaml:"cognitive_load,omitempty"` + ConceptCount *float64 `json:"concept_count,omitempty" yaml:"concept_count,omitempty"` + DependencyCount *float64 `json:"dependency_count,omitempty" yaml:"dependency_count,omitempty"` + ExceptionCount *float64 `json:"exception_count,omitempty" yaml:"exception_count,omitempty"` + OperationCount *float64 `json:"operation_count,omitempty" yaml:"operation_count,omitempty"` + ParameterDimensionality *float64 `json:"parameter_dimensionality,omitempty" yaml:"parameter_dimensionality,omitempty"` + SemanticOverlap *float64 `json:"semantic_overlap,omitempty" yaml:"semantic_overlap,omitempty"` + SurfaceArea *float64 `json:"surface_area,omitempty" yaml:"surface_area,omitempty"` +} + +type DecisionID string + +// Content address of an artifact. +type Digest string + +type EventID string + +// Opaque reference into the evidence store, conventionally ':' such +// as 'telemetry:invoice-pattern-2026w36'. +type EvidenceRef string + +type ExpectedOutcomeDirection string + +const ( + ExpectedOutcomeDirectionLower ExpectedOutcomeDirection = "lower" + ExpectedOutcomeDirectionHigher ExpectedOutcomeDirection = "higher" + ExpectedOutcomeDirectionUnchanged ExpectedOutcomeDirection = "unchanged" +) + +// Valid reports whether v is a defined ExpectedOutcomeDirection. +func (v ExpectedOutcomeDirection) Valid() bool { + switch v { + case ExpectedOutcomeDirectionLower, ExpectedOutcomeDirectionHigher, ExpectedOutcomeDirectionUnchanged: + return true + } + return false +} + +type ExpectedOutcome struct { + Baseline *float64 `json:"baseline,omitempty" yaml:"baseline,omitempty"` + Cohort string `json:"cohort,omitempty" yaml:"cohort,omitempty"` + Direction ExpectedOutcomeDirection `json:"direction" yaml:"direction"` + Metric string `json:"metric" yaml:"metric"` + Target float64 `json:"target" yaml:"target"` +} + +type ExperimentID string + +type FeedbackID string + +// FluidAPIStandards.md section 20. No universal scalar is defined on purpose. +type FitnessDimensions struct { + Availability *float64 `json:"availability,omitempty" yaml:"availability,omitempty"` + ClientUtility *float64 `json:"client_utility,omitempty" yaml:"client_utility,omitempty"` + Compatibility *float64 `json:"compatibility,omitempty" yaml:"compatibility,omitempty"` + Correctness *float64 `json:"correctness,omitempty" yaml:"correctness,omitempty"` + Discoverability *float64 `json:"discoverability,omitempty" yaml:"discoverability,omitempty"` + ImplementationCost *float64 `json:"implementation_cost,omitempty" yaml:"implementation_cost,omitempty"` + Maintainability *float64 `json:"maintainability,omitempty" yaml:"maintainability,omitempty"` + OperationalCost *float64 `json:"operational_cost,omitempty" yaml:"operational_cost,omitempty"` + Performance *float64 `json:"performance,omitempty" yaml:"performance,omitempty"` + Reliability *float64 `json:"reliability,omitempty" yaml:"reliability,omitempty"` + ResourceConsumption *float64 `json:"resource_consumption,omitempty" yaml:"resource_consumption,omitempty"` + Security *float64 `json:"security,omitempty" yaml:"security,omitempty"` + Simplicity *float64 `json:"simplicity,omitempty" yaml:"simplicity,omitempty"` +} + +type GuardrailOperator string + +const ( + GuardrailOperatorLt GuardrailOperator = "<" + GuardrailOperatorLte GuardrailOperator = "<=" + GuardrailOperatorEq GuardrailOperator = "==" + GuardrailOperatorNeq GuardrailOperator = "!=" + GuardrailOperatorGte GuardrailOperator = ">=" + GuardrailOperatorGt GuardrailOperator = ">" +) + +// Valid reports whether v is a defined GuardrailOperator. +func (v GuardrailOperator) Valid() bool { + switch v { + case GuardrailOperatorLt, GuardrailOperatorLte, GuardrailOperatorEq, GuardrailOperatorNeq, GuardrailOperatorGte, GuardrailOperatorGt: + return true + } + return false +} + +// A constraint that must not regress beyond threshold. Guardrails are evaluated +// deterministically. +type Guardrail struct { + Metric string `json:"metric" yaml:"metric"` + Operator GuardrailOperator `json:"operator" yaml:"operator"` + Threshold any `json:"threshold" yaml:"threshold"` +} + +type HypothesisID string + +// Stable identifier of the owning interface. +type InterfaceID string + +type ObservationWindow struct { + // Null while the window is still open. + End *Timestamp `json:"end,omitempty" yaml:"end,omitempty"` + Start Timestamp `json:"start" yaml:"start"` +} + +// FluidAPIStandards.md section 13. +type PressureClass string + +const ( + PressureClassNaturalUsage PressureClass = "natural_usage" + PressureClassSuccessfulButInefficientUsage PressureClass = "successful_but_inefficient_usage" + PressureClassRecoverableMisunderstanding PressureClass = "recoverable_misunderstanding" + PressureClassRepeatedExpectationMismatch PressureClass = "repeated_expectation_mismatch" + PressureClassPoorDiscoverability PressureClass = "poor_discoverability" + PressureClassMissingInterfaceCapability PressureClass = "missing_interface_capability" + PressureClassMissingBackendCapability PressureClass = "missing_backend_capability" + PressureClassOutOfScopeDemand PressureClass = "out_of_scope_demand" + PressureClassProhibitedDemand PressureClass = "prohibited_demand" + PressureClassImplementationFailure PressureClass = "implementation_failure" +) + +// Valid reports whether v is a defined PressureClass. +func (v PressureClass) Valid() bool { + switch v { + case PressureClassNaturalUsage, PressureClassSuccessfulButInefficientUsage, PressureClassRecoverableMisunderstanding, PressureClassRepeatedExpectationMismatch, PressureClassPoorDiscoverability, PressureClassMissingInterfaceCapability, PressureClassMissingBackendCapability, PressureClassOutOfScopeDemand, PressureClassProhibitedDemand, PressureClassImplementationFailure: + return true + } + return false +} + +type PressureID string + +type RevisionID string + +type SchemaVersion string + +type Timestamp = time.Time + +type UnitInterval float64 diff --git a/internal/contract/contract_test.go b/internal/contract/contract_test.go new file mode 100644 index 0000000..68e7482 --- /dev/null +++ b/internal/contract/contract_test.go @@ -0,0 +1,203 @@ +package contract + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" +) + +// fixtureDir holds JSON copies of the spec-derived YAML fixtures, written by +// conformance/validate_schemas.py. +const fixtureDir = "../../conformance/fixtures/json" + +// roundTrip decodes a fixture into T, re-encodes it, and reports any field the +// generated types dropped on the way through. +// +// This is the drift check that matters: a schema change that schemagen did not +// pick up shows here as a field that vanishes. +func roundTrip[T any](t *testing.T, name string) { + t.Helper() + + raw, err := os.ReadFile(filepath.Join(fixtureDir, name+".json")) + if err != nil { + t.Fatalf("read fixture: %v (run `make validate` first)", err) + } + + var typed T + dec := json.NewDecoder(bytesReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&typed); err != nil { + t.Fatalf("decode into %T: %v", typed, err) + } + + encoded, err := json.Marshal(typed) + if err != nil { + t.Fatalf("re-encode: %v", err) + } + + var before, after map[string]any + if err := json.Unmarshal(raw, &before); err != nil { + t.Fatalf("unmarshal fixture: %v", err) + } + if err := json.Unmarshal(encoded, &after); err != nil { + t.Fatalf("unmarshal re-encoded: %v", err) + } + + if missing := missingKeys(before, after, ""); len(missing) > 0 { + t.Errorf("round trip dropped fields: %v", missing) + } +} + +// missingKeys walks want and reports paths absent from got. +// +// Explicit nulls and empty collections are skipped. The schemas treat +// "end: null" and an absent "end" as the same statement (the window is still +// open), so omitempty dropping them on re-encode is correct rather than drift. +// Genuine drift -- a fixture field the generated types have no home for -- is +// caught by DisallowUnknownFields on the way in. +func missingKeys(want, got map[string]any, prefix string) []string { + var missing []string + for k, wv := range want { + path := k + if prefix != "" { + path = prefix + "." + k + } + if isEmptyValue(wv) { + continue + } + gv, ok := got[k] + if !ok { + missing = append(missing, path) + continue + } + wm, wok := wv.(map[string]any) + gm, gok := gv.(map[string]any) + if wok && gok { + missing = append(missing, missingKeys(wm, gm, path)...) + } + } + return missing +} + +// isEmptyValue reports values the schema treats as equivalent to absent. +func isEmptyValue(v any) bool { + switch t := v.(type) { + case nil: + return true + case []any: + return len(t) == 0 + case map[string]any: + return len(t) == 0 + } + return false +} + +func TestFixturesRoundTrip(t *testing.T) { + t.Run("pressure", func(t *testing.T) { roundTrip[PressureDocument](t, "pressure") }) + t.Run("hypothesis", func(t *testing.T) { roundTrip[HypothesisDocument](t, "hypothesis") }) + t.Run("revision", func(t *testing.T) { roundTrip[RevisionDocument](t, "revision") }) + t.Run("experiment", func(t *testing.T) { roundTrip[ExperimentDocument](t, "experiment") }) + t.Run("event", func(t *testing.T) { roundTrip[EventDocument](t, "event") }) + t.Run("feedback", func(t *testing.T) { roundTrip[FeedbackDocument](t, "feedback") }) + t.Run("backend-requirement", func(t *testing.T) { + roundTrip[BackendRequirementDocument](t, "backend-requirement") + }) + t.Run("revision-descriptor", func(t *testing.T) { + roundTrip[RevisionDescriptorDocument](t, "revision-descriptor") + }) + t.Run("routing-policy", func(t *testing.T) { roundTrip[RoutingPolicyDocument](t, "routing-policy") }) + t.Run("telemetry-envelope", func(t *testing.T) { + roundTrip[TelemetryEnvelopeDocument](t, "telemetry-envelope") + }) +} + +func TestKindOf(t *testing.T) { + cases := map[string]EntityKind{ + "H-000184": KindHypothesis, + "R-000221": KindRevision, + "E-000093": KindExperiment, + "P-1831": KindPressure, + "BR-0041": KindBackendRequirement, + "D-2811": KindDecision, + "EV-990281": KindEvent, + "F-9821": KindFeedback, + "C-17": KindCohort, + } + for id, want := range cases { + got, ok := KindOf(id) + if !ok || got != want { + t.Errorf("KindOf(%q) = %q, %v; want %q", id, got, ok, want) + } + } + + // BR- and EV- must not be swallowed by the single-letter prefixes. + if k, _ := KindOf("BR-1"); k != KindBackendRequirement { + t.Errorf("BR- prefix mis-classified as %q", k) + } + if k, _ := KindOf("EV-1"); k != KindEvent { + t.Errorf("EV- prefix mis-classified as %q", k) + } + + if _, ok := KindOf("something"); ok { + t.Error("unprefixed identifier should not classify") + } +} + +func TestRequireKind(t *testing.T) { + if err := RequireKind("R-1", KindRevision); err != nil { + t.Errorf("valid revision id rejected: %v", err) + } + err := RequireKind("H-1", KindRevision) + if err == nil { + t.Fatal("hypothesis id accepted where a revision id was required") + } + var wrong *ErrWrongKind + if !errorsAs(err, &wrong) { + t.Fatalf("expected *ErrWrongKind, got %T", err) + } + if wrong.Got != KindHypothesis || wrong.Want != KindRevision { + t.Errorf("unexpected error detail: %+v", wrong) + } +} + +// Small helpers keep the test file free of imports the generated code does not +// already require. +func bytesReader(b []byte) *jsonReader { return &jsonReader{b: b} } + +type jsonReader struct { + b []byte + i int +} + +func (r *jsonReader) Read(p []byte) (int, error) { + if r.i >= len(r.b) { + return 0, errEOF + } + n := copy(p, r.b[r.i:]) + r.i += n + return n, nil +} + +var errEOF = errorString("EOF") + +type errorString string + +func (e errorString) Error() string { return string(e) } + +func errorsAs(err error, target any) bool { + tv := reflect.ValueOf(target).Elem() + for err != nil { + if reflect.TypeOf(err).AssignableTo(tv.Type()) { + tv.Set(reflect.ValueOf(err)) + return true + } + u, ok := err.(interface{ Unwrap() error }) + if !ok { + return false + } + err = u.Unwrap() + } + return false +} diff --git a/internal/contract/event_gen.go b/internal/contract/event_gen.go new file mode 100644 index 0000000..a6f478a --- /dev/null +++ b/internal/contract/event_gen.go @@ -0,0 +1,48 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidEventEntityType string + +const ( + FluidEventEntityTypePressure FluidEventEntityType = "pressure" + FluidEventEntityTypeHypothesis FluidEventEntityType = "hypothesis" + FluidEventEntityTypeRevision FluidEventEntityType = "revision" + FluidEventEntityTypeExperiment FluidEventEntityType = "experiment" + FluidEventEntityTypeBackendRequirement FluidEventEntityType = "backend_requirement" + FluidEventEntityTypeIntent FluidEventEntityType = "intent" + FluidEventEntityTypeDecision FluidEventEntityType = "decision" + FluidEventEntityTypeRoutingPolicy FluidEventEntityType = "routing_policy" +) + +// Valid reports whether v is a defined FluidEventEntityType. +func (v FluidEventEntityType) Valid() bool { + switch v { + case FluidEventEntityTypePressure, FluidEventEntityTypeHypothesis, FluidEventEntityTypeRevision, FluidEventEntityTypeExperiment, FluidEventEntityTypeBackendRequirement, FluidEventEntityTypeIntent, FluidEventEntityTypeDecision, FluidEventEntityTypeRoutingPolicy: + return true + } + return false +} + +type FluidEvent struct { + Actor Actor `json:"actor" yaml:"actor"` + EntityID string `json:"entity_id" yaml:"entity_id"` + EntityType FluidEventEntityType `json:"entity_type" yaml:"entity_type"` + EventType string `json:"event_type" yaml:"event_type"` + EvidenceRefs []EvidenceRef `json:"evidence_refs,omitempty" yaml:"evidence_refs,omitempty"` + ID EventID `json:"id" yaml:"id"` + + // Identifiers of the records this transition drew on. + Inputs []string `json:"inputs,omitempty" yaml:"inputs,omitempty"` + OccurredAt Timestamp `json:"occurred_at" yaml:"occurred_at"` + Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` +} + +// Append-only record of a material lifecycle transition. FLUID evolution is itself +// a system behavior that must remain reconstructable +// (FluidHypothesisRevisionSchema.md section 15). +type EventDocument struct { + FluidEvent FluidEvent `json:"fluid_event" yaml:"fluid_event"` +} diff --git a/internal/contract/experiment_gen.go b/internal/contract/experiment_gen.go new file mode 100644 index 0000000..bf5cf9f --- /dev/null +++ b/internal/contract/experiment_gen.go @@ -0,0 +1,108 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidExperimentAmendmentsItem struct { + Actor Actor `json:"actor" yaml:"actor"` + At Timestamp `json:"at" yaml:"at"` + Change string `json:"change" yaml:"change"` + Reason string `json:"reason" yaml:"reason"` +} + +type FluidExperimentMechanism string + +const ( + FluidExperimentMechanismSandbox FluidExperimentMechanism = "sandbox" + FluidExperimentMechanismShadow FluidExperimentMechanism = "shadow" + FluidExperimentMechanismSynthetic FluidExperimentMechanism = "synthetic" + FluidExperimentMechanismReplay FluidExperimentMechanism = "replay" + FluidExperimentMechanismCanary FluidExperimentMechanism = "canary" + FluidExperimentMechanismOptIn FluidExperimentMechanism = "opt_in" + FluidExperimentMechanismCohort FluidExperimentMechanism = "cohort" + FluidExperimentMechanismTenant FluidExperimentMechanism = "tenant" + FluidExperimentMechanismPercentage FluidExperimentMechanism = "percentage" +) + +// Valid reports whether v is a defined FluidExperimentMechanism. +func (v FluidExperimentMechanism) Valid() bool { + switch v { + case FluidExperimentMechanismSandbox, FluidExperimentMechanismShadow, FluidExperimentMechanismSynthetic, FluidExperimentMechanismReplay, FluidExperimentMechanismCanary, FluidExperimentMechanismOptIn, FluidExperimentMechanismCohort, FluidExperimentMechanismTenant, FluidExperimentMechanismPercentage: + return true + } + return false +} + +// Primary metrics and guardrails are kept distinct so a scalar can never hide a +// regression (ArchitectureBlueprint.md section 48.5). +type FluidExperimentMetrics struct { + Guardrails []string `json:"guardrails,omitempty" yaml:"guardrails,omitempty"` + Learning []string `json:"learning,omitempty" yaml:"learning,omitempty"` + Primary []string `json:"primary" yaml:"primary"` + Secondary []string `json:"secondary,omitempty" yaml:"secondary,omitempty"` +} + +type FluidExperimentResultState string + +const ( + FluidExperimentResultStatePLANNED FluidExperimentResultState = "PLANNED" + FluidExperimentResultStateRUNNING FluidExperimentResultState = "RUNNING" + FluidExperimentResultStateSTOPPED FluidExperimentResultState = "STOPPED" + FluidExperimentResultStateCOMPLETED FluidExperimentResultState = "COMPLETED" + FluidExperimentResultStateABANDONED FluidExperimentResultState = "ABANDONED" +) + +// Valid reports whether v is a defined FluidExperimentResultState. +func (v FluidExperimentResultState) Valid() bool { + switch v { + case FluidExperimentResultStatePLANNED, FluidExperimentResultStateRUNNING, FluidExperimentResultStateSTOPPED, FluidExperimentResultStateCOMPLETED, FluidExperimentResultStateABANDONED: + return true + } + return false +} + +type FluidExperimentResult struct { + EvidenceRefs []EvidenceRef `json:"evidence_refs,omitempty" yaml:"evidence_refs,omitempty"` + PreferredRevision *RevisionID `json:"preferred_revision,omitempty" yaml:"preferred_revision,omitempty"` + State FluidExperimentResultState `json:"state" yaml:"state"` + StoppedReason *string `json:"stopped_reason,omitempty" yaml:"stopped_reason,omitempty"` +} + +type FluidExperiment struct { + // Shares by revision id or by the reserved key 'control'. Allocation is + // deterministic and enacted by the router, never by this record. + Allocation map[string]UnitInterval `json:"allocation" yaml:"allocation"` + + // Success criteria may not be changed after results are visible without recording + // the amendment (ArchitectureBlueprint.md section 18). + Amendments []FluidExperimentAmendmentsItem `json:"amendments,omitempty" yaml:"amendments,omitempty"` + CandidateRevisions []RevisionID `json:"candidate_revisions" yaml:"candidate_revisions"` + Cohorts []CohortID `json:"cohorts,omitempty" yaml:"cohorts,omitempty"` + ControlRevision RevisionID `json:"control_revision" yaml:"control_revision"` + + // An experiment without a hypothesis measures nothing in particular. + HypothesisRefs []HypothesisID `json:"hypothesis_refs" yaml:"hypothesis_refs"` + ID ExperimentID `json:"id" yaml:"id"` + InterfaceID InterfaceID `json:"interface_id" yaml:"interface_id"` + MaxDurationHours *float64 `json:"max_duration_hours,omitempty" yaml:"max_duration_hours,omitempty"` + Mechanism *FluidExperimentMechanism `json:"mechanism,omitempty" yaml:"mechanism,omitempty"` + + // Primary metrics and guardrails are kept distinct so a scalar can never hide a + // regression (ArchitectureBlueprint.md section 48.5). + Metrics FluidExperimentMetrics `json:"metrics" yaml:"metrics"` + PlannedEndAt *Timestamp `json:"planned_end_at,omitempty" yaml:"planned_end_at,omitempty"` + Result FluidExperimentResult `json:"result" yaml:"result"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + StartAt *Timestamp `json:"start_at,omitempty" yaml:"start_at,omitempty"` + StartConditions []string `json:"start_conditions,omitempty" yaml:"start_conditions,omitempty"` + + // Every experiment has a stop condition (ArchitectureBlueprint.md section 55, + // invariant 7). + StopConditions []string `json:"stop_conditions" yaml:"stop_conditions"` +} + +// Connects hypotheses to revisions under bounded conditions. Experiments must be +// interruptible (ArchitectureBlueprint.md section 16). +type ExperimentDocument struct { + FluidExperiment FluidExperiment `json:"fluid_experiment" yaml:"fluid_experiment"` +} diff --git a/internal/contract/feedback_gen.go b/internal/contract/feedback_gen.go new file mode 100644 index 0000000..b6b0a63 --- /dev/null +++ b/internal/contract/feedback_gen.go @@ -0,0 +1,33 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidFeedbackImpact struct { + Calls *int64 `json:"calls,omitempty" yaml:"calls,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + LatencyMS *float64 `json:"latency_ms,omitempty" yaml:"latency_ms,omitempty"` +} + +type FluidFeedback struct { + Attempt string `json:"attempt,omitempty" yaml:"attempt,omitempty"` + AttemptedOperations []string `json:"attempted_operations,omitempty" yaml:"attempted_operations,omitempty"` + Cohort *CohortID `json:"cohort,omitempty" yaml:"cohort,omitempty"` + Confidence *UnitInterval `json:"confidence,omitempty" yaml:"confidence,omitempty"` + Goal string `json:"goal" yaml:"goal"` + ID FeedbackID `json:"id" yaml:"id"` + Impact *FluidFeedbackImpact `json:"impact,omitempty" yaml:"impact,omitempty"` + InterfaceID InterfaceID `json:"interface_id" yaml:"interface_id"` + MissingCapability string `json:"missing_capability,omitempty" yaml:"missing_capability,omitempty"` + Outcome string `json:"outcome,omitempty" yaml:"outcome,omitempty"` + ReceivedAt Timestamp `json:"received_at" yaml:"received_at"` + Revision *RevisionID `json:"revision,omitempty" yaml:"revision,omitempty"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + Workaround any `json:"workaround,omitempty" yaml:"workaround,omitempty"` +} + +// Structured feedback from a consumer. Treated as evidence and never as authority +// to change anything (FluidAPIStandards.md section 15). +type FeedbackDocument struct { + FluidFeedback FluidFeedback `json:"fluid_feedback" yaml:"fluid_feedback"` +} diff --git a/internal/contract/hypothesis_gen.go b/internal/contract/hypothesis_gen.go new file mode 100644 index 0000000..a733919 --- /dev/null +++ b/internal/contract/hypothesis_gen.go @@ -0,0 +1,254 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidHypothesisAudit struct { + DecisionRefs []DecisionID `json:"decision_refs,omitempty" yaml:"decision_refs,omitempty"` + ImmutableEventRefs []EventID `json:"immutable_event_refs,omitempty" yaml:"immutable_event_refs,omitempty"` +} + +type FluidHypothesisBackendRequirements struct { + Required bool `json:"required" yaml:"required"` + RequirementRefs []BackendRequirementID `json:"requirement_refs,omitempty" yaml:"requirement_refs,omitempty"` +} + +// Competing hypotheses are legitimate (FluidAPIStandards.md section 19). +type FluidHypothesisCompetition struct { + Alternatives []HypothesisID `json:"alternatives,omitempty" yaml:"alternatives,omitempty"` + GroupID string `json:"group_id" yaml:"group_id"` +} + +type FluidHypothesisComplexity struct { + ExpectedDelta ComplexityDelta `json:"expected_delta" yaml:"expected_delta"` + Score *float64 `json:"score,omitempty" yaml:"score,omitempty"` +} + +type FluidHypothesisEconomicsExpectedValueClass string + +const ( + FluidHypothesisEconomicsExpectedValueClassLOW FluidHypothesisEconomicsExpectedValueClass = "LOW" + FluidHypothesisEconomicsExpectedValueClassMEDIUM FluidHypothesisEconomicsExpectedValueClass = "MEDIUM" + FluidHypothesisEconomicsExpectedValueClassHIGH FluidHypothesisEconomicsExpectedValueClass = "HIGH" +) + +// Valid reports whether v is a defined FluidHypothesisEconomicsExpectedValueClass. +func (v FluidHypothesisEconomicsExpectedValueClass) Valid() bool { + switch v { + case FluidHypothesisEconomicsExpectedValueClassLOW, FluidHypothesisEconomicsExpectedValueClassMEDIUM, FluidHypothesisEconomicsExpectedValueClassHIGH: + return true + } + return false +} + +// A hypothesis may be valuable but not yet worth exploring +// (ArchitectureBlueprint.md section 29). +type FluidHypothesisEconomics struct { + Currency string `json:"currency,omitempty" yaml:"currency,omitempty"` + EstimatedExperimentCost *float64 `json:"estimated_experiment_cost,omitempty" yaml:"estimated_experiment_cost,omitempty"` + EstimatedImplementationCost *float64 `json:"estimated_implementation_cost,omitempty" yaml:"estimated_implementation_cost,omitempty"` + ExpectedValueClass *FluidHypothesisEconomicsExpectedValueClass `json:"expected_value_class,omitempty" yaml:"expected_value_class,omitempty"` +} + +// Explanatory reach is a prioritization signal, never a correctness claim +// (FluidAPIStandards.md section 18). +type FluidHypothesisExplanationReach struct { + Explains []PressureID `json:"explains,omitempty" yaml:"explains,omitempty"` + Notes string `json:"notes,omitempty" yaml:"notes,omitempty"` + Score *float64 `json:"score,omitempty" yaml:"score,omitempty"` +} + +// What we think explains the observation. +type FluidHypothesisExplanation struct { + Claim string `json:"claim" yaml:"claim"` + + // Explanatory reach is a prioritization signal, never a correctness claim + // (FluidAPIStandards.md section 18). + Reach *FluidHypothesisExplanationReach `json:"reach,omitempty" yaml:"reach,omitempty"` +} + +type FluidHypothesisFailureCriteria struct { + Expression string `json:"expression" yaml:"expression"` +} + +type FluidHypothesisFitnessDimensions struct { + Expected *FitnessDimensions `json:"expected,omitempty" yaml:"expected,omitempty"` +} + +type FluidHypothesisLearningValue struct { + Notes string `json:"notes,omitempty" yaml:"notes,omitempty"` + Score *float64 `json:"score,omitempty" yaml:"score,omitempty"` +} + +// What was seen. Not what it means. +type FluidHypothesisObservation struct { + AffectedCohorts []CohortID `json:"affected_cohorts,omitempty" yaml:"affected_cohorts,omitempty"` + EvidenceRefs []EvidenceRef `json:"evidence_refs" yaml:"evidence_refs"` + ObservationWindow *ObservationWindow `json:"observation_window,omitempty" yaml:"observation_window,omitempty"` + Summary string `json:"summary" yaml:"summary"` +} + +type FluidHypothesisOutcomeStatus string + +const ( + FluidHypothesisOutcomeStatusCONFIRMED FluidHypothesisOutcomeStatus = "CONFIRMED" + FluidHypothesisOutcomeStatusREFUTED FluidHypothesisOutcomeStatus = "REFUTED" + FluidHypothesisOutcomeStatusINCONCLUSIVE FluidHypothesisOutcomeStatus = "INCONCLUSIVE" +) + +// Valid reports whether v is a defined FluidHypothesisOutcomeStatus. +func (v FluidHypothesisOutcomeStatus) Valid() bool { + switch v { + case FluidHypothesisOutcomeStatusCONFIRMED, FluidHypothesisOutcomeStatusREFUTED, FluidHypothesisOutcomeStatusINCONCLUSIVE: + return true + } + return false +} + +// What happened afterwards. Null until evaluation completes. +type FluidHypothesisOutcome struct { + EvidenceRefs []EvidenceRef `json:"evidence_refs,omitempty" yaml:"evidence_refs,omitempty"` + Status *FluidHypothesisOutcomeStatus `json:"status,omitempty" yaml:"status,omitempty"` + Summary *string `json:"summary,omitempty" yaml:"summary,omitempty"` +} + +type FluidHypothesisPressure struct { + Classes []PressureClass `json:"classes" yaml:"classes"` + Confidence *UnitInterval `json:"confidence,omitempty" yaml:"confidence,omitempty"` + PressureRefs []PressureID `json:"pressure_refs,omitempty" yaml:"pressure_refs,omitempty"` + Severity *UnitInterval `json:"severity,omitempty" yaml:"severity,omitempty"` +} + +type FluidHypothesisPriority struct { + DecidedBy string `json:"decided_by,omitempty" yaml:"decided_by,omitempty"` + Score *float64 `json:"score,omitempty" yaml:"score,omitempty"` +} + +type FluidHypothesisProposedAdaptationImplementationScope string + +const ( + FluidHypothesisProposedAdaptationImplementationScopeInterfaceOnly FluidHypothesisProposedAdaptationImplementationScope = "interface_only" + FluidHypothesisProposedAdaptationImplementationScopeInterfaceAndBackend FluidHypothesisProposedAdaptationImplementationScope = "interface_and_backend" + FluidHypothesisProposedAdaptationImplementationScopeDocumentationOnly FluidHypothesisProposedAdaptationImplementationScope = "documentation_only" +) + +// Valid reports whether v is a defined FluidHypothesisProposedAdaptationImplementationScope. +func (v FluidHypothesisProposedAdaptationImplementationScope) Valid() bool { + switch v { + case FluidHypothesisProposedAdaptationImplementationScopeInterfaceOnly, FluidHypothesisProposedAdaptationImplementationScopeInterfaceAndBackend, FluidHypothesisProposedAdaptationImplementationScopeDocumentationOnly: + return true + } + return false +} + +// What we propose to change. +type FluidHypothesisProposedAdaptation struct { + // Shape is protocol-specific and deliberately unconstrained. + CandidateContract map[string]any `json:"candidate_contract,omitempty" yaml:"candidate_contract,omitempty"` + Class AdaptationClass `json:"class" yaml:"class"` + ImplementationScope *FluidHypothesisProposedAdaptationImplementationScope `json:"implementation_scope,omitempty" yaml:"implementation_scope,omitempty"` + Summary string `json:"summary" yaml:"summary"` +} + +type FluidHypothesisRiskLevel string + +const ( + FluidHypothesisRiskLevelLOW FluidHypothesisRiskLevel = "LOW" + FluidHypothesisRiskLevelMEDIUM FluidHypothesisRiskLevel = "MEDIUM" + FluidHypothesisRiskLevelHIGH FluidHypothesisRiskLevel = "HIGH" + FluidHypothesisRiskLevelCRITICAL FluidHypothesisRiskLevel = "CRITICAL" +) + +// Valid reports whether v is a defined FluidHypothesisRiskLevel. +func (v FluidHypothesisRiskLevel) Valid() bool { + switch v { + case FluidHypothesisRiskLevelLOW, FluidHypothesisRiskLevelMEDIUM, FluidHypothesisRiskLevelHIGH, FluidHypothesisRiskLevelCRITICAL: + return true + } + return false +} + +type FluidHypothesisRisk struct { + Level FluidHypothesisRiskLevel `json:"level" yaml:"level"` + Reasons []string `json:"reasons,omitempty" yaml:"reasons,omitempty"` +} + +type FluidHypothesisState string + +const ( + FluidHypothesisStateDRAFT FluidHypothesisState = "DRAFT" + FluidHypothesisStateREADY FluidHypothesisState = "READY" + FluidHypothesisStatePRIORITIZED FluidHypothesisState = "PRIORITIZED" + FluidHypothesisStateDESIGNING FluidHypothesisState = "DESIGNING" + FluidHypothesisStateEXPERIMENTING FluidHypothesisState = "EXPERIMENTING" + FluidHypothesisStateEVALUATING FluidHypothesisState = "EVALUATING" + FluidHypothesisStateACCEPTED FluidHypothesisState = "ACCEPTED" + FluidHypothesisStateREJECTED FluidHypothesisState = "REJECTED" + FluidHypothesisStateSUPERSEDED FluidHypothesisState = "SUPERSEDED" + FluidHypothesisStateDEFERRED FluidHypothesisState = "DEFERRED" +) + +// Valid reports whether v is a defined FluidHypothesisState. +func (v FluidHypothesisState) Valid() bool { + switch v { + case FluidHypothesisStateDRAFT, FluidHypothesisStateREADY, FluidHypothesisStatePRIORITIZED, FluidHypothesisStateDESIGNING, FluidHypothesisStateEXPERIMENTING, FluidHypothesisStateEVALUATING, FluidHypothesisStateACCEPTED, FluidHypothesisStateREJECTED, FluidHypothesisStateSUPERSEDED, FluidHypothesisStateDEFERRED: + return true + } + return false +} + +type FluidHypothesisSuccessCriteria struct { + Expression string `json:"expression" yaml:"expression"` +} + +type FluidHypothesis struct { + Audit *FluidHypothesisAudit `json:"audit,omitempty" yaml:"audit,omitempty"` + BackendRequirements *FluidHypothesisBackendRequirements `json:"backend_requirements,omitempty" yaml:"backend_requirements,omitempty"` + CandidateRevisionRefs []RevisionID `json:"candidate_revision_refs,omitempty" yaml:"candidate_revision_refs,omitempty"` + + // Competing hypotheses are legitimate (FluidAPIStandards.md section 19). + Competition *FluidHypothesisCompetition `json:"competition,omitempty" yaml:"competition,omitempty"` + Complexity FluidHypothesisComplexity `json:"complexity" yaml:"complexity"` + CreatedAt *Timestamp `json:"created_at,omitempty" yaml:"created_at,omitempty"` + CreatedBy *Actor `json:"created_by,omitempty" yaml:"created_by,omitempty"` + + // A hypothesis may be valuable but not yet worth exploring + // (ArchitectureBlueprint.md section 29). + Economics *FluidHypothesisEconomics `json:"economics,omitempty" yaml:"economics,omitempty"` + + // A hypothesis must be falsifiable (FluidAPIStandards.md section 17). + ExpectedOutcomes []ExpectedOutcome `json:"expected_outcomes" yaml:"expected_outcomes"` + ExperimentRefs []ExperimentID `json:"experiment_refs,omitempty" yaml:"experiment_refs,omitempty"` + + // What we think explains the observation. + Explanation FluidHypothesisExplanation `json:"explanation" yaml:"explanation"` + FailureCriteria *FluidHypothesisFailureCriteria `json:"failure_criteria,omitempty" yaml:"failure_criteria,omitempty"` + FitnessDimensions *FluidHypothesisFitnessDimensions `json:"fitness_dimensions,omitempty" yaml:"fitness_dimensions,omitempty"` + Guardrails []Guardrail `json:"guardrails" yaml:"guardrails"` + ID HypothesisID `json:"id" yaml:"id"` + InterfaceID InterfaceID `json:"interface_id" yaml:"interface_id"` + LearningValue *FluidHypothesisLearningValue `json:"learning_value,omitempty" yaml:"learning_value,omitempty"` + + // What was seen. Not what it means. + Observation FluidHypothesisObservation `json:"observation" yaml:"observation"` + + // What happened afterwards. Null until evaluation completes. + Outcome *FluidHypothesisOutcome `json:"outcome,omitempty" yaml:"outcome,omitempty"` + Pressure FluidHypothesisPressure `json:"pressure" yaml:"pressure"` + Priority *FluidHypothesisPriority `json:"priority,omitempty" yaml:"priority,omitempty"` + + // What we propose to change. + ProposedAdaptation FluidHypothesisProposedAdaptation `json:"proposed_adaptation" yaml:"proposed_adaptation"` + Risk FluidHypothesisRisk `json:"risk" yaml:"risk"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + State FluidHypothesisState `json:"state" yaml:"state"` + SuccessCriteria FluidHypothesisSuccessCriteria `json:"success_criteria" yaml:"success_criteria"` + Title string `json:"title" yaml:"title"` +} + +// Why an interface change should be explored. Observation, explanation, prediction, +// intervention and result stay separate on purpose +// (FluidHypothesisRevisionSchema.md section 18). +type HypothesisDocument struct { + FluidHypothesis FluidHypothesis `json:"fluid_hypothesis" yaml:"fluid_hypothesis"` +} diff --git a/internal/contract/ids.go b/internal/contract/ids.go new file mode 100644 index 0000000..93d6cbc --- /dev/null +++ b/internal/contract/ids.go @@ -0,0 +1,91 @@ +package contract + +import ( + "fmt" + "strings" +) + +// Identifier prefixes from FluidHypothesisRevisionSchema.md section 16. +// +// Implementations may use UUIDs internally; these human-readable prefixes exist +// so that operational tooling and audit trails stay legible to people. +const ( + PrefixHypothesis = "H-" + PrefixRevision = "R-" + PrefixExperiment = "E-" + PrefixPressure = "P-" + PrefixBackendRequirement = "BR-" + PrefixDecision = "D-" + PrefixEvent = "EV-" + PrefixFeedback = "F-" + PrefixCohort = "C-" +) + +// EntityKind names the FLUID artifact an identifier refers to. +type EntityKind string + +const ( + KindHypothesis EntityKind = "hypothesis" + KindRevision EntityKind = "revision" + KindExperiment EntityKind = "experiment" + KindPressure EntityKind = "pressure" + KindBackendRequirement EntityKind = "backend_requirement" + KindDecision EntityKind = "decision" + KindEvent EntityKind = "event" + KindFeedback EntityKind = "feedback" + KindCohort EntityKind = "cohort" +) + +// prefixOrder matters: "BR-" must be tested before "B"-less single letters +// would otherwise mis-claim it, and "EV-" before "E-". +var prefixOrder = []struct { + prefix string + kind EntityKind +}{ + {PrefixBackendRequirement, KindBackendRequirement}, + {PrefixEvent, KindEvent}, + {PrefixHypothesis, KindHypothesis}, + {PrefixRevision, KindRevision}, + {PrefixExperiment, KindExperiment}, + {PrefixPressure, KindPressure}, + {PrefixDecision, KindDecision}, + {PrefixFeedback, KindFeedback}, + {PrefixCohort, KindCohort}, +} + +// KindOf reports which FLUID artifact an identifier names. +// +// Audit trails carry bare identifiers across record boundaries, so being able to +// classify one without knowing where it came from keeps trace reconstruction +// from needing a lookup table at every hop. +func KindOf(id string) (EntityKind, bool) { + for _, p := range prefixOrder { + if strings.HasPrefix(id, p.prefix) { + return p.kind, true + } + } + return "", false +} + +// ErrWrongKind reports an identifier used in the wrong position. +type ErrWrongKind struct { + ID string + Want EntityKind + Got EntityKind +} + +func (e *ErrWrongKind) Error() string { + if e.Got == "" { + return fmt.Sprintf("identifier %q has no recognized FLUID prefix, wanted a %s id", e.ID, e.Want) + } + return fmt.Sprintf("identifier %q is a %s id, wanted a %s id", e.ID, e.Got, e.Want) +} + +// RequireKind checks that id names the expected artifact. +func RequireKind(id string, want EntityKind) error { + got, ok := KindOf(id) + if !ok || got != want { + return &ErrWrongKind{ID: id, Want: want, Got: got} + } + return nil +} diff --git a/internal/contract/pressure_gen.go b/internal/contract/pressure_gen.go new file mode 100644 index 0000000..6735621 --- /dev/null +++ b/internal/contract/pressure_gen.go @@ -0,0 +1,58 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidPressureFrequency struct { + IndependentConsumers *int64 `json:"independent_consumers,omitempty" yaml:"independent_consumers,omitempty"` + Observations *int64 `json:"observations,omitempty" yaml:"observations,omitempty"` +} + +// ArchitectureBlueprint.md section 9. Pressure may remain unresolved on purpose. +type FluidPressureStatus string + +const ( + FluidPressureStatusOPEN FluidPressureStatus = "OPEN" + FluidPressureStatusANALYZING FluidPressureStatus = "ANALYZING" + FluidPressureStatusEXPLAINED FluidPressureStatus = "EXPLAINED" + FluidPressureStatusADDRESSED FluidPressureStatus = "ADDRESSED" + FluidPressureStatusDISMISSED FluidPressureStatus = "DISMISSED" + FluidPressureStatusOUTOFSCOPE FluidPressureStatus = "OUT_OF_SCOPE" +) + +// Valid reports whether v is a defined FluidPressureStatus. +func (v FluidPressureStatus) Valid() bool { + switch v { + case FluidPressureStatusOPEN, FluidPressureStatusANALYZING, FluidPressureStatusEXPLAINED, FluidPressureStatusADDRESSED, FluidPressureStatusDISMISSED, FluidPressureStatusOUTOFSCOPE: + return true + } + return false +} + +type FluidPressure struct { + AffectedCohorts []CohortID `json:"affected_cohorts,omitempty" yaml:"affected_cohorts,omitempty"` + Class PressureClass `json:"class" yaml:"class"` + Confidence *UnitInterval `json:"confidence,omitempty" yaml:"confidence,omitempty"` + + // A pressure record without evidence references is not auditable and must be + // rejected. + EvidenceRefs []EvidenceRef `json:"evidence_refs" yaml:"evidence_refs"` + FirstSeen Timestamp `json:"first_seen" yaml:"first_seen"` + Frequency *FluidPressureFrequency `json:"frequency,omitempty" yaml:"frequency,omitempty"` + ID PressureID `json:"id" yaml:"id"` + InterfaceID InterfaceID `json:"interface_id" yaml:"interface_id"` + LastSeen Timestamp `json:"last_seen" yaml:"last_seen"` + LinkedHypotheses []HypothesisID `json:"linked_hypotheses,omitempty" yaml:"linked_hypotheses,omitempty"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + Severity *UnitInterval `json:"severity,omitempty" yaml:"severity,omitempty"` + + // ArchitectureBlueprint.md section 9. Pressure may remain unresolved on purpose. + Status FluidPressureStatus `json:"status" yaml:"status"` + Summary string `json:"summary" yaml:"summary"` +} + +// Evidence that the interface differs materially from consumer needs. Pressure is +// evidence, not truth (FluidAPIStandards.md section 13). +type PressureDocument struct { + FluidPressure FluidPressure `json:"fluid_pressure" yaml:"fluid_pressure"` +} diff --git a/internal/contract/revision_descriptor_gen.go b/internal/contract/revision_descriptor_gen.go new file mode 100644 index 0000000..7e3f30d --- /dev/null +++ b/internal/contract/revision_descriptor_gen.go @@ -0,0 +1,215 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type RevisionContractType string + +const ( + RevisionContractTypeOpenapi RevisionContractType = "openapi" + RevisionContractTypeGraphql RevisionContractType = "graphql" + RevisionContractTypeProtobuf RevisionContractType = "protobuf" + RevisionContractTypeAsyncapi RevisionContractType = "asyncapi" + RevisionContractTypeJsonschema RevisionContractType = "jsonschema" + RevisionContractTypeCustom RevisionContractType = "custom" +) + +// Valid reports whether v is a defined RevisionContractType. +func (v RevisionContractType) Valid() bool { + switch v { + case RevisionContractTypeOpenapi, RevisionContractTypeGraphql, RevisionContractTypeProtobuf, RevisionContractTypeAsyncapi, RevisionContractTypeJsonschema, RevisionContractTypeCustom: + return true + } + return false +} + +type RevisionContract struct { + Digest Digest `json:"digest" yaml:"digest"` + + // Where the contract artifact can be fetched. + Source string `json:"source,omitempty" yaml:"source,omitempty"` + Type RevisionContractType `json:"type" yaml:"type"` +} + +// Which interface evolution intent governs this revision. Required: a revision with +// no governing intent cannot be audited. +type RevisionIntent struct { + Digest *Digest `json:"digest,omitempty" yaml:"digest,omitempty"` + Version string `json:"version" yaml:"version"` +} + +type RevisionPolicyCompatibility string + +const ( + RevisionPolicyCompatibilityBackwardCompatible RevisionPolicyCompatibility = "backward_compatible" + RevisionPolicyCompatibilityForwardCompatible RevisionPolicyCompatibility = "forward_compatible" + RevisionPolicyCompatibilityAdditive RevisionPolicyCompatibility = "additive" + RevisionPolicyCompatibilityBehavioralChange RevisionPolicyCompatibility = "behavioral_change" + RevisionPolicyCompatibilityBreaking RevisionPolicyCompatibility = "breaking" + RevisionPolicyCompatibilityMigrationOnly RevisionPolicyCompatibility = "migration_only" + RevisionPolicyCompatibilityInternalOnly RevisionPolicyCompatibility = "internal_only" +) + +// Valid reports whether v is a defined RevisionPolicyCompatibility. +func (v RevisionPolicyCompatibility) Valid() bool { + switch v { + case RevisionPolicyCompatibilityBackwardCompatible, RevisionPolicyCompatibilityForwardCompatible, RevisionPolicyCompatibilityAdditive, RevisionPolicyCompatibilityBehavioralChange, RevisionPolicyCompatibilityBreaking, RevisionPolicyCompatibilityMigrationOnly, RevisionPolicyCompatibilityInternalOnly: + return true + } + return false +} + +type RevisionPolicyPolicyCheck string + +const ( + RevisionPolicyPolicyCheckPending RevisionPolicyPolicyCheck = "pending" + RevisionPolicyPolicyCheckPassed RevisionPolicyPolicyCheck = "passed" + RevisionPolicyPolicyCheckFailed RevisionPolicyPolicyCheck = "failed" +) + +// Valid reports whether v is a defined RevisionPolicyPolicyCheck. +func (v RevisionPolicyPolicyCheck) Valid() bool { + switch v { + case RevisionPolicyPolicyCheckPending, RevisionPolicyPolicyCheckPassed, RevisionPolicyPolicyCheckFailed: + return true + } + return false +} + +type RevisionPolicySecurityCheck string + +const ( + RevisionPolicySecurityCheckPending RevisionPolicySecurityCheck = "pending" + RevisionPolicySecurityCheckPassed RevisionPolicySecurityCheck = "passed" + RevisionPolicySecurityCheckFailed RevisionPolicySecurityCheck = "failed" +) + +// Valid reports whether v is a defined RevisionPolicySecurityCheck. +func (v RevisionPolicySecurityCheck) Valid() bool { + switch v { + case RevisionPolicySecurityCheckPending, RevisionPolicySecurityCheckPassed, RevisionPolicySecurityCheckFailed: + return true + } + return false +} + +type RevisionPolicy struct { + Compatibility RevisionPolicyCompatibility `json:"compatibility" yaml:"compatibility"` + PolicyCheck *RevisionPolicyPolicyCheck `json:"policy_check,omitempty" yaml:"policy_check,omitempty"` + RollbackTo *RevisionID `json:"rollback_to,omitempty" yaml:"rollback_to,omitempty"` + SecurityCheck RevisionPolicySecurityCheck `json:"security_check" yaml:"security_check"` +} + +type RevisionRouting struct { + // Empty or absent means all cohorts are eligible. + EligibleCohorts []CohortID `json:"eligible_cohorts,omitempty" yaml:"eligible_cohorts,omitempty"` + + // Ceiling the router enforces regardless of what a routing policy asks for. + MaxTrafficShare *UnitInterval `json:"max_traffic_share,omitempty" yaml:"max_traffic_share,omitempty"` +} + +type RevisionRuntimeCircuitBreaker struct { + FailureThreshold *int64 `json:"failure_threshold,omitempty" yaml:"failure_threshold,omitempty"` + ResetAfterMS *int64 `json:"reset_after_ms,omitempty" yaml:"reset_after_ms,omitempty"` +} + +type RevisionRuntimeRetry struct { + BackoffMS *int64 `json:"backoff_ms,omitempty" yaml:"backoff_ms,omitempty"` + MaxAttempts *int64 `json:"max_attempts,omitempty" yaml:"max_attempts,omitempty"` + RetryOn []string `json:"retry_on,omitempty" yaml:"retry_on,omitempty"` +} + +// Out-of-process attachment: the adapter is an upstream reached over the network, +// so it may be implemented in any language. +type RevisionRuntime struct { + CircuitBreaker *RevisionRuntimeCircuitBreaker `json:"circuit_breaker,omitempty" yaml:"circuit_breaker,omitempty"` + Digest *Digest `json:"digest,omitempty" yaml:"digest,omitempty"` + Image string `json:"image,omitempty" yaml:"image,omitempty"` + Retry *RevisionRuntimeRetry `json:"retry,omitempty" yaml:"retry,omitempty"` + TimeoutMS *int64 `json:"timeout_ms,omitempty" yaml:"timeout_ms,omitempty"` + + // Base URL of the adapter process serving this revision. + Upstream string `json:"upstream" yaml:"upstream"` +} + +type RevisionSignatureAlgorithm string + +const ( + RevisionSignatureAlgorithmEd25519 RevisionSignatureAlgorithm = "ed25519" +) + +// Valid reports whether v is a defined RevisionSignatureAlgorithm. +func (v RevisionSignatureAlgorithm) Valid() bool { + switch v { + case RevisionSignatureAlgorithmEd25519: + return true + } + return false +} + +// The router accepts only signed published descriptors (ArchitectureBlueprint.md +// section 35). The signature covers the canonical form of this document with the +// signature member removed. +type RevisionSignature struct { + Algorithm RevisionSignatureAlgorithm `json:"algorithm" yaml:"algorithm"` + KeyID string `json:"key_id" yaml:"key_id"` + SignedAt *Timestamp `json:"signed_at,omitempty" yaml:"signed_at,omitempty"` + Value string `json:"value" yaml:"value"` +} + +// The router refuses to route to created, failed or retired revisions +// (ArchitectureBlueprint.md section 5.3). +type RevisionState string + +const ( + RevisionStateCreated RevisionState = "created" + RevisionStateVerified RevisionState = "verified" + RevisionStateExperiment RevisionState = "experiment" + RevisionStateCandidate RevisionState = "candidate" + RevisionStateStable RevisionState = "stable" + RevisionStateDeprecated RevisionState = "deprecated" + RevisionStateRetired RevisionState = "retired" +) + +// Valid reports whether v is a defined RevisionState. +func (v RevisionState) Valid() bool { + switch v { + case RevisionStateCreated, RevisionStateVerified, RevisionStateExperiment, RevisionStateCandidate, RevisionStateStable, RevisionStateDeprecated, RevisionStateRetired: + return true + } + return false +} + +type Revision struct { + Contract RevisionContract `json:"contract" yaml:"contract"` + ID RevisionID `json:"id" yaml:"id"` + + // Which interface evolution intent governs this revision. Required: a revision + // with no governing intent cannot be audited. + Intent RevisionIntent `json:"intent" yaml:"intent"` + Interface InterfaceID `json:"interface" yaml:"interface"` + Policy RevisionPolicy `json:"policy" yaml:"policy"` + Routing *RevisionRouting `json:"routing,omitempty" yaml:"routing,omitempty"` + + // Out-of-process attachment: the adapter is an upstream reached over the network, + // so it may be implemented in any language. + Runtime RevisionRuntime `json:"runtime" yaml:"runtime"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + + // The router accepts only signed published descriptors (ArchitectureBlueprint.md + // section 35). The signature covers the canonical form of this document with the + // signature member removed. + Signature *RevisionSignature `json:"signature,omitempty" yaml:"signature,omitempty"` + + // The router refuses to route to created, failed or retired revisions + // (ArchitectureBlueprint.md section 5.3). + State RevisionState `json:"state" yaml:"state"` +} + +// The runtime-facing projection of a revision, deliberately small enough to be +// consumed deterministically by infrastructure (ArchitectureBlueprint.md section +// 36). This is a wire artifact: the gateway must be able to read it without any +// fluid-core library. +type RevisionDescriptorDocument struct { + Revision Revision `json:"revision" yaml:"revision"` +} diff --git a/internal/contract/revision_gen.go b/internal/contract/revision_gen.go new file mode 100644 index 0000000..d327170 --- /dev/null +++ b/internal/contract/revision_gen.go @@ -0,0 +1,314 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +// Adoption is evidence, not proof of quality (FluidAPIStandards.md section 32). +type FluidRevisionAdoption struct { + ActiveConsumers *int64 `json:"active_consumers,omitempty" yaml:"active_consumers,omitempty"` + AdoptionRate *UnitInterval `json:"adoption_rate,omitempty" yaml:"adoption_rate,omitempty"` + EligibleConsumers *int64 `json:"eligible_consumers,omitempty" yaml:"eligible_consumers,omitempty"` + RetainedAdoptionRate *UnitInterval `json:"retained_adoption_rate,omitempty" yaml:"retained_adoption_rate,omitempty"` +} + +type FluidRevisionAudit struct { + ImmutableEventRefs []EventID `json:"immutable_event_refs,omitempty" yaml:"immutable_event_refs,omitempty"` +} + +type FluidRevisionCompatibilityClass string + +const ( + FluidRevisionCompatibilityClassBACKWARDCOMPATIBLE FluidRevisionCompatibilityClass = "BACKWARD_COMPATIBLE" + FluidRevisionCompatibilityClassFORWARDCOMPATIBLE FluidRevisionCompatibilityClass = "FORWARD_COMPATIBLE" + FluidRevisionCompatibilityClassADDITIVE FluidRevisionCompatibilityClass = "ADDITIVE" + FluidRevisionCompatibilityClassBEHAVIORALCHANGE FluidRevisionCompatibilityClass = "BEHAVIORAL_CHANGE" + FluidRevisionCompatibilityClassBREAKING FluidRevisionCompatibilityClass = "BREAKING" + FluidRevisionCompatibilityClassMIGRATIONONLY FluidRevisionCompatibilityClass = "MIGRATION_ONLY" + FluidRevisionCompatibilityClassINTERNALONLY FluidRevisionCompatibilityClass = "INTERNAL_ONLY" +) + +// Valid reports whether v is a defined FluidRevisionCompatibilityClass. +func (v FluidRevisionCompatibilityClass) Valid() bool { + switch v { + case FluidRevisionCompatibilityClassBACKWARDCOMPATIBLE, FluidRevisionCompatibilityClassFORWARDCOMPATIBLE, FluidRevisionCompatibilityClassADDITIVE, FluidRevisionCompatibilityClassBEHAVIORALCHANGE, FluidRevisionCompatibilityClassBREAKING, FluidRevisionCompatibilityClassMIGRATIONONLY, FluidRevisionCompatibilityClassINTERNALONLY: + return true + } + return false +} + +type FluidRevisionCompatibility struct { + BreakingChanges []string `json:"breaking_changes,omitempty" yaml:"breaking_changes,omitempty"` + Class FluidRevisionCompatibilityClass `json:"class" yaml:"class"` + CompatibilityEvidenceRefs []EvidenceRef `json:"compatibility_evidence_refs,omitempty" yaml:"compatibility_evidence_refs,omitempty"` + Deprecations []string `json:"deprecations,omitempty" yaml:"deprecations,omitempty"` + Supersedes []RevisionID `json:"supersedes,omitempty" yaml:"supersedes,omitempty"` +} + +type FluidRevisionComplexityBudgetStatus string + +const ( + FluidRevisionComplexityBudgetStatusWITHINBUDGET FluidRevisionComplexityBudgetStatus = "WITHIN_BUDGET" + FluidRevisionComplexityBudgetStatusATBUDGET FluidRevisionComplexityBudgetStatus = "AT_BUDGET" + FluidRevisionComplexityBudgetStatusOVERBUDGET FluidRevisionComplexityBudgetStatus = "OVER_BUDGET" +) + +// Valid reports whether v is a defined FluidRevisionComplexityBudgetStatus. +func (v FluidRevisionComplexityBudgetStatus) Valid() bool { + switch v { + case FluidRevisionComplexityBudgetStatusWITHINBUDGET, FluidRevisionComplexityBudgetStatusATBUDGET, FluidRevisionComplexityBudgetStatusOVERBUDGET: + return true + } + return false +} + +type FluidRevisionComplexity struct { + After *ComplexityDelta `json:"after,omitempty" yaml:"after,omitempty"` + Before *ComplexityDelta `json:"before,omitempty" yaml:"before,omitempty"` + BudgetStatus *FluidRevisionComplexityBudgetStatus `json:"budget_status,omitempty" yaml:"budget_status,omitempty"` + DeltaScore *float64 `json:"delta_score,omitempty" yaml:"delta_score,omitempty"` +} + +type FluidRevisionContractType string + +const ( + FluidRevisionContractTypeOpenapi FluidRevisionContractType = "openapi" + FluidRevisionContractTypeGraphql FluidRevisionContractType = "graphql" + FluidRevisionContractTypeProtobuf FluidRevisionContractType = "protobuf" + FluidRevisionContractTypeAsyncapi FluidRevisionContractType = "asyncapi" + FluidRevisionContractTypeJsonschema FluidRevisionContractType = "jsonschema" + FluidRevisionContractTypeCustom FluidRevisionContractType = "custom" +) + +// Valid reports whether v is a defined FluidRevisionContractType. +func (v FluidRevisionContractType) Valid() bool { + switch v { + case FluidRevisionContractTypeOpenapi, FluidRevisionContractTypeGraphql, FluidRevisionContractTypeProtobuf, FluidRevisionContractTypeAsyncapi, FluidRevisionContractTypeJsonschema, FluidRevisionContractTypeCustom: + return true + } + return false +} + +type FluidRevisionContract struct { + ArtifactRef ArtifactRef `json:"artifact_ref" yaml:"artifact_ref"` + Digest Digest `json:"digest" yaml:"digest"` + Type FluidRevisionContractType `json:"type" yaml:"type"` + Version string `json:"version,omitempty" yaml:"version,omitempty"` +} + +type FluidRevisionDeploymentCurrentExposure struct { + Cohorts []CohortID `json:"cohorts,omitempty" yaml:"cohorts,omitempty"` + TrafficShare *UnitInterval `json:"traffic_share,omitempty" yaml:"traffic_share,omitempty"` +} + +type FluidRevisionDeploymentEnvironmentsItem struct { + Name string `json:"name" yaml:"name"` + RoutingPolicyRef string `json:"routing_policy_ref,omitempty" yaml:"routing_policy_ref,omitempty"` + StartedAt *Timestamp `json:"started_at,omitempty" yaml:"started_at,omitempty"` +} + +type FluidRevisionDeployment struct { + CurrentExposure *FluidRevisionDeploymentCurrentExposure `json:"current_exposure,omitempty" yaml:"current_exposure,omitempty"` + Environments []FluidRevisionDeploymentEnvironmentsItem `json:"environments,omitempty" yaml:"environments,omitempty"` +} + +type FluidRevisionEconomics struct { + BuildCost *float64 `json:"build_cost,omitempty" yaml:"build_cost,omitempty"` + Currency string `json:"currency,omitempty" yaml:"currency,omitempty"` + ExperimentCostToDate *float64 `json:"experiment_cost_to_date,omitempty" yaml:"experiment_cost_to_date,omitempty"` +} + +type FluidRevisionFitnessMetricsValue struct { + Baseline *float64 `json:"baseline,omitempty" yaml:"baseline,omitempty"` + Current *float64 `json:"current,omitempty" yaml:"current,omitempty"` + Guardrail *float64 `json:"guardrail,omitempty" yaml:"guardrail,omitempty"` + Target *float64 `json:"target,omitempty" yaml:"target,omitempty"` +} + +type FluidRevisionFitness struct { + BaselineRevision *RevisionID `json:"baseline_revision,omitempty" yaml:"baseline_revision,omitempty"` + MeasurementWindow *ObservationWindow `json:"measurement_window,omitempty" yaml:"measurement_window,omitempty"` + Metrics map[string]FluidRevisionFitnessMetricsValue `json:"metrics,omitempty" yaml:"metrics,omitempty"` +} + +type FluidRevisionImplementation struct { + ArtifactRef ArtifactRef `json:"artifact_ref" yaml:"artifact_ref"` + BuildRef string `json:"build_ref,omitempty" yaml:"build_ref,omitempty"` + Digest Digest `json:"digest" yaml:"digest"` + SourceRef string `json:"source_ref,omitempty" yaml:"source_ref,omitempty"` +} + +// Every revision is governed by a specific intent version, so audit can ask whether +// the change was valid under the intent that existed at the time +// (ArchitectureBlueprint.md section 27). +type FluidRevisionInterfaceEvolutionIntent struct { + ArtifactRef *ArtifactRef `json:"artifact_ref,omitempty" yaml:"artifact_ref,omitempty"` + Version string `json:"version" yaml:"version"` +} + +type FluidRevisionPromotionRecommendedState string + +const ( + FluidRevisionPromotionRecommendedStateCREATED FluidRevisionPromotionRecommendedState = "CREATED" + FluidRevisionPromotionRecommendedStateVERIFIED FluidRevisionPromotionRecommendedState = "VERIFIED" + FluidRevisionPromotionRecommendedStateEXPERIMENT FluidRevisionPromotionRecommendedState = "EXPERIMENT" + FluidRevisionPromotionRecommendedStateCANDIDATE FluidRevisionPromotionRecommendedState = "CANDIDATE" + FluidRevisionPromotionRecommendedStateSTABLE FluidRevisionPromotionRecommendedState = "STABLE" + FluidRevisionPromotionRecommendedStateDEPRECATED FluidRevisionPromotionRecommendedState = "DEPRECATED" + FluidRevisionPromotionRecommendedStateRETIRED FluidRevisionPromotionRecommendedState = "RETIRED" +) + +// Valid reports whether v is a defined FluidRevisionPromotionRecommendedState. +func (v FluidRevisionPromotionRecommendedState) Valid() bool { + switch v { + case FluidRevisionPromotionRecommendedStateCREATED, FluidRevisionPromotionRecommendedStateVERIFIED, FluidRevisionPromotionRecommendedStateEXPERIMENT, FluidRevisionPromotionRecommendedStateCANDIDATE, FluidRevisionPromotionRecommendedStateSTABLE, FluidRevisionPromotionRecommendedStateDEPRECATED, FluidRevisionPromotionRecommendedStateRETIRED: + return true + } + return false +} + +type FluidRevisionPromotion struct { + AuthorizedAt *Timestamp `json:"authorized_at,omitempty" yaml:"authorized_at,omitempty"` + AuthorizedBy *Actor `json:"authorized_by,omitempty" yaml:"authorized_by,omitempty"` + Eligible *bool `json:"eligible,omitempty" yaml:"eligible,omitempty"` + RecommendationReason *string `json:"recommendation_reason,omitempty" yaml:"recommendation_reason,omitempty"` + RecommendedState *FluidRevisionPromotionRecommendedState `json:"recommended_state,omitempty" yaml:"recommended_state,omitempty"` +} + +type FluidRevisionProvenance struct { + DecisionRefs []DecisionID `json:"decision_refs,omitempty" yaml:"decision_refs,omitempty"` + ExperimentRefs []ExperimentID `json:"experiment_refs,omitempty" yaml:"experiment_refs,omitempty"` + TelemetryRefs []EvidenceRef `json:"telemetry_refs,omitempty" yaml:"telemetry_refs,omitempty"` +} + +type FluidRevisionRollback struct { + ProcedureRef string `json:"procedure_ref,omitempty" yaml:"procedure_ref,omitempty"` + Supported bool `json:"supported" yaml:"supported"` + TargetRevision *RevisionID `json:"target_revision,omitempty" yaml:"target_revision,omitempty"` +} + +type FluidRevisionState string + +const ( + FluidRevisionStateCREATED FluidRevisionState = "CREATED" + FluidRevisionStateVERIFIED FluidRevisionState = "VERIFIED" + FluidRevisionStateEXPERIMENT FluidRevisionState = "EXPERIMENT" + FluidRevisionStateCANDIDATE FluidRevisionState = "CANDIDATE" + FluidRevisionStateSTABLE FluidRevisionState = "STABLE" + FluidRevisionStateDEPRECATED FluidRevisionState = "DEPRECATED" + FluidRevisionStateRETIRED FluidRevisionState = "RETIRED" +) + +// Valid reports whether v is a defined FluidRevisionState. +func (v FluidRevisionState) Valid() bool { + switch v { + case FluidRevisionStateCREATED, FluidRevisionStateVERIFIED, FluidRevisionStateEXPERIMENT, FluidRevisionStateCANDIDATE, FluidRevisionStateSTABLE, FluidRevisionStateDEPRECATED, FluidRevisionStateRETIRED: + return true + } + return false +} + +type FluidRevisionVerificationPolicyCheck string + +const ( + FluidRevisionVerificationPolicyCheckPENDING FluidRevisionVerificationPolicyCheck = "PENDING" + FluidRevisionVerificationPolicyCheckPASSED FluidRevisionVerificationPolicyCheck = "PASSED" + FluidRevisionVerificationPolicyCheckFAILED FluidRevisionVerificationPolicyCheck = "FAILED" +) + +// Valid reports whether v is a defined FluidRevisionVerificationPolicyCheck. +func (v FluidRevisionVerificationPolicyCheck) Valid() bool { + switch v { + case FluidRevisionVerificationPolicyCheckPENDING, FluidRevisionVerificationPolicyCheckPASSED, FluidRevisionVerificationPolicyCheckFAILED: + return true + } + return false +} + +type FluidRevisionVerificationSecurityCheck string + +const ( + FluidRevisionVerificationSecurityCheckPENDING FluidRevisionVerificationSecurityCheck = "PENDING" + FluidRevisionVerificationSecurityCheckPASSED FluidRevisionVerificationSecurityCheck = "PASSED" + FluidRevisionVerificationSecurityCheckFAILED FluidRevisionVerificationSecurityCheck = "FAILED" +) + +// Valid reports whether v is a defined FluidRevisionVerificationSecurityCheck. +func (v FluidRevisionVerificationSecurityCheck) Valid() bool { + switch v { + case FluidRevisionVerificationSecurityCheckPENDING, FluidRevisionVerificationSecurityCheckPASSED, FluidRevisionVerificationSecurityCheckFAILED: + return true + } + return false +} + +type FluidRevisionVerificationStatus string + +const ( + FluidRevisionVerificationStatusPENDING FluidRevisionVerificationStatus = "PENDING" + FluidRevisionVerificationStatusPASSED FluidRevisionVerificationStatus = "PASSED" + FluidRevisionVerificationStatusFAILED FluidRevisionVerificationStatus = "FAILED" +) + +// Valid reports whether v is a defined FluidRevisionVerificationStatus. +func (v FluidRevisionVerificationStatus) Valid() bool { + switch v { + case FluidRevisionVerificationStatusPENDING, FluidRevisionVerificationStatusPASSED, FluidRevisionVerificationStatusFAILED: + return true + } + return false +} + +// The primary safety barrier between adaptive generation and deterministic runtime +// (ArchitectureBlueprint.md section 15). +type FluidRevisionVerification struct { + PolicyCheck *FluidRevisionVerificationPolicyCheck `json:"policy_check,omitempty" yaml:"policy_check,omitempty"` + SecurityCheck *FluidRevisionVerificationSecurityCheck `json:"security_check,omitempty" yaml:"security_check,omitempty"` + Status FluidRevisionVerificationStatus `json:"status" yaml:"status"` + TestRefs []string `json:"test_refs,omitempty" yaml:"test_refs,omitempty"` +} + +type FluidRevision struct { + AdaptationClasses []AdaptationClass `json:"adaptation_classes,omitempty" yaml:"adaptation_classes,omitempty"` + + // Adoption is evidence, not proof of quality (FluidAPIStandards.md section 32). + Adoption *FluidRevisionAdoption `json:"adoption,omitempty" yaml:"adoption,omitempty"` + Audit *FluidRevisionAudit `json:"audit,omitempty" yaml:"audit,omitempty"` + BackendRequirements []BackendRequirementID `json:"backend_requirements,omitempty" yaml:"backend_requirements,omitempty"` + Compatibility FluidRevisionCompatibility `json:"compatibility" yaml:"compatibility"` + Complexity *FluidRevisionComplexity `json:"complexity,omitempty" yaml:"complexity,omitempty"` + Contract FluidRevisionContract `json:"contract" yaml:"contract"` + CreatedAt *Timestamp `json:"created_at,omitempty" yaml:"created_at,omitempty"` + CreatedBy *Actor `json:"created_by,omitempty" yaml:"created_by,omitempty"` + Deployment *FluidRevisionDeployment `json:"deployment,omitempty" yaml:"deployment,omitempty"` + Economics *FluidRevisionEconomics `json:"economics,omitempty" yaml:"economics,omitempty"` + Fitness *FluidRevisionFitness `json:"fitness,omitempty" yaml:"fitness,omitempty"` + ID RevisionID `json:"id" yaml:"id"` + Implementation FluidRevisionImplementation `json:"implementation" yaml:"implementation"` + + // Every revision is governed by a specific intent version, so audit can ask + // whether the change was valid under the intent that existed at the time + // (ArchitectureBlueprint.md section 27). + InterfaceEvolutionIntent FluidRevisionInterfaceEvolutionIntent `json:"interface_evolution_intent" yaml:"interface_evolution_intent"` + InterfaceID InterfaceID `json:"interface_id" yaml:"interface_id"` + OriginatingHypotheses []HypothesisID `json:"originating_hypotheses,omitempty" yaml:"originating_hypotheses,omitempty"` + + // Null marks genesis. + ParentRevision *RevisionID `json:"parent_revision" yaml:"parent_revision"` + Promotion *FluidRevisionPromotion `json:"promotion,omitempty" yaml:"promotion,omitempty"` + Provenance FluidRevisionProvenance `json:"provenance" yaml:"provenance"` + RevisionNumber int64 `json:"revision_number" yaml:"revision_number"` + Rollback FluidRevisionRollback `json:"rollback" yaml:"rollback"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + State FluidRevisionState `json:"state" yaml:"state"` + + // The primary safety barrier between adaptive generation and deterministic runtime + // (ArchitectureBlueprint.md section 15). + Verification FluidRevisionVerification `json:"verification" yaml:"verification"` +} + +// What deterministic interface state was built, verified, exposed and measured. +// Published revisions should be immutable; corrections create successors +// (FluidAPIStandards.md section 8). +type RevisionDocument struct { + FluidRevision FluidRevision `json:"fluid_revision" yaml:"fluid_revision"` +} diff --git a/internal/contract/routing_policy_gen.go b/internal/contract/routing_policy_gen.go new file mode 100644 index 0000000..35d1e01 --- /dev/null +++ b/internal/contract/routing_policy_gen.go @@ -0,0 +1,82 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +// What keeps a long-lived consumer on one side of an experiment. +type RoutingPolicyRulesItemStickyBy string + +const ( + RoutingPolicyRulesItemStickyByConsumerID RoutingPolicyRulesItemStickyBy = "consumer_id" + RoutingPolicyRulesItemStickyByTenant RoutingPolicyRulesItemStickyBy = "tenant" + RoutingPolicyRulesItemStickyByCorrelationID RoutingPolicyRulesItemStickyBy = "correlation_id" + RoutingPolicyRulesItemStickyByNone RoutingPolicyRulesItemStickyBy = "none" +) + +// Valid reports whether v is a defined RoutingPolicyRulesItemStickyBy. +func (v RoutingPolicyRulesItemStickyBy) Valid() bool { + switch v { + case RoutingPolicyRulesItemStickyByConsumerID, RoutingPolicyRulesItemStickyByTenant, RoutingPolicyRulesItemStickyByCorrelationID, RoutingPolicyRulesItemStickyByNone: + return true + } + return false +} + +type RoutingPolicyRulesItem struct { + // Revision id to traffic share. Shares must sum to 1. Assignment of a given + // consumer must be stable across requests. + Allocation map[string]UnitInterval `json:"allocation" yaml:"allocation"` + Cohort *CohortID `json:"cohort,omitempty" yaml:"cohort,omitempty"` + Experiment *ExperimentID `json:"experiment,omitempty" yaml:"experiment,omitempty"` + + // What keeps a long-lived consumer on one side of an experiment. + StickyBy *RoutingPolicyRulesItemStickyBy `json:"sticky_by,omitempty" yaml:"sticky_by,omitempty"` + Tenant string `json:"tenant,omitempty" yaml:"tenant,omitempty"` +} + +type RoutingPolicySignatureAlgorithm string + +const ( + RoutingPolicySignatureAlgorithmEd25519 RoutingPolicySignatureAlgorithm = "ed25519" +) + +// Valid reports whether v is a defined RoutingPolicySignatureAlgorithm. +func (v RoutingPolicySignatureAlgorithm) Valid() bool { + switch v { + case RoutingPolicySignatureAlgorithmEd25519: + return true + } + return false +} + +type RoutingPolicySignature struct { + Algorithm RoutingPolicySignatureAlgorithm `json:"algorithm" yaml:"algorithm"` + KeyID string `json:"key_id" yaml:"key_id"` + Value string `json:"value" yaml:"value"` +} + +type RoutingPolicy struct { + // Where a request lands when no earlier resolution step matched. + DefaultRevision RevisionID `json:"default_revision" yaml:"default_revision"` + + // Monotonic. The router ignores a policy older than the one it holds. + Generation int64 `json:"generation" yaml:"generation"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Interface InterfaceID `json:"interface" yaml:"interface"` + IssuedAt *Timestamp `json:"issued_at,omitempty" yaml:"issued_at,omitempty"` + IssuedBy *Actor `json:"issued_by,omitempty" yaml:"issued_by,omitempty"` + + // Evaluated in order; the first matching rule wins. Matching must be a pure + // function of the request and its cohort assignment. + Rules []RoutingPolicyRulesItem `json:"rules" yaml:"rules"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + Signature *RoutingPolicySignature `json:"signature,omitempty" yaml:"signature,omitempty"` +} + +// Deterministic routing written by the experiment controller and consumed by the +// revision router. The controller never touches traffic itself; this separation +// keeps experimental intent out of the runtime decision mechanism +// (ArchitectureBlueprint.md section 17). +type RoutingPolicyDocument struct { + RoutingPolicy RoutingPolicy `json:"routing_policy" yaml:"routing_policy"` +} diff --git a/internal/contract/telemetry_envelope_gen.go b/internal/contract/telemetry_envelope_gen.go new file mode 100644 index 0000000..967bb6c --- /dev/null +++ b/internal/contract/telemetry_envelope_gen.go @@ -0,0 +1,172 @@ +// Code generated by tools/schemagen. DO NOT EDIT. +// Source: schemas/. Regenerate with `make generate`. + +package contract + +type FluidTelemetryAdoptionEvent string + +const ( + FluidTelemetryAdoptionEventFirstUse FluidTelemetryAdoptionEvent = "first_use" + FluidTelemetryAdoptionEventContinuedUse FluidTelemetryAdoptionEvent = "continued_use" + FluidTelemetryAdoptionEventMigration FluidTelemetryAdoptionEvent = "migration" + FluidTelemetryAdoptionEventReversion FluidTelemetryAdoptionEvent = "reversion" + FluidTelemetryAdoptionEventDeprecationResponse FluidTelemetryAdoptionEvent = "deprecation_response" +) + +// Valid reports whether v is a defined FluidTelemetryAdoptionEvent. +func (v FluidTelemetryAdoptionEvent) Valid() bool { + switch v { + case FluidTelemetryAdoptionEventFirstUse, FluidTelemetryAdoptionEventContinuedUse, FluidTelemetryAdoptionEventMigration, FluidTelemetryAdoptionEventReversion, FluidTelemetryAdoptionEventDeprecationResponse: + return true + } + return false +} + +type FluidTelemetryAdoption struct { + Event FluidTelemetryAdoptionEvent `json:"event" yaml:"event"` + FromRevision *RevisionID `json:"from_revision,omitempty" yaml:"from_revision,omitempty"` +} + +type FluidTelemetryErrorClass string + +const ( + FluidTelemetryErrorClassValidation FluidTelemetryErrorClass = "validation" + FluidTelemetryErrorClassUnknownPath FluidTelemetryErrorClass = "unknown_path" + FluidTelemetryErrorClassUnknownField FluidTelemetryErrorClass = "unknown_field" + FluidTelemetryErrorClassUnsupportedParameter FluidTelemetryErrorClass = "unsupported_parameter" + FluidTelemetryErrorClassAuthorization FluidTelemetryErrorClass = "authorization" + FluidTelemetryErrorClassBackendFailure FluidTelemetryErrorClass = "backend_failure" + FluidTelemetryErrorClassTimeout FluidTelemetryErrorClass = "timeout" + FluidTelemetryErrorClassPolicyRejection FluidTelemetryErrorClass = "policy_rejection" +) + +// Valid reports whether v is a defined FluidTelemetryErrorClass. +func (v FluidTelemetryErrorClass) Valid() bool { + switch v { + case FluidTelemetryErrorClassValidation, FluidTelemetryErrorClassUnknownPath, FluidTelemetryErrorClassUnknownField, FluidTelemetryErrorClassUnsupportedParameter, FluidTelemetryErrorClassAuthorization, FluidTelemetryErrorClassBackendFailure, FluidTelemetryErrorClassTimeout, FluidTelemetryErrorClassPolicyRejection: + return true + } + return false +} + +type FluidTelemetryError struct { + Class FluidTelemetryErrorClass `json:"class" yaml:"class"` + + // Redacted. Must not carry backend internals or consumer data. + Detail string `json:"detail,omitempty" yaml:"detail,omitempty"` + Field string `json:"field,omitempty" yaml:"field,omitempty"` +} + +// ArchitectureBlueprint.md section 6.1. +type FluidTelemetryKind string + +const ( + FluidTelemetryKindRequest FluidTelemetryKind = "request" + FluidTelemetryKindError FluidTelemetryKind = "error" + FluidTelemetryKindSequence FluidTelemetryKind = "sequence" + FluidTelemetryKindAdoption FluidTelemetryKind = "adoption" + FluidTelemetryKindFeedback FluidTelemetryKind = "feedback" +) + +// Valid reports whether v is a defined FluidTelemetryKind. +func (v FluidTelemetryKind) Valid() bool { + switch v { + case FluidTelemetryKindRequest, FluidTelemetryKindError, FluidTelemetryKindSequence, FluidTelemetryKindAdoption, FluidTelemetryKindFeedback: + return true + } + return false +} + +// What the privacy filter removed. Recorded so analysis knows what it cannot see. +type FluidTelemetryRedaction struct { + Applied bool `json:"applied" yaml:"applied"` + Rules []string `json:"rules,omitempty" yaml:"rules,omitempty"` +} + +type FluidTelemetryRequest struct { + LatencyMS *float64 `json:"latency_ms,omitempty" yaml:"latency_ms,omitempty"` + Method string `json:"method,omitempty" yaml:"method,omitempty"` + Operation string `json:"operation,omitempty" yaml:"operation,omitempty"` + RequestBytes *int64 `json:"request_bytes,omitempty" yaml:"request_bytes,omitempty"` + ResponseBytes *int64 `json:"response_bytes,omitempty" yaml:"response_bytes,omitempty"` + Route string `json:"route,omitempty" yaml:"route,omitempty"` + Status *int64 `json:"status,omitempty" yaml:"status,omitempty"` +} + +type FluidTelemetryResolutionReason string + +const ( + FluidTelemetryResolutionReasonExplicitRevision FluidTelemetryResolutionReason = "explicit_revision" + FluidTelemetryResolutionReasonBoundContract FluidTelemetryResolutionReason = "bound_contract" + FluidTelemetryResolutionReasonExperimentAssignment FluidTelemetryResolutionReason = "experiment_assignment" + FluidTelemetryResolutionReasonCohortRule FluidTelemetryResolutionReason = "cohort_rule" + FluidTelemetryResolutionReasonStableDefault FluidTelemetryResolutionReason = "stable_default" +) + +// Valid reports whether v is a defined FluidTelemetryResolutionReason. +func (v FluidTelemetryResolutionReason) Valid() bool { + switch v { + case FluidTelemetryResolutionReasonExplicitRevision, FluidTelemetryResolutionReasonBoundContract, FluidTelemetryResolutionReasonExperimentAssignment, FluidTelemetryResolutionReasonCohortRule, FluidTelemetryResolutionReasonStableDefault: + return true + } + return false +} + +// Why this revision was chosen. Revision resolution must be deterministic and +// auditable (ArchitectureBlueprint.md section 5.2). +type FluidTelemetryResolution struct { + PolicyGeneration *int64 `json:"policy_generation,omitempty" yaml:"policy_generation,omitempty"` + Reason FluidTelemetryResolutionReason `json:"reason" yaml:"reason"` +} + +// Interaction topology is often more informative than error counts +// (ArchitectureBlueprint.md section 6.4). +type FluidTelemetrySequence struct { + ChainID string `json:"chain_id,omitempty" yaml:"chain_id,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + Position *int64 `json:"position,omitempty" yaml:"position,omitempty"` + RepeatCount *int64 `json:"repeat_count,omitempty" yaml:"repeat_count,omitempty"` +} + +type FluidTelemetry struct { + Adoption *FluidTelemetryAdoption `json:"adoption,omitempty" yaml:"adoption,omitempty"` + Cohort *CohortID `json:"cohort,omitempty" yaml:"cohort,omitempty"` + + // Pseudonymous and stable. Never a raw end-user identifier. + ConsumerRef string `json:"consumer_ref,omitempty" yaml:"consumer_ref,omitempty"` + + // Ties an event to the response the consumer saw, and to the other events in its + // call chain. + CorrelationID string `json:"correlation_id,omitempty" yaml:"correlation_id,omitempty"` + Error *FluidTelemetryError `json:"error,omitempty" yaml:"error,omitempty"` + Experiment *ExperimentID `json:"experiment,omitempty" yaml:"experiment,omitempty"` + FeedbackRef *FeedbackID `json:"feedback_ref,omitempty" yaml:"feedback_ref,omitempty"` + ID string `json:"id" yaml:"id"` + InterfaceID InterfaceID `json:"interface_id" yaml:"interface_id"` + + // ArchitectureBlueprint.md section 6.1. + Kind FluidTelemetryKind `json:"kind" yaml:"kind"` + OccurredAt Timestamp `json:"occurred_at" yaml:"occurred_at"` + + // What the privacy filter removed. Recorded so analysis knows what it cannot see. + Redaction *FluidTelemetryRedaction `json:"redaction,omitempty" yaml:"redaction,omitempty"` + Request *FluidTelemetryRequest `json:"request,omitempty" yaml:"request,omitempty"` + + // Why this revision was chosen. Revision resolution must be deterministic and + // auditable (ArchitectureBlueprint.md section 5.2). + Resolution *FluidTelemetryResolution `json:"resolution,omitempty" yaml:"resolution,omitempty"` + Revision *RevisionID `json:"revision,omitempty" yaml:"revision,omitempty"` + SchemaVersion SchemaVersion `json:"schema_version" yaml:"schema_version"` + + // Interaction topology is often more informative than error counts + // (ArchitectureBlueprint.md section 6.4). + Sequence *FluidTelemetrySequence `json:"sequence,omitempty" yaml:"sequence,omitempty"` +} + +// The normalized interaction event. Designed for interface learning, not +// unrestricted behavioral capture: raw payload capture is never the default and +// redaction happens before an event reaches the store (ArchitectureBlueprint.md +// section 6.2). +type TelemetryEnvelopeDocument struct { + FluidTelemetry FluidTelemetry `json:"fluid_telemetry" yaml:"fluid_telemetry"` +} diff --git a/tools/schemagen/main.go b/tools/schemagen/main.go new file mode 100644 index 0000000..cb56160 --- /dev/null +++ b/tools/schemagen/main.go @@ -0,0 +1,495 @@ +// Command schemagen generates Go types from the FLUID wire-contract schemas. +// +// The record types are never hand-written: schemas/ is the source of truth, and +// drift between the specification and the implementation must fail the build +// rather than survive as a quietly diverging struct. +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "go/format" + "os" + "path/filepath" + "sort" + "strings" +) + +// schema is the subset of JSON Schema the FLUID contract actually uses. +type schema struct { + ID string `json:"$id"` + Ref string `json:"$ref"` + Title string `json:"title"` + Description string `json:"description"` + Type any `json:"type"` + Const any `json:"const"` + Enum []string `json:"enum"` + Format string `json:"format"` + Properties map[string]*schema `json:"properties"` + Required []string `json:"required"` + Items *schema `json:"items"` + Defs map[string]*schema `json:"$defs"` + OneOf []*schema `json:"oneOf"` + AdditionalProperties json.RawMessage `json:"additionalProperties"` + PropertyNames *schema `json:"propertyNames"` +} + +// additional reports how additionalProperties was written: as a schema +// (a map value type), as false, or absent. +func (s *schema) additional() (*schema, bool) { + if len(s.AdditionalProperties) == 0 { + return nil, false + } + var b bool + if err := json.Unmarshal(s.AdditionalProperties, &b); err == nil { + return nil, b // true means "anything"; false means closed + } + var sub schema + if err := json.Unmarshal(s.AdditionalProperties, &sub); err == nil { + return &sub, true + } + return nil, false +} + +func (s *schema) typeIs(want string) bool { + switch t := s.Type.(type) { + case string: + return t == want + case []any: + for _, v := range t { + if v == want { + return true + } + } + } + return false +} + +func (s *schema) nullable() bool { + if s.typeIs("null") { + return true + } + for _, o := range s.OneOf { + if o.typeIs("null") { + return true + } + } + return false +} + +var initialisms = map[string]string{ + "id": "ID", "ids": "IDs", "url": "URL", "uri": "URI", "api": "API", + "ms": "MS", "http": "HTTP", "json": "JSON", "yaml": "YAML", "sha": "SHA", + "p95": "P95", "iei": "IEI", "sla": "SLA", "ai": "AI", +} + +// splitCamel inserts a separator at lowercase-to-uppercase boundaries so that +// camelCase schema keys such as "cohortId" split into words the initialism map +// can then correct. +func splitCamel(s string) string { + var b strings.Builder + runes := []rune(s) + for i, r := range runes { + if i > 0 && r >= 'A' && r <= 'Z' && runes[i-1] >= 'a' && runes[i-1] <= 'z' { + b.WriteRune('_') + } + b.WriteRune(r) + } + return b.String() +} + +func goName(s string) string { + s = splitCamel(s) + parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' || r == '.' }) + var b strings.Builder + for _, p := range parts { + if up, ok := initialisms[strings.ToLower(p)]; ok { + b.WriteString(up) + continue + } + if p == "" { + continue + } + b.WriteString(strings.ToUpper(p[:1])) + b.WriteString(p[1:]) + } + return b.String() +} + +// generator accumulates named types while walking the schemas. +type generator struct { + commonDefs map[string]string // $defs key -> Go type name + decls []string + seen map[string]bool +} + +func (g *generator) emit(decl string) { + g.decls = append(g.decls, decl) +} + +func comment(indent, text string) string { + text = strings.Join(strings.Fields(text), " ") + if text == "" { + return "" + } + var out []string + line := indent + "//" + for _, w := range strings.Fields(text) { + if len(line)+1+len(w) > 84 && line != indent+"//" { + out = append(out, line) + line = indent + "//" + } + line += " " + w + } + out = append(out, line) + return strings.Join(out, "\n") + "\n" +} + +// resolveRef maps "common.schema.json#/$defs/foo" to its generated Go type. +func (g *generator) resolveRef(ref string) (string, bool) { + const marker = "#/$defs/" + i := strings.Index(ref, marker) + if i < 0 { + return "", false + } + key := ref[i+len(marker):] + name, ok := g.commonDefs[key] + return name, ok +} + +// goType returns the Go type for s, generating nested named types as needed. +// parent seeds the name of any anonymous struct or enum encountered. +func (g *generator) goType(parent, field string, s *schema, required bool) string { + if s.Ref != "" { + if name, ok := g.resolveRef(s.Ref); ok { + if !required { + return "*" + name + } + return name + } + return "any" + } + + nullable := s.nullable() + + // oneOf that is just "X or null" collapses to a pointer to X. + if len(s.OneOf) > 0 { + for _, o := range s.OneOf { + if o.typeIs("null") { + continue + } + inner := g.goType(parent, field, o, true) + return "*" + strings.TrimPrefix(inner, "*") + } + return "any" + } + + // A union of several concrete types (guardrail thresholds may be number, + // boolean or string) has no faithful Go equivalent but `any`. + if ts, ok := s.Type.([]any); ok { + concrete := 0 + for _, t := range ts { + if t != "null" { + concrete++ + } + } + if concrete > 1 { + return "any" + } + } + + name := parent + goName(field) + + switch { + case len(s.Enum) > 0: + g.emitEnum(name, s) + if !required || nullable { + return "*" + name + } + return name + + case s.typeIs("object"): + if len(s.Properties) > 0 { + g.emitStruct(name, s) + if !required || nullable { + return "*" + name + } + return name + } + if sub, ok := s.additional(); ok && sub != nil { + return "map[string]" + g.goType(name, "Value", sub, true) + } + return "map[string]any" + + case s.typeIs("array"): + if s.Items == nil { + return "[]any" + } + return "[]" + strings.TrimPrefix(g.goType(name, "Item", s.Items, true), "*") + + case s.typeIs("string"): + base := "string" + if s.Format == "date-time" { + base = "time.Time" + } + if nullable || (!required && base == "time.Time") { + return "*" + base + } + return base + + case s.typeIs("integer"): + if nullable || !required { + return "*int64" + } + return "int64" + + case s.typeIs("number"): + if nullable || !required { + return "*float64" + } + return "float64" + + case s.typeIs("boolean"): + if nullable || !required { + return "*bool" + } + return "bool" + } + + return "any" +} + +// symbolNames covers enum values that are operators rather than words, so the +// generated constants stay valid Go identifiers. +var symbolNames = map[string]string{ + "<": "Lt", "<=": "Lte", "==": "Eq", "!=": "Neq", ">=": "Gte", ">": "Gt", +} + +// enumConstName builds an identifier suffix for one enum value. +func enumConstName(v string, index int) string { + if sym, ok := symbolNames[v]; ok { + return sym + } + name := goName(v) + cleaned := make([]rune, 0, len(name)) + for _, r := range name { + if r == '_' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' { + cleaned = append(cleaned, r) + } + } + name = string(cleaned) + if name == "" || name[0] >= '0' && name[0] <= '9' { + name = fmt.Sprintf("Value%d", index) + } + return name +} + +func (g *generator) emitEnum(name string, s *schema) { + if g.seen[name] { + return + } + g.seen[name] = true + + var b strings.Builder + b.WriteString(comment("", s.Description)) + fmt.Fprintf(&b, "type %s string\n\nconst (\n", name) + for i, v := range s.Enum { + fmt.Fprintf(&b, "\t%s%s %s = %q\n", name, enumConstName(v, i), name, v) + } + b.WriteString(")\n\n") + + // A generated Valid method keeps enum checking in one place. + fmt.Fprintf(&b, "// Valid reports whether v is a defined %s.\n", name) + fmt.Fprintf(&b, "func (v %s) Valid() bool {\n\tswitch v {\n\tcase ", name) + quoted := make([]string, 0, len(s.Enum)) + for i, v := range s.Enum { + quoted = append(quoted, name+enumConstName(v, i)) + } + b.WriteString(strings.Join(quoted, ", ")) + b.WriteString(":\n\t\treturn true\n\t}\n\treturn false\n}\n") + + g.emit(b.String()) +} + +func (g *generator) emitStruct(name string, s *schema) { + if g.seen[name] { + return + } + g.seen[name] = true + + req := map[string]bool{} + for _, r := range s.Required { + req[r] = true + } + + keys := make([]string, 0, len(s.Properties)) + for k := range s.Properties { + keys = append(keys, k) + } + sort.Strings(keys) + + // Fields are resolved before the struct is written so nested types are + // declared in dependency order. + type field struct{ name, typ, tag, doc string } + fields := make([]field, 0, len(keys)) + for _, k := range keys { + p := s.Properties[k] + typ := g.goType(name, k, p, req[k]) + tag := k + if !req[k] { + tag += ",omitempty" + } + fields = append(fields, field{goName(k), typ, tag, p.Description}) + } + + var b strings.Builder + b.WriteString(comment("", s.Description)) + fmt.Fprintf(&b, "type %s struct {\n", name) + for i, f := range fields { + if f.doc != "" { + if i > 0 { + b.WriteString("\n") + } + b.WriteString(comment("\t", f.doc)) + } + fmt.Fprintf(&b, "\t%s %s `json:%q yaml:%q`\n", f.name, f.typ, f.tag, f.tag) + } + b.WriteString("}\n") + + g.emit(b.String()) +} + +func load(path string) (*schema, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var s schema + if err := json.Unmarshal(data, &s); err != nil { + return nil, fmt.Errorf("%s: %w", filepath.Base(path), err) + } + return &s, nil +} + +func write(path, pkg string, decls []string) error { + var b bytes.Buffer + b.WriteString("// Code generated by tools/schemagen. DO NOT EDIT.\n") + b.WriteString("// Source: schemas/. Regenerate with `make generate`.\n\n") + fmt.Fprintf(&b, "package %s\n\n", pkg) + body := strings.Join(decls, "\n") + if strings.Contains(body, "time.Time") { + b.WriteString("import \"time\"\n\n") + } + b.WriteString(body) + + src, err := format.Source(b.Bytes()) + if err != nil { + // Write the unformatted source so the error is diagnosable. + _ = os.WriteFile(path+".broken", b.Bytes(), 0o644) + return fmt.Errorf("format %s: %w", path, err) + } + return os.WriteFile(path, src, 0o644) +} + +func main() { + schemaDir := "schemas" + outDir := filepath.Join("internal", "contract") + + if err := os.MkdirAll(outDir, 0o755); err != nil { + fatal(err) + } + + common, err := load(filepath.Join(schemaDir, "common.schema.json")) + if err != nil { + fatal(err) + } + + g := &generator{commonDefs: map[string]string{}, seen: map[string]bool{}} + + // Pass one: name every common definition so record schemas can reference them. + defKeys := make([]string, 0, len(common.Defs)) + for k := range common.Defs { + defKeys = append(defKeys, k) + } + sort.Strings(defKeys) + for _, k := range defKeys { + g.commonDefs[k] = goName(k) + } + + // Pass two: declare them. + for _, k := range defKeys { + d := common.Defs[k] + name := g.commonDefs[k] + switch { + case len(d.Enum) > 0: + g.emitEnum(name, d) + case d.typeIs("object") && len(d.Properties) > 0: + g.emitStruct(name, d) + default: + if g.seen[name] { + continue + } + g.seen[name] = true + underlying := g.goType("", k, d, true) + // Scalar definitions become distinct types rather than aliases: + // passing a hypothesis id where a revision id belongs should not + // compile. Everything else stays an alias to keep call sites plain. + form := "type %s = %s\n" + switch underlying { + case "string", "float64", "int64": + form = "type %s %s\n" + } + g.emit(comment("", d.Description) + fmt.Sprintf(form, name, underlying)) + } + } + + if err := write(filepath.Join(outDir, "common_gen.go"), "contract", g.decls); err != nil { + fatal(err) + } + fmt.Printf("generated %s (%d declarations)\n", filepath.Join(outDir, "common_gen.go"), len(g.decls)) + + records := []string{ + "pressure", "hypothesis", "revision", "experiment", "event", + "feedback", "backend-requirement", "revision-descriptor", + "routing-policy", "telemetry-envelope", + } + + for _, rec := range records { + s, err := load(filepath.Join(schemaDir, rec+".schema.json")) + if err != nil { + fatal(err) + } + rg := &generator{commonDefs: g.commonDefs, seen: map[string]bool{}} + + // Each record schema is a single-property wrapper; generate the + // wrapper plus the payload type it names. + for key, prop := range s.Properties { + rg.emitStruct(goName(key), prop) + } + var keys []string + for k := range s.Properties { + keys = append(keys, k) + } + sort.Strings(keys) + var doc strings.Builder + doc.WriteString(comment("", s.Description)) + fmt.Fprintf(&doc, "type %sDocument struct {\n", goName(rec)) + for _, k := range keys { + fmt.Fprintf(&doc, "\t%s %s `json:%q yaml:%q`\n", goName(k), goName(k), k, k) + } + doc.WriteString("}\n") + rg.emit(doc.String()) + + out := filepath.Join(outDir, strings.ReplaceAll(rec, "-", "_")+"_gen.go") + if err := write(out, "contract", rg.decls); err != nil { + fatal(err) + } + fmt.Printf("generated %s\n", out) + } +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, "schemagen:", err) + os.Exit(1) +} diff --git a/workplans/FLUID-WP-0002-foundations-and-wire-contract.md b/workplans/FLUID-WP-0002-foundations-and-wire-contract.md index e67ff76..0a86bce 100644 --- a/workplans/FLUID-WP-0002-foundations-and-wire-contract.md +++ b/workplans/FLUID-WP-0002-foundations-and-wire-contract.md @@ -4,7 +4,7 @@ type: workplan title: "FLUID foundations and the wire contract" domain: infotech repo: fluid-core -status: active +status: done owner: worsch topic_slug: fluid-core created: "2026-09-04" @@ -43,7 +43,7 @@ boundary and the four invariants this repo defends hardest. ```task id: FLUID-WP-0002-T02 -status: todo +status: done priority: high state_hub_task_id: "5cf83eeb-819f-5972-a660-aa9f5345355f" ``` @@ -57,7 +57,7 @@ the interop surface, per its §1. ```task id: FLUID-WP-0002-T03 -status: todo +status: done priority: high state_hub_task_id: "da22bde0-5138-56db-a26d-f63a3195d6ed" ``` @@ -70,7 +70,7 @@ readable without any fluid-core code. ```task id: FLUID-WP-0002-T04 -status: todo +status: done priority: high state_hub_task_id: "ba9933b2-0b92-55c4-96b0-b46c955e25a6" ``` @@ -82,7 +82,7 @@ types are prohibited — drift between spec and implementation must fail CI. ```task id: FLUID-WP-0002-T05 -status: todo +status: done priority: high state_hub_task_id: "f835faf7-98f4-514f-937d-83b86b4b44a5" ``` @@ -95,7 +95,7 @@ build breaks. ```task id: FLUID-WP-0002-T06 -status: todo +status: done priority: medium state_hub_task_id: "2b0e42e0-c6ff-534e-8de4-3ff333af9e78" ``` @@ -107,7 +107,7 @@ identifier prefix helpers from schema doc §16: `H- R- E- P- BR- D- EV- F- C-`. ```task id: FLUID-WP-0002-T07 -status: todo +status: done priority: medium state_hub_task_id: "83b10a34-08e3-5ceb-81f4-f148a3d87183" ```