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"
|
||||
domain: infotech
|
||||
repo: fluid-core
|
||||
status: active
|
||||
status: done
|
||||
owner: worsch
|
||||
topic_slug: fluid-core
|
||||
created: "2026-09-04"
|
||||
|
|
@ -43,7 +43,7 @@ boundary and the four invariants this repo defends hardest.
|
|||
|
||||
```task
|
||||
id: FLUID-WP-0002-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5cf83eeb-819f-5972-a660-aa9f5345355f"
|
||||
```
|
||||
|
|
@ -57,7 +57,7 @@ the interop surface, per its §1.
|
|||
|
||||
```task
|
||||
id: FLUID-WP-0002-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "da22bde0-5138-56db-a26d-f63a3195d6ed"
|
||||
```
|
||||
|
|
@ -70,7 +70,7 @@ readable without any fluid-core code.
|
|||
|
||||
```task
|
||||
id: FLUID-WP-0002-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ba9933b2-0b92-55c4-96b0-b46c955e25a6"
|
||||
```
|
||||
|
|
@ -82,7 +82,7 @@ types are prohibited — drift between spec and implementation must fail CI.
|
|||
|
||||
```task
|
||||
id: FLUID-WP-0002-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f835faf7-98f4-514f-937d-83b86b4b44a5"
|
||||
```
|
||||
|
|
@ -95,7 +95,7 @@ build breaks.
|
|||
|
||||
```task
|
||||
id: FLUID-WP-0002-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "2b0e42e0-c6ff-534e-8de4-3ff333af9e78"
|
||||
```
|
||||
|
|
@ -107,7 +107,7 @@ identifier prefix helpers from schema doc §16: `H- R- E- P- BR- D- EV- F- C-`.
|
|||
|
||||
```task
|
||||
id: FLUID-WP-0002-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "83b10a34-08e3-5ceb-81f4-f148a3d87183"
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue