Cyclops Storm / Lab notes

Architecture comparison: evidence record

For anyone checking the Architecture page. The revisions, source locations, probe inputs and results, and measurements behind it, recorded on September 28, 2026 and extended during revision on September 29, with the detail the page leaves out and a record of how the page was reviewed.

Raw probe results · diagram script · Architecture · Migration

Revisions examined

Earlier engine Rewrite
Repository BadScienceFiction-CyclopsStorm AkiyaBlocks-CyclopsStorm
Commit e62758c23f66b26fa92c6d2dafaaf6bf3466499f 5d598de4448749dc0ce2cb81657aa952d748e2f5
Date Aug 30, 2026 (final commit) Sep 27, 2026; last source change 828c0fe; repository begun Sep 13
Package src/kb/ src/cyclops/
Entry points probed kb.engine.kb_engine.KBEngine; kb.integration.decide, affordances cyclops.KnowledgeBase; cyclops.api.decide, affordances
Working tree clean before and after clean before and after

The other pages pin the rewrite at 9be7804 (Sep 24). Seven source commits since then change behavior discussed here: cecc6d1 (game decisions gain indeterminate; explain reports unknown truth), de62694 (calculated effect times), 6d6a9a9 (circular story-time questions stop as EXHAUSTED), 2e0c386 (candidate fluents scoped to each requested time), 33cd9a9 (strict game checks), c736a1b (circular timeline sources) and 828c0fe (timeline preparation shares request limits). They touch api/affordance.py, engine.py, errors.py, solve/context.py, temporal/calculus.py and temporal/timeline.py; the reader, bindings, program, solver, tabling, built-ins and analysis are unchanged since 9be7804.

Method

Detail behind each page section

The Architecture page was cut twice on September 29, 2026: first to a shorter read, after which the second review round returned some qualifications to it in short form, then, aiming at a five-minute read, to a page of at most 1,300 words of visible prose, with each comparison reduced to a table row (what is shared, what differs, one symbol per engine) and one or two sentences of consequence (review record). Review round 3 then corrected that cut within the same word limits. The fuller detail is kept here, one heading per page section, in page order, each linking back to its section, with pointers to the probe tables and source map below. Nothing here is new evidence: the markings are the page's own (In the code, read in source at the pinned commits; Observed, from the probes in this record; What it means, interpretation).

Scope and layering

Page section: Who does each job.

The traced request

Page section: One question, asked of both.

Terms and clause storage

Page section: Terms and storage.

Routing, built-ins and recursion

Page section: Running a query.

Mutable state and caches

Page section: Mutable state.

Story time and the timeline

Page section: Story time.

Limits, statuses and cancellation

Page section: Can't finish.

Explanations and provenance

Page section: Explanations.

Host integration and extension

Page section: Host and extension.

Limits of the comparison

Page section: What this comparison can't tell you. Apart from the change-cost timing, timings are single runs on one machine and aren't used, and trace call counts aren't compared across engines (method). The earlier suite's 20 failures and errors all need the hypothesis package; the author's log recorded 6,660 passed by August 30 (measurements).

Probe inputs

The shared story, loaded as identical bytes into a fresh engine of each kind. The comment on its second line refers to the probe stage's working notes, which this page summarizes; SHA-256 hashes of every input are in the JSON.

% Shared story: may the player enter the laboratory after the door is locked?
% Identical input for both engines. Effect axioms are ground facts (see RESULTS.md).
time(t0). time(t1). time(t2). time(t3).
before(t0, t1). before(t1, t2). before(t2, t3).

initially(door_open(lab)).
happens(lock_door(lab), t1).
terminates(lock_door(lab), door_open(lab)).
initiates(lock_door(lab), locked(lab)).

can_enter(R) :- now(T), holds_at(door_open(R), T).

affords(enter_lab, corridor).
guard(enter_lab, 1, can_enter(lab), 'The laboratory door is locked.').
action_label(enter_lab, 'enter the laboratory').

Loaded after it, each as a separate text with the engine's ordinary load call (e.add(text) or kb.load(text)):

Probe results

Ordinary success

Goal (now(t2)) Earlier engine Rewrite
holds_at(door_open(lab), t0) [{}] one answer, complete
holds_at(door_open(lab), t1) [{}] one answer, complete
holds_at(door_open(lab), t2) [] no answers, complete
holds_at(door_open(lab), t3) [] no answers, complete
holds_at(locked(lab), T) T = t2, t3 T = t2, t3, complete
holds_at(F, t2) F = locked(lab) F = locked(lab), complete
now(T) T = t2 T = t2, complete
can_enter(lab) [] no answers, complete
decide(enter_lab, corridor) deny, not enabled, condition can_enter(lab), "The laboratory door is locked." same, plus order: 1, status: complete, notes: []
same with now(t0) allow, enabled allow, enabled, complete
affordances(corridor) enter_lab, "enter the laboratory", not enabled, same message same, plus kind: deny, status: complete

Request path for the first can_enter(lab) after loading

Functions entered, in first-entry order, with the line where each is defined (from the traced code objects). Paths are relative to src/.

Earlier engine (route recorded as LEGACY / default):

  1. KBEngine.query kb/engine/kb_engine.py:748 → QueryExecutionPipeline.execute kb/engine/query/pipeline/pipeline.py:70
  2. Stages under kb/engine/query/pipeline/stages/: CacheLookupStage.process cache_lookup_stage.py:37 (miss in PerformanceManager.get_cached_result, kb/engine/performance.py:153); ValidationStage.process validation_stage.py:26; ParsingStage.process parsing_stage.py:38, which calls TableManager.clear kb/engine/tabling/table_manager.py:563; RoutingStage.process routing_stage.py:24 → HandlerRegistry.get_handler_or_default kb/engine/query/registry.py:108; ExecutionStage.process execution_stage.py:26 → GeneralPredicateHandler.handle kb/engine/query/query_handlers.py:938
  3. Reasoner.evaluate_derived kb/engine/reasoning/reasoner.py:768 → QueryCache.get_derived_result kb/engine/query_cache.py:130 (miss) → QueryAnalyzer.analyze kb/routing.py:282 → RoutingFSM.decide kb/routing.py:453 → EngineRouter.eval_with_engine kb/engine/routing/engine_router.py:99 → LegacyDerivedEvaluator.evaluate_with_special_cases kb/engine/derived_evaluation/legacy_evaluator.py:416 → DerivedEvaluationTemplate.evaluate kb/engine/derived_evaluation/template.py:44 → _check_recursion legacy_evaluator.py:63 → BodyEvaluationCoordinator.evaluate_with_strategies kb/engine/evaluation/body_coordinator.py:57 → Reasoner._evaluate_literal reasoner.py:1225 (twice)
  4. Reasoner._evaluate_holds_at_literal reasoner.py:1374 → Reasoner.holds_at reasoner.py:705 → FluentEvaluator.holds_at kb/engine/reasoning/temporal/fluent_evaluator.py:96 → _compute_holds_at :164 → PersistenceEvaluator._is_terminated_before persistence.py:243 → TemporalReasoner.lt_time temporal_reasoner.py:58 (7 calls; reachability built lazily by _compute_reachability :277)
  5. QueryCache.cache_derived_result query_cache.py:158; ProjectionStage.process projection_stage.py:25 → PerformanceManager.cache_result performance.py:147; MetricsStage.process metrics_stage.py:30. The host receives [].

Rewrite:

  1. KnowledgeBase.ask cyclops/engine.py:339 → _run :384 → _context :646 → QueryContext.__init__ cyclops/solve/context.py:92
  2. KnowledgeBase._refresh engine.py:668 (revision changed): analyse cyclops/solve/analysis.py:114; a second, bootstrap QueryContext sharing the request's budget; KnowledgeBase._build_timeline engine.py:815 → build_timeline cyclops/temporal/timeline.py:120; DerivedState published
  3. solve cyclops/solve/machine.py:42 → resolve_clauses machine.py:275 → built-in table (machine.py:123; table assembled at engine.py:60) → holds_at cyclops/temporal/calculus.py:671
  4. prepare_effect_times timeline.py:306 → _holds calculus.py:487 → _explain calculus.py:548 → _starts_and_stops :404 → _effects :204 (4 calls, each back into solve) → _clipped :469 → Timeline.precedes timeline.py:92
  5. _run maps exceptions and ctx.status to a status (engine.py:430-462) and returns Solutions(answers=(), status=COMPLETE). On a fresh knowledge base this first request reports 103 steps, depth 5, because timeline preparation is charged to it; the same query repeated reports 22 (re-measured during revision; the draft of this record paired the cold trace with the warm count). ask("once(can_enter(lab))"), the form decide sends, reports 104 when cold.

One decide sent three KBEngine.query calls in the earlier engine and three KnowledgeBase.ask calls in the rewrite, which built four QueryContext objects: one per call plus one bootstrap. The three are the action (once(affords(enter_lab, corridor))), its guards (guard/4, all answers) and one once(Condition) per guard until one fails, so the count grows with the guards: the tunnel's two-guard decide sent four (affordance.py:178-199).

State held between requests

After Earlier engine (KBEngine object graph) Rewrite (KnowledgeBase)
Loading epoch 2; all caches empty attributes _derived, options, program; program.revision 16; _derived.revision −1
First can_enter(lab) FluentEvaluator._holds_cache 1 entry; QueryCache._derived_cache 1; pipeline result cache 1; answer tables 0 _derived.revision 16 (analysis, timeline, diagnostics); first-argument index built on can_enter/1, happens/2 and initially/1 (_index_revision −1 → 16); 0 live QueryContext objects (checked with gc)
Same query again answered from the pipeline cache; 16 functions entered; solver and story-time evaluator not entered; last_route is None 1 new QueryContext; analysis and timeline reused; solver and story-time evaluation re-run
First decide on a fresh engine pipeline result cache 3 entries; _holds_cache 1; _derived_cache 1 _derived.revision 16; 0 live contexts

Returned objects: in the earlier engine a second identical query("holds_at(locked(lab), T)") returned the same list object as the first, and that object was the value stored in the pipeline cache. That is a missing defensive copy, not a property of caching: the evidence overview measured a deep-copying repeat at 0.008 ms against 0.004 ms. In the rewrite each ask returned a new frozen Solutions whose answers are a tuple. Two separate KBEngine instances shared one routing registry and one structured logger. Process-wide state the rewrite touches per request: ask raises sys.setrecursionlimit for the call and restores it afterwards (engine.py:376-382; 1000 before and after in the probe), and variable numbers come from a module-level counter (terms.py:109), so non-ground answers get different variable names on each identical request (mk(X) gave f(_G22), then f(_G24)). Atoms are interned in a module-level dictionary (terms.py:165) that only grows: an atom first created by a request stays after it (round-3 probes).

Result shapes: the earlier query returns list of dict, with compound values as internal terms (Term('locked', (Term('lab', ()),))); its Decision.as_json has action, kind, enabled, condition, message. The rewrite's ask returns Solutions(goal, answers, status, notes, steps, depth) with values such as Struct('locked', (Atom('lab'),)); query() returns [{'F': locked('lab')}]; its Decision adds order, status and notes.

When evaluation can't finish

Public signatures, read with inspect.signature: earlier KBEngine.query(self, query_str: str) -> List[Dict[str, Any]], decide(engine, action, site=None), affordances(engine, site). Rewrite KnowledgeBase.ask(self, goal, *, limits=None, distinct=True, cancel=None), explain(self, fluent, when, *, limits=None, cancel=None); decide and affordances also take limits and cancel. Limits() defaults: 2,000,000 steps, depth 1,200, no answer limit.

Input (now(t0)) Earlier engine Rewrite
10 segments: reachable(corridor, lab) true true, complete
10 segments: answers to reachable(corridor, X) 10 10, complete
10 segments: decide allow allow, complete
60 segments: reachable(corridor, lab) []; standard output: WARNING: Recursion depth limit (50) reached for query: passage(tunnel49,lab) and ... reachable(tunnel50,lab); no log records true, complete (809 steps, depth 122)
60 segments: answers to reachable(corridor, X) (60 exist) 50 60, complete
60 segments: decide deny, condition reachable(corridor,lab), "You cannot find a way to the laboratory." allow, complete
44 to 56 segments: reachable(corridor, lab) true up to 50, false from 51 true at every length, complete
50 to 60 segments: decide (revision stage) 50: allow, one line on standard output (WARNING: Recursion depth limit (50) reached for query: reachable(lab,lab)); 51 to 60: deny, two lines each allow, complete, at every length

Rewrite only, 60 segments, on a knowledge base warmed by one ask("true") (82 steps, nearly all timeline preparation; whole-program analysis is not metered, because analyse(program, *, auto_table) takes no step callback, and cancellation is checked only before and after it). The decide sub-queries took 3, 3, 8 and 729 steps at default limits.

Call Result
ask("reachable(corridor, lab)", limits=Limits(steps=200)) no answers, exhausted, 201 steps
query(...) with the same limit raises IncompleteResult: query did not run to completion (status=exhausted, 0 answer(s) found so far)
decide(..., limits=Limits(steps=200)) indeterminate, not enabled, condition reachable(corridor, lab), order: 2, exhausted, message "Action availability could not be determined."
ask(..., cancel=lambda: True) no answers, cancelled, 0 steps, note "query cancelled"
decide(..., cancel=lambda: True) indeterminate, cancelled
decide(...) afterwards with defaults allow, complete
Cold knowledge base, ask("true", limits=Limits(steps=5)) exhausted, note "timeline preparation stopped (ResourceExhausted: …)"; derived state not published; a retry with defaults completes in 82 steps
ask("reachable(corridor, X)", limits=Limits(steps=300)) (revision stage) 1 answer, exhausted, 301 steps: ask returns the answers found so far
Unstratified p :- \+ q. q :- p. added (revision stage) ask("r(X)") and query("r(X)") raise SemanticConflict; decide returns indeterminate, not enabled, message "Action availability could not be determined.", status None, the error in its notes

Self-referential effect rule (now(t2)), each ask order (t3,t2,t1, t2,t3,t1, t1,t2,t3) on a fresh engine:

holds_at(door_open(lab), T) Earlier engine Rewrite
t1 true no answers, exhausted
t2 false no answers, exhausted
t3 false no answers, exhausted
decide deny, "The laboratory door is locked." indeterminate, exhausted
rewrite explain(door_open(lab), t2) holds: null, reason: unexplained, exhausted
rewrite affords(A, corridor), same knowledge base A = enter_lab, complete

With the effect written in three-argument form, terminates(auto_lock(lab), door_open(lab), t1) :- holds_at(door_open(lab), t3). (no happens/2 needed), the rewrite answered t1 complete with one answer in both ask orders, left t2 and t3 exhausted, and decide stayed indeterminate. The difference: the two-argument rule's body runs before it is joined to happens/2, as the rewrite's docs/LIMITATIONS.md says.

The rewrite's note: "holds_at(door_open(lab), t3) is defined in terms of itself at the same time point; circular temporal evaluation stopped without assuming true or false." Traces: the earlier FluentEvaluator.holds_at was entered once per question and its rule-effect lookup (WeakReasonerMixin.evaluate_causal_rules) never, so the self-referential rule never ran. The rewrite's _explain was entered for (door_open, t1), then (door_open, t3), then (door_open, t3) again, which found its key in progress and raised TemporalCycle. The rewrite's docs/LIMITATIONS.md at 5d598de describes the case under "Time-scoped candidates do not reorder rule bodies". Why the earlier engine never ran the rule: PersistenceEvaluator._is_terminated_before (persistence.py:243) checks stored termination facts first and returns as soon as one precedes the queried time, which the lock_door fact does for t2 and t3; for t1 no event precedes it. Its own cycle guard (fluent_evaluator.py:133-137, which returns false) is code-verified but was not reached. The single consistent answer, which the earlier engine gives, is open at t1 and closed at t2 and t3: the ordinary lock closes the door whatever the rule does, so the rule's condition is false.

Explanations

Request Earlier engine Rewrite
Door at t2 (now(t2)) why(holds_at(door_open(lab), t2)): type: temporal, reason: does_not_hold, no cause explain("door_open(lab)", "t2"): holds: false, reason: clipped, clipped_by: lock_door(lab) @ t1, complete
Lock at t2 why(holds_at(locked(lab), t2)): initiated_by lock_door(lab) at t1, supporting happens and initiates facts explain("locked(lab)", "t2"): holds: true, reason: initiated, start lock_door(lab) @ t1
The derived can_enter(lab) (now(t2)) why: type: derived, no_derivation_found; derivation: []; why(\+ can_enter(lab)): no_derivation_found; evidence_chain(holds_at(door_open(lab), t2)): one child, initially(door_open(lab)), no cause (revision stage) no rule-level explanation; ask("why(can_enter(lab))") is [], complete; with unknown_fails=False it raises UnknownPredicateError
The derived can_enter(lab) (now(t0)) why and derivation: rule_derivation, rule text can_enter(R) :- now(T),holds_at(door_open(R),T)., bindings {'R$1': 'lab', 'T$2': 't0'} no equivalent
A rule head passed to the story-time explainer explain("can_enter(lab)", "t2"): holds: false, reason: never_initiated, complete; also with now(t0), when ask("can_enter(lab)") has one answer (revision stage): explain treats any term as a fluent
Method-level story-time explanation KBEngine.explain_holds_at("door_open(lab)", "t2") returns the string "Error parsing fluent: _parse_term usage detected in production code …" KnowledgeBase.explain, above

At the boundary tick both engines say door_open(lab) holds at t1 because it held initially; for locked(lab) at t1 the earlier engine says does_not_hold and the rewrite never_initiated. Neither says the lock takes effect after t1.

Which code produced what: the earlier answer came from HoldsAtHandler.handle (query_handlers.py:172) → Reasoner.holds_at → FluentEvaluator.holds_at. Its temporal why came from WhyHandler (kb/engine/query/provenance_handlers.py:89) → _explain_temporal (:169), which calls Reasoner.holds_at for the verdict and builds a positive reason from a separate scan of stored facts whose time test (:205) also accepts an initiation at the queried time; no divergence was observed in this story. Its derived why (_explain_derived, :226) called LegacyDerivedEvaluator.evaluate_with_special_cases directly and did not enter the router. In the rewrite, ask and KnowledgeBase.explain (engine.py:505) both reach calculus._explain (:548); _holds (:487) is _explain(...).holds is True.

Input-design check: effect facts with variables

Goal (now(t2)) Earlier engine Rewrite
holds_at(door_open(lab), t2) [{}] (true) no answers, complete
holds_at(locked(lab), T) [] T = t2, t3, complete
can_enter(lab) [{}] (true) no answers, complete

The earlier engine files effects under the event's text (FactStore.add_termination(event: str, fluent), kb/engine/storage.py:168) and finds an effect's time by looking that text up in event_time_map (get_event_time, :223; used in persistence.py:249-253). No happens/2 fact has the text lock_door(R), so the effect is skipped without an error. The rewrite solves effect goals with unification (_effects, calculus.py:204). The shared story therefore writes its effect facts ground.

Written as rules, terminates(lock_door(R), door_open(R)) :- room(R). and initiates(lock_door(R), locked(R)) :- room(R). with room(lab)., the same effects work in the earlier engine (revision stage): holds_at(door_open(lab), t2) → [], holds_at(locked(lab), T) → t2, t3. Its rule-effect path (WeakReasonerMixin.evaluate_causal_rules) unifies; only the fact path is keyed by text.

Single-engine probes cited on the page

Page claim Input and result
The earlier games run on a facade that ignores rules Story with can_enter(P, lab, T) :- holds_at(in(P, corridor), T), \+ holds_at(locked(lab_door), T). Legacy CyclopsStormKB.query("can_enter", "player", "lab", "t1") → []; KBEngine.query("can_enter(player, lab, t1)") → [{}]. The facade's holds_at("locked(lab_door)", "t1") → True (immediate effect); KBEngine → []. The CLI and games import CyclopsStormKB (kb/cli/interactive.py:20, kb/cli/query_engine.py:13, kb/rooms_game.py:22, kb/gameplay/__init__.py:20)
Answers depend on a predicate's name 12,000 facts n(1)…n(12000); human(X) :- n(X), X > 11990. → 10 answers, route LEGACY/default; the same body under person(X) → 0 answers, route DFS/allowlist; person(11995) → [], human(11995) → [{}]. Allowlist child, person, sibling (kb/routing.py:96); DFS budget MAX_BACKTRACKING_ITERATIONS = 10000 (kb/engine/constants.py:84)
Answers depend on the entry path holds_at(in(player, corridor), T) at the top level → t1, t2, t3; open_then(T) :- holds_at(in(player, corridor), T). then open_then(T) → []. In a rule body, holds_at with an unbound time and no cutoff returns no answers (Reasoner._evaluate_holds_at_literal, reasoner.py:1374)
Engines in one process share state KBEngine().reasoner.registry is KBEngine().reasoner.registry → True; adding human to one engine's allowlist is visible from the other; loggers and tracker shared
Snapshots cover the fact store only snapshot(), then add time(t9), before(t3, t9), an unlock event, vip(player) and can_enter(P, lab, T) :- vip(P)., then restore(): vip(player) gone, but 2 can_enter rules remain and the TemporalReasoner still knows t9 (FactStore.snapshot, kb/engine/storage.py:376)
Name capture length(rope, 5). then length(rope, N) → [], also through a rule; sort(crate, heavy). then sort(crate, W) → []
A caller's edit changes a later answer appending {"Who": "intruder"} to the list returned by can_enter(Who, lab, t1) made the next identical query return both rows
Rewrite: analysis conflicts stop every query an unstratified p :- \+ q. q :- p., or a cut in a recursive predicate, makes an unrelated r(X) raise SemanticConflict; load itself succeeds; with auto_table=False the cut example runs
Rewrite: mutation rebuilds derived state retract("now(t0)"), assert_("now(t2)."): revision 16 → 18; the next decide is deny, complete; kb.analysis and kb.timeline are new objects with the same content
Rewrite: spans_for doesn't report incompleteness fact_id(door_open(lab), f1)., supports(span(doc1, 0, 10), f1)., supports(span(doc2, 0, 5), f1) :- deep(5000). with a recursive deep/1 and auto_table=False: kb.spans_for("door_open(lab)") → ['span(doc1, 0, 10)'] with no status, while ask on the same goal reports exhausted (KnowledgeBase.spans_for, engine.py:580-608)

Revision-stage probes

Run on September 29, 2026 on fresh git archive exports of both pinned commits, with PYTHONDONTWRITEBYTECODE=1, python -B and PYTHONHASHSEED=0; each answers a reviewer's counterexample or a reader's question. Scripts and raw output: the JSON, key revision_stage_2026_09_29.

Page claim Input and result
Rewrite: a request leaves a revision-keyed clause index behind _index_revision of can_enter/1, happens/2, initially/1: −1 before the first ask("can_enter(lab)"), 16 after (program.revision 16); 0 live QueryContext objects (Predicate.matching, program.py:133-170)
Rewrite: value conversion loads with the package; JSON and game hooks on first use fresh process, after import cyclops: cyclops.api.values loaded, cyclops.api.json_api and cyclops.api.affordance not (engine.py:47; api/__init__.py:50-70)
Rewrite: diagnostics live on the knowledge base initiates(e, g, t9). with t9 unordered: ask("holds_at(g, t1)") complete with no notes; kb.diagnostics holds the warning "initiates(e, g, t9). acts at t9, which nothing orders …" (engine.py:624-636)
Rewrite: the timeline is read from before/2 answers, closed transitively with before(t0, t1). before(t1, t2).: ask("before(t0, t2)") complete with 0 answers; kb.timeline.precedes(t0, t2) true (timeline.py:92-98, :149-158)
Rewrite: a before/2 clause that raises is recorded, not fatal adding before(t3, X) :- X is foo + 1.: ask("true") complete and derived state published (revision 17); holds_at(door_open(lab), t1) 1 answer, exhausted, note "before(A, B) could not be read while building the time order (TypeCheckError …)"
Rewrite: preparation is charged to the request; an interrupted preparation isn't kept fresh knowledge base, ask("can_enter(lab)", limits=Limits(steps=1)): exhausted, 2 steps, note "timeline preparation stopped …", _derived.revision −1; retry with defaults: complete, 103 steps, _derived.revision 16
Rewrite: a clause for a built-in is refused KnowledgeBase().load("length(rope, 5).") raises PermissionErrorCS: "length/2 is a builtin and cannot be redefined by the knowledge base"
Earlier: a registered handler doesn't reach rules BaseQueryHandler registered as weight/2 (crate 12, rope 2): weight(crate, W) → [{'W': 12}]; weight(crate, 12) → [{}]; weight(crate, W), W > 10 → []; heavy(crate) with heavy(X) :- item(X), weight(X, W), W > 10. → []; light(rope) with light(X) :- weight(X, W), W < 5. → []; decide for a guard light(rope) → deny. The registry is consulted only by the pipeline's routing stage (engine/query/pipeline/stages/routing_stage.py:49) and one forall/2 special case (engine/evaluation/literal_evaluator.py:1164)
Earlier: authoring checks on by default add("can_enter(R) :- now(T), holds_at(door_open(R), X).") prints "WARNING: Variable 'X' appears only once - may be unintentional" and the same for T; add("p(a) :- q(b") raises KBSyntaxError "[SYNTAX_INVALID_QUERY] Unmatched parentheses …" with a suggestion list. The rewrite loads the first silently (no diagnostics) and reports the second as SyntaxErrorInSource: expected ')' (found 'end of input') at <string>:1:12; its docs/LIMITATIONS.md lists an authoring analyser as deferred

Review-round-2 probes

Run on September 29, 2026 on new git archive exports of both pinned commits, with PYTHONDONTWRITEBYTECODE=1 and python -B, under PYTHONHASHSEED 0, 1 and 2; each engine's output was identical across the seeds. Each checks a counterexample from the second accuracy review. Script and raw output: the JSON, key review_round_2_2026_09_29.

Page claim Input and result
Both: an undefined or unsupported predicate fails, and a guard on it refuses Lab story, now(t0). and guard(enter_lab, 2, catch(can_enter(lab), _, true), 'Guard two failed.').: can_enter(lab) has one answer in both engines; catch(...) has none (rewrite: complete, no notes); decide is deny, "Guard two failed." (rewrite: complete). With the guard can_entre(lab), a typo: deny in both, complete in the rewrite. Rewrite with unknown_fails=False: both are indeterminate, status None, note "UnknownPredicateError: unknown predicate: catch/3" (or can_entre/1)
Earlier: the re-entry guard answers "false" silently Without the ordinary lock: the time points, initially(door_open(lab))., happens(auto_lock(lab), t1). and terminates(auto_lock(lab), door_open(lab)) :- holds_at(door_open(lab), t3). No answer is consistent. Earlier engine, asked t3, t2, t1 and t1, t2, t3: holds_at(door_open(lab), T) is [{}] (yes) at t1, t2 and t3, nothing printed. Rewrite: no answers, exhausted, at all three, note "holds_at(door_open(lab), t3) is defined in terms of itself at the same time point; …"
Earlier timeline reads facts only; the rewrite honors rules for before/2 and happens/2 Times dawn, noon, dusk, night; next/2 facts; before(X, Y) :- next(X, Y).; the lock at noon. Earlier: door open at noon and dusk, locked(lab) never, before(noon, dusk) → []. Rewrite: open at noon, closed at dusk, locked at dusk and night, before(noon, dusk) true, all complete. With lock_time(t1). and happens(lock_door(lab), T) :- lock_time(T).: both answer happens(lock_door(lab), T) with t1; earlier: door open at t2, never locked; rewrite: closed at t2, locked at t2 and t3, complete
Rewrite: effects written as rules work room(lab)., terminates(lock_door(R), door_open(R)) :- room(R)., initiates(lock_door(R), locked(R)) :- room(R)., now(t2).: holds_at(door_open(lab), t2) no answers, locked(lab) at t2 and t3, can_enter(lab) no answers, all complete; the earlier engine gives the same
Both: tabling stops recursion over a cycle edge(a, b). edge(b, c). edge(c, a). with left-recursive and with right-recursive path/2: path(a, X) gives a, b, c and path(a, a) is true in both engines (rewrite: complete)
Rewrite: some authoring warnings are recorded, not printed Lab story plus :- initialization(main).: loads; ask("true") complete with no notes; kb.diagnostics holds "[warning] :17: ignored unsupported directive: initialization(main)"

Review-round-3 probes

Run on September 29, 2026 on a new git archive export of the rewrite at 5d598de, with PYTHONDONTWRITEBYTECODE=1 and python -B, under PYTHONHASHSEED 0, 1 and 2; the output was identical across the seeds. Each checks a claim from the third accuracy review. Scripts and raw output: the JSON, key review_round_3_2026_09_29.

Page claim Input and result
Rewrite: atoms a request creates outlive it terms._ATOM_CACHE is a module-level dictionary (terms.py:165), so every knowledge base in the process shares it by construction, and nothing in the package removes an entry (its only uses are terms.py:170-173). After loading p(a). and ask("true"): 5 entries. After ask("X = zz_new_atom_1"): 6. After ask("atom_concat(zz_made_, at_runtime, A)") (complete, A = zz_made_at_runtime): 9, still 9 after a second KnowledgeBase is created
Rewrite: the difference case at 51, 55 and 60 segments Lab story, now(t0). and the tunnel (probe inputs), each length on a cold knowledge base and on one warmed by ask("true"): decide with Limits(steps=200) is indeterminate, exhausted; with cancel=lambda: True, indeterminate, cancelled; at default limits, allow, complete

Change-cost measurement

The lab story plus now(t0) and N unrelated facts item(1) … item(N), loaded into a fresh engine and queried once. Then, 25 times: add one unrelated fact seen(k) (kb.assert_ in the rewrite, KBEngine.add in the earlier engine) and ask can_enter(lab), timed together; then ask again, timed alone. Medians in milliseconds, CPython 3.12.10 on Windows 11, one machine, September 29, 2026; the three rows per cell are hash seeds 0, 1 and 2.

N Rewrite: change, then query Rewrite: repeat Earlier: change, then query Earlier: repeat
0 0.26, 0.25, 0.25 0.071, 0.071, 0.070 0.35, 0.36, 0.35 0.011, 0.011, 0.011
1,000 0.53, 0.65, 0.54 0.071, 0.086, 0.072 0.35, 0.35, 0.33 0.011, 0.011, 0.011
10,000 3.37, 3.05, 3.45 0.087, 0.073, 0.087 0.36, 0.36, 0.35 0.011, 0.011, 0.011

The rewrite's cost after a change grows with the program because _refresh re-analyzes all of it and rebuilds the timeline; the earlier engine clears its caches but rebuilds nothing. These are indicative only: small programs, facts only, one machine.

Source map

Paths are relative to src/kb/ for the earlier engine and src/cyclops/ for the rewrite unless shown in full; line numbers are at the pinned commits.

Page section Earlier engine (e62758c) Rewrite (5d598de)
Scope Applications cli/, gameplay/, rooms_game.py, rooms_loader.py, rooms_cli_integration.py, app.py; ontology_validator.py; legacy facade core.py:41 CyclopsStormKB (query :218, holds_at :323); instrumentation engine/metrics/, engine/structured_logging/, engine/diagnostics/; query_decomposer/ has no importer; src/tools/ is excluded from the package Kernel: 17 modules; host boundary api/values.py (imported by the facade, engine.py:47, the one permitted kernel-to-api edge), and api/json_api.py, api/affordance.py, loaded on first use (api/__init__.py:50-70). Import layering: tests/architecture/test_boundaries.py:126-172 (LAYERS, test_dependencies_only_point_downwards); test_nothing_imports_the_predecessors_package (:87); differential runner tests/differential/runner.py
Terms and storage Reader output core_types.py; engine terms kb_core/model.py (Var :24, Term :166, walk :723); conversion engine/kb_engine.py:819; unify/unifier.py:79 Unifier.unify, :468 clean_substitution; backtracking.py:73 Trail, :102 unify_inplace; engine/storage.py:22 FactStore (static_by_functor :38, add_happens :125); engine/facts/processor.py:23 routes facts by functor; engine/compilation/rule_compiler.py:14 rules_by_functor terms.py:75-158 (variable identity :112-131); bindings.py:21-93 Bindings, :105 unify, :156 rename; program.py:112-170 Predicate and first-argument index, :185 Program
Running a query engine/query/pipeline/pipeline.py:52-70; engine/query/registry.py:39 register, :108 get_handler_or_default; registrations engine/query/handler_setup.py:47-95; routing.py:216 get_default_registry, :453 RoutingFSM.decide; engine/routing/engine_router.py:99; backtracking.py:262 DFSEngine; tabling engine/tabling/tabling_coordinator.py:30, tables cleared per query engine/query/pipeline/stages/parsing_stage.py:62; recursive predicates registered on rule add engine/reasoning/reasoner.py:538; engine/kb_engine.py:789 last_route solve/machine.py:42 solve, :123-126 built-in lookup, :257 _user_predicate, :275 resolve_clauses; engine.py:60 BUILTINS; solve/analysis.py:114 analyse (no step callback: unmetered), conflicts :152-163 and :188-200; solve/tabling.py:99 solve_tabled, :155 _run_fixpoint
State engine/query/pipeline/query_context.py:15 (pipeline QueryContext); engine/kb_engine.py:257 thread-local query slots; engine/caching/epoch_manager.py:27 EpochCacheManager; engine/performance.py:134 PerformanceManager (TTL :144); engine/query_cache.py:52 QueryCache; engine/reasoning/temporal/fluent_evaluator.py:78 _holds_cache; cache cleared on add engine/facts/manager.py:190; cache hit returns the stored list engine/query/pipeline/stages/cache_lookup_stage.py:37; uid counter kb_core/model.py:20; flags engine/flags.py solve/context.py:65-136 QueryContext; engine.py:167-185 DerivedState, :668 _refresh, :682 revision check, :694-705 publish only if complete; program.py:133-170 index keyed by revision; process-wide terms.py:109, :165; recursion limit raised per call engine.py:376-382
Story time engine/reasoning/temporal/fluent_evaluator.py:96 holds_at, :133-137 re-entry returns false, :164 _compute_holds_at, :389-418 event-first rule effects; weakref_mixin.py:50 WeakReasonerMixin, :193 evaluate_causal_rules; persistence.py:243-253; temporal_reasoner.py:58 lt_time, :160 numeric names; exclusivity.py:212 seven exclusive names; exclusive_fluent/1 declarations engine/facts/processor.py:105; tie-break policy engine/kb_engine.py:80; other holds_at paths engine/query/query_handlers.py:158, engine/reasoning/reasoner.py:1181 and :1374, engine/evaluation/literal_evaluator.py:253, core.py:323 temporal/calculus.py:671 holds_at, :487 _holds, :548 _explain, :554-564 cycle check, :204 and :246-253 _effects, :469 _clipped, :747 TEMPORAL_BUILTINS; engine.py:815 _build_timeline; temporal/timeline.py:92 precedes, :120 build_timeline, :306 prepare_effect_times (enlarged per-request copy); exclusivity calculus.py:39-58, :85 LEGACY_EXCLUSIVE_FLUENTS (off by default), :167 exclusive_fluent/1; Options engine.py:82-108 (occurs_check, tie_break, exclusive_fluents, unknown_fails, auto_table)
Limits engine/constants.py:53 MAX_TRANSITIVE_DEPTH = 50, :84 MAX_BACKTRACKING_ITERATIONS = 10000; engine/derived_evaluation/legacy_evaluator.py:63-91 _check_recursion; template.py:91-93; engine/tabling/tabling_coordinator.py:27 pass limit; engine/kb_engine.py:748 query, :776-786 errors relabeled as parse errors; integration/affordance.py:153-165 _holds solve/context.py:28-45 Limits, :155-179 step, cancel (every 512 steps) and depth checks; errors.py:42 Completion; engine.py:430-462 status mapping, :476-486 query raises; api/affordance.py:125-146 _ask, :200-209 indeterminate
Host, explanation, extension integration/json_api.py:125 query_json; integration/affordance.py:168 decide, :201 affordances; engine/query/provenance_handlers.py:27, :77, :253; engine/debugging/explanation.py:43 explain_holds_at; codec.py:248 guard; engine/storage.py:551 spans_for_term; engine/indexing/canonical.py:141 api/json_api.py:156 answers_to_json; api/affordance.py:165 decide, :213 affordances; engine.py:246-278 assert_, retract, :280-335 snapshot, restore, :505 explain, :580-608 spans_for; solve/builtins.py:60-71 @builtin

Measurements, current and historical

Current values were measured on September 28, 2026 with the method of measure-migration.py (physical lines from pinned Git snapshots). Historical values are as the other pages recorded them.

Measurement Current Historical
Rewrite package 25 files, 7,904 lines at 5d598de: kernel 7,096 (17 modules, including the engine.py facade and errors.py), host boundary 592, __init__ files 216 25 files, 7,426 lines at 9be7804 (Migration, How it works)
Earlier package src/kb/ 187 files, 54,387 lines at e62758c (unchanged) same
Earlier src/ including src/tools/ 198 files, 61,715 lines same
Rewrite tests/ 52 files, 16,345 lines 38 files, 13,137 lines at 9be7804
Rewrite suites on an isolated export: 1,790 passed and 5 skipped (encoder tests that need the earlier engine importable), 45 deselected; with the earlier engine's export available, those 5 pass, 37 of 37 differential tests pass, and 8 benchmarks pass 1,795 correctness cases, 37 differential, 8 benchmarks recorded at 828c0fe
Earlier suite on an isolated export of src, tests, models, scripts and pyproject.toml, -m "not benchmark": 6,647 passed, 27 skipped, 65 deselected (benchmarks), 4 xfailed, 5 failed, 15 errors; all 20 failures and errors are ModuleNotFoundError: No module named 'hypothesis', which wasn't installed (September 29, 2026) 6,660 passed (author's log, by August 30, 2026)
Repeated-query speed lab story: 0.011 ms earlier vs about 0.07 ms rewrite (change-cost measurement) evidence overview: repeated query 0.004 ms earlier vs 0.44 ms rewrite; first query 6.3 ms vs 2.4 ms (September 2026, earlier revisions)

Line counts measure scope, not quality or simplicity. The earlier src/kb/ includes applications and instrumentation that the rewrite doesn't have.

Documentation checked against code

Review record

This is AI-simulated review, not testing with real users. Independent AI agents played readers and adversarial reviewers in three rounds: round 1 on the first, longer draft; round 2 on the page after it was cut to a shorter read; round 3 on the second cut. In every round the readers got the rendered page with no further explanation and answered five questions: what the projects share, the two most important architectural differences, where a request's mutable state lives, what happens when evaluation can't finish, and what would affect their choice. The reviewers were asked to find unsupported conclusions, unfair comparisons, omitted strengths of the earlier engine, misleading diagrams and counterexamples, and they ran their own isolated probes. Approval was not treated as evidence: each finding below was checked, then fixed or its uncertainty stated. Round 3 had one reader and one accuracy reviewer (below).

Second cut: toward a five-minute page

What changed. After round 2 the page was cut again on September 29, 2026, aiming at a five-minute read: at most 1,300 words of visible prose and 1,800 visible words in all (prose, tables, and figure questions and captions; the collapsed text equivalents and the code block aren't counted). Counted by a script over the rendered HTML, prose fell from 2,012 to 1,267 words, tables from 407 to 265, and figure questions and captions from 247 to 176, so the visible total fell from 2,666 to 1,708. The code block has 43 words and the collapsed text equivalents 706. Each comparison is now one table row (what is shared, what differs, and one symbol per engine to look up), followed by one or two sentences of consequence under its own heading. The traced example keeps the ordinary success and one difference case, the tunnel. Below 600 px the comparison table shows each row as a block of labelled lines instead of scrolling sideways.

Where earlier changes went. The Change and Now columns below describe the page after round 2. These left the page's visible text for the detail unless another place is named: the examples of Figure 1's legend (its text equivalent); the Options examples; decide's list of queries and the per-query budgets; the entry-path probe and last_route; the three-argument effect form (detail) and the two-argument cause of the open circular question (probe results, when evaluation can't finish); the tunnel's 50-segment row; explain on a rule head; which engine's snapshots cover what; the warnings the rewrite records on kb.diagnostics; that loading succeeds before a SemanticConflict; the legacy facade's immediate effects; what hash seeds change (method, added in round 3); and the concurrency note. Round 3 put some back on the page in short form: explain on a rule head, the per-query limits (in the table, with "a decide makes several"), the concurrency note, and the earlier engine's snapshot restore. The qualifications behind round 2's two major accuracy findings stay on the page: unknown predicates quietly fail in both engines, and the earlier engine's re-entry guard answers false without saying so.

Reviewed in round 3. One simulated reader and one accuracy reviewer read this cut; their findings and the corrections are below.

Round 3: the second cut

AI-simulated review, not testing with real users.

What was reviewed. The second cut above, before this round's corrections: 1,267 words of visible prose and 1,708 visible words in all. One AI-simulated reader got the rendered page with no further explanation and answered the five questions. A technical-accuracy reviewer compared the cut with the committed page and with the code at both pinned commits, rebuilt a copy of the site, checked links, anchors and the phone-width table, and ran new probes; both engine repositories stayed clean. There was no second or third reader, and no fairness or visual reviewer, in this round. The accuracy report first arrived cut off partway through its eleventh finding. The full report, recovered afterwards, has thirteen findings, two of them (6 and 13) older than the cut.

Reader What they read Minutes
One reader; its choice is framed for a turn-based game The page, relying on the text equivalents for both figures 17

Answers. All five in substance as the page intends. Shared: in-process Python libraries for Prolog-style questions with Event Calculus story time, immutable terms and a binding trail, goal-by-goal search with automatic tabling, the same game hooks and JSON, fail-closed hooks, provenance from fact_id/2 and supports/2, and the same answers on the lab-door story under three hash seeds. Two differences: one path instead of many, with the human/1 and person/1 probe; and request-owned state, with its cost, a reanalysis and timeline rebuild after every change, even advancing the clock. Mutable state: each lifetime in both engines, from the rewrite's per-call QueryContext, revision-keyed DerivedState and clause index to the earlier engine's result cache (next add or 300 seconds), answer tables, derived-result and holds_at caches and process-wide routing. Can't finish: fixed limits that return a plain partial list, against budgets, cancellation and three statuses, with what ask, query, decide and spans_for each do; the tunnel's false refusal; SemanticConflict; the open circular question; and a complete "no" for an undefined predicate. Choice: the rewrite, for answers that don't depend on names or process-wide state, an honest "didn't finish", assert and retract, and isolation between instances; weighing indeterminate in game logic, the rebuild on every assert and clock tick (0.25 ms, about 3.4 ms with 10,000 more facts), no rule-level "why", slower repeats (0.07 against 0.011 ms), no authoring warnings and no bundled applications.

Where answers differed from the page's intent. The reader's list of the rewrite's process-wide state had no atom table, because the page left it out (accuracy finding 6). The reader also took SemanticConflict to break the knowledge base for good, because the cut had dropped "until the program changes" (finding 3).

Confusion Change
The earlier engine is add-only, yet the story sets a clock: how does an earlier game move it? The page says the earlier engine can't retract a fact, only restore a snapshot (KBEngine.restore replaces the fact store and clears the caches). How its games keep a clock wasn't examined, and moving the clock by restoring a snapshot wasn't probed
Why "didn't finish" is "a separate choice" and not a third difference Now "a third difference, one either design could have made": a result status belongs to the API and doesn't follow from Difference 1 or 2
"indeterminate (not enabled)": is the action refused, or is a feature off? Defined where decide is introduced: indeterminate "leaves the action disabled"
SemanticConflict: unrelated queries too, and is there any way back? "Every later query, related or not, … until the program changes", with negation through recursion as the other trigger (finding 3)
The circular rule and the tunnel can't be pictured without their code The self-referential rule is quoted on the page, and the tunnel's guard is named, reachable(corridor, lab) over passage/2 facts; the full inputs stay under probe inputs
The earlier games bypass the compared reasoner: how much does the comparison matter to them? "Its games bypass the main reasoner compared here": the page compares KBEngine with KnowledgeBase, and the legacy facade only as scope
Two things called "legacy", and a QueryContext class in each engine No change on the page, for space; the detail names both legacy things, and mutable state both classes
"Apart from variable names" "Generated names of unbound variables aside" (finding 9)
Whether the earlier engine's 0.35 ms is for the small or the large program "0.35 ms on the earlier engine at every size", in the same sentence as the rewrite's figures (finding 10)
Threads are never discussed, though the rewrite raises a process-wide recursion limit "Neither is built for threads"; the table's state row gives the earlier engine's thread-local state and locks and the rewrite's lack of either (finding 5)
Which preparation counts against the budget, and why a stopped preparation is simply redone Figure 2's text equivalent: the request's budget doesn't meter the analysis but does meter the timeline build; a stopped preparation may be incomplete, so it isn't kept
Exclusive fluents, the tie-break option and "main APIs" unexplained; tabling, SLD resolution and cut only as glossary links The table glosses exclusive fluents and the tie-break; "both main APIs" is now "both KBEngine and KnowledgeBase". The glossary links stay, for space
The figures' legends rely on visual encodings, and Figure 2's text equivalent doesn't set out its six lifetimes The text equivalent now lists each engine's lifetimes from shortest to longest
The intro says How it works and Migration predate seven changes: which page to trust? "Where they differ, this page is current"
Project context is assumed; which game would adopt the rewrite? No change: outside this page's scope (History)

Technical accuracy. The reviewer confirmed that the rebuilt copy matched the working tree, that the link check found no problems over 22 pages, and that all 16 links into this record, its 10 links back and the new glossary anchors resolve. It also confirmed the rewrite's first commit date, that the earlier games, CLI and app.py import only CyclopsStormKB, the layering test and that the solver never imports story time, every symbol in the comparison table, the difference case at 51, 55 and 60 segments, the timing figures, this record's word counts for the cut, and the phone-width table at 360, 390 and 599 px. Its findings, as received:

Finding Resolution
1. Explanations kept an unobserved risk for the earlier engine and dropped an observed defect of the rewrite Both on the page: the earlier why "could disagree with the answer (none seen)"; on the rule head can_enter(lab) the rewrite's explain said "never initiated", complete, even when the rule succeeded
2. The host row's "Frozen results with a status" also covered query and spans_for, which carry no status "ask returns frozen results with a status"
3. "Every later query raises SemanticConflict" read as permanent and independent of defaults, and dropped negation "By default … or negation through recursion … every later query, related or not, … until the program changes"
4. The earlier handler's reach was overstated: a top-level conjunction also failed "Answered a single goal typed at the top level, not a conjunction or rule"
5. Rewrite costs that round 2 had put on the page were gone: per-query budgets, cooperative cancellation, the concurrency note The table's can't-finish row gives limits per query ("a decide makes several") and cancellation polled every 512 steps; the state row and section say neither engine is built for threads, with the earlier engine's thread-local state and locks. Difference 2 names the process-wide recursion limit
6. The rewrite's process-wide state left out the interned-atom table (older than the cut) Re-probed (round-3 probes); added to Difference 2, Figure 2 (drawing, caption and text equivalent) and the detail
7. Observed and code-read facts sat under What it means, and the difference case had no marking Each moved under Observed or In the code; the difference case is marked Observed. The undefined-predicate result is now carried by the findings and the table's shared column, with its probe in the detail
8. "A re-entry guard silently answers false" lost the example that showed its effect "Treats the inner question as false: a paradoxical variant got yes at every time"
9. "Apart from variable names" changed the meaning "Generated names of unbound variables aside"
10. The change-cost comparison lost "same measure" and "at every size" Both engines' after-change figures in one sentence, the earlier one "at every size"
11. "Where earlier changes went" named content the detail doesn't have: what hash seeds change, and the two-argument cause Method says what the seeds change; the list now says where each item lives and which ones round 3 put back on the page
12. The record called the second cut a five-minute page and read, though its own figures give 5.5 to 6.5 minutes for the prose alone This record only; the page makes no reading-time claim. The cut is now described as "aiming at a five-minute read", its headings as "toward a five-minute page" and "the second cut", and remaining uncertainty gives the reading rate five minutes would need
13. "Caching made the earlier engine's repeats about 6 times faster" compared the two engines, not the earlier engine with and without its cache This record only: the detail now says the earlier engine's cached repeats were about 6 times faster than the rewrite's (0.011 against 0.07 ms), and about 100 times faster in an earlier measurement. No measurement of the earlier engine without its cache exists
While 12 and 13 were missing Older page claims were re-checked in the code: both engines' terms are immutable (the earlier Var and Term are frozen dataclasses, and unify_inplace binds variables in a substitution, never in a term); the rewrite's JSON answers carry the status (answers_to_json); holds() raises when it gives up with nothing found; the rewrite has no thread-local state or locks, and the earlier engine has both. Nothing further was found, which is not the same as a complete review

What changed since round 2.

Round 2: the shortened page

What was reviewed. The page as cut on September 29, 2026, about 2,270 visible words (prose, tables and figure captions) against about 3,500 in the round-1 draft, and its evidence record. Received in full: all three readers' answers and confusions, and a technical-accuracy review that compared the cut page with the committed one and ran new probes. There was no fairness or visual reviewer in this round; the rendering checks after the rebuild were the reviser's own.

Reader What they read Minutes
N: technically curious non-specialist (Python and web; no Prolog or Event Calculus) The whole page, both figures, text equivalents and tables; one link, to the glossary 25
G: game developer choosing an engine to embed (basic Prolog) The page, both figures and text equivalents; no links 18
E: software engineer maintaining or extending it (Python and Prolog) The page and both text equivalents; no links 18

Answers. All three answered the five questions in substance as the page intends. Shared: in-process Python libraries with the same term model, goal-by-goal search with automatic tabling, Event Calculus story time with delayed effects, fail-closed game hooks, provenance from fact_id/2 and supports/2, and the same game hooks; the same answers on the lab-door story. Two differences: one path instead of many (with the human/1 and person/1 probe and the entry-path probe), and a request's state belonging to the request. Mutable state: each gave both engines' lifetimes accurately, from the rewrite's discarded QueryContext, revision-keyed DerivedState and clause index to the earlier engine's result cache (next add or 300 seconds), holds_at cache and process-wide routing. Can't finish: fixed limits and a silent partial list against per-request Limits, cancel and three statuses, with what ask, query, decide and spans_for each do, the tunnel's false refusal, SemanticConflict and the circular rule left open. Choice: all three leaned to the rewrite for honest statuses and answers that don't depend on names, weighing the rebuild after every change (including advancing now/1), slower repeats, no rule-level explanation, closed extension and handling indeterminate; the engineer would first measure the rebuild at their real program size and clock rate.

Where answers or readings differed from the page's intent. N read "so only the rewrite can say 'didn't finish'" as a consequence of Difference 2; it is a separate design choice. N and G first took Difference 2 to mean the rewrite has no process-wide state. N and G couldn't tell which engine snapshots. No reader said that a complete "no" can come from an undefined predicate, because the cut page no longer said so (accuracy finding 1 below).

Confusion (readers) Change
9be7804 read as a commit of the earlier engine; whether the linked pages are stale (N, G, E) The intro calls it an earlier commit of the rewrite, from before seven behavior changes, and links the list
Repositories, packages and classes: which is which (N, G, E) The intro says both are versions of Cyclops Storm in separate repositories and names each package and class. How to install either isn't covered: the code is private
"So only the rewrite can say…" seemed to follow from Difference 2 (N) The finding now opens "A separate choice", without "So"
Difference 2 versus the rewrite's process-wide variable counter and recursion limit; whether raising the limit is safe with threads (N, G, E) Difference 2 names both; the state section says the limit is process-wide and restores what each engine has for concurrency
The earlier engine also clears answer tables per query; the 300-second expiry is never discussed (N) No change: Figure 2's text equivalent gives every lifetime, including the 300 seconds, which only limits how long an unchanged result is reused
Figure 2's empty results: which clock, and do they mean no? What does a yes look like? (N, G) The caption says the trace ran under now(t2), so both answers are no; the text equivalent shows a yes in each engine
"All eight goals", five rows shown (N, G, E) The page says the table shows three of the eight goals and links them
The 1 in guard/4; "the last three facts" when now(t2) is last; decide's outcomes looked exhaustive (N, E) The 1 is the guard's order; the hooks are named; "The rewrite adds indeterminate"
Why now(t2) is loaded separately; how an add-only engine changes its clock (N) "Each clock value went into a fresh engine, because the earlier engine can't retract facts"
Undefined terms: facade, revision, timeline, options, whole-program analysis (N, G, E) Facade: the original API class, and the rewrite's KnowledgeBase; revision and timeline defined in Figure 2's text equivalent; Options with two examples (full list in the detail); analysis "picks what to table and checks cuts and negation", unmetered by the budget
Why "imports pointing downward" matters (N) "Stops lower modules importing higher ones, so the solver doesn't depend on story time"
Event-first versus rule-first; rules that define "the order" of what (N, G, E) Each glossed in the table; "rules that define before/2 or happens/2", with the earlier engine's side now probed
"Depending on how an effect is written" with no example or authoring advice (G, E) The three-argument form and its result are on the page. General authoring advice isn't established by these probes
The story-index caveat: its cost, and whether the rewrite shares the variable-effect pitfall (N, G, E) "Its cost is speed, not measured here"; the rule form works in the rewrite too (round-2 probes)
The routing sentence took two reads; why holds_at failed inside a rule; where the route is reported (N, E, G) Rewritten one route at a time; a rule body "reaches a different holds_at that needs a bound time"; the route is recorded in last_route
"Neither engine explained the failed rule" next to explain naming the lock; what "separate code" implies (N, E) Split into the rule and the fluent inside it; "so explanation and answer could disagree; the rewrite's can't"
explain on a rule head: pitfall or intended? (N, G) Called misleading: explain treats any term as a fluent and says "never initiated" even when the rule succeeds
Which engine snapshots (N, G) "Its snapshots cover the whole program (the earlier engine's, the fact store only)"
"Warns about authoring slips, which the rewrite defers" versus Figure 1's diagnostics (N, G) The rewrite defers the analyzer and records only timeline and directive warnings on kb.diagnostics (accuracy finding 8)
SemanticConflict: unrelated queries too? When is it detected? Why under "can't finish"? (N, G, E) "Every later query, related or not"; loading succeeds; introduced as the rewrite rejecting some programs; decide returns indeterminate. Whether diagnostics show it before a query wasn't probed
spans_for without a status: bug or intent? (N, E) "So partial provenance looks complete". The rewrite's documentation doesn't mention it (documentation checks), so intent is unknown
The tunnel row merging 10 and 50 segments; the warning at 50 (N) Split; the warning comes from a search past the lab
Whether the earlier engine's 0.35 ms is also measured after a change (N, G, E) "Same measure"
Every change, including the clock, pays the rebuild; game-computed values must be asserted (G, E) Joined: "each assert is a change that pays the rebuild above". A cheaper way to keep a clock isn't established
"Tabling … so recursion finishes" versus the depth cap; which limit ended the tunnel (G) The gloss is narrowed (accuracy finding 6); the page names the Legacy evaluator's depth cap
decide's worst-case budget; whether indeterminate blocks (G, E) "indeterminate, not enabled"; decide's queries are listed under Figure 2 and budgets apply to each
ask versus query; the same name query behaves differently in each engine (G, E) The host row names ask's status; the can't-finish section says the earlier query returns the partial list
A small host budget and timeline preparation (E) Figure 2's text equivalent: an interrupted preparation isn't kept, "so the next request prepares again"
The rewrite's breaking point in the tunnel; whether notes are structured (E) Not on the page. At 60 segments it used 809 steps and depth 122 (when evaluation can't finish); longer tunnels weren't probed. Notes are text (Solutions.notes: tuple[str, ...], engine.py:145)
Only the earlier engine's test results are given (N) Both suites' results
The code-size remark without sizes (N, G) Sizes moved to "What this comparison can't tell you", with the caveat
Figure 1's legend words Combined and Elsewhere (G) The caption gives an example of each; the drawn legend is unchanged
Glossary links land at the top of the glossary (N, E) Every glossary entry has an anchor, and the page links to each term
Whose "my interpretation" (N) "Interpretation"

Technical accuracy. The reviewer compared the cut page, both figures' captions, alt text, text equivalents and SVG text with the code at both commits, the committed page and this record, and ran new probes on fresh exports; both engine repositories stayed clean. It confirmed the revisions and dates, the seven intervening commits, the Limits defaults and the 512-step cancel poll, the three statuses, query raising and decide returning indeterminate, spans_for as the only result API without a status, both SemanticConflict triggers, what uses the legacy facade, the earlier engine being add-only apart from snapshots, the fail-closed _holds, the depth cap printing through print(), the re-entry guard, the change-cost figures, all eight goals matching under three seeds and a clean link check. Its findings, each re-probed where it made a behavioral claim (round-2 probes):

Finding Resolution
Major: the cut removed "unknown predicates quietly fail", so a complete "no" from the rewrite read as trustworthy Restored: the can't-finish row and section say an undefined or unsupported predicate fails in both engines and the rewrite marks the refusal complete unless unknown_fails=False; the findings say neither flags one by default
Major: the cut removed the earlier engine's silent "false" cycle guard, tilting "the rewrite's caution has a cost" Restored in the story-time section, with the paradox probe: the earlier engine said yes at every time, silently; the rewrite reported exhausted
Minor: only the rewrite's half of name capture was left Both halves on the page
Minor: inherited exclusivity and tie-break, and the earlier engine's concurrency work, were dropped The story-time row lists exclusive fluents and a tie-break option as shared; the state section restores the partial start
Minor: "only the rewrite can say 'didn't finish'" broadened the claim "Only the rewrite's results can say"; the finding notes the warning on standard output
Minor: "so recursion finishes" clashed with the page's own "finish" "So recursion over a cycle doesn't loop"; both engines terminate on cyclic left and right recursion
Minor: "depending on how an effect is written" had no support on the page The three-argument form and its result are given
Minor: "which the rewrite defers" over-generalized The rewrite defers the analyzer and records timeline and directive warnings on kb.diagnostics
Minor: the SemanticConflict sentence implied decide raises "decide returns indeterminate with the error in its notes"
Minor: round 1's resolution column described page content the cut had moved A Now column in the round-1 tables below
Minor: the legacy facade "matches stored facts only"; "both engines use the delayed-effect reading" The facade's own story-time check, with immediate effects, is named; "both engines' main APIs"
Minor: two page claims had evidence for one side only Both sides probed and recorded: rules for before/2 and happens/2 in the earlier engine, and effects written as rules in the rewrite
Minor: hash seeds don't change dictionary order; one path is relative to the repository, not the package "Which change set order"; the page marks the test's path as the repository's

What changed since round 1.

Round 1: the first draft

What reached the revision. The reports came through an automated hand-off that cut part of them off. Received in full: the non-specialist reader, the technical review of the rewrite's claims, and the fairness review's summary and first four findings (the fifth in part). Received in part: the game developer's answers, without their list of confusions. Not received: the software-engineer reader and the visual review's written findings. The visual reviewer's scripts and screenshots were available, so its checks were re-run. Missing parts are recorded here as not received, not as clean.

Now says where each change stands after the cut and round 2: page; detail, only in this record; restored, dropped by the cut and back on the page in round 2; or revised, changed again in round 2.

Technically curious non-specialist (a Python programmer with no Prolog or Event Calculus; 18 minutes, including three linked pages). Answered all five questions correctly in substance: the shared jobs and execution model; both differences (one path, and engine-wide caches versus a discarded per-request object); where state lives in each engine; statuses, budgets, cancellation and the false refusal; and a reasoned choice, the rewrite if a false "no" is worse than an honest "don't know", budgeting for explanations and extension. Noted that choosing "the two most important" differences was their own judgment.

Confusion Change Now
Figure 1's caption called the row order the rewrite's import layering, but the rows don't follow it Caption says row order is for reading, not dependency; the tested layering and the runtime recursion are described under the figure page
What ask() returns when it runs out, versus query() Stated: ask returns the answers found so far with the status; query raises page
Status words didn't match the glossary, which named two and said "in this engine" Glossary entry rewritten for the rewrite (complete, exhausted, cancelled; indeterminate for decisions); tables say "indeterminate (status exhausted)" page and glossary
Why the rewrite's example used once(...) Figure 2 shows the traced ask("can_enter(lab)") page
"Delayed-effect reading" undefined Defined under the story page
Whether the earlier engine's correct circular answer was by design; what "answerable" meant The one consistent answer is spelled out; the earlier engine never needed the rule because stored facts settle each question first, and its guard would have answered "false" restored, with the paradox probe
Two different "legacy" things Both named as the earlier engine's own words: its original rule evaluator and its original API page
Which "options" answers depend on Options listed revised: two examples on the page, the list in detail
How an add-only engine advances the clock Each clock value went into a fresh engine, because the earlier engine can't retract restored
No timing for the rebuild after every change New change-cost measurement page
What happens when preparation runs out of budget Preparation is charged to the request; an interrupted one isn't kept and is redone page (Figure 2's text equivalent)
SemanticConflict missing from "can't finish"; what decide does with it Added, with decide's indeterminate result restored
"Looks like no" versus the warnings on standard output "A warning may be printed to standard output, but nothing reaches the caller" revised: the findings mention the warning
Eight goals probed, five shown Points to the evidence record restored: the page links the eight goals
Why hash seeds matter They change set and dictionary order revised: "set order"; dictionary order doesn't depend on the seed
The consequence of handing out the cache's own list Stated (an appending caller changed the next answer) and labeled a missing copy detail
"Its smaller size": whose? "The rewrite's smaller code base" revised: sizes under "What this comparison can't tell you"
Which revision the linked pages describe The intro says they use 9be7804 and links the later commits revised: "an earlier commit of the rewrite"
Legend swatches gray while the chips are colored; "Elsewhere" versus "somewhere unexpected" Legend redrawn in both engine colors, with the chips' own words page (Figure 1)
"abstain" unexplained Defined at first use page
Budgets per decide query Spelled out with the tunnel's four queries detail; the page lists decide's queries

Game developer embedding an engine (Python, basic Prolog; read the page, both figures and five linked pages). Answered correctly: shared ground, both differences, state lifetimes including the aliasing evidence, can't-finish behavior including the indeterminate message. Would pick the rewrite for predictability, weighing slower repeats, the per-change rebuild ("could be the dominant cost, but it isn't measured"), no rule-level explanation, closed extension, a global SemanticConflict, and handling indeterminate in the UI. Their list of confusions wasn't received. From the answers, two things changed: the findings now label Difference 1 and Difference 2, because the reader said several findings could pass for the second difference; and the change cost is now measured. Both are on the page.

Software engineer maintaining or extending it. Not received.

Technical accuracy of the rewrite's claims (checked against 5d598de with the reviewer's own probes; working tree clean). It confirmed the line counts, the six changed source files, the solver's dispatch order, _holds, the default limits and cancel interval, query raising, decide's indeterminate results, no surviving QueryContext, SemanticConflict from analysis, the circular-rule and variable-effect results, and tunnel lengths 57 to 59.

Finding Resolution Now
Major: "labels every result" is false for spans_for, which drops an incomplete status spans_for named as the exception in "can't finish", host integration and Figure 1 page
Major: Figure 1's caption claimed the row order is the tested import layering; it isn't Claim removed; the real layering and the runtime recursion described under the figure page
Major: Figure 2 and the state section omitted rewrite state that outlives or escapes the request: the lazily built clause index, the recursion limit and the variable counter Added to Figure 2, its text equivalent and the state section; takeaway now "no answers or evaluation state outlive the request; only structures tied to the current revision do" restored: Difference 2 and the state section; Figure 2 kept it throughout
Minor: "caches across queries" contradicted the reused analysis and timeline "caches answers across queries" page
Minor: unbound-variable names differ between identical requests "apart from the generated names of unbound variables"; probe page
Minor: this record paired the cold trace with a warm step count and called analysis budgeted Corrected (103 cold, 22 warm); the page says analysis runs outside the budget this record; page
Minor: Figure 2 said once(can_enter(lab)), but the plain goal was traced Label is ask("can_enter(lab)"); the once form's count is recorded page
Minor: not all of api/ loads on demand Value conversion is imported by the facade; only JSON and game hooks load on first use page (Figure 1's text equivalent)
Minor: diagnostics aren't notes on each result "diagnostics on the knowledge base; per-query reasons as notes" page (Figure 1)
Minor: the timeline can be enlarged per request, and a partial read is kept Both stated detail
Minor: "the timeline can't disagree with its solver" overstated "read from the solver's before/2 answers", closed transitively page ("read through the solver"); detail
Minor: advice to pass the decision's condition to explain gives a confidently wrong answer The page says to explain the fluents inside the condition and warns what a rule head returns revised: called misleading
Minor: the circular cost depends on the two-argument spelling Cause restated; the three-argument result added restored
Minor: "decide sends three queries" holds for one guard only "each guard's condition in order until one fails"; four for the tunnel page
Minor: lengths 57 to 59 unrecorded; at 50 the earlier engine printed one line Sweep of 50 to 60 re-run in both engines; 50-segment row added page
Minor: the rewrite's side of name capture missing "the rewrite refuses that clause at load" revised: both sides

Fairness to the earlier engine (checked against e62758c with the reviewer's own probes and a test run). The reviewer judged the page fair overall: real strengths named, size disclaimed, the tunnel input flagged as atypical.

Finding Resolution Now
Major: "'Why can't I enter?' gets a richer answer from the earlier engine" contradicted the page's own probes Rewritten: richer for "why can I?"; neither engine explains a failed rule; only the rewrite names the event that ended the fluent page
Major: extension overstated: a registered handler reaches single top-level goals only Qualified on the page; probe added page
Major: the cache handing out its own list, a fixable missing copy, was presented as a principal architectural trait The findings state the architectural contrast (engine-wide caches versus request-owned state); the aliasing moved to the state section as a missing copy and was removed from Figure 2 detail
Major: Figure 1's takeaway ("same jobs", "a single place", "more chips mean … not more capability") framed fewer places as the finding and misdescribed the figure; "13 files" sized one side only Takeaway rewritten around "one mechanism of each kind", naming the jobs the rewrite lacks; file count and "one component per job" removed page
Minor: the earlier suite was called impossible to rerun Rerun on an isolated export: 6,647 passed; all 20 failures and errors need hypothesis page
Minor (summary only): default-on authoring checks omitted Added to host integration, with a probe revised: what the rewrite records is stated
Minor (summary only): the variable-effect example hid that the rule form works in the earlier engine Page and evidence show the rule form working in both page
Minor (summary only): inherited exclusivity and tie-break not credited Added to story time's shared list; the rewrite makes the seven built-in names opt-in restored in the table; opt-in names in detail
Minor (summary only): the explain_holds_at error string is bug catalogue Removed from the page; kept under documentation checks this record
Minor (summary only): Figure 2's alt text said five lifetimes where six are shown Alt text says six page

Visual presentation. Written findings not received. The reviewer's checks were re-run in Edge at 360, 390, 899, 900, 960 and 1000 px:

Problem the re-run showed Resolution Now
The Prolog code blocks scroll sideways at 360 and 390 px and weren't marked focusable. Edge 153 focuses a scrolling block by itself; Safari doesn't The site builder makes code blocks focusable, with the same focus outline as tables, in a separate commit because it changes every page builder
From 600 to 899 px, the 360 px phone figure sat at natural size in a column up to 860 px wide; at 900 px the wide figure scaled to 0.89 A 560 px medium layout for 600 to 939 px; the wide layout starts at 940 px, where it scales no lower than 0.94 page
Figure 1's legend didn't match its chips (also reader confusion 19) Redrawn page

Remaining uncertainty

Round 3's corrections have not been re-read by a fresh simulated reader or re-reviewed. Round 3 had one reader, not three, and its accuracy report arrived in two parts (13 findings). Neither round 2 nor round 3 had a fairness or visual reviewer. Round 1's maintainer reading and the visual reviewer's written findings were never received. The change-cost timing uses facts only, on one machine; rules and events would add to the rewrite's analysis and timeline work. Figure text widths were calibrated in Edge on Windows; other platforms' fonts were not checked. The second cut, as corrected in round 3, has 1,294 words of visible prose and 1,771 visible words in all: about 5.5 to 6.5 minutes for the prose alone at 200 to 240 words a minute. The one round-3 reader took 17 minutes over the uncorrected cut, including the figures and their text equivalents; five minutes would need about 260 words a minute over the prose alone.