Generate contract types, pin fixtures to spec, add build and ADRs
Some checks failed
ci / build (push) Failing after 1m6s
Some checks failed
ci / build (push) Failing after 1m6s
Completes FLUID-WP-0002. The record types in internal/contract are generated from schemas/ by a dependency-free generator; fixtures transcribed from the spec's worked examples validate against those schemas and round-trip through the generated types with DisallowUnknownFields, so a spec change that misses the schemas fails CI rather than drifting silently. Adds identifier prefix helpers, Makefile, GitHub Actions, and five ADRs recording the Go choice, out-of-process attachment, the wire contract as boundary, the evidence store, and revision identity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014KmVxhJ35tCo7rE7UnLwWu Assistant: claude-code Assistant-Model: opus Assistant-Process: 1116572@bnt-lap001 Assistant-Session: 8ba9bb93-a72a-4883-b189-2499cce5c400
This commit is contained in:
parent
fbbf56df7a
commit
76912adef8
44 changed files with 4010 additions and 7 deletions
42
.github/workflows/ci.yml
vendored
Normal file
42
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -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
|
||||||
54
Makefile
Normal file
54
Makefile
Normal file
|
|
@ -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}'
|
||||||
28
conformance/fixtures/README.md
Normal file
28
conformance/fixtures/README.md
Normal file
|
|
@ -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:<digest>`. 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 |
|
||||||
37
conformance/fixtures/backend-requirement.yaml
Normal file
37
conformance/fixtures/backend-requirement.yaml
Normal file
|
|
@ -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"
|
||||||
24
conformance/fixtures/event.yaml
Normal file
24
conformance/fixtures/event.yaml
Normal file
|
|
@ -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"
|
||||||
41
conformance/fixtures/experiment.yaml
Normal file
41
conformance/fixtures/experiment.yaml
Normal file
|
|
@ -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: []
|
||||||
22
conformance/fixtures/feedback.yaml
Normal file
22
conformance/fixtures/feedback.yaml
Normal file
|
|
@ -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
|
||||||
156
conformance/fixtures/hypothesis.yaml
Normal file
156
conformance/fixtures/hypothesis.yaml
Normal file
|
|
@ -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"
|
||||||
38
conformance/fixtures/json/backend-requirement.json
Normal file
38
conformance/fixtures/json/backend-requirement.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
conformance/fixtures/json/event.json
Normal file
22
conformance/fixtures/json/event.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
conformance/fixtures/json/experiment.json
Normal file
44
conformance/fixtures/json/experiment.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
26
conformance/fixtures/json/feedback.json
Normal file
26
conformance/fixtures/json/feedback.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
164
conformance/fixtures/json/hypothesis.json
Normal file
164
conformance/fixtures/json/hypothesis.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
conformance/fixtures/json/pressure.json
Normal file
25
conformance/fixtures/json/pressure.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
conformance/fixtures/json/revision-descriptor.json
Normal file
35
conformance/fixtures/json/revision-descriptor.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
150
conformance/fixtures/json/revision.json
Normal file
150
conformance/fixtures/json/revision.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
conformance/fixtures/json/routing-policy.json
Normal file
25
conformance/fixtures/json/routing-policy.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
conformance/fixtures/json/telemetry-envelope.json
Normal file
34
conformance/fixtures/json/telemetry-envelope.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
conformance/fixtures/pressure.yaml
Normal file
29
conformance/fixtures/pressure.yaml
Normal file
|
|
@ -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"
|
||||||
32
conformance/fixtures/revision-descriptor.yaml
Normal file
32
conformance/fixtures/revision-descriptor.yaml
Normal file
|
|
@ -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"
|
||||||
134
conformance/fixtures/revision.yaml
Normal file
134
conformance/fixtures/revision.yaml
Normal file
|
|
@ -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"
|
||||||
20
conformance/fixtures/routing-policy.yaml
Normal file
20
conformance/fixtures/routing-policy.yaml
Normal file
|
|
@ -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"
|
||||||
32
conformance/fixtures/telemetry-envelope.yaml
Normal file
32
conformance/fixtures/telemetry-envelope.yaml
Normal file
|
|
@ -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"
|
||||||
193
conformance/validate_schemas.py
Executable file
193
conformance/validate_schemas.py
Executable file
|
|
@ -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 "<root>"
|
||||||
|
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())
|
||||||
46
docs/adr/0001-go-as-implementation-language.md
Normal file
46
docs/adr/0001-go-as-implementation-language.md
Normal file
|
|
@ -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.
|
||||||
43
docs/adr/0002-out-of-process-attachment.md
Normal file
43
docs/adr/0002-out-of-process-attachment.md
Normal file
|
|
@ -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.
|
||||||
43
docs/adr/0003-wire-contract-as-boundary.md
Normal file
43
docs/adr/0003-wire-contract-as-boundary.md
Normal file
|
|
@ -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.
|
||||||
37
docs/adr/0004-evidence-store-sqlite-to-postgres.md
Normal file
37
docs/adr/0004-evidence-store-sqlite-to-postgres.md
Normal file
|
|
@ -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.
|
||||||
47
docs/adr/0005-revision-identity.md
Normal file
47
docs/adr/0005-revision-identity.md
Normal file
|
|
@ -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.
|
||||||
100
internal/contract/backend_requirement_gen.go
Normal file
100
internal/contract/backend_requirement_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
207
internal/contract/common_gen.go
Normal file
207
internal/contract/common_gen.go
Normal file
|
|
@ -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 '<kind>:<locator>' 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
|
||||||
203
internal/contract/contract_test.go
Normal file
203
internal/contract/contract_test.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
48
internal/contract/event_gen.go
Normal file
48
internal/contract/event_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
108
internal/contract/experiment_gen.go
Normal file
108
internal/contract/experiment_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
33
internal/contract/feedback_gen.go
Normal file
33
internal/contract/feedback_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
254
internal/contract/hypothesis_gen.go
Normal file
254
internal/contract/hypothesis_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
91
internal/contract/ids.go
Normal file
91
internal/contract/ids.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
58
internal/contract/pressure_gen.go
Normal file
58
internal/contract/pressure_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
215
internal/contract/revision_descriptor_gen.go
Normal file
215
internal/contract/revision_descriptor_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
314
internal/contract/revision_gen.go
Normal file
314
internal/contract/revision_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
82
internal/contract/routing_policy_gen.go
Normal file
82
internal/contract/routing_policy_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
172
internal/contract/telemetry_envelope_gen.go
Normal file
172
internal/contract/telemetry_envelope_gen.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
495
tools/schemagen/main.go
Normal file
495
tools/schemagen/main.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ type: workplan
|
||||||
title: "FLUID foundations and the wire contract"
|
title: "FLUID foundations and the wire contract"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: fluid-core
|
repo: fluid-core
|
||||||
status: active
|
status: done
|
||||||
owner: worsch
|
owner: worsch
|
||||||
topic_slug: fluid-core
|
topic_slug: fluid-core
|
||||||
created: "2026-09-04"
|
created: "2026-09-04"
|
||||||
|
|
@ -43,7 +43,7 @@ boundary and the four invariants this repo defends hardest.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLUID-WP-0002-T02
|
id: FLUID-WP-0002-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "5cf83eeb-819f-5972-a660-aa9f5345355f"
|
state_hub_task_id: "5cf83eeb-819f-5972-a660-aa9f5345355f"
|
||||||
```
|
```
|
||||||
|
|
@ -57,7 +57,7 @@ the interop surface, per its §1.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLUID-WP-0002-T03
|
id: FLUID-WP-0002-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "da22bde0-5138-56db-a26d-f63a3195d6ed"
|
state_hub_task_id: "da22bde0-5138-56db-a26d-f63a3195d6ed"
|
||||||
```
|
```
|
||||||
|
|
@ -70,7 +70,7 @@ readable without any fluid-core code.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLUID-WP-0002-T04
|
id: FLUID-WP-0002-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "ba9933b2-0b92-55c4-96b0-b46c955e25a6"
|
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
|
```task
|
||||||
id: FLUID-WP-0002-T05
|
id: FLUID-WP-0002-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "f835faf7-98f4-514f-937d-83b86b4b44a5"
|
state_hub_task_id: "f835faf7-98f4-514f-937d-83b86b4b44a5"
|
||||||
```
|
```
|
||||||
|
|
@ -95,7 +95,7 @@ build breaks.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLUID-WP-0002-T06
|
id: FLUID-WP-0002-T06
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "2b0e42e0-c6ff-534e-8de4-3ff333af9e78"
|
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
|
```task
|
||||||
id: FLUID-WP-0002-T07
|
id: FLUID-WP-0002-T07
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "83b10a34-08e3-5ceb-81f4-f148a3d87183"
|
state_hub_task_id: "83b10a34-08e3-5ceb-81f4-f148a3d87183"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue