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" ```