Skip to content

Commit 4487fed

Browse files
hyperpolymathclaude
andcommitted
docs: add .affex face-interop manifest spec draft (refs #84)
Writes docs/specs/affex-spec.md, covering: - Design decision: Shape 3 (face-interop manifest) as the primary form, with Shape 1 (side-by-side listing) as a derived view generated on demand - File extension policy: tooling-only, not parsed by the compiler - JSON format schema (affex/1): package manifest with per-declaration face renderings, body digest, override blocks - CLI surface: affex generate, affex render, affex diff - LSP integration plan - Generation strategy and CI recommendation - Explicit satisfaction criteria for closing #84 (per the requirements-target closure rule) Does not close #84 — closure requires explicit mutual agreement recorded in the issue thread. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 04501be commit 4487fed

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

docs/specs/affex-spec.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# `.affex` — AffineScript Face-Interop Manifest Specification v0.1
2+
3+
**Status**: Draft · Linked to requirements-target issue #84
4+
5+
---
6+
7+
## 1. Problem
8+
9+
AffineScript supports multiple *faces* — surface syntaxes that all lower to the
10+
same canonical AST. A programmer fluent in `canonical` reading a file written in
11+
`lucid` or `cafe` cannot tell what is going on, even though the semantics are
12+
identical.
13+
14+
`.affex` is a **tooling-only, per-package manifest** that captures
15+
face-specific renderings of every top-level declaration, so readers can work in
16+
their preferred face without leaving their `.affine` files.
17+
18+
---
19+
20+
## 2. Design decision: Shape 3 (face-interop manifest)
21+
22+
The four speculative shapes from the issue were:
23+
24+
| # | Shape | Chosen? |
25+
|---|-------|---------|
26+
| 1 | Rosetta-stone side-by-side listing | Derived output only |
27+
| 2 | Canonical-equivalence annotations | Not chosen |
28+
| **3** | **Face-interop manifest** | **Primary form** |
29+
| 4 | Mixed hand-written + auto | Supported (override blocks) |
30+
31+
**Why Shape 3**: the compiler already holds the canonical↔face bijection
32+
through the lowering pass. A manifest that captures this mapping once per
33+
package lets all tooling (LSP, CLI renderer, web viewer) derive any face view
34+
on demand — without storing N copies of source. Shape 1 (side-by-side listing)
35+
is a *derived view* generated from Shape 3 + the source, not a stored artifact.
36+
37+
---
38+
39+
## 3. File extension policy
40+
41+
`.affex` files are **tooling artifacts only**. The compiler (`check`, `compile`,
42+
`eval`) does not parse or consume them. They are generated by the CLI and
43+
consumed by the LSP and renderer.
44+
45+
Scope: **one `.affex` file per package**, placed at the package root alongside
46+
`dune` and the package's `.affine` source files.
47+
48+
```
49+
src/
50+
dune
51+
pixi.affine
52+
renderer.affine
53+
pixi.affex ← generated manifest for this package
54+
```
55+
56+
---
57+
58+
## 4. File format
59+
60+
`.affex` files are UTF-8 JSON with schema version `"affex/1"`.
61+
62+
### 4.1 Top-level structure
63+
64+
```json
65+
{
66+
"affex": "1",
67+
"package": "affinescript-pixijs",
68+
"generated_at": "2026-05-11T00:00:00Z",
69+
"source_hash": "sha256:abc123...",
70+
"faces": ["canonical", "lucid", "cafe"],
71+
"mappings": [ ... ]
72+
}
73+
```
74+
75+
| Field | Type | Description |
76+
|-------|------|-------------|
77+
| `affex` | `"1"` | Schema version. Must be `"1"` for this spec. |
78+
| `package` | string | Package name (matches `dune` library name). |
79+
| `generated_at` | ISO 8601 | Timestamp of last generation run. |
80+
| `source_hash` | string | SHA-256 of all `.affine` source files combined, to detect staleness. |
81+
| `faces` | string[] | Faces present in this manifest. Always includes `"canonical"`. |
82+
| `mappings` | object[] | One entry per top-level declaration. See §4.2. |
83+
84+
### 4.2 Mapping entry
85+
86+
```json
87+
{
88+
"id": "init_pixi",
89+
"kind": "fn",
90+
"source_file": "pixi.affine",
91+
"canonical_span": { "start": [3, 1], "end": [5, 1] },
92+
"faces": {
93+
"canonical": {
94+
"head": "pub fn init_pixi(width: Int, height: Int) -> Application",
95+
"body_digest": "sha256:def456..."
96+
},
97+
"lucid": {
98+
"head": "pub function init_pixi(width :: Int, height :: Int) :: Application",
99+
"body_digest": "sha256:def456...",
100+
"override": false
101+
},
102+
"cafe": {
103+
"head": "pub init_pixi = (width: Int, height: Int) -> Application",
104+
"body_digest": "sha256:def456...",
105+
"override": true,
106+
"override_source": "pixi.affex#overrides/init_pixi/cafe"
107+
}
108+
}
109+
}
110+
```
111+
112+
| Field | Type | Description |
113+
|-------|------|-------------|
114+
| `id` | string | Canonical name of the declaration. |
115+
| `kind` | `"fn"` \| `"type"` \| `"const"` \| `"effect"` \| `"trait"` \| `"impl"` | Declaration kind. |
116+
| `source_file` | string | Relative path to the `.affine` file. |
117+
| `canonical_span` | `{start: [line, col], end: [line, col]}` | Source location of the canonical declaration. |
118+
| `faces.<name>.head` | string | Face-specific rendering of the declaration *signature* (no body). |
119+
| `faces.<name>.body_digest` | string | SHA-256 of the canonical body. Identical across faces — a mismatch signals the manifest is stale. |
120+
| `faces.<name>.override` | bool | `true` if the face rendering was hand-written rather than auto-generated. |
121+
| `faces.<name>.override_source` | string? | Pointer to the override block (§5) when `override` is `true`. |
122+
123+
**`head` only, not `body`**: the manifest stores *signatures*, not bodies. Full
124+
body rendering is done on demand by the renderer using the compiler's face
125+
lowering pass. This keeps the manifest compact and avoids duplicating source.
126+
127+
### 4.3 Override blocks
128+
129+
When the auto-generated `head` for a face is wrong or unnatural, a developer
130+
can add a hand-written override in a top-level `"overrides"` section:
131+
132+
```json
133+
{
134+
"affex": "1",
135+
...
136+
"overrides": {
137+
"init_pixi": {
138+
"cafe": "pub init_pixi: (Int, Int) -> Application"
139+
}
140+
},
141+
"mappings": [ ... ]
142+
}
143+
```
144+
145+
The generator preserves existing `overrides` on regeneration and merges them
146+
with newly auto-generated entries.
147+
148+
---
149+
150+
## 5. Tooling
151+
152+
### 5.1 CLI commands
153+
154+
#### Generate
155+
156+
```
157+
affinescript affex generate [<package_dir>]
158+
```
159+
160+
Reads all `.affine` files in `<package_dir>` (default: current directory),
161+
runs the face detection and lowering passes, and writes
162+
`<package_dir>/<package_name>.affex`. Existing override blocks are preserved.
163+
164+
Options:
165+
- `--faces canonical,lucid,cafe` — limit faces to generate (default: all installed faces)
166+
- `--force` — regenerate even if source hash is unchanged
167+
168+
#### Render
169+
170+
```
171+
affinescript affex render --face <face> <file.affine>
172+
```
173+
174+
Reads the package's `.affex` manifest, then renders `<file.affine>` in
175+
`<face>` to stdout. If no manifest exists, falls back to running the face
176+
lowering pass directly (slower).
177+
178+
#### Diff
179+
180+
```
181+
affinescript affex diff --face <face_a> --face <face_b> <file.affine>
182+
```
183+
184+
Renders `<file.affine>` in both faces side-by-side (Shape 1 output) using the
185+
manifest as the source of truth.
186+
187+
### 5.2 LSP integration
188+
189+
The LSP reads the nearest `.affex` manifest when opening a `.affine` file.
190+
When the user configures a preferred face (e.g. `"affinescript.face": "lucid"`
191+
in editor settings), hover tooltips, inlay hints, and the document outline
192+
render declaration signatures in that face rather than the source face.
193+
194+
The LSP does not rewrite file contents — it only affects rendered metadata.
195+
196+
---
197+
198+
## 6. Generation strategy
199+
200+
| Scenario | Approach |
201+
|----------|----------|
202+
| Clean package with no `.affex` | `affex generate` creates one from scratch |
203+
| Source changed since last generate | `source_hash` mismatch; re-run `affex generate` |
204+
| Override present for a declaration | Generator merges override, marks `"override": true` |
205+
| New declaration added | Generator appends mapping; existing entries untouched |
206+
| Declaration removed | Generator removes stale mapping entry |
207+
208+
**CI recommendation**: add `affinescript affex generate --check` (exits non-zero
209+
if manifest is stale) to CI for estates that adopt multi-face conventions.
210+
211+
---
212+
213+
## 7. What this is NOT
214+
215+
- Not a compiler concern. The compiler lowers all faces independently.
216+
- Not an AffineScript↔non-AffineScript FFI layer. See `extern fn` for that.
217+
- Not required for single-face estates. A project using only `canonical` has no
218+
need for a `.affex` file.
219+
220+
---
221+
222+
## 8. Open questions (pre-implementation)
223+
224+
- [ ] Should `affinescript affex generate` be a separate binary or a subcommand of the main `affinescript` CLI?
225+
- [ ] Face names in the manifest should match exactly the values in `lib/face.ml`'s `face` type. Confirm the canonical string identifiers: `canonical`, `python`, `js`, `pseudocode`, `lucid`, `cafe`.
226+
- [ ] Should `head` rendering use the compiler's pretty-printer or store the raw source span? Raw source span is simpler and always correct; pretty-printed form is more useful when the source face differs from the reader face.
227+
- [ ] Staleness detection: `source_hash` over all `.affine` files in the package is coarse. Consider per-file hashes to support incremental regeneration.
228+
- [ ] Override syntax: embedding overrides inside the JSON file is workable but verbose. An alternative is a sidecar `.affex.overrides` file.
229+
230+
---
231+
232+
## 9. Satisfaction criteria for closing #84
233+
234+
This issue closes when Claude and the user explicitly agree, in a recorded
235+
exchange on this issue, that the following are satisfied:
236+
237+
1. This spec (or a revised version) is merged into `docs/specs/affex-spec.md`.
238+
2. The file format schema (§4) is agreed to be correct and complete.
239+
3. The tooling surface (§5) matches the CLI and LSP integration plan.
240+
4. At least one of the open questions in §8 has a recorded resolution (or is
241+
explicitly deferred with a rationale).
242+
243+
*The presence of a PR, a draft, or a partial implementation does not satisfy
244+
these criteria alone — explicit mutual agreement in a comment on issue #84 is
245+
required.*

0 commit comments

Comments
 (0)