Commit Graph

2025 Commits

Author SHA1 Message Date
1f7f98d0ce sx-gitea Phase 2: access — acl-backed permissions, collaborators, teams, auth-gated routes (TDD, 194/194)
lib/gitea/access.sx: repo role groups (admin>write>read) as acl facts
saturated by the datalog engine; user-owner => admin; collaborators
(per-repo role, upsert); org teams (one role, 'all' or scoped repo
list); org-admin?; visible-repos; create-allowed?; bearer tokens in kv.
Facts derived from forge state, acl db cached in the forge handle and
rebuilt only when facts change.

lib/gitea/web.sx: every repo route now requires read (404 hides private
repos); repo create needs owner/org-admin, delete + collaborator API
need admin (401 no credentials / 403 not allowed); index + /api/repos
list only visible repos; PUT/DELETE collab endpoints.

tests/access.sx (103) + repo suite updated for gating (91). Fixed a
web.sx corruption from the known sx_find_all/sx_replace_node path
mismatch by rewriting via sx_write_file; suite timeout 300->600s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:21:57 +00:00
c037aca51f sx-gitea Phase 1: repo — forge core (owners, repo CRUD, per-repo git stores) + dream browse views (TDD, 91/91)
lib/gitea/repo.sx: forge handle over persist kv; owner principals
(user/org directory, identity-backed in Phase 2); repo records with
visibility/default-branch metadata; per-repo sx-git namespaces
(forge/<owner>/<name>) so delete is a prefix purge; ref resolution
(branch/tag/cid, annotated tags peeled) and tree-path navigation.

lib/gitea/web.sx: dream routes — repo index, repo home, branches,
tree/blob/raw browse at any ref, commit log, single-commit diff view,
JSON API for repo create/list/delete (201/400/409 semantics).

lib/gitea/tests/repo.sx (91 tests) + conformance.sh + scoreboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:45:05 +00:00
7d3f267503 sx-git Phase 7: porcelain — init/add/commit/branch/checkout/tag/reset/merge/log/diff (TDD)
End-to-end topology story: fork, diverge, real merge commit (parents in
order), fast-forward + up-to-date, annotated + lightweight tags, soft/mixed
reset, detached HEAD commits, staged/unstaged unified diffs. Conflicted
merges park MERGE_HEAD + stage the marker tree; git/merge-commit! concludes
with two parents after resolution. Extensible commit meta flows through
porcelain (agentic-sx shape verified). 40/40 — ROADMAP COMPLETE 227/227.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:25:26 +00:00
989dc278c1 sx-git Phase 6: merge — diff3 textual 3-way, tree merge, ff detection (TDD)
Textual diff3 built on the Myers scripts: non-eq regions clustered by strict
base-interval overlap (same-point insert pairs cluster too); one-sided
clusters apply, two-sided take shared result or emit <<<<<<</|||||||/=======/
>>>>>>> markers with base section. Per-path 3-way tree merge with blob-level
auto-merge and delete/modify flagging; merge-commits handles up-to-date /
fast-forward / merged / conflicts, unrelated histories merge over an empty
base. (Content CvRDT not reused deliberately: its state-based LWW block
semantics differ from base-anchored 3-way; the path-set merge here is the
same idea applied natively.) 28/28, total 187/187.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:22:35 +00:00
4d5a60a754 sx-git Phase 5: diff — Myers line diff + structural tree diff + unified render (TDD)
Myers O(ND) forward/backtrack over line vectors (dict-vec), edit script
{:op eq|del|add :line}, reconstruction invariants both sides, paper example
D=5 verified; unified hunks with context 3, merged ranges, exact header
math for empty sides; tree/commit structural diff over flattened trees;
whole-commit unified render. 27/27, total 159/159.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:15:51 +00:00
125d9f1398 sx-git Phase 4: worktree — tree materialization, index overlay, status (TDD)
Worktree is a value (path->data dict). tree-from-files/tree-files round-trip
through real tree objects (cid-identical to hand-built trees); index =
{:base tree-cid :staged overlay} in kv with add!/rm!/unstage!/index-tree!;
status = three-way dict diff (HEAD vs index vs worktree) with
staged/unstaged/untracked. 26/26, total 132/132.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:11:04 +00:00
eda6806989 sx-git Phase 3: commit DAG — log/ancestry/merge-base SX-side + Datalog bridge (TDD)
Topo log = reverse DFS postorder over parent edges from commit objects;
reflexive is-ancestor?, all-LCA merge-bases (criss-cross verified). Datalog
bridge exports (git-parent child parent) facts under a minimal 2-rule
ancestor closure, cross-checked against the SX walk. 30/30, total 106/106.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:07:06 +00:00
74c2521926 sx-git Phase 2: refs — branches/tags/HEAD over persist kv, CAS + reflog (TDD)
Ref value = {:cid} | {:symref}; atomic moves via persist/kv-cas old-value
expect, create-only branches via kv-put-new; bounded symref resolution;
per-ref append-only reflog on the persist log facet. 38/38, total 76/76.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:03:36 +00:00
9a85b52d1a sx-git Phase 1: blob/tree/commit/tag as content-addressed typed objects (TDD)
Objects are plain dicts over persist kv, addressed by sx1:<sha256> of the
artdag/canon canonical form (sorted dict keys) — native CIDs, extensible
fields participate in identity. 38/38.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:01:11 +00:00
99401ae21e H7: adjacency streams — per-(node,kind) edge reads, no more full kv scans (TDD)
Failing tests first (2 red: relate! wrote no adjacency streams). Every edge write now maintains
per-pair event streams (rel:src|kind ← {:dst}, rin:dst|kind ← {:src}); host/blog-out/-in/--out-raw
(+ new --in-raw) fold ONLY the pair's stream — O(edges of that node under that kind) instead of
O(all kv keys) per read. Append-only ⇒ no read-modify-write race (duplicate :adds fold to a set).
The edge:* kv rows remain (whole-graph consumers: subtype-closure, relations admin block) and feed
host/blog-reindex-edges! — the idempotent boot migration serve.sh now runs, so pre-H7 live stores
read correctly. Collapsed host/blog--add-edge-kv! into add-edge! (the type-algebra conj/disj edges
were bypassing the streams — caught by the existing algebra tests going red).

blog suite 256/256 (+6); FULL conformance 658/658.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:53:32 +00:00
f8b96b3d81 H6: durable activity dedup — same :id processed at most once, ever (TDD)
Failing tests first (3 red: a redelivered activity reran its behavior — behavior/process starts
from an empty trace, so dedup evaporated per call). host/blog--process-local! now atomically
claims the :id on persist stream 'activities:processed' via ev/book! (the same append-expect
acquire as seats/votes) and returns a :deduped trace on duplicates. Store-backed → survives outbox
retries AND restarts. Prerequisite for non-idempotent effects (payment). Id-less activities process
unchecked.

blog suite 250/250 (+3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:45:55 +00:00
edbb2d4a37 H5: two-phase buy — hold → guarded mint → confirm/release (TDD, seat-leak fix)
Failing tests first (red: a failed mint left the seat consumed — the cross-domain leak; a RAISING
mint escaped the handler entirely, proving no guard). host/blog--mint-ticket is now an injectable
seam (default: signed HTTP to the shop). buy-ticket: ev/hold! reserves (capacity-counted, atomic) →
mint runs inside (guard (e (true "")) ...) → 'ticket:' ⇒ ev/confirm! + sold edges + a 'sell'
activity (H4's missing emission); anything else ⇒ ev/release! frees the seat. Held seats count
toward capacity, so a pending mint can't be oversold either.

blog suite 247/247 (+3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:43:26 +00:00
e5fd4f8e0b H4: P2 restored — cinema/poll mutations emit activities (TDD)
Failing tests first (6 red: no cinema/poll handler emitted anything — films/showings/votes were
invisible to federation and other peers' behaviors). Now: new-film→create(film),
new-showing→schedule(showing), offering-add→offer, offering-update→update (id carries new values so
distinct changes federate, replays dedup), offering-remove→retract, add-poll→create(poll),
new-event→schedule(event), vote→vote(poll) — voter kept OFF the wire (seat number makes the id
unique; pinned by test). All through host/blog--emit! (engine + durable activity log + outbox).
buy-ticket's sell emission lands with H5's injectable mint.

blog suite 244/244 (+6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:16:40 +00:00
f1238c1a38 H3: votes are atomic claims on the persist stream (TDD)
Failing tests first (2 red: the vote wasn't on the stream, and dedup vanished if the projection
edge was removed — proving it was an edge-scan). host/blog-vote now acquires on stream
'vote:<poll>' via ev/book! (append-expect, retry-on-conflict — the same atomicity as seats);
the option --voted--> edge is a projection recorded only on :booked. Removed the read-check-write
host/blog--voted-in-poll?. Governance-grade: no double vote under concurrency, dedup survives
projection wipes + restarts (store-backed).

blog suite 238/238 (+4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:12:32 +00:00
eb54d17df9 H2: auth-gate cinema/poll admin ops (TDD)
Failing tests first (6 red: unauth /new-film created a film, etc). new-film / new-showing /
offering-add|update|remove / add-poll / new-event moved from the public route list into
host/blog-write-routes behind protect-html — same gate as every blog write. /vote, /buy-ticket,
/buy stay public (voters + customers) with explicit tests pinning that.

blog suite 234/234 (+9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:09:58 +00:00
8e0f06aa28 H1: HMAC-gate internal endpoints /ticket /order /person (TDD)
Failing tests first (4 red: unsigned POSTs returned 200 and minted objects), then the gate:
host/blog--int-verify? checks x-int-sig = sess-sig(fed-secret, request TARGET) (params live in the
query, body is empty); host/blog--protect-internal wraps the three routes → 403 unsigned. Secret
unset = open (dev/tests). Callers (events→shop /ticket + /order, shop→identity /person) sign via
host/blog--int-headers. Closes the live capacity-bypass (anyone could mint tickets directly).

blog suite 225/225 (218 + 7 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:07:40 +00:00
a7533b26b1 polls on blog posts: a non-commercial Claim (vote), proving the grammar off the commerce axis
Demonstrates a DIFFERENT configuration on the same substrate: a post carries polls; a vote is the
same 'acquire, deduped by actor' shape as a booking, with money + capacity turned OFF.

- host/blog-add-poll: a poll is-a poll (field question), post --has-poll--> poll, options as option
  posts (is-a option, field label), poll --option--> opt.
- host/blog-vote: one vote per voter per poll (host/blog--voted-in-poll? checks all options), records
  option --voted--> voter. No capacity, no payment — a Claim with those axes off.
- host/blog--post-polls / --poll-view / --poll-form: results (per-option counts) + a vote form per
  option + an Add-a-poll form, shown on every post page.

LIVE on blog.rose-ash.com/welcome: Dune 2 / Oppenheimer 1 / Barbie 0 (a repeat voter refused). Same
dedup as ev/book!, zero new mechanism. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:48:23 +00:00
ab058147fc wire lib/events capacity-safe booking under the cinema buy path (first subsystem integration)
The events peer's ticket purchase now goes through lib/events' ATOMIC booking instead of a
read-check-write race — real domain logic dropping in under the same clickable cinema, exactly as
the seam promised.

- MODULES (serve.sh + conformance.sh): load lib/persist/concurrency.sx (append-expect/conflict?) +
  lib/events/booking.sx. host/blog-store (persist/open) is stream-capable, same backend lib/events
  tests use.
- host/blog-buy-ticket: replaced '(< (len sold) capacity)' with (ev/book! host/blog-store <showing>
  <capacity> <actor>); proceed only on :status :booked. occ-key = showing slug, capacity =
  host/blog--showing-capacity, actor = email + current roster len (unique per seat, collapses
  double-clicks, allows a person to hold several seats). persist append-expect retries on conflict —
  no oversell even under concurrent buys.
- Per-offering cap + the sold-edge display are unchanged (render-safe); ev/book! is the authoritative
  gate in the handler.

VERIFIED LIVE on events.rose-ash.com: cap-2 showing → 3 buys, exactly 2 booked (3rd refused);
cap-1 showing → 10 CONCURRENT buys → exactly 1 sold, SOLD OUT. Sandbox: ev/book! returns
booked/booked/full/already for a cap-2 occ. blog 218/218 (with the two new modules loaded).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:12:36 +00:00
ee4dbf3be9 offering editor: per-showing ticket types with prices + per-offering caps
Each showing's offerings are now independently editable — the 'some / all / extra + special offer,
different prices, different caps' from the cinema model.

- host/blog--offering-editor: a collapsible '⚙ Manage offerings' panel on the showing page — per
  offering an inline price+cap Save form and a Remove button, plus an Add-offering form.
- host/blog-offering-update: edit an offering's price + cap.
- host/blog-offering-remove: unlink an offering from the showing (sold tickets keep their record).
- host/blog-offering-add: add an offering, CREATING the ticket type first if new (e.g. special-offer
  → seeds the ticket-type + is-a). host/blog--offering-showing resolves the parent showing.
- Per-offering CAP enforcement: host/blog--offering-available? (offering sold < its cap, else only the
  showing capacity limits it). buy-ticket checks it and tallies offering --sold--> ticket per offering;
  the tickets section shows 'type — £price (sold/cap)'.

This covers the layout-style variable caps too (seated / tables / standing = per-offering caps).

LIVE: on the Dune showing — set adult £12 cap 2, added special-offer £5 cap 1, removed u18; buying the
special-offer twice yields 1/1 sold (second blocked). blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:43:31 +00:00
2037ae45d1 identity: /people view (Persons, email-keyed) for id.rose-ash.com
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:07:28 +00:00
78d8fd54c5 cinema tickets: capacity-enforced buy across events→shop→identity (4 domains)
People can now buy tickets, from the web UI, with capacity enforcement — the heart of the model.

- Showing page (events): a 🎟 Tickets section (host/blog--showing-extras) shows capacity/sold + a Buy
  form per Offering (ticket type + price). host/blog--showing-capacity = the showing's override else
  its calendar's screen's default (via on-calendar → has-calendar reverse).
- host/blog-buy-ticket (events): CAPACITY-CHECKED (sold < capacity), then POSTs to shop /ticket and
  records showing --sold--> ticket. Sold out → the Buy form is replaced by 'sold out'.
- host/blog-ticket (shop): issues a Ticket (is-a ticket, for showing, bought-as offering, owned-by
  the person's email) + registers the person on the identity peer.
- host/blog-person (identity): find-or-create a Person keyed by email (login-optional) → person:<id>.
- IDENTITY is a new 4th fed-sx peer (sx_identity, SX_DOMAIN=identity, id.rose-ash.com-ready); shop
  gets SX_IDENTITY_BASE. serve.sh gains shop 'ticket' type + identity 'person' type seeds.

LIVE end-to-end: events.rose-ash.com/<showing> → Buy adult (alice@example.com) → sold 0→1, a ticket
on market.rose-ash.com, a person on identity. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:02:38 +00:00
7aaf2c9b17 cinema model (events core): Cinema/Screen/Calendar/Film/Showing/Offering + /cinema admin
The Rose Ash Cinema object model, operable on events.rose-ash.com/cinema.

- host/blog-seed-cinema!: seeds the type-posts (cinema/screen/calendar/film/ticket-type/showing/
  offering) + Rose Ash Cinema with two screens (each capacity 100 + a calendar) + ticket types
  (adult/u18/concession/standing). Idempotent. Called from serve.sh's events block.
- /cinema page: screens (with capacity) → their calendars → showings; a Films list (each with its
  ticket types); an Add-film form; a Book-a-showing form.
- host/blog-new-film: creates a film is-a film + default ticket types (adult, u18).
- host/blog-new-showing: books a Film onto a Calendar at a time (showing of-film / on-calendar,
  calendar --scheduled--> showing), with an optional per-showing capacity override, and SNAPSHOTS
  the film's ticket types as Offerings (offering of-type, showing --offers--> offering, price field).
- Views: host/blog--screens-view / --calendar-view / --films-view (all via out-raw for federated refs).

LIVE: events.rose-ash.com/cinema shows the two screens + calendars; add 'Dune' → film with adult/u18;
book Dune on cal-screen-1 Fri 8pm → a showing with offerings, listed under the calendar. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:56:30 +00:00
12b4e15569 shop: /orders view on market.rose-ash.com (ticket orders per event)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:17:41 +00:00
f28f960481 cross-domain UX: link from blog's allocate form to events.rose-ash.com/calendars
The allocations live on the events PEER (events.rose-ash.com), not on blog — blog federates them
away. Repointed Caddy events.rose-ash.com → sx-dev-sx_events-1 (in /root/caddy/Caddyfile, external
to this repo; needed a 'docker service update --force caddy_caddy' because a single-file bind mount
is inode-pinned — editing swaps the inode). Added a '→ view on events' link on the blog allocate
form so the workflow is navigable. events.rose-ash.com/calendars now shows allocated posts +
scheduled events + ticket sales, publicly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:07:25 +00:00
a5a6698772 cross-domain slices 2-3: events + shop peers, full workflow operable from the web UI
The whole vision, clickable end-to-end across THREE fed-sx peers (blog / events / shop), each a
lib/host instance with SX_DOMAIN-selected types + behaviors.

- EVENTS peer: a 'calendar' type (on-allocate → real allocated relation, via the driver now
  PERFORMING relate effects) + an 'event' type. /calendars UI: allocated posts, scheduled events
  (each with its featured post + a Buy-ticket button + sold count), and a Schedule-an-event form.
  host/blog-new-event schedules an event on main, optionally featuring an allocated post.
- SHOP peer: an 'order' type. POST /order?event= creates an order (is-a order, related to the event)
  → 'order:<id>'. Replaces the Python shop service.
- BUY: events POSTs a cross-domain order to shop (host/blog--http-order), then links event--sold-->
  order. host/blog--out-raw reads cross-domain edges (host/blog-out filters to local slugs, which
  would drop federated refs — the bug that hid allocated posts + sold counts).
- BLOG: every post page shows an 'Allocate to a calendar' form.
- serve.sh: SX_DOMAIN gates blog/events/shop seeds; SX_EVENTS_BASE / SX_SHOP_BASE wire the chain.
  docker-compose: sx_events + sx_shop peers (own stores, shared fed secret, externalnet-ready).

LIVE, all via the browser: allocate 'welcome' on blog → events /calendars shows it → schedule
'Summer Gig' featuring welcome → Buy ticket → shop order → tickets sold increments. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:14:51 +00:00
4a0d53ac43 cross-domain UI: allocate operable from the web + real relations + /calendars view
Makes slice 1 clickable and real. The effect DRIVER now PERFORMS action-effects (closing the loop):
a 'relate' effect {:args (src kind dst)} mutates the relation graph. The events calendar behavior's
allocate-link DAG emits (effect relate (field target) 'allocated' (field slug)), so an allocated post
becomes a real main--allocated-->post edge on events (not just a log line).

UI: every blog post page shows an 'Allocate to a calendar' form (host/blog--allocate-form, shown when
a peer is configured) → POST /:slug/allocate reads the form field. events gets a /calendars page
listing posts allocated to 'main'. serve.sh seeds a 'main' calendar (is-a calendar) on events.

LIVE: submit the allocate form on blog.rose-ash.com/welcome → events /calendars shows 'welcome'
allocated to main, a real federated relation. blog 218/218.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:56:06 +00:00
355bcbefdc cross-domain slice 1: events as a fed-sx peer + allocate-a-post-to-a-calendar (LIVE)
The first cross-domain federated workflow — behaviors defined by TYPES, across domains.

- events.rose-ash.com is now a fed-sx PEER: a lib/host instance with SX_DOMAIN=events whose 'calendar'
  TYPE declares an on-allocate behavior. Replaces the Python events service (no strangler). serve.sh
  gates domain types/behaviors on SX_DOMAIN (blog=article publish/digest; events=calendar+allocate).
- DIRECTED cross-domain delivery: an activity with :to <peer-base> is delivered to that peer's inbox
  (∪ followers). The wire gains 'to'. So 'allocate' targets the events peer specifically.
- host/blog--allocate-activity/allocate! + POST /:slug/allocate?calendar=<id>; the events calendar
  type's allocate-link DAG (an execute-fold effect) fires on receipt.
- docker-compose: the sx_events service (own store, shared SX_FED_SECRET, externalnet for a future
  events.rose-ash.com Caddy route).

LIVE PROOF: publish 'Gig Night' on blog.rose-ash.com → POST /gig-night/allocate?calendar=main → the
events peer RECEIVES the directed, signed activity (/activities: 'allocate article gig-night') and
its calendar type's on-allocate behavior FIRES (/flows: 'linked gig-night'). blog 218/218, full
conformance green.

NEXT: events runs lib/events (real calendars/recurrence/ticketing); link event→post; shop
(lib/commerce) sells tickets — same federated, type-declared shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:25:37 +00:00
43c085e8e8 federation production layer: actor model + follower graph + delivery timer + signatures (LIVE)
The full fed-sx production layer, live-verified across A (blog.rose-ash.com) and B (sx_host_b).

ACTOR MODEL + FOLLOWER GRAPH: activities carry a real :actor (SX_ACTOR); delivery targets FOLLOWERS,
not a static peer list. A peer subscribes by POSTing {verb:follow, actor, base} to /inbox
(host/blog--add-follower!); B follows A at boot (SX_FOLLOW) so A delivers to B. host/blog--{actor,
self-base, followers, follow!, delivery-bases} + durable followers store.

BACKGROUND DELIVERY TIMER: serve.sh's detached _fed_delivery_loop hits GET /fed-tick every 15s
(over /dev/tcp) → re-follow (idempotent, recovers a target that was down at boot) + flush the durable
outbox. Federation is eventually-consistent, not best-effort-at-emit.

SIGNATURE VERIFICATION: every federated POST is signed (host/blog--fed-sign = dr/sess-sig shared-secret
MAC over the body, SX_FED_SECRET); /inbox rejects a bad/missing signature with 403 (empty secret =
open). Applies to both follows and activity delivery.

PUBLIC DOMAIN: B joins externalnet so Caddy CAN reverse_proxy a subdomain to it — the DNS + Caddy
route itself is external ops config (no local Caddyfile).

LIVE PROOF: B follows A (followers:1); publish on A → SIGNED delivery to follower B → B verifies +
fires validate+notify; a forged POST (bad x-fed-sig) → 403; B down → publish queues → the background
timer auto-delivers the backlog when B returns (no manual flush). blog 218/218, full conformance green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:03:35 +00:00
afb9ce5e90 TA-live: real A→B federation over HTTP + a durable outbox (LIVE-VERIFIED)
Step 3 — federation, live-verified with TWO real host instances.

- host/ta.sx: host/ta--post/make-http-wire/federate (POST a serialized activity to a peer's /inbox
  over real HTTP). host/blog.sx: POST /inbox (host/blog-inbox → receive! → process locally, does NOT
  re-federate — no loops).
- DURABLE OUTBOX (fed-sx reliability, after the user asked 'if B is down does it still work?'):
  emit! processes locally (always succeeds), QUEUES per-peer to a persisted outbox, delivers
  best-effort. A peer being DOWN no longer fails the publish — delivery is GUARDED (SX guard catches
  the http-request connection error), failed items stay queued and retry on next emit / on boot /
  manual /flows?flush=1. /flows shows the outbox depth.
- serve.sh: SX_PEERS → peers; boot load+flush of the outbox. docker-compose: a 2nd host sx_host_b
  (peer B, own store, no peers).

LIVE PROOF: (1) a peer POSTs create/article to blog.rose-ash.com/inbox → A fires validate+notify.
(2) publish on A → federates to B → B fires ITS behaviors on A's activity (B's /flows + /activities).
(3) RESILIENCE: publish with B DOWN → A returns 303 (was 500) + queues; start B + flush → B receives
the backlog + fires. blog 218/218 (+TA receive test), full host conformance green.

A = blog.rose-ash.com (public/Caddy); B = sx_host_b (internal docker DNS only, no public domain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:47:07 +00:00
cb0d866002 RA-live: durable business logic in production — host drives the kernel service (LIVE)
Steps 1+2 of RA-live/TA-live, live-verified end-to-end on blog.rose-ash.com.

(1) DEPLOY: docker-compose.dev-sx-host.yml gains an sx_kernel service running next/kernel/serve.sh
(the durable-execution kernel), SX_HTTP_HOST=0.0.0.0 so the host container reaches it at
http://sx_kernel:8930.

(2) HOST AS CLIENT: lib/host/ra.sx gains a KERNEL runner — host/ra--make-kernel-runner drives the
kernel over HTTP (http-request, native primitive; returns {status headers body}). It advertises
{effect,branch,each,suspend}, so select-runner routes a durable DAG to it. host/blog.sx: the DAG
registry + runner fleet are now mutable (register-dag!/add-runner!); emit! records SUSPENSIONS in a
durable pending log; /flows shows suspended instances with a resume link (?resume=<id>) driving
host/ra--kernel-resume. serve.sh wires it: set kernel-base, add the kernel runner, register the
durable 'blog-digest' DAG, declare a DURABLE behavior on article (create→publish SYNC, update→
blog-digest DURABLE), add a 'category' field.

LIVE PROOF: editing a published newsletter article → Update → routes to the kernel runner → POST
/flow/start/newsletter → kernel SUSPENDS (instance 5, shown pending on /flows) → /flows?resume=5 →
host re-drives the kernel → DONE → digest-sent effect + pending cleared. Durable suspend/resume
across separate HTTP requests, on a deployed persistent kernel. urgent edits complete immediately
(digest). http-request works in the serving context. blog 217/217, full conformance green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:31:33 +00:00
39e5f906f2 host TA: the fed-sx transport adapter — federation loop proven at the seam
lib/host/ta.sx — a seam transport {:emit :deliver} over a DIRECTIONAL wire (out = outbox→followers,
in = inbox←follows). The transport is the SERIALIZATION boundary: activities cross the wire as
SX-source strings (host/ta--serialize/deserialize map the keyword-keyed activity ↔ a flat
string-keyed wire form of the P2 activity fields). host/ta--make-transport(out-wire, in-wire) +
host/ta--make-mem-wire (an in-memory directional queue for tests).

Proven (ta 5/5): content + relation activities round-trip through the wire; the FEDERATION LOOP —
instance A emits an activity → the wire carries it → instance B's behavior/pump delivers + processes
it → B's engine fires ITS behavior on A's activity; DIRECTIONAL (B re-emits to its own outbox, not
back into the inbox — no loop). 'Everything works over fed-sx', proven at the seam.

TA-live (deferred, same shape as RA-live): swap the mem-wire for the real next/ delivery wire —
needs a PERSISTENT next/ kernel (gen_servers don't survive across erlang-eval-ast calls) + the ACTOR
MODEL (peer_actors/follower_graph decide who the out-wire delivers to) + pushing /activities onto it.

Full host conformance green (+ta 5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:48:30 +00:00
d13c4dd5fe host P2: all state changes emit canonical activities (LIVE-VERIFIED)
Generalizes emission beyond publish to the full event source. TWO ActivityPub-faithful classes:
- CONTENT (host/blog--content-activity): Create on first publish, Update on a subsequent published
  edit. object-type is DERIVED from the post's is-a (host/blog--post-type), not hardcoded 'article'.
- RELATION (host/blog--relation-activity): Add/Remove, carrying :relation + :target (the edge).

host/blog--emit! runs any activity through behavior/process (logged + matched). emit-content-change!
(create/update) wired into form-submit + edit-submit; emit-relation! (add/remove) wired into
relate-submit + unrelate-submit.

DEBT #1 FIXED — per-EVENT :id (not the bare CID): content = create:/update:+cid; relation =
add:/remove:+src:kind:dst (EDGE-based, because a relation change doesn't shift the CID, so a
CID-based id would false-dedup different edges on one object).

The activity log is now the DURABLE EVENT SOURCE (string-keyed records under 'activitylog',
boot-loaded), surfaced at /activities — what TA will push to peers.

LIVE PROOF (blog.rose-ash.com): publish → /activities 'create article <cid>'; relate → 'add article
p2-events — add welcome related'; unrelate → 'remove …'. blog 217/217 (+4 P2, reframed P0.3 fire
tests for Update semantics), full host conformance 614/614.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:43:57 +00:00
9d29295820 host P1: types DECLARE behavior, runner DERIVED (LIVE-VERIFIED)
Generalizes the hardcoded publish trigger into declared, capability-routed behavior.

- Types carry :behavior — flat string-keyed bindings {"verb" "type" "dag"} on the type-post
  (persist-safe, like :type-relations). The "article" type declares on-create → the "publish" DAG.
- host/blog--load-behaviors! gathers ALL posts' declarations into a registry at boot (serve.sh); the
  trigger match (host/blog--triggers :match = host/blog--match-behaviors) consults it. Hardcoded
  create+article trigger removed.
- Runner DERIVED (DEBT #2 fixed): match resolves :dag via host/blog--dag-registry and picks the
  runner via host/flow--select-runner over host/blog--runner-fleet ([exec-runner]; RA joins at
  RA-live). Each binding carries its :runner; behavior/-run-binding now uses the binding's runner
  (else the engine default) — so the capability model drives the LIVE engine.
- The type-def view shows each behavior + its derived runner (host/blog--behavior-lines).

LIVE PROOF: /article shows 'on create → publish DAG · needs {effect, branch} · runner: synchronous
(exec-fold)'; publishing on blog.rose-ash.com fired /flows validate+notify via the DECLARED path.
blog 213/213 (+3 P1), full host conformance 610/610. FINDING: load-behaviors! scans all posts, not
is-type?-filtered (article failed is-type? on the durable store though it passed in-memory).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:34:39 +00:00
c21be815f3 host RA: the Erlang durable runner adapter — built + tested (module + integration)
lib/host/ra.sx — a PURE-SX seam runner (advertises {effect,branch,each,suspend}) with an INJECTED
erl-eval (real = er-to-sx-deep ∘ erlang-eval-ast; mock in unit tests), so it loads in the plain host
(Erlang refs resolve lazily inside lambdas) and is unit-testable without the Erlang runtime.
host/ra--{atom,bin,erl-src,start-expr,resume-expr,parse,make-runner,resume,real-eval}: marshals our
canonical activity → Erlang source (CID as <<"…">> binary, atoms single-quoted), starts a named
next/ flow via flow_store, parses (ok Id (flow_done V))→{:status done :effects V :flow-id} /
(ok Id (flow_suspended T))→{:status suspended :resume {:id :tag}}.

DUAL-RUNNER ROUTING (flows.sx): host/flow--required-caps now handles a {:erl-flow :needs} DAG
(declared caps, since a foreign flow can't be introspected); host/flow--select-runner picks the
cheapest runner whose capabilities cover the DAG's needs. The capability model is now REAL with two
runners — an {effect,branch} composition lands on exec-runner; a {suspend} DAG routes to RA.

Verified: ra 9/9 (mock erl-eval) + plans/ra-integration.sh 4/4 (the REAL module driving live
flow_store: urgent→done, newsletter→suspended with a resume handle, digest_sent effect-as-data).
Full host conformance 607/607; next/tests/triggers_e2e.sh 10/10 baseline intact.

FINDING → RA-LIVE deferred: gen_servers don't persist across separate erlang-eval-ast calls (flow
README), so true cross-call suspend/resume needs a PERSISTENT next/ kernel process. The runner +
marshalling + suspend/resume mechanics are proven; RA-live is process lifecycle + wiring, documented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 16:20:36 +00:00
a5d43246e0 host: P0 review — fix edit-submit ordering bug + record carried-forward debt
REVIEW at the P0-complete milestone found one live bug and several forward prerequisites.

FIX (was live): edit-submit ran maybe-publish! BEFORE set-field-values!, so an edit that set a
category and published in one submit fired the publish activity on the STALE category (wrong branch).
Reordered — fields land before the transition fires. Regression test added (fields-first →
newsletter→digest, not stale→notify). blog 210/210.

Recorded carried-forward debt in the plan: activity identity (DEBT #1, blocks P2 — :id=CID false-
dedups relation events), capability bind not wired into the live engine (DEBT #2, P1), synchronous-
in-request dispatch (DEBT #3, RA needs the async boundary + background pump), the 'urgent' default
smell (DEBT #4). Sequencing note: P1's runner-derivation is vacuous until RA adds a 2nd runner, and
RA is the load-bearing risk — recommend a narrow RA spike next to de-risk the durable/federated half.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:17:48 +00:00
77e89a9965 host P0.4: canonical seam activity shape + RA marshaller (LIVE-VERIFIED) — P0 COMPLETE
host/blog--publish-activity now emits the CANONICAL seam shape {:verb :actor :object <cid>
:object-type :slug :category :delta :id}: :object is a content-addressed REFERENCE (the CID, not an
inlined dict), :id the dedup identity, :slug+:category the domain fields the DAG reads. Consumers
reconciled — the on-publish trigger matches :verb+:object-type; publish-ctx reads top-level
:category+:slug. Added host/blog--activity->erl: marshals the canonical activity → next/'s Erlang
proplist for the Erlang runner adapter (RA) — defined + tested, unused until RA so the reconcile is
complete and RA's bridge is ready. (:ts/:prev omitted — no clock primitive in the host; deferred.)

LIVE PROOF: published on blog.rose-ash.com → /flows fired validate+notify with the canonical
activity. blog 209/209, full host conformance 597/597.

P0 COMPLETE: the synchronous publish workflow runs end-to-end on the live host through the
substrate-agnostic seam, durably, in the canonical shape, with the RA marshaller staged. RA (Erlang
runner) + TA (fed-sx transport) plug in next without touching the DAG or the wiring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:07:58 +00:00
67d2fad8d8 host P0.3b: durable flow log — survives restart (LIVE-VERIFIED)
The driver now persists each effect record to the blog store (string-keyed to dodge the keyword/
persist top-level split), and host/blog-load-flowlog! rebuilds the in-memory log on boot (wired into
serve.sh after load-edges!). So /flows survives a restart — closing the P0.3 gap.

LIVE PROOF: published a post on blog.rose-ash.com → /flows showed validate+notify → RESTARTED the
container (in-memory log lost) → /flows STILL showed them, reloaded from the durable store.
Round-trip also covered by a conformance test (persist → clear → reload → identical). blog 208/208,
full host conformance 599/599. Note: whole-list rewrite per effect — fine at P0 volume, cap/rotate later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:01:10 +00:00
9ac6a8afd5 host P0.3: wire the seam into the live publish path (LIVE-VERIFIED)
Publishing a post now fires the on-publish behavior DAG through the seam. host/blog--{transport
(activity log), triggers (on-publish: create+article → publish-DAG), driver (records each effect in
the flow log), publish-engine (behavior/make-engine over the four adapters + the execute-fold runner
+ publish-ctx), fire-publish!, maybe-publish!}. Both write handlers (form-submit POST /new,
edit-submit POST /:slug/edit) detect the draft→published TRANSITION (fire-once) in the handler body
and run behavior/process. GET /flows renders the flow log (the effect-as-data the driver dispatched).

LIVE PROOF: logged in + POST /new on blog.rose-ash.com → /flows shows 'validate' + 'notify' (the
publish-DAG branched on the default urgent category), driven end-to-end by the real behavior engine.
Every piece is a seam adapter — swapping the runner for Erlang (RA) or the transport for fed-sx (TA)
federates this same wiring unchanged.

blog 207/207 (+4 P0.3), full host conformance 595/595. GAP: flow log is in-memory (P0.3b = persist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:56:00 +00:00
564fa7dd7d plan: don't calcify P0.2 — artdag may grow to contain business logic (phase AX)
Per the user: the execute-fold-vs-artdag split from P0.2 is a capability SNAPSHOT, not a permanent
boundary. artdag MAY grow +{effect,branch,each} node-kinds; business logic then migrates onto it to
inherit the DAG-engine superpowers — content-addressed memoization (recompute only on input-CID
change), optimize (fuse/dedup/dce), schedule, and above all FEDERATION (a flow result reused across
peers by content-id — the federation vision, for free). The capability model makes the migration
seamless (same DAGs + seam; the runner just advertises more). Named the real design work: dynamic
control in a static DAG (branch prunes a path); effect nodes non-cacheable vs pure nodes memoized.
Demand-driven (phase AX); execute-fold stays the lean default for cheap synchronous flows. Annotated
the P0.2 finding + flows.sx header so the finding doesn't harden into dogma.

Doc/comment-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:38:36 +00:00
e38a8381d4 host P0.2: publish-DAG + execute-fold runner + capability check (hypothesis confirmed)
The hypothesis test. FINDING: a synchronous business flow expresses NATURALLY as an EXECUTE-FOLD
composition (host/execute.sx: seq/effect/alt — the category branch IS 'alt'), NOT an artdag
DATAFLOW DAG (which has no control flow). So 'business logic = art-dag' holds at the ABSTRACTION
(both content-addressed op-DAGs) and is REFINED at the vocabulary: the synchronous control-flow
runner is the execute-fold (caps {effect,branch,each}); artdag is the dataflow sibling. Two
instances of one thing, run very differently — exactly the framing.

lib/host/flows.sx: capability typing (host/flow--node-cap/required-caps derive a DAG's capability
set from its node vocabulary; effect→effect, alt→branch, each→each, wait→suspend), the execute-fold
seam runner (advertises {effect,branch,each}), and host/flow--bind (required ⊆ advertised → derive
the runner, else fail-fast). host/blog--publish-dag (the publish workflow) + publish-ctx.

Verified: publish-DAG required-caps = {effect,branch} → binds to the sync runner; runs →
newsletter→[validate,digest] / urgent→[validate,notify] / other→[validate,skip]; a  node →
{suspend} → binds FAIL-FAST against the exec-runner (would need the Erlang runner, RA). Runner is
DERIVED, not chosen. flows 7/7, blog 203/203, full host conformance 591/591.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 14:18:08 +00:00
6ed523623b host: correct the seam's async-completion contract + prove it (2nd review)
Second review of the (core) seam caught a subtle one — and that my first 'fix' was itself wrong.
The async completion of a SUSPENDED durable flow happens AFTER the synchronous process call has
returned, so an :emit captured in the run env would be stale. The correct seam is construction-
wiring: a durable runner is wired to the transport's INBOUND channel at construction and injects
its completion activity there, out-of-band; a later behavior/pump drains it → effects flow. So the
engine code was already right (pump is the async re-entry seam); only the contract comment was
wrong — corrected. New test proves the loop: process(wait) suspends (no effect), then pump drains
the out-of-band completion → the flow's digest effect flows. Also clarified: dedup is per-
invocation (global idempotency = emitter fire-once + durable inbox); retry is flow-level; the
engine-facing runner result is {:status :effects :resume :error} (:results is runner-internal).

behavior 10/10 (+ async-completion). No engine change — comment + test only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:55:32 +00:00
e4fc66bfeb host: enrich the adapter seam to be substrate-agnostic (review fixes)
After review, the seam was only synchronous-complete; the durable/celery-sx runners couldn't
plug in cleanly. Additive fixes (pipeline unchanged): (1) :status branch in run-binding — 'done'
dispatches effects, 'suspended' records the flow + :resume (a durable runner holds it; completion
re-enters as a new activity via pump), 'failed' records + :error for retry/dead-letter. (2) richer
runner env — :ctx (per-activity, via engine :ctx-of) + injected :effects (external-read interfaces,
e.g. a deterministic fetch_followers). (3) dedup by content :id — a cycle is caught by identity,
not just the depth guard. (4) behavior/pump — drain transport.deliver for inbound (peer activities
+ async runner completions), sharing one trace so dedup spans the batch.

behavior 9/9 (+ suspended/failed/dedup/env/pump); full host conformance 580/580.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:50:41 +00:00
5d04da748a host: the adapter seam for business-logic-as-composition (design-first)
lib/host/behavior.sx — the substrate-independent seam every runner/transport/registry/driver
plugs into. An engine bundles four dict-of-functions adapters (trigger-registry, runner,
transport, driver); behavior/process folds an ACTIVITY through the pipeline: emit → match
triggers → run each behavior DAG → dispatch each effect-as-data → recurse on new activities
(loop closure, depth-guarded at 8). Every stage injected, so the same DAG + engine run over the
synchronous op-table runner / Erlang durable / celery-sx / fed-sx transport unchanged.

Reference tests (mock adapters) prove the contract: publish→trigger→runner→effect flows; a
non-matching activity fires nothing (log complete, execution precise); an effect that emits a new
activity re-triggers (loop closes); an unbounded loop is depth-guarded (terminates). Wired into
conformance.sh + serve.sh MODULES. behavior 4/4; full host conformance 575/575.

Next: P0 supplies the REAL adapters (publish activity ← host/blog--publish-activity, local-SX
trigger, sync op-table runner over a publish-DAG, host driver) — same engine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:42:04 +00:00
3675d059b5 host P0.1: publish-activity contract for federated composition-flows
Business logic as federated composition-flows (plans/business-logic-fed-flows.md). P0.1: the
host describes a published post as a fed-sx activity — host/blog--publish-activity(slug) →
{:type "create" :actor "site" :id <CID> :object {:type "article" :slug :category}} — the
exact shape next/'s trigger machinery consumes (verified: next/tests/triggers_e2e.sh 10/10).
category (drives the flow branch: newsletter suspends / urgent fires / else skip) comes from
the "category" field-value, else the first tag, else "urgent". + host/blog--post-category.

Design decided: activity log = every CID delta (event source); triggers = declared subscriptions
(DefineTrigger); flows hybrid (SX composition for simple via the execute-fold, named Erlang flows
for complex); federated execution = Erlang (next/); the type carries content+relations+behavior.

blog 200/200 (+3: contract, category fallback, missing-post nil).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:55:08 +00:00
1cdfaa5035 otel: reliable bar tooltips (<title> inside <rect>, label pointer-events:none) 2026-07-02 11:56:34 +00:00
47a88ea158 otel: waterfall prefers newest multi-span trace (real page render) 2026-07-02 11:49:52 +00:00
6c2a6ccf07 otel: waterfall shows latest REAL trace (skip dashboard's own poll routes) 2026-07-02 11:48:05 +00:00
76941277fd otel: child spans in the blog render path (waterfall breakdown) 2026-07-02 11:44:00 +00:00
ca80df9ade otel: hover tooltips on waterfall bars (SVG <title>) 2026-07-02 11:00:40 +00:00
4f17a40187 otel: relative 'Ns ago' timestamp on recent traces 2026-07-02 10:53:02 +00:00