Skip to content

Commit 44c9257

Browse files
Merge branch 'main' into claude/pr351-no-techdebt
2 parents 5fc8f74 + 79a5298 commit 44c9257

19 files changed

Lines changed: 1104 additions & 226 deletions

.claude/CLAUDE.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,117 @@ Rule: An issue is closed only by explicit merge of a PR with "Closes #N" OR by e
140140
Action (Hypatia): When asked to close an issue, confirm via a reply on the issue thread before doing so.
141141
Action (gitbot): Never use GitHub's "close issue" API directly; only close via PR merge with "Closes #N" keywords.
142142

143+
## Agent operations notes
144+
145+
Practical guidance for agents (Claude / other) operating in this repo,
146+
captured from parallel-bot session experience. Read once; saves turns.
147+
148+
### CI signal reliability
149+
150+
**"PR merged" does NOT mean "build green".** Auto-merge on this repo
151+
currently fires even when `build` or `lint` is failing — multiple
152+
recently-merged PRs (#334, #335, #336, #344) landed with `build` red,
153+
and the red persisted until PR #346's `FnExtern` interp fix. If you
154+
inherit a session reasoning about a recently-merged PR, do not assume
155+
its CI was green; check `mcp__github__pull_request_read` with method
156+
`get_check_runs` for the actual statuses, and check whether `main`
157+
itself is currently red before treating a build failure on your own
158+
PR as something *you* introduced.
159+
160+
### Reading CI logs
161+
162+
`WebFetch` against the GitHub Actions UI returns the React skeleton,
163+
not the log content. The fast paths for an agent are:
164+
165+
* `mcp__github__pull_request_read` with method `get_check_runs`
166+
per-job status (queued / in_progress / success / failure) with
167+
`details_url`. Sufficient for "did the build pass".
168+
* `mcp__github__pull_request_read` with method `get_status`
169+
combined commit status.
170+
* For actual log lines on a failed run, hand back to the user with
171+
`gh run view --log-failed <run-id>`; do not loop trying to scrape
172+
the UI.
173+
174+
### Known-failing baseline checks
175+
176+
These checks currently fail on *every* PR for repo-wide reasons, not
177+
because of any individual PR's changes. Do not waste turns
178+
investigating them on a per-PR basis:
179+
180+
* `vscode-smoke` — npm 404 on `@hyperpolymath/affine-vscode` (the
181+
in-editor harness depends on a not-yet-published npm package).
182+
* `migration-assistant` — was fixed by #342, but any branch created
183+
from a base older than #342 will still see it red until rebased.
184+
* `governance / Language / package anti-pattern policy` — flags the
185+
approved TypeScript exemptions (`affinescript-deno-test/*.ts`,
186+
`editors/vscode/test/*.js`, etc., all documented in this file's
187+
exemptions tables); the check has no allowlist for them.
188+
* The Hypatia security-scan bot comment — 143 findings; the bulk are
189+
the same TypeScript exemption hits + pre-existing root files. A
190+
real new finding will show as a *delta* in the count; otherwise
191+
ignore.
192+
193+
If a check from this list *changes status* on a PR (e.g.
194+
`vscode-smoke` suddenly passes, or Hypatia surfaces a new class of
195+
finding), that's signal worth investigating.
196+
197+
### Branching discipline with concurrent merges
198+
199+
When multiple agents are spawned in parallel, branch-creation time
200+
can lag `main` by hours and a stale base will silently revert other
201+
agents' work at merge time. **Before pushing any branch, run:**
202+
203+
```
204+
git fetch origin main
205+
git rebase origin/main
206+
```
207+
208+
Not just at branch-creation; immediately before push, after any
209+
in-session work. This guards against parallel-merge drift. Claude 1's
210+
STDLIB-04c branch (#337) accidentally reverted #334 and #335 because
211+
its base was stale; force-rebased to fix. Cheap to prevent, expensive
212+
to clean up.
213+
214+
### Post-squash-merge branch divergence
215+
216+
When a PR is squash-merged, the squashed commit on `main` gets a
217+
*new* SHA, distinct from any of the source-branch commits. If you
218+
then reset your local branch to `main` (or simply re-resolve it),
219+
`git status` reports N "ahead of origin/branch" — but those N
220+
commits are just the main-side commits the obsolete remote
221+
branch-tip never saw, not unpushed work.
222+
223+
Recognising the situation:
224+
225+
* The branch was already merged (PR closed, `merged: true`).
226+
* The local working tree matches `main`.
227+
* The remote branch still points at the *pre-merge* tip
228+
(`origin/<branch>` is an old SHA, not the squashed one).
229+
* `git log origin/<branch>..HEAD` lists commits that look like
230+
other people's work.
231+
232+
Safe fix — pick the one matching intent:
233+
234+
```
235+
git push origin --delete <branch> # done with the branch
236+
git push --force-with-lease origin <branch> # align the remote to main
237+
```
238+
239+
`--force-with-lease` is safe here because nothing on the remote
240+
branch is unmerged work; force-push without `--lease` only matters
241+
if someone else pushed concurrently, which is irrelevant for an
242+
already-merged branch you're cleaning up.
243+
244+
### Test-fixture hygiene for latent bug surfaces
245+
246+
When you add a stdlib `extern fn` (or any other new declaration
247+
shape), add a test that feeds it to *every* downstream consumer
248+
(parse, resolve, typecheck, interp, every codegen target that
249+
shouldn't reject it). The PR #346 `FnExtern` interp bug had survived
250+
since the interpreter was written because no test had ever fed an
251+
inline `extern fn` to `Interp.eval_program` — STDLIB-04a's tests
252+
were the first, and only then did the missing match arm fire.
253+
254+
Treat "first user of an existing-but-untested declaration shape" as a
255+
class-level surface, not a single test case.
256+

.machine_readable/6a2/META.a2ml

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -985,8 +985,22 @@ Staged plan (ledger INT-03; each row = one gated PR):
985985
post-codegen step wraps it with the standard preview1->preview2
986986
adapter into a component; ownership section survival asserted;
987987
wasmtime component-run smoke. Codegen unchanged ⇒ reversible.
988-
- S4: native `wasi:clocks` + environment + argv via preview2 (wasmtime
989-
host-testable), replacing the preview1 shims behind the component path.
988+
- S4 (DONE, slices S4a clock + S4b env_count/arg_count): on-demand
989+
preview1 imports (`wasi_snapshot_preview1.clock_time_get`,
990+
`environ_sizes_get`, `args_sizes_get`) via the Effect_sites
991+
pre-scan, canonical-order indexing through
992+
`ctx.wasi_func_indices`. NOTE divergence from this ADR's original
993+
S4 wording ("native `wasi:clocks` + environment + argv via
994+
preview2"): the slice shipped as preview1-shim, NOT native
995+
preview2 calls. This is sufficient under the component path
996+
because both adapters bridge the preview1 imports to
997+
`wasi:clocks` / `wasi:cli` internally; switching the codegen
998+
to emit `wasi:clocks/monotonic-clock@0.2.x.now()` directly only
999+
pays off once preview2 is the default (deferred into S6c). The
1000+
ADR is amended here to record what actually shipped rather than
1001+
pretending S4 fulfilled the original wording. String accessors
1002+
(env_at/arg_at) need byte-level wasm IR (I32Load8U/I32Store8) for
1003+
buffer marshalling and remain a tracked follow-up.
9901004
- S5: `wasi:filesystem` (open/read/write/close) — unblocks INT-06.
9911005
- S6a (WIT export lifting, DONE): codegen emits a `_start : () -> ()`
9921006
shim that calls `main` and drops its i32 result whenever a
@@ -999,13 +1013,33 @@ Staged plan (ledger INT-03; each row = one gated PR):
9991013
the component (exit 0), the ownership section survives, and the
10001014
lift is asserted. Purely additive: reactor consumers + game-loop
10011015
hooks are byte-unchanged.
1002-
- S6b: `wasi:sockets`.
1016+
- S6b (sockets on-ramp, DONE): new `net_shutdown(fd, how) -> Int`
1017+
builtin (Effect `Net`, reserved) lowers to
1018+
`wasi_snapshot_preview1.sock_shutdown` via the same on-demand
1019+
canonical-order pattern as S4a/S4b (`optional_wasi` entry 4 in
1020+
`lib/codegen.ml`). The command adapter bridges to
1021+
`wasi:sockets/tcp` internally without surfacing a host-side
1022+
`wasi:sockets/*` requirement; `tests/componentize/sockets_smoke.sh`
1023+
gates the lift + a clock+env+sock combo for canonical-order
1024+
regression. Real-host invoke under wasmtime exits 0 on
1025+
`net_shutdown(stdin, RDWR)` (ENOTSOCK dropped). Larger socket
1026+
primitives (recv/send/accept) need byte-level wasm IR for buffer
1027+
/ network-address marshalling and remain a tracked follow-up
1028+
alongside env_at/arg_at.
10031029
- S6c: flip the default wasm target to component and demote the
1004-
preview1 stdout path to a named legacy target.
1030+
preview1 stdout path to a named legacy target. Bundled cleanup:
1031+
replace the on-demand preview1 shims emitted by S4a/S4b/S6b
1032+
(clock_time_get, environ_sizes_get, args_sizes_get,
1033+
sock_shutdown) with native preview2 calls
1034+
(`wasi:clocks/monotonic-clock`, `wasi:cli/environment`,
1035+
`wasi:sockets/tcp`) — this only pays off once preview2 is the
1036+
default, hence its placement here rather than as a standalone
1037+
pre-flip slice.
10051038
"""
10061039
consequences = """
10071040
- End-state is one-way (the component model becomes the canonical wasm
1008-
target) but reversible-in-progress: preview1 retained through S5.
1041+
target) but reversible-in-progress: preview1 retained as the default
1042+
through S6b; the flip happens at S6c.
10091043
- S3..S6 are HARD-GATED on S2 (toolchain). Tracked as a follow-up
10101044
issue; this is disclosed, not hidden.
10111045
- typed-wasm ownership-section ABI is unchanged; the coordination
@@ -1070,34 +1104,23 @@ Thread per-call-site effect rows from typecheck to codegen via a
10701104
`let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing
10711105
`is_async_prim_call`/`async_primitives`.
10721106
- *Fallback / safety.* If the table has no entry for a site (e.g. a
1073-
pre-typecheck embedder path, or a synthesised node), or the consumer
1074-
detects a producer/consumer count-mismatch, [Effect_sites.is_async_call]
1075-
returns false ⇒ the CPS transform simply does not fire for that call.
1076-
The pre-S4 plan retained the structural recogniser as the fallback
1077-
*until the table path was proven*; S4 (PR #278) retired the
1078-
hardcoded `async_primitives` set, so the steady-state fallback is
1079-
"no transform" — over-conservative, always sound.
1107+
pre-typecheck embedder path, or a synthesised node), codegen falls
1108+
back to the structural recogniser. The hardcoded set is retired only
1109+
once the table path is proven (final slice); over-conservative
1110+
fallback is always sound (= today's behaviour).
10801111

10811112
Staged (ledger #234; each a gated PR, full `dune test --force` +
10821113
wasm e2e):
1083-
- S1 (ADR-016 + plan, PR #270): DONE — no code change.
1084-
- S2a (`lib/effect_sites.ml` shared numbering, PR #275): DONE — pure,
1085-
gate-neutral.
1086-
- S2b (typecheck builds & returns the side-table, PR #276): DONE — no
1087-
codegen behaviour change yet.
1088-
- S3 (pipeline threads the table; codegen boundary predicate switches
1089-
to the table with structural fallback; new e2e
1090-
`tests/codegen/effect_async_boundary.affine` proving a *user-defined*
1091-
`Async` fn triggers the transform — PR #277): DONE. All existing
1092-
http_cps_* / http_response_reader stay green.
1093-
- S4 (retire the hardcoded `async_primitives` set; boundary is now
1094-
exactly `Effect_sites.is_async_call`; fallback remains for table-
1095-
empty / count-mismatch only — PR #278): DONE.
1096-
1097-
*Delivery status:* CLOSED 2026-05-19 end-to-end. Issue #234 closed
1098-
completed (`hyperpolymath/affinescript#234`). The structural name set
1099-
no longer exists; the boundary is single-sourced from the typecheck
1100-
effect side-table via the shared `Effect_sites` ordinal.
1114+
- S1 (this): ADR-016 + plan. No code change.
1115+
- S2: `lib/effect_sites.ml` shared numbering + typecheck builds &
1116+
returns the side-table. NO codegen behaviour change (table built,
1117+
unused) — pure, gate-neutral.
1118+
- S3: pipeline threads the table; codegen boundary predicate switches
1119+
to the table with structural fallback; new e2e proving a
1120+
*user-defined* `Async` fn triggers the transform (the payoff). All
1121+
existing http_cps_* / http_response_reader stay green.
1122+
- S4: retire the hardcoded `async_primitives` set (fallback remains
1123+
for table-miss only); doc truthing.
11011124
"""
11021125
consequences = """
11031126
- Generalises to user-defined `Async` functions; new async primitives
@@ -1286,8 +1309,9 @@ references = [
12861309

12871310
[[adr]]
12881311
id = "ADR-020"
1289-
status = "proposed"
1312+
status = "accepted"
12901313
date = "2026-05-23"
1314+
ratified = "2026-05-24"
12911315
title = "Ownership-section schema versioning (0xAF sentinel + u8 version)"
12921316
context = """
12931317
The `affinescript.ownership` Wasm custom section has been frozen at
@@ -1366,8 +1390,9 @@ references = [
13661390

13671391
[[adr]]
13681392
id = "ADR-021"
1369-
status = "proposed"
1393+
status = "accepted"
13701394
date = "2026-05-23"
1395+
ratified = "2026-05-24"
13711396
title = "Multi-producer ABI coordination model for typed-wasm carriers"
13721397
context = """
13731398
typed-wasm is a separate, language-agnostic compilation target with

docs/ECOSYSTEM.adoc

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ made real; #228/ADR-014 module-qualified paths (estate port unblocker). |
8080
|*E* |typed-wasm convergence hardening (the transition runway): the
8181
AffineScript↔typed-wasm contract widened from L7+L10 toward full
8282
L1–6/L13–16 emitted-wasm enforcement; estate-wide re-validation (#235);
83-
effect-threaded async-boundary recogniser (#234, DELIVERED 2026-05-19 —
84-
ADR-016, S1..S4 / PRs #270/#275/#276/#277/#278); the #225/#160 convergence
83+
effect-threaded async-boundary recogniser (#234); the #225/#160 convergence
8584
ABI matured to "shared with Ephapax". |*planned* |Begins when D's substrate
8685
(INT-01..04, CORE-01) is closed; ends at a stable, multi-producer
8786
typed-wasm convergence ABI.
@@ -97,17 +96,6 @@ on the same footing as the other producers (ephapax). See the contract below.
9796

9897
[IMPORTANT]
9998
====
100-
This section is a *summary* of the contract. The *authoritative*
101-
formal specification is
102-
link:specs/TYPED-WASM-INTERFACE.adoc[specs/TYPED-WASM-INTERFACE.adoc]
103-
(+ machine-readable companion
104-
link:specs/TYPED-WASM-INTERFACE.a2ml[specs/TYPED-WASM-INTERFACE.a2ml]).
105-
The roadmap of what would make typed-wasm a *natural and optimal*
106-
compilation target is
107-
link:specs/TYPED-WASM-ROADMAP.adoc[specs/TYPED-WASM-ROADMAP.adoc].
108-
ADR-020 (schema versioning) and ADR-021 (multi-producer model) are
109-
filed as PROPOSED in `.machine_readable/6a2/META.a2ml`.
110-
11199
typed-wasm is a *separate, language-agnostic compilation target* with its own
112100
repository (`hyperpolymath/typed-wasm`), its own Idris2 proofs, its own
113101
ReScript surface, its own Rust verifier crate, and *its own other producers*
@@ -240,21 +228,30 @@ smoke `tests/componentize/command_smoke.sh` proves the lift, the
240228
ownership section survives, and `wasmtime run <component>` exits
241229
0 on real-host invoke. Purely additive: reactor consumers, the
242230
`__indirect_function_table` export, and game-loop hooks are
243-
byte-unchanged.** String accessors (env_at/arg_at) gated on a
231+
byte-unchanged.** **S6b (sockets on-ramp) DONE: new
232+
`net_shutdown(fd, how) -> Int` builtin (Effect `Net`, reserved)
233+
lowers to `wasi_snapshot_preview1.sock_shutdown` via the same
234+
on-demand canonical-order pattern as S4a/S4b (`optional_wasi`
235+
entry 4); the command adapter bridges to `wasi:sockets/tcp`
236+
internally without surfacing a host-side `wasi:sockets/*`
237+
requirement; `tests/componentize/sockets_smoke.sh` gates the
238+
lift + a clock+env+sock combo for canonical-order regression;
239+
wasmtime real-host invoke exits 0 on `net_shutdown(stdin, RDWR)`
240+
(ENOTSOCK errno is dropped). Larger socket primitives
241+
(recv/send/accept) require byte-level wasm IR for buffer /
242+
network-address marshalling — tracked alongside env_at/arg_at.**
243+
String accessors (env_at/arg_at) gated on a
244244
byte-level wasm-IR extension (I32Load8U/I32Store8 absent today)
245-
— tracked as the next slice before/with S5 filesystem. Remaining
246-
sub-slices: S5 (native clocks/env/argv via preview2), S6b
247-
(`wasi:sockets`), S6c (flip the default wasm target from preview1
248-
→ component). WIT world of record: `wit/affinescript.wit`
249-
bridges to `wasi:clocks`/`wasi:cli`. Real-host main-invoke deferred
250-
to S6 (WIT export-lifting / wasi:cli/run command shape).
251-
**S5 string accessors (env_at/arg_at) DONE: the wasm IR gained
252-
the byte-level load/store family (I32Load8U/I32Store8 + the full
253-
WebAssembly 1.0 §5.4.6 row, opcodes 0x2C..0x35 / 0x3A..0x3E);
254-
accessors lower to on-demand `environ_get`/`args_get` paired with
255-
the existing `*_sizes_get` import (dedup keeps each WASI import
256-
once even when both `*_count` and `*_at` are used).** WIT world
257-
of record: `wit/affinescript.wit`
245+
— tracked as the next slice before/with S5. Remaining sub-slices
246+
(per ADR-015 canonical numbering in META.a2ml): S5 — bring up
247+
`wasi:filesystem` (open/read/write/close), which unblocks INT-06
248+
(server-side runtime profile); S6c — flip the default wasm
249+
target from preview1 → component, bundled with the optional
250+
native-preview2 cleanup that replaces the on-demand preview1
251+
shims emitted by S4a/S4b/S6b with direct `wasi:clocks` /
252+
`wasi:cli` / `wasi:sockets` calls (only pays off once preview2
253+
is the default, hence the bundling). WIT world of record:
254+
`wit/affinescript.wit`
258255
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |runtime
259256
packaging READY (affine-js + affinescript-tea JSR dry-run green;
260257
manual-only `publish-jsr.yml`; docs/PACKAGING.adoc). INT-01 dep

0 commit comments

Comments
 (0)