From e1336986cd87ac3fc278ee1e74a6a9a544b35dec Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 5 Jun 2026 08:03:42 +0000 Subject: [PATCH] fed-sx-m1: tick Step 6e as superseded by 8c-post-publish-http MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "HTTP handler for POST /activity glue" bullet (6e) pre-dates the Step 8 dispatch refactor that landed the same functionality with broader test coverage. `http_server:route/2` already wires POST `/activity` to `nx_kernel:publish/1` when the kernel process is registered (success → 200 with `cid: ` body via `cid_response/1`; sig/replay failure → 422 via `validation_failed_response/0`), and falls back to the stub `post_activity_response/0` when the kernel isn't running. Per-format response variants (json / sx / cbor / activity+json) followed in 8d-dispatch-post via `cid_response_for/2` + `post_activity_response_for/1`. Verified by the standing suites: `next/tests/http_publish.sh` 10/10 and `next/tests/http_post_format.sh` 13/13. Plan-only commit — no source changes, no test changes. Routes the next iteration past 6e onto the next genuinely unticked sub-deliverable. Co-Authored-By: Claude Opus 4.7 (1M context) --- plans/fed-sx-milestone-1.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plans/fed-sx-milestone-1.md b/plans/fed-sx-milestone-1.md index 17129323..efc96d78 100644 --- a/plans/fed-sx-milestone-1.md +++ b/plans/fed-sx-milestone-1.md @@ -396,7 +396,7 @@ projection fold maintains it.) - [x] **6c-schema-pure** — `pipeline:stage_schema/2` (direct) + `stage_schema/1` (factory closed over a SchemaLookup callback). SchemaLookup is `fun(Type) -> {ok, SchemaFn} | not_found`; SchemaFn is `fun(Object) -> bool`. Open-world default: unknown type → ok; no :object skips the check. `next/tests/pipeline_schema.sh` (14 cases). SX-source eval bridge will plug into the same shape later. - [x] **6d-cs** — `outbox:construct/4` (skeleton + CID-derived :id via `cid:to_string`) + `outbox:sign/2` (HMAC over canonical bytes, append :signature pair from KeySpec) + `cid_of/1` accessor. Verified end-to-end: construct→sign→envelope:verify_signature passes; wrong key material fails with bad_signature. `next/tests/outbox_construct.sh` (13 cases). - [x] **6d-publish** — `outbox:publish/2(Request, Context)` orchestrates construct + sign + `pipeline:run_stages([envelope, signature, replay])` + `log:append`. Returns `{ok, [{cid, _}, {activity, _}], NewLog}` or `{error, Reason, LogState}` on stage halt. Replay catches duplicate publishes; bad key material surfaces `bad_signature`. `next/tests/outbox_publish.sh` (13 cases). -- [ ] **6e** — HTTP handler for POST /activity glue (depends on Step 8 http server) +- [x] **6e** — HTTP handler for POST /activity glue. **Superseded by 8c-post-publish-http** — `http_server:route/2` already calls `nx_kernel:publish/1` when the kernel process is registered (success → 200 `cid: ` via `cid_response/1`; sig/replay failure → 422 via `validation_failed_response/0`), falling back to the stub `post_activity_response/0` when not. Verified by `next/tests/http_publish.sh` 10/10 and `next/tests/http_post_format.sh` 13/13. The 6e bullet pre-dates the Step 8 dispatch refactor and the per-format response variants — no separate work remains. **Deliverables:** @@ -1005,6 +1005,7 @@ A few things still under-specified; resolve as work begins. Newest first. One line per sub-deliverable commit. Erlang conformance gate (`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry. +- **2026-06-05** — Step 6e ticked as **superseded**: the "HTTP handler for POST /activity glue" bullet pre-dates the Step 8 dispatch refactor. `http_server:route/2` already wires POST `/activity` to `nx_kernel:publish/1` (kernel-registered: 200 with `cid: ` body via `cid_response/1`; sig/replay failure: 422 via `validation_failed_response/0`) and falls back to the stub when the kernel isn't running. Per-format response variants (json / sx / cbor / activity+json) followed in 8d-dispatch-post via `cid_response_for/2` + `post_activity_response_for/1`. Verified via `next/tests/http_publish.sh` 10/10 and `next/tests/http_post_format.sh` 13/13 — both already part of the standing suite. No new code or tests; plan-only commit to tick the redundant bullet and route the next iteration past it. Erlang conformance 761/761. - **2026-06-05** — Step 3c.b gen_server-mediated concurrent appends: `next/kernel/log_server.erl` (behaviour gen_server) wraps the pure Step 3c.a `log` substrate. `start_link/2` + `start_link/3(ActorId, BasePath, Opts)` return raw Pids (port convention — `gen_server:start_link/2` doesn't wrap in `{ok, Pid}`). Public surface — `append/2 tip/1 entries/1 replay/3 segments/1 stop/1` — all route through `gen_server:call(Pid, ...)`, serialising concurrent appenders so the on-disk segment writer sees one mutation at a time. `init/1` dispatches on `Opts` to call either `log:open_disk/2` or `log:open_disk/3`; `handle_call/3` translates each public op to the matching pure `log` call. New `next/tests/log_server.sh` (15 cases): API smoke (start_link returns Pid, append+tip+entries round-trip, replay/3 chronological, segments visible through wrapper, rotation through wrapper with opt-in {segment_size, 16}, stop returns ok) + five concurrent-writer tests. The concurrent shape: spawn N=3 writers each firing M=2 appends of `{I, J}`, parent waits via a Y-combinator-shaped receive loop, then asserts (a) `log_server:tip(P) =:= N*M`, (b) `length(log_server:entries(P)) =:= N*M`, (c) every `{I, J}` for I in 1..N, J in 1..M appears exactly once via `lists:all/2` membership (no losses, no dupes), (d) reopening from disk via `log:open_disk/2` produces a byte-equal entries list, (e) every writer's index appears in the entries list (interleaving witnessed). **Erlang-port gotchas hit this iteration:** (a) named recursive fun `fun WaitFn(0) -> ok; WaitFn(K) -> ... end` errors as "fun-ref syntax not yet supported" — rewrite as `fun (_, 0) -> ok; (Self, K) -> ... Self(Self, K - 1) end` then call `Wait(Wait, N)`. (b) `lists:foreach/2` isn't registered (only `lists:map/2`) — use `lists:map/2` and discard the result list when running side-effecting closures. (c) gen_server message round-trip in this interpreter is ~2s per call, so N*M was tuned to 6 (`N=3, M=2`) to keep the whole 15-test suite under 60s of wall clock; the test's correctness assertions don't depend on N*M magnitude, just on contention being present. Erlang conformance **761/761** unchanged (log_server.erl is in next/, not lib/erlang/). Step 3c now fully ticked. - **2026-06-05** — Step 3c.a segment rotation: `next/kernel/log.erl` rewritten around a `seg_lens :: [N0, N1, ...]` bookkeeping list (one entry-count per segment in numeric order, last is active) + `seg_size` threshold. Filename scheme now `-NNNNNN.log` (6-digit zero-padded so `file:list_dir`'s alphabetical sort = numeric). `open_disk/3(ActorId, BasePath, [{segment_size, N}])` opts a caller into a smaller rotation threshold; `open_disk/2` keeps a 1 GiB default that effectively never rotates (preserves Step 3b acceptance). Rotation rule (`place_append/4`): if the active segment's pre-append serialized size already ≥ threshold AND it holds at least one entry, the new activity opens a fresh segment — otherwise it extends current active. Single huge entry > threshold stays alone (no recursive rotation, no loop). On reopen, `load_all_segments` lists the directory, filters `-NNNNNN.log`, sorts numerically (insertion sort, since `lists:sort/1` isn't registered in this port — only `lists:append/2`/`lists:reverse/1`/`lists:filter/2` etc.), reads each via `try_read_segment`, and concatenates to rebuild flat `entries` + `seg_lens`. **Erlang-port gotchas hit & worked around:** (a) Erlang string literals like `"foo"` in this port are NOT charlists — `[H|T] = "foo"` badmatches, `length("foo")` errors as "not a proper list". `parse_segment_name` had to build prefix/suffix from `atom_to_list/1` + explicit `[$-]` / `[$., $l, $o, $g]` cons. (b) Cross-arg variable repetition (`strip_prefix([C | Rest], [C | PRest])`) works in tuple patterns but I rewrote it to explicit `case C =:= P of true -> ... false -> ...` for robustness. (c) `Pattern = Binding` syntax in a case clause (`[_|_] = Lst when length(Lst) > 1 -> ...`) errors "unsupported pattern type 'match'" — used `Lst when is_list(Lst), length(Lst) > 1` instead. New `next/tests/log_rotate.sh` 10/10: no-opt single-seg-after-3, rotation-fires-on-threshold, rotated-chronological, reopen-rebuilds-history, reopen-rebuilds-same-seg-shape, huge-single-entry-stays-1-seg, append-after-huge-keeps-order, tip-monotonic-across-rotations. Existing `next/tests/log_disk.sh` updated to the new filename (`corrupted-000000.log`) and stays 12/12. Erlang conformance **761/761** unchanged (log.erl is in next/, not lib/erlang/). Step 3c.a ticked; 3c.b (gen_server-mediated concurrent appends) is the next iteration. - **2026-06-05** — Step 3b on-disk log: `next/kernel/log.erl` gains `open_disk/2(ActorId, BasePath)` and a write-through `append/2`. New state field `{persisted, true} | {path, CharList}` keys the polymorphism — 3a's in-memory `open/2` stays untouched and tests unchanged. `segment_path/2` builds the path as a charlist (`base_chars(BasePath) ++ "/" ++ atom_to_list(ActorId) ++ ".log"`) so it works whether the caller passes a binary or charlist BasePath; everything flows through `er-source-to-string` cleanly. On-disk frame format: 4-byte big-endian length prefix + `term_codec:encode(Activity)`. Restart path: `try_read_segment` reads the whole segment, length-decodes each frame, decodes via `term_codec`, returns `{ok, Entries}`; missing file → `{ok, []}`; throw/error during decode → `{error, {corrupt, _}}`. `next/tests/log_disk.sh` 12/12: open-missing-fresh, append+reopen-entries-match, tip-resumes, replay-chronological, mixed-types (atom/int/binary/tuple/list) round-trip, append-after-reopen, corrupted-segment, per-actor isolation, 3a back-compat. Erlang conformance **761/761** unchanged (log.erl is in next/, not lib/erlang/). Step 3b is now FULLY ticked; 3c (segment rotation + gen_server-mediated concurrent appends) remains for the next iteration.