Generate contract types, pin fixtures to spec, add build and ADRs
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:
tegwick 2026-09-04 02:03:12 +02:00
parent fbbf56df7a
commit 76912adef8
44 changed files with 4010 additions and 7 deletions

View 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` §4243) 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.

View 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.

View 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.

View 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.

View 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.