Skip to content

Commit 3f866b4

Browse files
docs(affine): capture the AffineScript interface constraints durably (#68)
<!-- SPDX-License-Identifier: CC-BY-SA-4.0 --> ## Summary The three AffineScript compiler constraints that shaped `src/ui/tea/*.affine` (#64) only existed as a line in a `STATE.a2ml` session-log entry. This PR lifts them into discoverable source docs and adds an in-context guardrail, so a future contributor doesn't rediscover them the hard way or try to "simplify" code that is the way it is on purpose. This is the follow-up cleanup I flagged after closing out the completion roadmap. **It is docs/comments only** — the `.affine` logic is unchanged. ## Changes - **`src/ui/tea/README.adoc`** (new) — what the TEA interface is (pure `init`/`update`/`view`/`subs` core + the `cmd_*` `/ IO` effect layer), how to typecheck it (module resolution is cwd-relative → `just affine-check` from `src/ui/tea`), the symbol-contract guard (`just affine-contract` / `affine-ffi-contract-check.sh`, also in CI), and the three language constraints with rationale. - **`gsa_gui.affine`** — an in-context "AffineScript 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 `Model` 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 `server_handle` naming convention and the README. ## Why no code refactor The obvious "cleanup" would be `with*` updater helpers to collapse the ~15 longhand `update` arms. I deliberately **did not** do that: AffineScript has no compiler in this environment and CI only checks the FFI symbol contract, not a typecheck, so the files currently compile but a refactor could not be verified here — a bad trade against working code. The docs instead make the constraint explicit and point the next editor (who *can* run `just affine-check`) at where a real refactor would belong if the compiler ever gains record spread. ## RSR Quality Checklist ### Required - [x] No banned language patterns / functions - [x] SPDX headers present (CC-BY-SA-4.0 on the new doc; existing AGPL headers on the `.affine` files unchanged) - [x] No secrets/credentials included ### As Applicable - [x] Documentation updated (this PR is documentation) - [x] Verified: `just affine-check` / `affine-contract` recipes exist and `affine-contract` is wired into `just quality`; `Model` has exactly the 13 fields the docs cite ## Testing Docs/comments only — no build or test surface changes. Facts were verified against the repo: the two `just` recipes exist, `affine-contract` is part of `just quality`, and the `Model` field count (13) matches the text. The `.affine` sources' logic is byte-for-byte unchanged apart from added comment lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TaWWedv6VQqeZaPvc94keN --- _Generated by [Claude Code](https://claude.ai/code/session_01TaWWedv6VQqeZaPvc94keN)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 76b1421 commit 3f866b4

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)