Correctness fixes across the durable index, the snapshot, the JTMS, the export
dump and the bounded prover, a sweep that gives every refusal a :type, the one
wire contract 0.2.0's own sweep left qualified, and the serialization both
servers' storage layer already assumed. Then a run of inference and belief
work: two orders that reached two answers (a firing refused before it could
become a justification, and a definitional clash whose halves sat either side of
a genlContext edge), the two doors that disagreed about an inherited claim, and
two enumerations that grew with the vocabulary rather than with their own answer.
Eight entries are marked Breaking — they refuse input 0.2.0 accepted or
change an observable contract, which is why this is 0.3.0 and not 0.2.1; the rest
are compatible.
:type keywords are plain — :not-edn,
:cross-origin, :bad-host, :body-too-large, where the namespace serving
them qualified each one. This finishes tree-wide what 0.2.0's own breaking
entry claimed, so a client discriminating on a :type matches on what went
wrong rather than on where the code lives.VAELII_MAX_BODY_BYTES override (16 MiB by default) live in
vaelii.impl.guard, which the daemon and the browser both read, so the
browser answers 413 for an oversized form body where only the daemon did —
a body 0.2.0's browser accepted, which is why this is breaking on the same
grounds 0.2.0 marked the daemon's own cap so. A daemon read is also fully
realized inside the write monitor — wire-safe's walk is what realizes a
lazy answer, so running it after the monitor released let a :query straddle a
concurrent :assert.disk/kv's apply-ops! folds
against a map read outside its lock and publishes outside it too. Interleave
two and the WAL holds both frames while the RAM map holds one, so the running
index and the one replayed on the next open disagree. The browser now takes one
process-wide monitor around every content write, as the daemon always did. A
concurrent write waits rather than racing, which is observable as ordering.ex-info the engine throws carries a :type. Twenty refusals threw
an untyped map, so a caller had to guess from which keys were present —
lookup, import!, the catalog loader, the deferred join, the solver bridges,
the disk codec and the token dictionary among them. Two forms that threw a raw
Java exception now answer instead: (genl ?x ?x) / (disjoint ?x ?x) answer
the question one variable in both positions asks — which members the relation
holds of themselves — rather than raising Duplicate key.ist form must have exactly three elements. (ist Ctx S) is
the form; 0.2.0 read assert and check positionally, so (ist Ctx S junk)
asserted with the extra silently ignored and (ist Ctx) raised a raw
IndexOutOfBoundsException. Both now refuse with :type :shape, the same
problem check already reported.layout.edn sits
beside format.edn under <dir>/index, and a log whose stamp does not match
kv/index-layout-version is cleared, rebuilt from the records and restamped,
:recover? notwithstanding — an absent stamp over a populated log counts as
stale, since that is what an index written before the sentinel looks like. The
stamp lands only after the rebuild, and the directory is marked mid-rebuild
before the clear, so a crash anywhere between the two reads as still-stale
rather than as a fresh directory needing no work. The gate reads the index
kind, so it fires on a durable index and never on a fork, whose inherited
half is held to the sentinel by the :base refusal below.
A 0.2.0 durable store carries no stamp, so
its first open under 0.3.0 pays one automatic reindex: O(records), logged at
:warn with the record count and the time it took, and paid once. Without the
gate such a log replays cleanly and then misses every read whose key shape
moved — populated-looking counts over queries that answer nothing.open-kb refuses a :base whose durable index is at an older key
layout (:type :stale-index-layout). A fork's base is held to the same
sentinel as the fork's own half and gets the other answer: the repair is a
write, and a base is mounted read-only, so the refusal names the one place the
rebuild can happen — open that directory as a KB, then mount the fork over it.(fork (fork base)) is refused (:type :stacked-fork), which is
what docs/overlay.md has always stated. core/fork passes :base-stores,
which names no backend for the opts check to catch, so the stores are asked
directly (mount/forked?).open-kb refuses a :recover? setting it does not name. :auto
is the default, true is accepted as an alias for it, and :warn and false
are the rest of the roster; any other value read as the warn branch and handed
back an empty TMS over a store that is not empty, which answers [] to
everything. A stale derived index — one describing records that are gone —
is dropped on open whatever :recover? says: false asks for silence about an
unrecovered store, not for an index answering out of records nobody holds.close! releases a durable fork's own directory. A fork's
writable half takes the same exclusive lock and holds the same file handles as
any durable KB, so without its own :dir it could never be handed to another
process short of exiting the JVM. 0.2.0's docstring promised the opposite in as
many words — close! on a fork was a no-op that kept its lock — so code that
closed a fork in a finally and kept reading it worked and now does not. The
base's directory stays unnamed here: it is mounted read-only and shared by
every fork over it.<log>.compact
behind, and f/open-log seeks to the end while f/open-idx does not truncate,
so the next compaction in the same session opened that temp and appended to
it. Its replay then put back records deleted in between. The cleanup is scoped
to the pre-commit phase: past the marker the temps are the only complete copy,
and the next open finishes the replay off the marker, which is what it is for.
The record store and the KV index both take the three-handle open under a guard
too, so a throw from the second or third no longer leaks the ones before it.open-kv-backend and open-token-log replayed their logs outside any guard, so
an unrecognized op frame or a torn dictionary entry propagated to a caller that
answers a failed open by releasing the lock — leaving it released while this JVM
still held an open handle, the one state close-dir! exists to prevent.kv-entries is realized under its monitor. Both halves were
lazy, so the seq handed back from inside the lock realized outside it, and every
element it then produced called the merged-view reads the monitor exists to
serialize. An export of a fork taken while anything wrote it projected two
states at once. Same class as the daemon's wire-safe fix above.HashMap racing its own rehash can leave a reader spinning on a
probe loop that never terminates. Its check-then-put is one step now too, so two
callers cannot each build an alpha and leave the loser's permanently unmaintained.load-source claims the catalog under one monitor. The busy test, the
already-loaded test and the registration were three separate reads of the
catalog state, so two requests arriving together each passed all three and
spawned a loader — two background loaders writing the same stores, which is the
case the guard exists to refuse.Throwable, as the daemon does. A
deeply nested form overflows the reader's stack with a StackOverflowError,
which an Exception catch lets escape — a 500 where an unreadable term, line or
textarea is the ordinary answer these three sites exist to give.roots-fallback.nippy carries the argument-root postings, which
are primary index truth, and a missing or torn blob loaded as [] behind a
warning while every argument-root read answered #{} out of a snapshot that
opened clean. The meta records the blob's entry count and byte length, the
decision checks the length exactly, and the load thaws strictly — a torn blob
or a count mismatch throws into the rebuild path. The reloaded token dictionary
is checked against the log by count, since the two sides key on different
equality, and a mismatch rebuilds from the records rather than citing the wrong
entry for every mapped edge..tmp until the swap — so a shutdown cannot close the records under save!
and leave it having deleted the commit mark of the image it was writing. A
failed open likewise gives back the handles it took: open-kind,
open-record-store, the token log and the durability registration each undo
their own opens, so a throw cannot outlive the directory lock that made the
handles safe to hold.export → import → export is byte-stable. The provenance walk covers justification handles as
well as sentex ones, so a stamp add-provenance put on a justification is no
longer dropped, and import remaps a justification's own handle to replay it.
Import also stores a justification's antecedents as a vector, the shape the
engine's own write path stores, so a round-tripped dump is byte-for-byte the
dump it came from.:on-progress callback, and with
overlapping handle spaces — could answer one KB's dedup question out of the
other's supports. Every reader and both wholesale clears hand the map out only
to that TMS, and anything else falls back to the reference scan. Keys coerce
fixnum boxing to Long at the boundary, since the map compares with Java
equals where the scan compares with =, and an Integer-boxed handle would
silently split a key.prove-within prepares its goal, through the same prepare-goal-for-read
every other read path takes, so a reifiable NAT or a merge-retired spelling is
the same question under the bounded prover that it is under ask.genl sub-predicates at every
arity, as the reference res/match-pattern does. Fanning only for a
two-element sentence gave the opt-in matcher a different belief set from the
default engine's on any rule whose antecedent had another arity.place-conseq does not place a firing whose exceptWhen exception or
(unknown S) antecedent already holds, and such a firing left no
justification, no node, and nothing in jtms/blocked — so a settle pass, which
reads a release off the justifications that left the blocked set, could not see
it, and the conclusion stayed suppressed after the block lifted. The same
knowledge in the other order concluded it. The refusal is now recorded as
[rule handle, bindings] in a per-KB record; a queued rule's entries are
re-evaluated under the narrowing the placed firings already take, and a
released one is re-derived from its own bindings rather than by re-joining the
rule over the fact extent. The record is capped at 4096 entries per rule, past
which the rule falls back to that re-join; recover rebuilds it by re-firing,
and the two edge triggers that narrowed on a rule's placed firings ask their
question of its refusals too. See docs/exceptions.md, "A refused firing is
remembered as bindings".contradictions names the same side of a
clash whatever order the two arrived in — handle-naming's exact-match arm was
a bare ffirst over matches that fan across the whole genlContext cone, and
asymmetry-problem's tie-break stopped at the context name; both order on
content. The two settle sweeps that share one exposure-instance budget walk
their moved region in content order, so which nogoods are minted past the
budget does not depend on arrival order either. query with {:proof? true :portfolio? true} returns each answer once: portfolio-solutions dedups on
the bindings rather than on the whole result map, so a solution two racers
proved differently is one answer, as core/query's contract says.
negation-nogoods writes with a compare-and-set, so a note-opposed! landing
between the read and the write is carried rather than dropped. And the node
engine's inline join plans with the :est-override belonging to its registry
leaf, the pair prove-seq is handed.sentexes-matching and ask stop disagreeing about the same knowledge.
(argPreserving largerThan 1 genl) beside (largerThan dog cat) licenses
(largerThan chihuahua maine_coon), which ask reached through
ArgPreservingProver while the fixpoint fired only on the claims that were
written — so the same rule over the same KB answered differently depending on
which door the reader came in, and the conclusion it never drew had no why,
no retraction path and no way to be an antecedent. The join now contributes
the handles the inherited claim was read from — the claim that was stated,
the declaration licensing the move, the reach edges, and (transitive R) for
a fact-relation — so retracting any of them withdraws the conclusion and
placement descends to the microtheory that can see them all. A more specific
contrary claim withdraws a firing with nothing retracted, blocked and revived
by the machinery exceptWhen already uses. Union, never replacement: a stored
claim matches exactly as it did, and a KB declaring no preservation pays two
set-cardinality reads per datum. One asymmetry is left and is narrower than
the one this closes: the declaration is an antecedent of the firing, which is
what makes retracting it withdraw the conclusion, and a justification confers
the weakest class it rests on — so a :monotonic claim declared preserved by
a :default declaration draws a :default conclusion, where ask answers
without weighing either. Assert the declaration :monotonic if the derived
conclusion should carry the claim's own strength. docs/inherit.md.skolem/frontier-vars subtracts a post-join literal's output, so the Skolem
NAT no longer takes a variable into its argument list. In the same pass,
matches-visible's cache key omits no retrieval-strategy var, so a
differential oracle cannot compare a result against itself.disjoint goal is enumerated from the declarations rather than from
the vocabulary. A separation convicts two subtrees, so the answers are the
subtypes of what a visible declaration names
(taxonomy/separating-partners, taxonomy/separating-pairs) and the cost is
the answer's own size; 0.2.0 asked taxonomy/disjoint? once per type in the
KB, and once per pair of types with both arguments open. On a KB of 4,000
types carrying one separation that is 15.4 ms to 0.13 ms with an argument
bound — flat in the type count where it grew linearly — and at 1,000 types the
two-variable goal goes from 2.5 s to 4 ms. lein perf's
disjoint-enumeration check is the claim. Two answers arrive with it: the
open goal binds a disjoint metatype's members even where they carry no genl
edge, which is what the ground goal has always said of them, and the candidate
set (the taxonomy's nodes plus the declared pairs) did not hold. The
prover's est-bindings is sized off the declarations too, so a disjoint
conjunct in a registry-planned join is costed at what it will yield.settle asked each
candidate's question from that sentex's own — so a pair whose halves sit either
side of a genlContext edge was answerable from exactly one of the two, and
only when that half was the one the settle moved. Known-true content in a
general microtheory sat beside a default that denies it, in one arrival order
and not the other. settle/clash-askers runs the check from the candidate's own
context and from the maximal common descendant of it and each context holding a
sentex the candidate could pair with; nothing is widened, since a vantage
already sees both halves. Runs under the KB's :arbitrate policy, like the
retroactive sweeps — under :refuse a pair neither writer could see stays the
exposure pass's report.checks/disjoint-problems and the asymmetric arm named one handle each, so the
content-first of the two was weighed and the other left believed beside content
that contradicts it; functional-problems counted its clashes per sentex all
along. The asymmetric arm reads the stored converses beside inherit/surviving,
which answers one claim per tuple by design. clash_oracle_test covers the
cross-context pair rather than excluding it, and is what found both.Not a drop-in upgrade from 0.1.0. Several of the changes below refuse input 0.1.0 accepted or change an observable contract — each such entry is marked Breaking — which is why this is 0.2.0 and not 0.1.1. Entries between here and the 0.1.0 header are in it, newest first.
[:argument-root pred pos term]), so a materialising join reads one literal's postings rather than
wading through every functor's at a shared slot. An [:argument-slot pos term] roster, reference-counted off those postings, keeps the
predicate-agnostic reads answerable as a union over the predicates present.
The packed long has no room for a fourth key part, so the dense roots route
the family to their boxed fallback. index-layout-version is 2: an index
written by 0.1.0 reads as :layout-changed and is rebuilt on first open —
no action needed, but a large durable store pays a reindex for it.:bad-handle) —
the vector assert returns for a conjunctive rule included, which 0.1.0's
retract! silently answered with {:removed-sentexes 0}. nil stays a
question with an answer (in? false, why {:stored? false},
add-provenance a no-op), and check-edit reports what edit throws. why
also takes {:max-depth n} (default 256), marks a capped branch
{:truncated? true} instead of overflowing, and refuses bad opts
(:unknown-option).close! releases a durable KB's directory without waiting for JVM exit,
and import! is export!'s inverse. An unclean close still releases the
lock and registry; the first component failure is rethrown after.argIsa / interArgIsa / argGenl refusal names its convicting
declaration in content order, not in whichever order retrieval enumerated.assert refuses a non-map opts (:unknown-option) —
(assert kb s ctx :monotonic) stored a defeasible sentence in 0.1.0.
check already reported the same request; the two agree now.open-kb recovers by default (:recover? :auto). The old
:warn default handed back a KB that answered wrongly from a reopened
store. The cost moves to construction — O(records) on a populated store —
and {:recover? false} defers it. :warn and false remain.vaelii.core
plus thin shims vaelii.client, vaelii.starter, vaelii.web,
vaelii.serve and vaelii.cli over the impl namespaces they front. The
boundary is now what the docs said it was.vaelii.client's assert and assert-rule are spelled bare,
without the ! 0.1.0 gave them. A ! marks a fn that destroys stored
knowledge and neither does — both are additive, and retract! is what takes
one back — so the client now spells them exactly as vaelii.core does. A call
site writing c/assert! or c/assert-rule! no longer resolves.:type keywords are plain across the tree
(:unknown-source, not :vaelii.impl.catalog/unknown-source), and
open-kb's backend refusals carry one. Swept in the same pass: the settle
re-check queue no longer drops entries queued by a concurrent thread, and
foreign/register refuses with ex-info rather than an elidable {:pre}.:preserve-eval-meta needs it, and 2.9
ignores the key silently.POST /op requires Content-Type: application/edn. The type
is not CORS-simple, so a browser must preflight and the daemon answers no
CORS headers — which closes cross-site request forgery against a loopback
daemon. A client that sent no content-type is refused; add the header.VAELII_MAX_BODY_BYTES adjusts the cap.Host naming the interface the server was started on. A request with no
Host still passes (a non-browser client carries no ambient browser
context); a reverse proxy or local alias sets VAELII_ALLOWED_HOSTS.+with-foreign names a coordinate that exists
(com.vaelii/vaelii-foreign); the bare id it carried resolved nothing.The first release. What follows is the development log that produced it, newest first; every entry below is in 0.1.0.
(symmetric P),
(transitive P), (inverse P Q) and the argPreserving forms change what
may be concluded with no fact arriving; (functional P) sweeps the extent
when it lands.exceptWhen, unknown and census reads.why does.:ground-first by default; the goal-stack chainer drives one solution at a
time and level 7 streams its search.lein gate: lint, the suite, and the scaling claims, measured and failed on
rather than asserted; five checks added for costs that grow with what they
must not.<records>-<index>, all seven.argIsa entails as well as constrains, behind a toggle, retroactively too.lein browser; OpenCyc loading went from 378s to 277s.genl edges it subsumed through — belief and strength
run through them like any antecedent, checked against all 24 orderings.xz, an importer, and an oracle
comparing two knowledge bases; a dump lands every record at its handle.inherit declared rather than assumed; definitional checks reach every
term; argGenl constrains one level up.:memory-dense integer
postings, the :memory-columnar int-token trie with CSR compaction (3.18x
whole-index), and a bitmapped TMS behind a protocol.rewriteOf extended over predicates and types.ask-within normalizes its goal.exceptWhen canonicalized into the record, blocking excepted conclusions
with only reachable firings re-checked; its query reified the way a fact
is.different prover, a specification
suite, and wiring into assert; stratification is checked on edge change.assumptionRules with persistent solve and labeling contexts, proven on a
sudoku.exceptWhen began as a failing suite.core moved under vaelii.impl.*; ! reserved for
irreversible operations; tests became net-neutral, and a second concurrent
run fails fast rather than corrupting the first.The first day: a contextualized common-sense knowledge base with a trie index, inference and truth maintenance.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |