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
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue