cd094b6dfb
Five findings from the second review round, and the first is this PR's own standard applied to its own P1. **The two fixes improvise-dj1l names had no discriminating test.** Reverting `has_key` to unification, or dropping `canon_key` from the duplicate gate, left kb-test at 27 passed. Case 11 was carried by `check_total` refusing with a DIFFERENT message, and case 10 by the other half of the fix. Both now assert which refusal fired (`said()`), plus two cases aimed at each half: a rm key with a variable in a non-final position must be refused as ABSENT rather than matching everything, and a keyed FACT whose key holds a variable must ADD without excluding the siblings it would unify with. Each proved by reverting exactly one fix and watching exactly that case fail. **Keying a rule on its whole clause made `replace` on an edited rule silently become `add`** -- both arms live, the stale one still firing, and `check_total` cannot see it because `NO + NF` is correct arithmetic for an add. Reachable on the 15 `kb_error/1` arms and the `kb_text/2` rules, so editing the check improvise-2f97 adds is the first thing to hit it. `check_rule_replace` refuses and names the route (rm, then add); the negative arm -- replace of a rule present verbatim -- is tested, or the refusal would just be "replace never works on rules". **The `decision(` arm was dead code.** `cited_name` took the first field, and the repo spells `decision(<ticket>, <name>)`, whose ticket id fails `name_shaped/1` on the hyphen -- so that marker could never fire for a real citation. A second `cited_name` clause takes the second field. All three markers are now exercised. Minor, from the same round: `_JS_TEST` no longer accepts `test.only`, which disables every other test in its file, with a case asserting a heading cannot be satisfied by one; and `node-test-test.sh`'s last case no longer duplicates an earlier one. Filed rather than fixed: improvise-w8ih, extracting `web/assemble.sh`'s gate so its own absence is visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
503 lines
20 KiB
Bash
Executable File
503 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check_the_checker for scripts/kb: every refusal is proved by injecting the
|
|
# fault it names, and every refusal must leave the knowledge base UNCHANGED.
|
|
set -uo pipefail
|
|
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
repo="$(cd "$here/.." && pwd)"
|
|
KB="$repo/prolog-sessions/kb.pl"
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
pass=0
|
|
fail=0
|
|
ok() { printf ' ok %s\n' "$1"; pass=$((pass+1)); }
|
|
bad() { printf ' FAIL %s\n' "$1"; fail=$((fail+1)); }
|
|
|
|
# Each case runs against its own copy, so a bug cannot reach the real file.
|
|
fresh() { cp "$KB" "$work/kb.pl"; }
|
|
kb() { KB_FILE="$work/kb.pl" "$here/kb" "$@"; }
|
|
unchanged() { diff -q "$KB" "$work/kb.pl" >/dev/null; }
|
|
# A refusal is only evidence when it is the refusal you NAMED: two of these
|
|
# cases passed against a tree with the fix reverted, because a DIFFERENT guard
|
|
# caught the same delta and the assertion was only "fails and kb unchanged"
|
|
# (review of PR #69). said() asserts which one fired.
|
|
said() { grep -qF "$2" "$1"; }
|
|
|
|
echo "kb-test: the refusals"
|
|
|
|
# 1. A duplicate key is refused, and nothing is written.
|
|
fresh
|
|
cat > "$work/dup.pl" <<'EOF'
|
|
doctrine(bug_first,'a second, contradicting text for a name that already exists').
|
|
EOF
|
|
if kb add "$work/dup.pl" >/dev/null 2>&1; then
|
|
bad "add accepted a duplicate key"
|
|
elif unchanged; then
|
|
ok "add refuses a duplicate key and leaves the kb unchanged"
|
|
else
|
|
bad "add refused a duplicate key but MODIFIED the kb"
|
|
fi
|
|
|
|
# 2. replace is the door that duplicate goes through, and it replaces rather
|
|
# than appending -- a count that grew would be two clauses for one name.
|
|
fresh
|
|
before="$(kb count | grep '^doctrine/2=')"
|
|
if kb replace "$work/dup.pl" >/dev/null 2>&1; then
|
|
after="$(kb count | grep '^doctrine/2=')"
|
|
if [ "$before" = "$after" ]; then
|
|
ok "replace overwrites in place ($after)"
|
|
else
|
|
bad "replace changed the clause count: $before -> $after"
|
|
fi
|
|
else
|
|
bad "replace refused a duplicate key"
|
|
fi
|
|
|
|
# 3. A delta that does not parse is refused.
|
|
fresh
|
|
printf 'doctrine(broken, unterminated\n' > "$work/bad.pl"
|
|
if kb add "$work/bad.pl" >/dev/null 2>&1; then
|
|
bad "add accepted an unparseable delta"
|
|
elif unchanged; then
|
|
ok "add refuses an unparseable delta and leaves the kb unchanged"
|
|
else
|
|
bad "add refused an unparseable delta but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4. A delta that parses but breaks kb_consistent is refused. This is the one
|
|
# that matters: the candidate is checked BEFORE it becomes the kb.
|
|
fresh
|
|
cat > "$work/illegal.pl" <<'EOF'
|
|
crate_depends_on(improvise_core,improvise_command).
|
|
EOF
|
|
if kb add "$work/illegal.pl" >/dev/null 2>&1; then
|
|
bad "add accepted a delta that breaks kb_consistent"
|
|
elif unchanged; then
|
|
ok "add refuses a kb_consistent violation and leaves the kb unchanged"
|
|
else
|
|
bad "add refused a violation but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4b. A delta trealla cannot even take apart must refuse, not crash into a
|
|
# success. `-g main,halt` exits 0 on a throw, so this is the guard for it.
|
|
fresh
|
|
printf '42.\n' > "$work/notaclause.pl"
|
|
if kb add "$work/notaclause.pl" >/dev/null 2>&1; then
|
|
bad "add accepted a term that is not a clause"
|
|
elif unchanged; then
|
|
ok "add refuses a non-clause term and leaves the kb unchanged"
|
|
else
|
|
bad "add refused a non-clause term but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4c. rm removes by key, and a key that is not there is REFUSED rather than
|
|
# silently doing nothing -- a typo would otherwise report success.
|
|
# The clause removed is one this case ADDS. It used to name bug_first, which
|
|
# the citation check (improvise-2f97) now refuses to retire -- correctly, since
|
|
# before_fixing/2 calls it and kb_issue_found names it in prose.
|
|
fresh
|
|
printf "doctrine(a_probe_doctrine_to_remove,'a clause nothing else names').\n" > "$work/probe.pl"
|
|
kb add "$work/probe.pl" >/dev/null 2>&1
|
|
printf "doctrine(a_probe_doctrine_to_remove,_).\n" > "$work/drop.pl"
|
|
before="$(kb count | grep '^doctrine/2=' | cut -d= -f2)"
|
|
if kb rm "$work/drop.pl" >/dev/null 2>&1; then
|
|
after="$(kb count | grep '^doctrine/2=' | cut -d= -f2)"
|
|
if [ "$((before - 1))" = "$after" ]; then
|
|
ok "rm removes exactly one clause ($before -> $after)"
|
|
else
|
|
bad "rm moved the count wrongly: $before -> $after"
|
|
fi
|
|
else
|
|
bad "rm refused a key that exists"
|
|
fi
|
|
|
|
fresh
|
|
printf "doctrine(a_key_that_does_not_exist_at_all,_).\n" > "$work/nokey.pl"
|
|
if kb rm "$work/nokey.pl" >/dev/null 2>&1; then
|
|
bad "rm accepted a key that is not present"
|
|
elif unchanged; then
|
|
ok "rm refuses an absent key and leaves the kb unchanged"
|
|
else
|
|
bad "rm refused an absent key but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4d. A DIRECTIVE is callable, so a callable test alone lets it through -- and
|
|
# the candidate is validated by CONSULTING it, so `:- halt(0).` exits the
|
|
# check with success before kb_consistent runs. The gate passes by never
|
|
# running. Found by review on PR #68; demonstrated before it was fixed.
|
|
fresh
|
|
printf ':- halt(0).\n' > "$work/directive.pl"
|
|
if kb add "$work/directive.pl" >/dev/null 2>&1; then
|
|
bad "add accepted a directive that disables its own gate"
|
|
elif unchanged; then
|
|
ok "add refuses a directive and leaves the kb unchanged"
|
|
else
|
|
bad "add refused a directive but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4e. Keys were checked only against the EXISTING file, so two clauses sharing a
|
|
# previously-unused key clashed with nothing and the merge dropped the first
|
|
# -- add discarding part of its own payload, visible only as a delta count
|
|
# that did not match the clauses added.
|
|
fresh
|
|
cat > "$work/selfdup.pl" <<'EOD'
|
|
doctrine(a_name_used_twice_in_one_delta,'first').
|
|
doctrine(a_name_used_twice_in_one_delta,'second').
|
|
EOD
|
|
if kb add "$work/selfdup.pl" >/dev/null 2>&1; then
|
|
bad "add accepted a delta naming one key twice"
|
|
elif unchanged; then
|
|
ok "add refuses a delta that names one key twice"
|
|
else
|
|
bad "add refused a self-duplicate but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4f. The gate can be REMOVED. kb_consistent then throws existence_error, and
|
|
# tpl exits 0 when a goal throws before reaching halt -- so the check passes
|
|
# by not existing and the unchecked candidate is committed.
|
|
fresh
|
|
printf 'kb_consistent.\n' > "$work/killgate.pl"
|
|
if kb rm "$work/killgate.pl" >/dev/null 2>&1; then
|
|
bad "rm removed the gate that validates the write"
|
|
elif unchanged; then
|
|
ok "rm refuses to remove the validating gate"
|
|
else
|
|
bad "rm refused to remove the gate but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4g. And REPLACED with something trivially true, which passes forever after.
|
|
fresh
|
|
if kb replace "$work/killgate.pl" >/dev/null 2>&1; then
|
|
bad "replace made the gate trivially true"
|
|
elif unchanged; then
|
|
ok "replace refuses to disarm the validating gate"
|
|
else
|
|
bad "replace refused to disarm the gate but MODIFIED the kb"
|
|
fi
|
|
|
|
# 4h. An intentional change to the gate is still possible, deliberately.
|
|
fresh
|
|
if KB_ALLOW_GATE=1 kb replace "$work/killgate.pl" >/dev/null 2>&1; then
|
|
ok "KB_ALLOW_GATE lets a deliberate gate change through"
|
|
else
|
|
bad "KB_ALLOW_GATE did not allow a gate change"
|
|
fi
|
|
|
|
# 4i. Two writers reading the same file would both validate, and the later mv
|
|
# would silently discard the earlier addition. The stamp makes the loser
|
|
# loud. KB_TEST_RACE simulates the other writer landing mid-flight.
|
|
fresh
|
|
cp "$work/kb.pl" "$work/other.pl"
|
|
printf "doctrine(written_by_the_other_writer,'x').\n" >> "$work/other.pl"
|
|
cat > "$work/mine.pl" <<'EOD'
|
|
doctrine(written_by_me,'y').
|
|
EOD
|
|
if KB_TEST_RACE="$work/other.pl" kb add "$work/mine.pl" >/dev/null 2>&1; then
|
|
bad "a concurrent write was silently discarded"
|
|
elif grep -q written_by_the_other_writer "$work/kb.pl" && ! grep -q written_by_me "$work/kb.pl"; then
|
|
ok "a concurrent write is refused, and the other writer's addition survives"
|
|
else
|
|
bad "the race was refused but the kb is not the other writer's file"
|
|
fi
|
|
|
|
# 4j. A multi-valued relation must accept several clauses. The key rule keyed
|
|
# on all-but-last, so a crate depending on two crates read as a duplicate
|
|
# key and was unaddable -- found by trying to model a design, not by a test.
|
|
fresh
|
|
cat > "$work/multi.pl" <<'EOD'
|
|
crate(improvise_probe,'crates/improvise-probe','a probe crate').
|
|
crate_depends_on(improvise_probe,improvise_core).
|
|
crate_depends_on(improvise_probe,improvise_protocol).
|
|
layer(probe,improvise_probe).
|
|
EOD
|
|
if kb add "$work/multi.pl" >/dev/null 2>&1; then
|
|
ok "a multi-valued relation accepts several clauses"
|
|
else
|
|
bad "add refused a legitimate multi-valued relation"
|
|
fi
|
|
|
|
# 4k. check --with asks whether a design the tree does not have is one the model
|
|
# accepts. It must be able to say BOTH, and must never write.
|
|
fresh
|
|
printf 'crate_depends_on(improvise_core,improvise_command).\n' > "$work/bad-design.pl"
|
|
if kb check --with "$work/bad-design.pl" >/dev/null 2>&1; then
|
|
bad "check --with accepted a design that inverts the layering"
|
|
elif unchanged; then
|
|
ok "check --with refuses an unsound design and writes nothing"
|
|
else
|
|
bad "check --with refused a design but MODIFIED the kb"
|
|
fi
|
|
|
|
if kb check --with "$work/multi.pl" >/dev/null 2>&1 && unchanged; then
|
|
ok "check --with accepts a sound design and writes nothing"
|
|
else
|
|
bad "check --with rejected a sound design, or wrote to the kb"
|
|
fi
|
|
|
|
echo "kb-test: the happy paths"
|
|
|
|
# 5. A genuinely new clause lands, and the count moves by exactly one.
|
|
fresh
|
|
cat > "$work/new.pl" <<'EOF'
|
|
doctrine(a_name_that_is_certainly_not_taken,'text for a brand new doctrine').
|
|
EOF
|
|
before="$(kb count | grep '^doctrine/2=' | cut -d= -f2)"
|
|
if kb add "$work/new.pl" >/dev/null 2>&1; then
|
|
after="$(kb count | grep '^doctrine/2=' | cut -d= -f2)"
|
|
if [ "$((before + 1))" = "$after" ]; then
|
|
ok "add lands one new clause ($before -> $after)"
|
|
else
|
|
bad "add moved the count wrongly: $before -> $after"
|
|
fi
|
|
else
|
|
bad "add refused a valid new clause"
|
|
fi
|
|
|
|
# 6. The new clause is readable back as itself -- a count can move without the
|
|
# content being what was asked for.
|
|
if KB_FILE="$work/kb.pl" "$here/kb" query \
|
|
"doctrine(a_name_that_is_certainly_not_taken,T), T == 'text for a brand new doctrine'" \
|
|
>/dev/null 2>&1; then
|
|
ok "the added clause reads back with the text it was given"
|
|
else
|
|
bad "the added clause is not readable back as itself"
|
|
fi
|
|
|
|
# 7. Canonical form is a fixed point: a second fmt changes nothing.
|
|
fresh
|
|
kb fmt >/dev/null 2>&1
|
|
cp "$work/kb.pl" "$work/once.pl"
|
|
kb fmt >/dev/null 2>&1
|
|
if diff -q "$work/once.pl" "$work/kb.pl" >/dev/null; then
|
|
ok "fmt is idempotent"
|
|
else
|
|
bad "fmt is not idempotent -- every add would churn the file"
|
|
fi
|
|
|
|
# 8. Canonicalizing loses no clause. A serializer that drops a predicate is the
|
|
# wipe-the-kb failure, so the count is checked across the reformat.
|
|
fresh
|
|
b="$(kb count)"
|
|
kb fmt >/dev/null 2>&1
|
|
a="$(kb count)"
|
|
if [ "$b" = "$a" ]; then
|
|
ok "fmt preserves every clause count"
|
|
else
|
|
bad "fmt changed the counts"
|
|
fi
|
|
|
|
# 9. A delta holding two clauses whose heads are VARIANTS -- same shape, fresh
|
|
# variables -- is two clauses. check_self_consistent compared keys with ==
|
|
# over findall results, which never holds for fresh variables, so the delta
|
|
# passed; the merge then matched by UNIFICATION and the second removed the
|
|
# first. Two in, one out, reported as a success (improvise-dj1l).
|
|
cat > "$work/rules.pl" <<'EOF'
|
|
probe_rule(X) :- doctrine(X,_).
|
|
probe_rule(Y) :- invariant(Y,_).
|
|
EOF
|
|
fresh
|
|
b="$(kb count | grep '^total=' | cut -d= -f2)"
|
|
if kb add "$work/rules.pl" >/dev/null 2>&1; then
|
|
a="$(kb count | grep '^total=' | cut -d= -f2)"
|
|
if [ "$((a - b))" -eq 2 ]; then
|
|
ok "two variant-headed rules in one delta both land ($b -> $a)"
|
|
else
|
|
bad "two clauses in the delta, $((a - b)) landed"
|
|
fi
|
|
else
|
|
bad "add refused two variant-headed rules"
|
|
fi
|
|
|
|
# 10. A non-ground key must not match a ground clause. `probe_fact(X) :- ...`
|
|
# unified with `probe_fact(a)`, which DELETED the fact and, worse, bound X
|
|
# to a, so the clause written was not the clause supplied. The round-trip
|
|
# check cannot see that: it compares the file against an intended list
|
|
# already corrupted by the same unification.
|
|
fresh
|
|
printf 'probe_fact(a).\nprobe_fact(b).\n' > "$work/facts.pl"
|
|
kb add "$work/facts.pl" >/dev/null 2>&1
|
|
printf 'probe_fact(X) :- doctrine(X,_).\n' > "$work/rule.pl"
|
|
# `add`, not `replace`: the rule is new, and a rule that is not already there
|
|
# is what case 15 makes replace refuse. This is also the discriminating door --
|
|
# under unification `conflicts` sees the non-ground key match both facts and
|
|
# refuses the add outright, so the reverted fix fails HERE.
|
|
if kb add "$work/rule.pl" >/dev/null 2>&1; then
|
|
kept=0
|
|
grep -q '^probe_fact(a)\.$' "$work/kb.pl" && grep -q '^probe_fact(b)\.$' "$work/kb.pl" && kept=1
|
|
if [ "$kept" = 1 ]; then
|
|
ok "a non-ground rule does not delete the ground clauses it would unify with"
|
|
else
|
|
bad "a non-ground rule deleted a ground clause"
|
|
fi
|
|
if grep -q '^probe_fact(a):-' "$work/kb.pl"; then
|
|
bad "the clause written is not the clause supplied: its variable was bound"
|
|
elif grep -qE '^probe_fact\([A-Z][A-Za-z0-9_]*\):-doctrine\(' "$work/kb.pl"; then
|
|
ok "the rule is written as supplied, with its variable still a variable"
|
|
else
|
|
bad "the rule was not written at all"
|
|
fi
|
|
else
|
|
bad "add refused a rule over a predicate holding facts: a variable key matched them"
|
|
fi
|
|
|
|
# 11. The duplicate gate must still fire once keys are compared as variants:
|
|
# two clauses that really are the same key are still two clauses for one key.
|
|
fresh
|
|
printf 'probe_rule(X) :- doctrine(X,_).\nprobe_rule(Z) :- doctrine(Z,_).\n' > "$work/same.pl"
|
|
if kb add "$work/same.pl" >"$work/out" 2>&1; then
|
|
bad "add accepted the same clause twice in one delta"
|
|
elif ! unchanged; then
|
|
bad "add refused a duplicate but MODIFIED the kb"
|
|
elif said "$work/out" "names one key twice"; then
|
|
ok "add still refuses one key named twice, compared as variants"
|
|
else
|
|
bad "the delta was refused, but not BY THE DUPLICATE GATE (canon_key is unreached)"
|
|
fi
|
|
|
|
# 12. The arithmetic check, proved by injecting the fault it exists for: a
|
|
# merge that silently drops the last clause of its delta. The count was
|
|
# PRINTED beside the delta size all along and nothing compared them, which
|
|
# is how the real loss reported itself as "2 in delta, 537 after". The
|
|
# injection is aimed at the fold rather than at the keys, because the check
|
|
# must catch a dropped clause whatever dropped it.
|
|
sed 's/^foldl_merge(_, \[\], _, Acc, Acc)\./foldl_merge(_, [_], _, Acc, Acc)./' \
|
|
"$here/kb-merge.pl" > "$work/merge-dropping.pl"
|
|
if ! grep -q '^foldl_merge(_, \[_\], _, Acc, Acc)\.$' "$work/merge-dropping.pl"; then
|
|
bad "the injection did not apply -- case 12 proves nothing"
|
|
else
|
|
fresh
|
|
if KB_MERGE="$work/merge-dropping.pl" kb add "$work/rules.pl" >/dev/null 2>&1; then
|
|
bad "a merge that dropped a clause was accepted: the total is unchecked"
|
|
elif unchanged; then
|
|
ok "a merge that drops a clause is refused by the total it already printed"
|
|
else
|
|
bad "the dropped clause was caught but the kb was MODIFIED"
|
|
fi
|
|
fi
|
|
|
|
# 13. Clauses cite each other by NAME inside their text, and nothing checked
|
|
# that the name resolved -- so retiring a clause silently dangled every
|
|
# clause referring to it, which is the fact-stated-twice hazard the KB's
|
|
# own doctrine warns about in prose (improvise-2f97). The token rule is
|
|
# deliberately narrow: only a name-shaped token inside doctrine(...),
|
|
# invariant(...) or decision(...) brackets, so ordinary prose cannot
|
|
# false-positive.
|
|
fresh
|
|
echo 'doctrine(an_advisory_gate_is_not_a_gate,_).' > "$work/cited.pl"
|
|
if kb rm "$work/cited.pl" >/dev/null 2>&1; then
|
|
bad "rm retired a clause that another clause cites by name"
|
|
elif unchanged; then
|
|
ok "retiring a cited clause is refused, and the kb is unchanged"
|
|
else
|
|
bad "the dangling citation was caught but the kb was MODIFIED"
|
|
fi
|
|
|
|
# The other direction: the arm must FIRE on a citation that resolves to
|
|
# nothing, and must not fire on one that resolves. A check that only ever
|
|
# reports success is untested (kb_issue_found(checker_never_failed)).
|
|
cat > "$work/dangling.pl" <<'EOF'
|
|
doctrine(a_probe_that_cites_nothing,'cites doctrine(a_name_that_was_retired), which does not exist').
|
|
EOF
|
|
cat > "$work/resolving.pl" <<'EOF'
|
|
doctrine(a_probe_that_cites_a_real_one,'cites doctrine(an_advisory_gate_is_not_a_gate), which exists').
|
|
EOF
|
|
fresh
|
|
if kb check --with "$work/dangling.pl" >/dev/null 2>&1; then
|
|
bad "the model accepted a design citing a clause that does not exist"
|
|
elif kb check --with "$work/resolving.pl" >/dev/null 2>&1; then
|
|
ok "a citation is checked, and one that resolves is still accepted"
|
|
else
|
|
bad "the citation check refuses a name that DOES resolve"
|
|
fi
|
|
|
|
# 14. The case only same_key/2 can catch. A rm key with a variable in a
|
|
# NON-final position -- work_done keys on two arguments -- matches nothing
|
|
# as a variant and everything by unification. Under unification it removed
|
|
# an arbitrary bug_fix entry and reported success; here it must be refused
|
|
# as ABSENT, by that message and no other.
|
|
fresh
|
|
printf 'work_done(_,bug_fix,_).\n' > "$work/wild.pl"
|
|
if kb rm "$work/wild.pl" >"$work/out" 2>&1; then
|
|
bad "rm removed clauses by unifying a variable key against them"
|
|
elif ! unchanged; then
|
|
bad "rm refused a wildcard key but MODIFIED the kb"
|
|
elif said "$work/out" "no such clause to remove"; then
|
|
ok "a key with a variable in it matches nothing, rather than everything"
|
|
else
|
|
bad "the wildcard rm was refused, but not as ABSENT (same_key is unreached)"
|
|
fi
|
|
|
|
# 15. A rule keys on its whole clause, so an EDITED rule matches nothing and
|
|
# replace would silently become add -- leaving the stale arm firing beside
|
|
# the new one, with correct arithmetic (found by review on PR #69).
|
|
fresh
|
|
printf 'probe_rule(X) :- doctrine(X,_).\n' > "$work/rule1.pl"
|
|
printf 'probe_rule(X) :- doctrine(X,_), X \\== b.\n' > "$work/rule2.pl"
|
|
kb add "$work/rule1.pl" >/dev/null 2>&1
|
|
before="$(kb count | grep '^total=' | cut -d= -f2)"
|
|
if kb replace "$work/rule2.pl" >"$work/out" 2>&1; then
|
|
after="$(kb count | grep '^total=' | cut -d= -f2)"
|
|
bad "replace ADDED an edited rule beside the old one ($before -> $after)"
|
|
elif said "$work/out" "kb rm the old one"; then
|
|
ok "replace refuses a rule it cannot find, and names the route"
|
|
else
|
|
bad "the edited rule was refused, but without naming rm/add"
|
|
fi
|
|
|
|
# The negative arm: replace of a rule that IS there must still work, or the
|
|
# refusal above would just be "replace never works on rules".
|
|
if kb replace "$work/rule1.pl" >/dev/null 2>&1; then
|
|
ok "replace of an unchanged rule is still accepted"
|
|
else
|
|
bad "replace refuses a rule that is present verbatim"
|
|
fi
|
|
|
|
# 16. All three citation markers must FIRE. doctrine( and invariant( take the
|
|
# name as the first field; decision( takes the SECOND, because the repo
|
|
# spells decision(<ticket>, <name>) and a ticket id is not name-shaped --
|
|
# so a single-field rule left that marker unreachable (review, PR #69).
|
|
cat > "$work/cite-d.pl" <<'EOF'
|
|
doctrine(a_probe_citing_a_doctrine,'cites doctrine(a_name_that_was_retired), which does not exist').
|
|
EOF
|
|
cat > "$work/cite-i.pl" <<'EOF'
|
|
doctrine(a_probe_citing_an_invariant,'cites invariant(a_rule_that_was_never_written), which does not exist').
|
|
EOF
|
|
cat > "$work/cite-x.pl" <<'EOF'
|
|
doctrine(a_probe_citing_a_decision,'cites decision(improvise-nope, a_name_that_was_never_decided), which does not exist').
|
|
EOF
|
|
fresh
|
|
markers=0
|
|
for m in d i x; do
|
|
kb check --with "$work/cite-$m.pl" >/dev/null 2>&1 || markers=$((markers+1))
|
|
done
|
|
if [ "$markers" -eq 3 ]; then
|
|
ok "all three citation markers are reachable, not just the first"
|
|
else
|
|
bad "only $markers of 3 citation markers fire"
|
|
fi
|
|
|
|
# 17. The case aimed at has_key/2 itself. A keyed FACT with a variable in a
|
|
# key position -- work_done keys on (id, kind) -- is a key no existing
|
|
# clause is a variant of, so the add must land and every sibling survive.
|
|
# Under unification the fold excludes the clauses the key happens to match
|
|
# and the add is refused instead (by the total, which is the backstop, not
|
|
# the fix). Asserted as the OUTCOME, because that is what a wrong has_key
|
|
# destroys.
|
|
fresh
|
|
before="$(kb count | grep '^work_done/3=' | cut -d= -f2)"
|
|
printf "work_done(_,maintenance,'a probe clause with a variable id').\n" > "$work/wildfact.pl"
|
|
if kb add "$work/wildfact.pl" >"$work/out" 2>&1; then
|
|
after="$(kb count | grep '^work_done/3=' | cut -d= -f2)"
|
|
if [ "$((before + 1))" = "$after" ]; then
|
|
ok "a fact whose key holds a variable adds without excluding its siblings"
|
|
else
|
|
bad "adding one clause moved work_done/3 from $before to $after"
|
|
fi
|
|
else
|
|
bad "add refused a key holding a variable: the fold matched by unification"
|
|
fi
|
|
|
|
printf '\nkb-test: %d passed, %d failed\n' "$pass" "$fail"
|
|
[ "$fail" -eq 0 ]
|