Orchestrata Ultimate › Plans › prolog-tabling-restraint-plan
Plan: Prolog bounded tabling, tnot/1 safety audit, CLIPS-vs-SWI spike
Source: plans/prolog-tabling-restraint-plan.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-10 · 131 lines.
Prose pending.
Plan text
Plan: Prolog bounded tabling, tnot/1 safety audit, CLIPS-vs-SWI spike
Worktree: /home/mohamed/workspace/orchestrata-kimi-wt3
Branch: kimi/prolog-tabling off c717bfcb (current master)
Scope: P-1 bounded tabling on knowledge.pl:ownership_chain/3, P-2 tnot/1 safety audit around constitutional.pl:assess_harm/3, C1 standalone CLIPS-vs-SWI-tabling micro-benchmark.
1. Context (from plans/swi-tabling-capabilities.md)
depends_on_transitive/2is already tabledas incremental;depends_on/2isas incremental.ownership_chain/3recurses over the same dynamicowns/depends_ongraph and can loop on cycles. It is currently untabled and unbounded.assess_harm/3contains self-referential\+ assess_harm(...)in its default clause. Tabling it without converting totnot/1would produce incomplete/wrong answers under SLG; it is safe only while untabled.- CLIPS 6.4.2 is installed at
/usr/bin/clips; SWI-Prolog 10.0.2 + janus-swi are the production runtime.
2. P-1 — Bounded tabling on knowledge.pl:ownership_chain/3
File changes
src/orchestrata_ultimate/prolog/knowledge.pl- Change
:- dynamic owns/2.→:- dynamic owns/2 as incremental.so a tabledownership_chain/3can be incremental. - Add directive before the predicate:
:- table ownership_chain/3 as (incremental, max_answers(20)). - Add a comment explaining:
- why
max_answersis needed (cycles, unbounded list growth); - that answers beyond the cap are marked undefined (
answer_count_restraint); - call-site policy: undefined/capped results ⇒ deny + escalate.
- why
- Keep the existing base/recursion clauses unchanged (no semantic drift).
- Change
Test design — new tests/knowledge_graph/test_ownership_chain_tabling.py
Use the existing real_prolog fixture + test_id prefixes for parallel safety.
test_ownership_chain_terminates_on_cycle- Assert facts forming a 2-node dependency cycle (
a -> b -> a). - Query
knowledge:ownership_chain(TestAgent, TargetNode, Chain). - Assert the query returns within a short timeout and produces at most 20 answers.
- Assert the process does not hang (this is the janus cycle-termination proof).
- Assert facts forming a 2-node dependency cycle (
test_ownership_chain_acyclic_returns_expected_chains- Assert a simple acyclic graph.
- Verify returned chains match the current predicate semantics (AgentId repeated for the recursion depth).
test_ownership_chain_rejects_undefined_tail- Set up a graph that would produce more than 20 answers.
- Verify that any nonground/conditional answers are treated as a denial (the test itself asserts that no nonground Chain is accepted as a valid permission).
Verification
PYTHONPATH=/home/mohamed/workspace/orchestrata-kimi-wt3/src uv run pytest tests/knowledge_graph/test_ownership_chain_tabling.py -v
3. P-2 — tnot/1 safety audit: do not table constitutional.pl:assess_harm/3
File changes
src/orchestrata_ultimate/prolog/constitutional.pl- Add a comment block directly above
assess_harm/3documenting:- the self-referential
\+ assess_harm(...)in the default clause; - that tabling would require converting
\+totnot/1and accepting WFS/undefined answers; - that the predicate is deliberately left untabled because it is a cheap ground-fact lookup and harm decisions must not be three-valued.
- the self-referential
- Add a comment block directly above
tests/governance/test_tabling_safety.py(new)test_assess_harm_is_not_tabled: load constitutional.pl via a real PrologBridge and assertpredicate_property(constitutional:assess_harm(_,_,_), tabled)is false.test_no_self_negated_predicate_is_tabled(audit): enumeratesrc/orchestrata_ultimate/prolog/*.pl, for each:- tabledirective verify the tabled predicate does not containtnot/1or self-referential\+in its body (lightweight static audit).
Verification
PYTHONPATH=/home/mohamed/workspace/orchestrata-kimi-wt3/src uv run pytest tests/governance/test_tabling_safety.py -v
4. C1 — Standalone CLIPS-vs-SWI-tabling micro-benchmark
File
scripts/benchmark_prolog_clips.py
Design
- Generate a deterministic DAG/chain of
Nnodes andEedges. - SWI path: use janus-swi to consult
knowledge.pl, assert edges asknowledge:depends_on/2, and timeknowledge:depends_on_transitive(Src, Dst)queries (cold first query + warm repeated queries). - CLIPS path: write a temporary
.clpfile with(deftemplate edge ...), edge facts, transitivereachrules,(run),(facts),(exit), invoke/usr/bin/clips -f <file>, and time total wall-clock. - CLI:
python scripts/benchmark_prolog_clips.py --nodes 100 --edges 200 --queries 50 --output /tmp/bench.json - Output JSON includes: setup time, cold query time, warm query time, total time, facts/rules counts, speedup ratio, and whether CLIPS was available.
- If
clipsis missing, print a clear warning and emit SWI-only results.
Verification
PYTHONPATH=/home/mohamed/workspace/orchestrata-kimi-wt3/src python scripts/benchmark_prolog_clips.py --nodes 50 --edges 100 --queries 10
5. File targets summary
| File | Change |
|---|---|
src/orchestrata_ultimate/prolog/knowledge.pl |
owns/2 as incremental; table ownership_chain/3 as (incremental, max_answers(20)); comments |
src/orchestrata_ultimate/prolog/constitutional.pl |
Document why assess_harm/3 must stay untabled |
tests/knowledge_graph/test_ownership_chain_tabling.py |
Cycle-termination + bounded-answer janus tests |
tests/governance/test_tabling_safety.py |
assess_harm/3 not tabled + static tabling audit |
scripts/benchmark_prolog_clips.py |
Standalone CLIPS vs SWI-tabling benchmark |
plans/prolog-tabling-restraint-plan.md |
This plan |
6. Test / gate plan
- Run the two new test modules in isolation.
- Run
python scripts/gate_parallel_safety.pyandpython scripts/audit_test_purity.pyfor changed test files. - Run the full regression via
python scripts/run_tests.py --skip-gatesbefore final commit. - Do not commit until the plan is reviewed and ACK'd on
#agents.
7. Risks / open questions
max_answers(20)is a first-guess cap. If production ownership delegation graphs are deeper, we can raise it or make it env-driven; the test suite will pin the behavior.incremental+max_answerstogether need a runtime smoke test in SWI 10.0.2. If the combination is rejected, fallback ismax_answers(20)alone and keepowns/2plain dynamic.- CLIPS benchmark measures subprocess wall-clock, not in-process query latency; the JSON will label this clearly so numbers are not misinterpreted as a like-for-like microsecond comparison.