federation.sx — peer-asserted subjects, advisory and trust-gated. An
assertion is accepted only from an explicitly trusted peer (else
{error, untrusted}) and is flagged {peer_asserted, Peer}, never promoted to
local authority; acl decides what a peer-asserted identity may do. Cross-
instance subject mapping namespaces remote subjects by peer
({federated, Peer, Remote}) so two peers' "alice" never collide, with
optional explicit aliasing. Adds an audit-completeness test. New
tests/federation.sx. All four phases done — 124/124.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
audit.sx is an append-only ledger process. token.sx gains start/1(Audit)
and emits an event on every grant transition (issue, refresh, revoke —
including reuse-triggered revoke); start/0 stays unaudited so existing use
is unchanged (token.sx has no compile-time dep on the audit module, it just
sends to a pid). The ledger answers (identity/audit subject) via
audit/actions/count/all, chronological. In-memory event stream; persist
backing is a later Erlang<->persist bridge, out of scope. 111/111.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cache.sx — a process wrapping the token registry, memoising introspect.
Revocation stays real via generation invalidation: any revoke/refresh bumps
a generation counter, so every cached positive instantly becomes a miss and
re-validates against the live registry. A revoked token never reads valid
out of cache, not for a millisecond. stats() exposes hits/misses. New
tests/cache.sx. 101/101.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
membership.sx — coop membership as a guarded state machine
(none→pending→active→lapsed⇄active, any→revoked terminal); invalid
transitions return explicit {error, CurrentStatus}, never silent no-ops.
project(Subject, App) renders the one canonical state into a per-app claim
({member,Tier,App} / {pending,App} / {lapsed,App} / {denied,App} /
{non_member,App}) — identity reports what the membership is; acl decides
whether the app should honour it. New tests/membership.sx. 92/92.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
suggest/suggestN rank indexed terms by edit distance to a (misspelled) query
term, alphabetical tiebreak. 234/234.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oauth.sx now owns a session registry. establish creates a subject session;
silent_authorize (OIDC prompt=none §3.1.2.1) asks "does this subject have a
live session?" — if yes it mints a code skipping consent, bound to client +
redirect_uri + PKCE exactly like a consented code; if no it returns
login_required (a negative state, not a login redirect). One session serves
many clients; end_session closes the fast-path. New tests/sso.sx. 75/75.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
exchange now issues an access+refresh pair (RFC 6749 §4.1.4/§5.1) via
token.sx issue_grant; added the refresh grant (§6) delegating to token
rotation. End-to-end: code-exchange → refresh → introspect (active),
refresh-token reuse rejected (invalid_grant), and revoke-then-refresh
blocked by grant cascade. oauth 17/17, 65/65.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grant {Subject,Client,Scope,Status} becomes the unit of authorization
and cascade; access + refresh tokens reference it. issue_grant returns an
access+refresh pair; refresh (RFC 6749 §6) supersedes the presented refresh
token and mints a fresh pair; reusing a superseded refresh token is treated
as theft (RFC 6819 §5.2.2.3) and revokes the whole family, killing the live
descendant. revoke of any token cascades to the grant. All prior token
behaviour preserved. token 18/18, 62/62.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
identity:start() spawns one coordinator owning the token table + session
registry and exposes the whole-domain ops. The coordinator is the owner
sessions notify on idle timeout, so an expired session deregisters itself
— timeout-driven, never swept. verify/2 answers identity only ({active,
Subject, Client, Scope}); permission is delegated to acl. 39/39.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
searchRankTfIdf/searchRankBm25 parse a boolean query, filter docs via evalQuery,
then rank survivors by relevance over the query's leaf terms (queryTerms) — the
filter-then-rank pattern. 225/225.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Directory process holding (SessionId, Subject, Client, Pid) rows. Answers
the SSO probe lookup(Subject, Client) and the fan-out sessions_for(Subject)
(one subject, many clients). Routes only — no grant state, decides nothing.
Integration-tested: register a live session, route to it, confirm active.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Token table is a process; the token is an opaque make_ref carrying no
information. introspect() is a live table lookup every time, so
revocation is real (RFC 7009 §2): a revoked token reads {inactive} on
the next introspection with no validity window. Reply shapes follow
RFC 7662 §2.2 ({active, Subject, Client, Scope} / {inactive}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Session is an Erlang process holding {subject, client, status}. lookup/
touch/expire/revoke are messages; expiry is the process's own
`receive ... after Ttl` timeout (RFC-agnostic; no global sweep), which
notifies the owner and tombstones. Tombstoned sessions answer lookups
with an explicit {error, expired|revoked}, never a silent dead mailbox.
Adds the conformance harness + scoreboard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A synonym map [(Term,[Term])] expands a query term to itself + synonyms
(expandTerm); synDocs unions and synRankTfIdf ranks the expanded set. 214/214.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nearDocs k t1 t2 returns docs where both terms occur within k positions
(unordered); candidates from the posting intersection, filtered on positional
postings. 205/205.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic English suffix stripping (stem), stemText/stemTokens, indexStemmed.
Worked around two haskell-on-sx string gotchas: take/drop over a String yield
char codes (rebuild via joinChars . map chr), and isSuffixOf's reverse trips ++
(manual suffix compare). 196/196.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New adversarial/cross-phase coverage: diamond resource+group hierarchies
(deny wins per path), chain inheritance + leaf deny, cycle termination,
multi-peer delegation, fact validation, audit snapshot/restore round-trip.
Adds acl-validate-facts/acl-facts-valid? (schema) and acl-audit-snapshot/
restore!/copy (audit). Fixed acl-audit-restore! rebuilding the live log via
map (append! silently no-ops on map-derived lists).
Suite is prover-free: a substrate JIT bug loops the recursive proof
reconstructor on deep chains in warm processes (documented in Blockers);
acl-permit? is unaffected. 145/145.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared durable-state substrate (lib/persist) other subsystems build on:
log + kv facets over an injectable backend, projections, subscriptions,
snapshots + compaction, optimistic concurrency, a durable backend over the
kernel perform IO boundary (blobs by reference), plus extensions (materialized
views, kv CAS, stream catalog, query helpers, atomic batch, schema-evolution
upcasters, exactly-once append, global commit ordering) and a worked ACL
reference migration. 201/201 tests across 20 suites. Durability awaits the
host-side storage adapter (tracked in the plan's Blockers; loops/host-persist).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166/166 across 11 suites, Phases 1-8. Combinators (sequence/parallel/branch/attempt/
map-flow/while/until + retry/timeout/try-catch/recover/tap/fail-model), durable
suspend/resume via deterministic replay (guest call/cc is escape-only), crash
recovery, fed-sx distribution (remote-node/failover/replication/handoff), operational
API + hygiene, and a host integration ABI + reference driver for art-dag / human-in-
the-loop. New lib/flow/** only; imports lib/scheme read-only.
highlight marks query-matching (normalized) tokens with [..]; snippet extracts a
context window around the first match. 178/178.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
editDist as an O(m*n) row-based Levenshtein DP (naive recursion is exponential
and times out under load); fuzzyTerms/fuzzyDocs/fuzzyRankTfIdf expand a term to
indexed terms within a max edit distance. 166/166.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
paginate windows a ranked list (take lim . drop off); pageTfIdf/pageBm25 and
resultCount. 148/148.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
examples/acl.sx: a tested template migrating an ACL-grants store from a
hand-rolled ephemeral map to persist — grants/revokes as events, current set as
a projection, O(1) checks via a materialized view, audit via read-window.
Header carries the BEFORE->AFTER diff. Proves grants survive restart on the
durable backend (the capability the BEFORE version lacked). The pattern other
subsystem loops copy; does not touch the real lib/acl. 201/201.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
global.sx: persist/gappend records a pointer in a reserved $global index whose
seq is the global commit position; read-global/project-global replay every
event in commit order; global-from for incremental consumers. Opt-in (plain
append untouched); $-prefixed streams now reserved + hidden from the public
catalog (streams-all reveals them). Gives feed its unified timeline.
Deterministic across restart. 191/191.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
prefixTerms matches indexed terms by prefix (allTerms + isPrefixOf); prefixDocs
unions their docs; prefixRankTfIdf ranks via the matched terms. 136/136.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fedIndex merges per-peer inverted indices (union posting lists per term) after
relabelling local DocIds to global gid = peer*1000 + local — dedupe by
(peer,doc-id) is automatic and positions survive, so ranking runs once over the
merge and interleaves peers by score. ACL is a post-rank filter over an injected
permit predicate (searchTfIdfAcl/topNTfIdfAcl/searchBm25Acl). Roadmap complete,
122/122.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rankTfIdf and rankBm25 (configurable k1/b) over the candidate set, float scores
with deterministic DocId tiebreak; topNTfIdf/topNBm25. df/idf derived from
posting-list length. Tests cover tf/idf behavior, a BM25-vs-TF-IDF flip from
length-norm + tf-saturation, the b-parameter effect, tiebreak stability. 101/101.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mod/triage-pipeline domain r reports actor composes domain-policy decision →
explanation → AP activity → wire into one bundle. Integration test runs the whole
federated path across 5 modules (decide → wire → peer → trust-gated apply),
confirming the module-by-module subsystem composes end to end. +15 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mod/defrule collects trailing conditions via &rest; mod/ruleset assembles rules.
No macro needed — conditions are plain data, fn supports &rest here. Produces
structurally identical rules to mk-rule (asserted) and works in the engine
unchanged. Closes the roadmap's original defrule surface. +11 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mod/register-policy! domain rules + mod/decide-in domain r reports give each
rose-ash domain its own rule set; unregistered domains fall back to default-rules
(never unmoderated). Same spam report → remove under a strict market policy, hide
under blog default. Engine already took rules as a param, so this is registry +
fallback, no engine change. +14 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the host ABI from work-queue to driver loop: the host supplies only a
(kind payload) -> answer dispatch fn; flow-drive-host services one tick of pending
requests, flow-run-host ticks until quiescent (bounded). Tested via the art-dag
render -> human-review -> publish pipeline driven entirely by flow-run-host. The
art-dag integration is now: define dispatch, call flow-run-host. 166/166, 11 suites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>