Correctness fixes found by reading the engine against its own stated invariants, in the places 0.2.0 and 0.3.0 did not reach: a backward-chaining loop guard that made a conjunctive query answer nothing, doors that disagreed about what they would accept, an index trusted without being checked against the records it describes, slots and keys that let arrival order decide belief, and derived caches a settle read one revival out of date. Thirteen entries are marked Breaking — they refuse input 0.3.0 accepted, rename what it exported, or change an observable contract — which is why this is 0.4.0. The Refusal entries (CONTRIBUTING §3.8) cover input that is newly refused where what 0.3.0 did with it was corrupt state or answer a different question in silence, so no working caller loses anything it had. Each entry says what a reader would have observed; the mechanism is in the subsystem's doc.
[(anc Tom ?y) (anc Tom ?z)] was empty where (anc Tom ?y) answered twice, because
the per-path loop guard grew for a whole frame and a queued conjunct is a sibling of
the expansion, not a descendant. Silent in every direction: forward chaining and the
node engine both answered, provable? said false, prove-within reported :status :complete, and the planner became semantic. docs/inference.md, "The loop guard's
scope is the subtree, not the frame".assert refuses a sentence that is not an s-expression. A string — what
a failed EDN read hands back, from impl.cli's read-arg and the daemon's :args —
was stored, indexed and believed as an object no query can match; nil likewise; a
symbol, number or map threw a bare UnsupportedOperationException with no :type.
check refused all five, so the door built to predict assert disagreed with it.
Migration: nothing a working caller sent is refused; fix the producer that handed
assert unread text, and discriminate on :shape.exceptWhen query's literals are held to the naming invariants.
(exceptWhen (lives_in ?x cold_place) …) stored a literal docs/naming.md says is
refused, as an exception no query could match — so the rule read as guarded and fired
as bare. Both doors now read each conjunct, before the rule is stored, so a refused
exception leaves no bare rule believed. Migration: spell the exception's literals to
the invariants (livesIn, not lives_in), and re-check any rule 0.3.0 left bare.edit! batch key nothing reads is refused. {:adds […]} bound nil,
so edit! wrote nothing and reported {:added [] :removed {…0}} — a success — while
check-edit, whose job is to predict exactly that, reported no problem. Over the
daemon it was a 200 {:ok true} for a write that did not happen. Migration: spell
the batch {:add […] :remove […]}; a batch under any other key wrote nothing.:record-space and :index-space default independently, so
{:backend :memory :record-space 77} paired a private record store with the
process-default index every other in-memory KB writes. assert then found the other
KB's handle, read it as a duplicate, stored nothing, and returned a handle in?
answered true for. A fork's :base and :overlay halves take the same keys and are
refused the same way. Migration: name both or neither, in every opts map.layout.edn
gates the index's key shape; nothing gated its coverage, so a short index opened
clean, answered short forever and re-cemented its own stamp — and re-asserting a fact
it could not find minted a second handle for a sentence already stored. Three ways in:
a torn kv.log tail, a directory grown under a derived-index mode, and a crash
between the record write and the index batch. docs/storage.md.assert acts on :direction instead of accepting and dropping it. Only
assert-rule read the key, so a rule asserted {:direction :backward} stored :both
and forward-chained, materializing the cross product a backward-only rule exists to
avoid. A :direction on a non-rule, one contradicting the sentence's own wrapper, and
a value outside the roster are refused rather than resolved. In the same pass a
non-map opts answers :unknown-option from both doors, where check said :shape.
Migration: spell the direction :backward (:forward :backward :inert :both);
a check caller matching :shape for a non-map opts matches :unknown-option now.implies after a set/inertRule stayed inert and never fired; after a
set/defaultRule it stayed defeasible and lost to a monotonic rival it should have
tied with. The resolution reaches conclusions already derived, since a justification
bakes the rule's contribution in as its :strength at fire time.
docs/canonicalization.md. Migration: the join only widens a slot; to narrow one,
retract! the handle and re-assert the intended spelling.clear-defeats! revived. A settle
lifts last settle's defeats at its top, but the cached closures were refreshed only in
settle-finish — after constraint-nogoods had read them — so discovery asked its
question against a vocabulary one settle out of date. A P/¬P pair made visible by
a revived genlContext edge went unarbitrated and retract! returned with both
believed, a state recover over the same records disagrees with.apply-ops!
read @data before acquiring and published after releasing, while compact! runs on
the durability daemon's executor — a thread the single-writer contract says nothing
about — so a compaction in either window rewrote the log from a map missing the
in-flight write. kv-clear! was sharper: a compaction between its truncate and its
publish wrote the entire pre-clear map back over the log just emptied.:type, not 500 with none.
docs/operations.md promises every {:ok false} carries the type the engine threw;
an unreadable body, a wrong argument count and an unknown op all answered untyped, the
first two as 500s. The engine's whole refusal vocabulary now answers 400, unlogged
— answered 500 they count as backend faults at every reverse proxy and 5xx alarm.
Migration: a client branching on the status code should branch on :type; every
{:ok false} carries a non-nil keyword./propose/* EDN read catches Throwable, as every other
untrusted-EDN read in the namespace already does. A deeply nested form raises
StackOverflowError, which an Exception catch let escape — and the browser has no
exception middleware, so it left the handler entirely.query refuses a non-map opts and a negative or non-integer
:max-depth. Both read as "no depth", which is not an error condition but a
different question — the no-rule-expansion answer, returned as if it were the
bounded one asked for. {:max-depth 0} is admitted: it is that answer asked for by
name. Migration: none for a working caller.edit! refuses what check-edit reports, before applying anything. The
two disagreed in both directions: a 4-element :add entry applied with the extra
silently dropped where the dry run reported :shape, and a non-sequential entry threw
a bare ISeq error from every door. An unknown :remove handle is refused before any
entry is applied, so a checked-clean batch cannot half-apply. Migration: a
remove-if-present batch filters its handles through in? first.not- or
ist-headed consequent read its own frame as the predicate, so every frame-headed
antecedent was "the recursive literal" — two orderings of a negated-head rule minted
two handles, and a genuinely recursive rule with a negated head lost the hold-back,
turning right-recursion left-recursive.termOfUnit content, a handle in
stored content that order independence rules out. docs/skolem.md. Migration:
rebuild the KB from its assertions (export! / import! replays firings) rather than
carrying both spellings.edit is edit!, and edit-with-consequences is
edit-with-consequences!. The batch's :remove half runs the same
retract-storage! sweep retract! runs, while the name read as additive — the one
gap in the ! roster the convention exists to close. Migration: rename the calls;
the wire op stays :edit, as :retract stays for retract!.:bad-opt is retired, and one compression spelling survives. Two
keywords split one failure class on no rule a reader could predict — seven sites said
:bad-opt where thirty-four said :unknown-option. Migration: discriminate on
:unknown-option and :unsupported-compression.meta.edn names its dialect :vaelii. Decorative on the read
side — the frame decides how a sentence is reconstructed — but it is a value in the
frozen format and a documented key of import-dump's return, so the name it carries
is now-or-never. Migration: a reader matching the old value matches :vaelii;
import-dump reads dumps written either way.exceptWhen, can rewrite one goal to the same
canonical residual through the genl fan; keyed on the count the two children were
one key, so the second was dropped before it was enqueued and every answer only its
exception admits was lost — silently, on the path query routes to whenever
:max-depth is given. docs/inference.md.except queues the same re-check as its arrival.
Only the store and removal chokepoints called recheck-except, so an except defeated
by a settle's resolution revived nothing it hid: backward proving answered yes while
the store held nothing, and which belief set the KB ended with depended on the order
the except and its defeater arrived.recover reads only positive, atomic declarations into the taxonomy.
sentexes-with-functor returns both polarities and the rebuild arms destructure the
positive shape positionally, so a stored (not (genl a b)) bound its inner sentence as
a taxonomy node and nil as the other — poisoning every cache on any recover, the
default {:recover? :auto} reopen included.:neg nogood is an at-least-one in every reader. The ASP translation's soft
branch emitted only the positive body atoms, so a :neg-only nogood — what
set/softConstraint over negated choice literals produces — emitted its violation
witness as an unconditional fact: no steering pressure, and :violated reported a
satisfied at-least-one as broken. docs/solving.md.conflicts and contradictions are content-ordered. Each report's sides were
already ordered by content; the list came off a hash set of handle-keyed nogoods, so
which pair (first (contradictions kb)) returned was an answer about which was typed
first. docs/nmtms.md.implies at
arity 2 threw a bare IndexOutOfBoundsException while arity 4 stored a silently
truncated rule check read as clean; (not A B) stored as a positive fact whose
record and index disagreed; a bare symbol passed as a rule literal was accepted,
unmatchable; and a non-finite measure magnitude stored cleanly, then threw out of every
later duration goal in the context. Migration: nothing a working caller sent is
refused — every one of these stored an object no query could match.find-terms and abduce take key rosters (a
misspelt :mtch ran the prefix default; a misspelt :keep? tore down the scratch
context whose handles the caller meant to commit), the CLI refuses a flag outside its
roster, escalate refuses a floor outside 0–7, and import-dump refuses an unknown
:framing where it guessed a reader and failed as a ZipException. Migration: spell
the key or flag as the refusal's roster lists it.vaelii.web --listen with no address parsed to a nil host — Jetty's wildcard bind,
with the Host allowlist reading nil as any — so a truncated command line put the
browser's unauthenticated write routes on every interface with the rebinding guard
off. serve read its positionals as a prefix, so 4200 --listen 0.0.0.0 /var/lib
dropped the directory and ran a disk daemon in memory. Migration: none beyond
completing the command line.assert, why, query and open-kb, and every other door took the misspelt
key in silence — answering a different question than the one asked. Now refused: an
open-kb mount or durability key without its axis, an opts key nothing reads at
forward-chain, the extent readers, preview, export!, import! and the anytime
budget maps. Migration: spell the key as the refusal's roster lists it.lein cli assert '(dog Fido)' Ctx --strength stored known-true
content at :default — and now exits 1 naming the flag; --memory --dir is refused as
a contradiction. Migration: none beyond completing the command line.Throwable, so a deeply nested form answers error: and a next prompt; the
browser's retract POST makes the check-edit round-trip docs/operations.md promises,
so a stale handle answers the problem panel rather than a success-styled "Retracted 0
sentexes".vaelii.* / VAELII_* switches was a
membership test or an equality against one spelling, so none of them had a wrong value
— every misspelling was the other branch, silently.
vaelii.disk.auto-compact=disabled read as compaction on; vaelii.disk.fsync=always
read as the three-second tick, the level the operator was trying to leave. The three
numeric reads had no catch at all. docs/storage.md. Migration: none for a working
setup, but two spellings now act where they were ignored — vaelii.disk.tokens=1 and
vaelii.index.snapshot=1 turn their features on, and vaelii.disk.lock=0 disables the
lock. Spell what you mean.vaelii.index.snapshot on macOS and Linux only — docs/storage.md said so and
nothing enforced it, so on Windows the publish failed part-way through a four-file
commit, in a place naming neither the cause nor the fix. Migration: none — the
property never worked where it is now refused; unset it and :disk-columnar rebuilds
its index on open.assert-rule refuses a rule literal whose predicate is a variable.
(implies ((?p ?x ?y) (transitive ?p)) (?p ?y ?x)) asserted cleanly and was indexed
under ?var0, which no arriving fact and no goal can spell — so the rule answered no
backward goal at all and fired forward only when the concrete-predicate antecedent
beside it arrived. Two arrival orders, two answers, from a rule the engine reported as
accepted. An :inert rule is exempt, which is what CoreContext's decontextualized-
predicate lift is. Migration: assert the instantiated rules, one per predicate the
metarule ranged over.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, 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.VAELII_MAX_BODY_BYTES override (16 MiB) live in vaelii.impl.guard, which both
read, so the browser answers 413 for an oversized form body where only the daemon
did. 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.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. 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.ist form must have exactly three elements. 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 refuse with :shape.kv/index-layout-version is cleared, rebuilt from the records and restamped,
:recover? notwithstanding; without the gate such a log replays cleanly and then
misses every read whose key shape moved. 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,
paid once. docs/storage.md.open-kb refuses a :base whose durable index is at an older key
layout (:stale-index-layout). 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 (:stacked-fork), which is what
docs/overlay.md has always stated.open-kb refuses a :recover? setting it does not name. :auto is the
default, true an alias for it, :warn and false the rest; 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 is dropped on open whatever
:recover? says.close! releases a durable fork's own directory. A fork's writable half
takes the same exclusive lock 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, so code that closed a fork in a finally and kept reading it worked and
now does not.<log>.compact behind, and 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.open-kv-backend and open-token-log replayed their logs outside any guard, so a torn
frame propagated to a caller that answers a failed open by releasing the lock —
leaving it released while this JVM still held an open handle.kv-entries is realized under its monitor. Both halves were lazy,
so the seq handed back from inside the lock realized outside it. An export of a fork
taken while anything wrote it projected two states at once.HashMap racing its own rehash can leave a reader spinning on a probe loop that never
terminates. Its check-then-put is one step too, so two callers cannot leave the loser's
alpha permanently unmaintained.load-source claims the catalog under one monitor. The busy test, the
already-loaded test and the registration were three separate reads, so two requests
arriving together each passed all three and spawned a loader.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 is the ordinary answer.roots-fallback.nippy carries 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
count and byte length, and the load thaws strictly..tmp until the
swap. A failed open likewise gives back the handles it took.export → import → export is
byte-stable. The provenance walk covers justification handles as well as sentex ones,
and import stores a justification's antecedents as a vector, the shape the engine's
own write path stores.:on-progress callback, with overlapping handle
spaces — could answer one KB's dedup question out of the other's supports. Keys coerce
fixnum boxing to Long at the boundary, since the map compares with Java equals
where the scan compares with =.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 on any rule whose antecedent had another
arity.place-conseq does not place a firing whose exceptWhen exception already holds, and
such a firing left no justification and nothing in jtms/blocked — so a settle pass
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 recorded as [rule handle, bindings], capped at 4096 entries per rule. docs/exceptions.md.contradictions names the same side of a clash
whatever order the two arrived in; the two settle sweeps sharing one exposure-instance
budget walk their moved region in content order; query with {:proof? true :portfolio? true} returns each answer once; negation-nogoods writes with a
compare-and-set; and the node engine's inline join plans with the :est-override
belonging to its registry leaf.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 while the fixpoint fired only
on the claims that were written — so the conclusion it never drew had no why, no
retraction path and no way to be an antecedent. The join contributes the handles the
inherited claim was read from, so retracting any of them withdraws the conclusion. One
asymmetry is left: a justification confers the weakest class it rests on, so a
:monotonic claim declared preserved by a :default declaration draws a :default
conclusion. 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.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 and the cost is the answer's own size; 0.2.0 asked
taxonomy/disjoint? once per type, and once per pair with both arguments open. On
4,000 types carrying one separation that is 15.4 ms to 0.13 ms with an argument bound —
flat 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.genlContext edge was answerable from exactly one of the two, and only when
that half was the one the settle moved. 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 it could pair with; nothing is widened.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 |