Fram has been absorbed into Beagle as
beagle store. This repository hosts the final standalone releases; v0.8.0 is the last. Active development continues in the Beagle repository.
The store is the heap: the engine belongs in the same substrate as the
language and its programs, so code, state, and history can share one typed
system. The store still carries Fram names in FRAMRPC, FRAMLOG, and
framlog because those protocol and history surfaces are part of the current
engine contract; the product now lives with Beagle.
Fram is a persistent engine for recursive, typed triples with neutral, position-addressed structure.
The semantic kernel has three definitions:
Atom := String | Int | Float | Bool | Keyword | Instant
Term := Atom | Triple
Triple := (Term, Term, Term)
The positions are t1, t2, and t3. The kernel does not impose
subject/predicate/object roles, and any Triple can occupy any position of another
Triple. The intended identity contract for an Atom is its kind plus canonical
payload. Current Float handling does not yet satisfy that contract end to end:
host interning makes NaN unequal to itself and treats +0.0 and -0.0 as
equal, while the wire canonicalizes NaN bits and distinguishes signed zero. A
Triple has recursive structural identity. Constructing or nesting one creates a
Term; it does not assert anything. TripleRow and integer term handles are
private storage mechanics.
Three identities stay separate:
Atom identity target Atom kind + canonical payload
Proposition identity recursive structural Triple equality
Assertion identity occurrence coordinate
A Triple takes the role of proposition content when an occurrence carries it as an assertion or retraction. A profile may constrain which Triple structures it admits and how it interprets their positions. Fram records that a writer asserted content; Fram does not certify it as true. A nested Triple may instead be a compound value, and nesting never asserts it independently.
In a profile that reads the middle position as a relation, the relation must actually state the relationship. These are three separately asserted domain propositions:
(:contactable_at, :member_of, :contact_relations)
("alice@example.com", :member_of, :email_addresses)
("Alice", :contactable_at, "alice@example.com")
The direct String form is correct only when ordinary String canonicalization and equality are exactly the equality contract wanted for email addresses. Use a distinct Atom kind when intrinsic validation or equality differs; that is a deliberate kernel and codec extension, not ontology spelling. Mint a resource identity only when the address has continuity or a mutable representation independent of the String. Membership contextualizes the String; it never changes the underlying Atom.
Committing proposition content creates an occurrence with a coordinate and an
assert or retract action. Equal propositions can have distinct assertion
occurrences. A successful content retraction withdraws the newest live equal
assertion occurrence; another equal occurrence remains live if one exists. A
no-match retraction still records an occurrence and advances the version, but
reports stateChanged = false and creates no withdrawal. FRAMLOG stores these
signed operations; the query engine exposes them as
occurrence(coordinate, action, proposition) and exposes a successful targeted
retraction as withdrawal(retraction, assertion).
Live occurrence state preserves multiplicity: rpc/scan returns one matching
row per live assertion occurrence, including structurally equal duplicates.
Datalog's triple relation is instead a structural set projection. Logical
transaction order is intrinsic to the occurrence coordinate; wall-clock,
valid, and observation time are metadata and never part of proposition
identity.
“Turtles” names the architectural prior—turtles all the way down: prefer the same recursive Term language for semantic content and structural coordinates whenever the model permits. Operation and withdrawal rows remain explicit history machinery, not manufactured domain propositions. It is not a second primitive or a code type. See the naming ledger.
- Architecture — semantic kernel, physical rows, database, server, and projections.
- Glossary — the single vocabulary source for current documents.
- Query reference —
triple,occurrence, andwithdrawal, recursion, filters, arithmetic, and aggregates. - Ontology — modeling rules, the canonical normalized example, profiles, and semantic hints.
- Guarantees — guarantees, concurrency, workload envelope, and client obligations.
- Naming ledger — durable naming verdicts and rejected alternatives.
- Bun FRAMRPC client — the complete direct builder and application data plane.
- Isolation and deployment — trust domains, the three deployment shapes, and the wasm embed contract.
- Coming from Datomic — the datom-to-occurrence bridge, the exact-difference table, and the honest not-yet list.
- Tool catalog — exactly five public MCP data verbs.
The checkout runtime needs Babashka for the CLI and Clojure/JVM for the server.
Beagle is needed only when rebuilding graph-authored source; compiled Clojure is
committed under out/.
$ git clone https://github.com/Autonymy/fram && cd fram
$ export FRAM_SPACE_ID=fram-demo
$ export FRAM_LOG=/tmp/fram-demo.framlog
$ export FRAM_SERVER_RUNTIME=jvm-dev # explicit checkout fallback
$ bin/fram-up
$ bin/fram tell :contactable_at :member_of :contact_relations
$ bin/fram tell '"alice@example.com"' :member_of :email_addresses
$ bin/fram tell '"Alice"' :contactable_at '"alice@example.com"'
$ bin/fram show '"Alice"'
$ bin/fram query '{:find "emails" :rules [{:head {:rel "emails" :args [{:var "who"} {:var "email"}]} :body [{:rel "triple" :args [{:var "who"} :contactable_at {:var "email"}]}]}]}'
$ bin/fram occurrences
$ bin/fram validateBare Alice is local CLI shorthand for the String "@Alice"; the quoted
arguments above preserve the exact canonical-example Strings. Keywords,
numbers, recursive three-element vectors, and {:instant [seconds nanos]} are
lowered to Terms before the socket opens. EDN is only human CLI syntax. The live
engine wire is binary FRAMRPC.
bin/fram-serveris the native-first server launcher. Its default route requiresFRAM_NATIVE_ARTIFACT_DIRto name a READY artifact containingbin/fram-server-native; it never falls back silently.jvm-oracleandjvm-devare explicit retained routes. The launched server owns one database (SpaceIdplushistory.framlog), accepts FRAMRPC v2's closed data surface of thirteen operations (exact wire version 2.0), and holds writer authority for its active lifetime. The native server additionally accepts the separately namedrpc/checkpointoperator capability.bin/framroutes public data commands (tell,retract,show,query,scan,occurrences,version,status, andvalidate) over FRAMRPC. Explicit local migration/projection/admin commands are separate from that wire path.bin/fram-backupis the Bun-first native operator path. It takes a live checkpoint, copies the exact durable FRAMLOG prefix at that cutoff, and publishes a canonical hash manifest with the SpaceId, served version, and exact native artifact READY receipt. It does not add an application data operation or treat the derived snapshot image as authoritative backup data.bin/fram-mcpis a JSON-RPC-over-stdio edge with exactly five public data tools:tell,retract,show,ask, andvalidate. Graph authoring and deployment control are separate sealed services.clients/bun/framrpc.mjsis the official zero-dependency Bun 1.3.13+ client for direct builder and application traffic. It preserves recursive Terms, batches, versions, occurrence replay, paging/cursors, snapshot selectors, and leases across all thirteen FRAMRPC v2 data operations. Its optional@tompassarelli/framrpc/schemaentry point composes those operations into occurrence-correct single-value replacement, unique creation/upsert, identity-resolved guarded updates, and mixed create/update transactions without adding domain roles to the kernel. The same module owns the codec for the separately named, fixedframNativeCheckpointoperator capability used byfram-backup; that capability is deliberately absent from the ordinaryframClientobject.- The Cloudflare shim accepts closed JSON with tagged recursive Terms and lowers it to FRAMRPC. It does not accept EDN or an untyped escape hatch.
- The engine also links as a library:
native/fram.hpublishes embedding ABI v1 (libfram.a,libfram.so), and--host wasm-embedlinks the same ABI into a wasm32 module an isolate embeds with no server and no socket. Both take one canonical FRAMRPC v2 frame in and give one out, and the wasm engine answers byte-for-byte what the native library answers. See isolation and deployment.
Fram's differentiator is not “a triple plus an id.” It is the uniform recursive term model: a Triple is itself a Term, so relationships, compound values, identity coordinates, and domain metadata can use the same three positions without a privileged attribute position or bolt-on statement entity. Assertion identity remains an occurrence coordinate in the explicit operation history.
The storage implementation interns Atoms and Triples and keeps compact
TripleRow/operation tables, but those handles are deliberately not semantic
identity. Querying is position-neutral and history remains addressable after a
withdrawal. The exact executable contracts live in
tests/triple_kernel_test.clj,
tests/database_test.clj, and
tests/triple_query_test.clj.
Fram is pre-1.0. There is no engine access control: isolate by process, network, SpaceId, and FRAMLOG, and put authenticated public edges in front. The concurrency receipts cover one machine and one writer; they are not distributed consensus.
Fram is dual-licensed under your choice of the MIT License or
the Apache License, Version 2.0
(MIT OR Apache-2.0).