Commit Graph

2049 Commits

Author SHA1 Message Date
8ed44f7770 lib/gitea: fix the fetch-pack-over-HTTP hang — native parse fast path
The sx-forge native-loop blocker: clone! of the live giles/rose-ash
never returned over gitea/http-app. Root cause was NOT the transport —
pack-line-parse ran every pack line through the interpreted spec parser
(~6.6KB/s on the CEK machine; a full-repo pack = hours), and a non-hex
byte in a pkt length header parsed negative (index-of -1), walking the
scan index backwards forever.

- gitea/parse-obj: use the host reader (open-input-string + read,
  ~3700x faster, value-identical) when the host provides it; hosts
  without string ports keep sx-parse. Feature-detected at load.
- pkt-sections-loop: (< n 4) guard — malformed lengths error instead
  of hanging.
- push-cmd!: haves = every advertised remote ref held locally, so a
  NEW branch pushes only its delta, not the whole repo closure.
- tests/wire.sx: malformed-len errors, truncated-pkt clamps, parse-obj
  = sx-parse equivalence (blob/commit + cid). 83/83.
- tests/wire-http.sh + wire-http-client.sx: end-to-end over REAL
  http-listen/http-request on :8943 — ls-remote/clone/push-new-branch/
  fresh-clone-verify/delete. The coverage gap that hid all this.

Proven vs the live forge (in sx-gitea-1): full 4468-file clone in 77s
(was: hang), commit, push heads/sx-smoke-test ok, branch advertised on
sx.sx-web.org. Conformance 620/620.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 00:30:59 +00:00
915cc29a52 R3: test runner records a raising test as a failure (TDD); R2 deferred (mutex finding)
Failing test first (red: a probe with a raising actual-expr VANISHED — delta 0, total unchanged —
because the loader skips a raising top-level form and args are eager). Fix: host-bl-test is now a
MACRO expanding to (host-bl--check name (fn () actual) expected); the check evaluates the thunk under
(guard (e (true {:__raised …})) …), so an SX raise is recorded as a failure with the error instead
of disappearing. Native exceptions still escape guard — those already fail loud via conformance's
error grep, so this closes the actual silent-skip gap. Keeps the next TDD loop honest.

R2 DEFERRED: investigating it surfaced that lib/host serializes ALL handler evaluation per peer under
one mutex (held across persist IO + the outbound http-request) — zero intra-peer concurrency, so the
outbox 'race' is masked. Logged in plans + memory as the real concurrency task: narrow the handler
mutex for throughput (the multi-co-op future forces it, and that's when masked races become real).

blog suite 260/260; full conformance 662/662.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:47:29 +00:00
72a3989fed Merge branch 'loops/gitea' into architecture 2026-07-03 18:44:53 +00:00
07d74db211 sx-gitea import: snapshot imports + custom commit message + remote replace
Import from an IMMUTABLE snapshot (git archive), not the live tree — a
replay diverges the moment a source file changes (the forge's own
non-fast-forward check caught exactly that). import-stage-msg! carries
the source SHA in the commit message; import-delete-remote! + push
replaces a partial import's history in two requests.

rose-ash mirrored to sx.sx-web.org/giles/rose-ash: 4468 files @
4a7c05a2, one commit, zero skipped, single push under the linear wire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:44:53 +00:00
4a7c05a2b9 Merge branch 'loops/gitea' into architecture 2026-07-03 17:34:31 +00:00
e48dbd42b4 sx-gitea: linear closure walk + working-tree importer (78/78 wire)
The closure walk rebuilt its seen-set with assoc — which on this kernel
copies the entire hashtable per call — and stacked pending cids with
concat; pack-cids then insertion-sorted the result. All three are
quadratic, which surfaced the moment a real repo (4.5k files) went over
the wire: a single push spent an hour in the walk. The seen-set is now a
private dict mutated in place (dict-set!, the acl engine's own pattern),
pending cids are cons-stacked, and packs are unsorted (order is
irrelevant to the receiver). Wire suite stays 78/78; every clone/fetch/
push on repo-scale histories now walks each object once.

lib/gitea/import.sx: working-tree importer — file-read + http-request
adapt the Phase 3 wire client to a live server (gitea/http-app);
staging (deterministic commits, so an interrupted import replays to
identical CIDs and resumes without re-pushing) is separate from the
single delta push; pack lines that exceed the pkt limit are skipped and
reported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:34:31 +00:00
2769962ef9 Merge branch 'loops/gitea' into architecture 2026-07-03 15:20:14 +00:00
9c009b07db sx-gitea deploy: live serving for sx.sx-web.org
lib/gitea/serve.sx: durable live forge on the kernel persist store
(SX_PERSIST_DIR) with idempotent seeding (instance id, admin user +
rotating token, welcome repo), blocking in the native http-listen loop
via host/native-handler — the same wiring that serves blog.rose-ash.com.

lib/gitea/serve.sh: full-stack launcher (every substrate the eight
phases compose, in dependency order, + dream/session for the cookie
bridge) — container entrypoint and local launcher in one.

docker-compose.dev-sx-gitea.yml: sx_docs image, bind-mounted worktree +
binary, /root/sx-gitea-persist for durable state, externalnet so Caddy
can proxy sx.sx-web.org. Serving JIT off until validated for this path.

Smoke-tested locally: pages, authed API, markdown-rendered issues,
pkt-line ref advertisement, 401 gating, and full state survival across
a restart against the same persist dir.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:20:14 +00:00
de9ace7056 Merge branch 'loops/agentic' into architecture 2026-07-03 15:11:19 +00:00
4a9fd74239 Merge branch 'loops/gitea' into architecture 2026-07-03 15:11:19 +00:00
50e6da2ae9 sx-gitea Phase 8: fed — ForgeFed federation (TDD, 615/615 all suites)
lib/gitea/fed.sx: forges federate as peers. Each forge carries an
instance id; users and repos project as AP actor documents (Person/
Group/Repository with inbox/outbox + clone endpoint); the outbox is the
activity log in an AP-shaped envelope.

Trust follows the events-federation pattern — a kv set of peer ids
RE-CHECKED on every operation (inbox, mirror sync, delivery), so
revoking a peer takes effect immediately; peer transports (dream app
fns) live only in the runtime cache.

Inbox (POST /api/ap/inbox, trust-gated): every accepted activity lands
in a federated log with :origin provenance; open-issue/comment/open-pr
MATERIALIZE — the foreign author becomes an auto-created proxy user
'<name>@<peer>' and the issue/comment/PR is created locally under that
identity. fed-deliver! pushes public-repo activities (cursor-based,
never private) to every trusted peer's inbox. Cross-instance repo
follow = mirror!/mirror-sync! over the Phase 3 wire client.
fed-timeline merges local + foreign activities with provenance tags.

Suite: two in-memory forges federating end to end — actor docs, trust
lifecycle, materialization, proxy-user reuse, wire inbox 400/403/200,
mirrors (clone/sync/trust-revocation), cursor delivery, timelines.

Adds lib/gitea/README.md (composition map, architectural rules, known
limits). Final scoreboard: 615/615 across repo/access/wire/issues/pr/
activity/search/fed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:07:47 +00:00
c8385bd003 sx-gitea Phase 7: search — code + issue/PR search over search-on-sx (TDD, 35/35)
lib/gitea/search.sx: the forge builds document corpora SX-side — code
files from the default branch head (path + blob text), issues (title +
body + comments), PRs (title + body + reviews) — embeds them as one
haskell-on-sx program and asks searchRankTfIdf for ranked doc ids
(terms, AND/OR/NOT, phrases).

Cost model honored: one evaluation parses the Haskell layers
(~20s CPU), extra queries are nearly free — so the core primitive is
gitea/search-multi (any number of corpora and queries in a single
evaluation; each corpus an idxN binding) and only the six layers
searchRankTfIdf needs are compiled, not the full search/src. The test
suite runs its thirteen SX-level queries over five corpora as ONE
evaluation.

Global search spans exactly the repos the caller can read. Web:
/:owner/:name/search page (kind filter), repo + global JSON search.
Suite timeout raised to 900s for the haskell-backed suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:00:36 +00:00
b4fbfa5603 sx-gitea Phase 6: activity — feed timelines, dashboard, durable notifications (TDD, 520/520)
lib/gitea/activity.sx: every forge action lands as a feed activity in an
append-only persist log stream. Instrumentation is done IN the runtime —
repo-create!/issue-create!/issue-comment!/pr-create!/pr-review!/pr-merge!
are redefined around their originals, so SX callers and web handlers emit
activity with zero call-site edits (failed mutations emit nothing).

Timelines are lib/feed (APL) queries: global/repo/user, newest-first,
visibility follows repo access (private-repo activity invisible to
non-readers). Follows (user: or repo: targets) drive a dashboard of
followed actors/repos minus one's own actions.

Notifications ride lib/events durable delivery: activities after a
cursor expand to (id recipient body) messages (comment -> author+
participants, review/merge -> PR author, open-issue -> assignees, never
the actor), ev/deliver-messages runs the at-least-once digest flow, and
delivered messages file into per-user kv inboxes; the cursor advance
makes reruns no-ops.

Web: /activity + /:owner/:name/activity pages, user-activity/dashboard/
follow/notifications/notify-run JSON API. gitea/all-routes now hoists
every /api/* route ahead of the wildcard /:owner/:name patterns so later
packs can add API endpoints without being shadowed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:25:10 +00:00
24821e3f77 sx-gitea Phase 5: pr — merge-base diffs, reviews, flow lifecycle, 3-way merge, merge queue (TDD, 460/460)
lib/gitea/pr.sx: PRs as kv records sharing the per-repo number counter
with issues. Diffs are LIVE, computed from the merge base of the current
branch heads to the source head via sx-git (no spurious deletions when
the target moves on). Reviews: latest verdict per reviewer wins; authors
cannot review their own PR; approved? = some approve and no outstanding
request-changes.

Lifecycle is a lib/flow durable workflow (deterministic-replay suspend):
open -(approval)-> approved -(merge)-> merged; review! resumes the
approval suspend when the verdict set first approves, merge! resumes the
rest, close! cancels, reopen! starts a fresh flow. The flow env lives in
the forge handle; the record's :state stays the source of truth.

Merge via git/merge-commits over the merge base: up-to-date, fast-
forward (ref move only), true two-parent merge commit, or conflicts with
the conflicting paths. Every ref move is branch-cas! — concurrent pushes
surface as 'stale'. Merge queue: approved PRs merge in order,
failures stay queued.

Web: pulls list + PR page (body html, reviews, lifecycle, unified diff),
JSON API for create/review/merge (409 on conflicts/stale)/close (author
or write)/enqueue/queue-process.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:07:29 +00:00
d96529effe sx-gitea Phase 4: issues — content-document bodies + relations graph (TDD, 360/360)
lib/gitea/issues.sx: issues as kv records (zero-padded per-repo
numbering, title/author/state, sorted label+assignee sets, Markdown
body, comment thread). Bodies and comments are content-on-sx documents:
content/from-markdown -> block doc -> content/html for pages, with the
round-trip law asserted in the suite. The issue graph (issue->repo
parent, author origin, assignee member, label link, commenter reply) is
DERIVED into lib/relations facts and rebuilt on fact change — same
pattern as the acl db, so deleting a repo can never dangle edges.

Views: open/closed/by-label/by-assignee; graph queries: repo-issue-nodes,
user-authored, user-assigned, label-issues, issue-participants.

Web: issues list + issue page (rendered HTML body + comments), JSON API:
create (any authenticated reader), comment, close/reopen (author or
write), label/assignee management (write). All read-gated like the rest.

Infra: gitea/route-packs registry — wire/issues append their routes at
load; gitea/app serves all packs. repo-delete! now purges collab/issue/
issue-seq rows too (ghost-state regression tested). Conformance runner
gains per-suite extra modules; the issues suite loads relations +
smalltalk + content (~5s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:53:21 +00:00
83a8a2f8db sx-gitea Phase 3: wire — smart-HTTP protocol over native CIDs (TDD, 272/272)
lib/gitea/wire.sx: git-style pkt-line framing (byte-compatible hex4
lengths + flush sections); object closure walker (commits/trees/blobs/
tags) with missing-object detection; wants/haves pack negotiation.
Objects travel as '<cid> <serialized-sx>' pkt lines — receivers re-derive
the CID from the bytes, so packs are tamper-evident by construction.

Server endpoints: GET info/refs (read-gated advertisement incl. '@ HEAD'
symref line), POST git-upload-pack (read), POST git-receive-pack (write;
401/403/404 like the rest of the API) with per-ref command application:
create/update/delete via ref-CAS, fast-forward enforcement on heads/*,
closure-completeness check, stale detection, heads|tags-only.

Client: gitea/remote over any dream app fn — ls-remote, clone! (sets
HEAD + default-branch, cleans up on unreachable remote), mirror fetch!,
push!/push-delete! with local pack computation. Suite syncs two
in-memory forges end to end: clone, incremental fetch, push, non-ff
rejection + recovery, branch create/delete, tag push, private-repo
credentialed round trip.

sx-parse comes from spec/parser.sx on the OCaml server host — added to
the conformance load order. Also merged loops/git (git-wire export/
import adapters, 267/267) for future stock-git interop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:35:40 +00:00
336a61ae66 Merge branch 'loops/git' into loops/gitea 2026-07-03 13:23:22 +00:00
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
c66ee35010 agentic-sx Phase 4: durable — agent sessions as durable flow workflows (TDD)
Deterministic replay IS the durability mechanism: every transition re-runs a
self-contained flow program (defflow source + flow/start + replay of all
recorded resume values), so the only durable state is {:flow :input :resumes}
in persist kv — restart-safe by construction (fresh space handles over the
same backend resume mid-flight runs). fork-an-agent-run = copy the record;
the two replays diverge independently. Effects are data (suspend tags +
typed request envelopes surface as plain SX); transitions ride the Phase-3
trace buffer so session history travels with the next commit. Guest numeric
results compared with = per house convention. 43/43 (196/196 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:13:50 +00:00
b92095ccaf agentic-sx Phase 3: trace — console output as attached CID objects (TDD)
Per-agent buffer = persist append-only log stream + kv drain cursor;
commit-with-trace! drains everything-since-last-commit into a console-trace
object and binds it git-note style (ref notes/trace/<commit-cid> -> trace
cid). Trace never enters the commit tree; binding is a re-bindable ref layer
over immutable objects; failed commits keep the buffer; plain commit! leaves
binding to the agent. 35/35 (153/153 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:58:06 +00:00
88c4963fd0 agentic-sx Phase 2: branch — one branch = one agent (TDD)
space handle (repo + relations Datalog db); spawn! = branch-from-briefing
with a genesis spawn commit at the fork point; commit! verb snapshots a full
worktree VALUE into a typed agent-commit and CAS-advances the branch (no
shared index — multi-agent safe). Topology: fork-point via merge-base,
agents from refs, typed edges sub-agent-of/reviews/merges. Session merges
always record a two-parent session-merge commit (no-ff); conflicts commit
nothing and conclude via merge-resolve!. 53/53 (118/118 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:53:58 +00:00
2c9e8e4850 sx-git extension: git-wire import — the inverse adapter, round-trip law (TDD)
lib/git/import.sx parses loose payloads back to native objects bottom-up
over an export-set table: tree mode/name/raw-sha triples, ident lines,
header/message split, committer stored only when distinct so export
defaults regenerate identical bytes. Laws verified: export->import->export
is BYTE-IDENTICAL (head sha + every object), imported blobs/default-mode
trees regain their original native cids, 100755/tags/distinct-committer/
multi-line messages all survive. 15/15, total 267/267.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:52:24 +00:00
eff216ef40 agentic-sx Phase 1: schema — typed agentic objects over sx-git store (TDD)
Type registry (briefing / console-trace / behaviour TAG / agent-commit +
spawn/finding/refactor/test/session-merge/decision subtypes) with reflexive
transitive is-a? and create-only register-type!. Agent commits ARE git
commits (:agent-type rides as an open field, participates in the CID, DAG
machinery applies unchanged). 65/65.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:47:45 +00:00
e228d462eb sx-git extension: git-wire export adapter — byte-exact loose objects + SHA-1 (TDD)
lib/git/sha1.sx: SHA-1 in pure SX over host bitwise prims (FIPS vectors +
multi-block verified). lib/git/export.sx: native objects -> git payloads
"<type> <len>\0<body>" with real git identity, golden-verified against git
CLI (hash-object/mktree/commit-tree/mktag with pinned idents): tree entry
sorting with dirs keyed "name/", raw 20-byte child shas, "40000" subtree
mode, :mode overrides, deterministic ident defaults, trailing-newline
message rule. export-closure/export-set emit a host-writable object table.
Adapter-at-the-edge: native model untouched; zlib/packfiles remain host-side
concerns. 25/25, total 252/252.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:46:54 +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
071c2f9a8a R1: per-offering cap is atomic stock — the store-shape of the seat race (TDD)
Failing tests first (3 red: no offering pool stream; the sold-EDGE count was the only gate, so a
buy went through after the projection was wiped; the pool-refused buy leaked a showing seat). A
ticket now acquires from TWO atomic pools: the showing seat (physical capacity) AND the offering
allocation (stream 'offering:<off>', cap = its :cap field, ∞ if unset — so uncapped offerings are
unaffected). Both ev/hold! → guarded mint → confirm both / release both; offering-full releases the
seat. This is the co-op's product stock: in the store shape the offering IS the product and its cap
is the only pool, now genuinely atomic. Advisory offering-available? stays for button-hiding only.

blog suite 259/259 (+3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:58:24 +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