Skip to content

Commit 34b535e

Browse files
committed
docs(affine): capture the AffineScript interface constraints durably
The three AffineScript compiler constraints that shaped src/ui/tea/*.affine only lived in a STATE.a2ml session-log entry. Lift them into discoverable source docs so they aren't rediscovered the hard way, and guard the code against well-meaning "simplification": - New src/ui/tea/README.adoc — what the TEA interface is (pure core + cmd_* effect layer), how to typecheck (cwd-relative, `just affine-check`), the symbol-contract guard (`just affine-contract`, CI), and the three language constraints with rationale. - gsa_gui.affine — an in-context "constraints (read before editing)" header block: `handle` is reserved (use `server_handle`); record-spread does not typecheck on nominal structs, so every `update` arm restating all 13 fields is DELIBERATE, not oversight — do not DRY it with a spread; module resolution is cwd-relative. - gsa_ffi.affine — a one-line pointer to the naming convention + README. Docs/comments only. No behavioural change; the .affine sources are unchanged in logic (AffineScript has no compiler in this environment, so a blind refactor of working code was deliberately avoided). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaWWedv6VQqeZaPvc94keN
1 parent 76b1421 commit 34b535e

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

src/ui/tea/README.adoc

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: CC-BY-SA-4.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
= GSA AffineScript TEA Interface
4+
:toc:
5+
6+
The AffineScript surface of Game Server Admin: a formally-structured,
7+
resource-safe GUI interface that can drive *any* game GSA supports. It is the
8+
estate's AffineScript half of the seam `AffineScript (interface) → Zig (FFI) →
9+
Idris2 (ABI)`, and it is a parallel interface to the Ephapax/Gossamer GUI —
10+
both talk to the same `libgsa` C ABI.
11+
12+
== Files
13+
14+
[cols="1,3"]
15+
|===
16+
| File | Role
17+
18+
| `gsa_gui.affine`
19+
| The **pure TEA core** — `init` / `update` / `view` / `subs`. No I/O. The
20+
`Model` is driven by the A2ML game-profile registry (18 shipped profiles +
21+
runtime adds), with generic protocol-fingerprint fallback for unknown games.
22+
| `gsa_ffi.affine`
23+
| The **effect layer** — typed `extern` declarations for the `libgsa` C ABI
24+
(all 38 `gossamer_gsa_*` exports) plus `cmd_*` wrappers carrying the `/ IO`
25+
effect row. A `ResultCode` enum mirrors `src/interface/abi/Types.idr`
26+
(codes 0–17).
27+
|===
28+
29+
The core performs no effects: the host runs a `cmd_*` and feeds the outcome
30+
back in as a `Msg`.
31+
32+
== Typechecking
33+
34+
AffineScript module resolution is **cwd-relative**, so typecheck from this
35+
directory:
36+
37+
[source,bash]
38+
----
39+
just affine-check # typecheck gsa_gui.affine + gsa_ffi.affine
40+
----
41+
42+
The AffineScript compiler is not packaged in every environment (it is not
43+
available in CI or the web sandbox), so a local check is the only way to
44+
typecheck the `.affine` sources. What *is* enforced everywhere is the FFI
45+
symbol contract:
46+
47+
[source,bash]
48+
----
49+
just affine-contract # scripts/affine-ffi-contract-check.sh
50+
----
51+
52+
This asserts that every `extern` declared in `gsa_ffi.affine` is a real Zig
53+
export and that the operational core symbol set is fully declared. It runs
54+
locally and as the `affine-ffi-contract` job in
55+
`.github/workflows/abi-contract.yml`. Because CI cannot typecheck the sources,
56+
**run `just affine-check` locally before pushing changes to these files.**
57+
58+
== Language constraints (why the code looks the way it does)
59+
60+
Three properties of the current AffineScript compiler shape both files. None is
61+
a defect in this code — each is a constraint the code is written *around*. They
62+
are repeated in the `gsa_gui.affine` header so an editor sees them in context.
63+
64+
`handle` is a reserved word::
65+
FFI handle parameters are named `server_handle`, never `handle`. Do not
66+
rename them back.
67+
68+
Record update by spread does not typecheck on nominal structs::
69+
There is no `#{ m with field: ... }` form for nominal structs. Every arm of
70+
`update` therefore reconstructs the full `Model` literal — all 13 fields,
71+
changing only the one or two that move. This verbosity is **deliberate**, not
72+
an oversight; do not try to DRY it with a spread (it will not compile). If a
73+
future compiler gains record spread on nominal structs, the `update` arms can
74+
be collapsed then — until then, restating the record is the supported idiom.
75+
76+
Module resolution is cwd-relative::
77+
Always typecheck from `src/ui/tea` (see above). A run from the repo root will
78+
fail to resolve `use gsa_ffi::…`.
79+
80+
== Relationship to the rest of GSA
81+
82+
* Result codes here are a mirror of the single source of truth,
83+
`src/interface/abi/Types.idr` — see
84+
`docs/wikis/05-The-Proven-ABI.md` for how that contract is machine-checked.
85+
* The C ABI symbols come from the Zig FFI — see
86+
`docs/developer/FFI-MODULE-REFERENCE.adoc`.
87+
* For the panel/wizard semantics the `view` renders, see
88+
`docs/wikis/07-GUI-and-Nexus-Setup.md`.

src/ui/tea/gsa_ffi.affine

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
// `scripts/affine-ffi-contract-check.sh` keeps this file honest: every
1515
// symbol declared here must exist as a Zig export, and the operational
1616
// core set must all be declared here. Run it after touching either side.
17+
//
18+
// Naming note: `handle` is a reserved word in AffineScript, so the handle
19+
// params below are named `server_handle`. See src/ui/tea/README.adoc for the
20+
// full set of language constraints that shape this interface.
1721

1822
module gsa_ffi;
1923

src/ui/tea/gsa_gui.affine

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ use gsa_ffi::{cmd_bootstrap, cmd_probe, cmd_server_action, cmd_fetch_logs,
2424
// Stdlib canon extern (pure; see affinescript stdlib/effects.affine).
2525
extern fn int_to_string(n: Int) -> String;
2626

27+
// ── AffineScript constraints (read before editing) ──────────────────────────
28+
//
29+
// Three properties of the current AffineScript compiler shape this file. They
30+
// are documented here (and in src/ui/tea/README.adoc) so they are not
31+
// rediscovered the hard way. None is a bug in this code — each is a language
32+
// constraint the code is written *around* on purpose:
33+
//
34+
// 1. `handle` is a reserved word. FFI externs and params therefore use
35+
// `server_handle`, never `handle`. Do not "simplify" the name back.
36+
//
37+
// 2. Record update by spread (`#{ m with status_line: ... }`) does NOT
38+
// typecheck on nominal structs. Consequently every arm of `update`
39+
// below reconstructs the full `Model` literal, restating all 13 fields
40+
// and changing only the one or two that move. This verbosity is
41+
// DELIBERATE — it is the cost of nominal-struct + affine semantics, not
42+
// an oversight. Do not try to DRY it with a spread; it will not compile.
43+
// (If a future compiler gains spread, collapse the arms then.)
44+
//
45+
// 3. Module resolution is cwd-relative. Typecheck from `src/ui/tea`
46+
// (`just affine-check`). The extern↔Zig symbol contract is enforced by
47+
// `scripts/affine-ffi-contract-check.sh` (`just affine-contract`, also
48+
// the `affine-ffi-contract` job in `.github/workflows/abi-contract.yml`).
49+
//
2750
// ── Types ─────────────────────────────────────────────────────────────────
2851

2952
enum Panel {

0 commit comments

Comments
 (0)