fix(kb): the dj1l cases discriminate, and a replaced rule cannot silently become an add
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
This commit is contained in:
@@ -221,6 +221,7 @@ doctrine(design_against_the_model_before_implementing,'AGENTS.md has always said
|
||||
doctrine(a_key_rule_must_ask_whether_the_relation_is_multi_valued,'scripts/kb keyed every clause on the functor plus all arguments but the last, which is right for doctrine(Name,Text) and wrong for any RELATION: crate_depends_on(A,B) then keyed on A, so a crate depending on two crates read as one key twice and was unaddable. Found by trying to model a proposed crate, not by any test -- the tool had only ever been pointed at single-valued facts. The fix cannot read the KB own functional/1, because that check keys an arity-3 predicate on its FIRST argument, which is right for crate/3 and wrong for decision/3 where one ticket holds several decisions under different names. So key_arity/2 is a hand-maintained table and anything unlisted keys on the WHOLE head, which is the safe direction: an unlisted relation accepts many clauses and only an exact repeat is a duplicate.').
|
||||
doctrine(a_local_shell_is_not_the_ci_shell,'zsh ABORTS on an unmatched glob and bash leaves it as a literal, so a CI step written and probed in zsh can fail in a way no developer shell can reproduce. node --test web/tests/*.test.js under bash therefore globbed the literal itself, matched nothing, reported 0 tests and exited 0 -- both node steps gated nothing the moment their directory moved (improvise-vz54). Local shell vs CI shell is a fault-injection axis in its own right: probe a CI line under the shell the workflow names, not the one you are typing in. The fix moves the glob into scripts/node-test.sh so the expansion is the script\'s and can be refused.').
|
||||
doctrine(a_convention_that_keeps_a_checker_quiet_is_the_one_that_blinds_it,'context/test-evidence used #### for browser tests precisely BECAUSE --check-evidence resolved names against Rust fn names only, so a ### heading naming a Playwright test would be reported as an orphan. The workaround made every browser entry invisible, and a deleted test left its heading promising coverage for a whole release (improvise-5ray). Cure: collect ### and #### alike and decide by SHAPE -- a bare identifier is a Rust test, a quoted string is a JS test, anything else is prose naming nothing. When a checker forces a convention on the thing it checks, ask what that convention hides.').
|
||||
doctrine(a_refusal_is_evidence_only_when_it_is_the_one_you_named,'Two kb-test cases passed against a tree with the fix they guarded reverted, because a DIFFERENT guard refused the same delta and the assertion was only \"fails, and the kb is unchanged\" (review of PR #69). A tool with several guards will usually refuse a broken delta somehow, so \"it was refused\" is nearly free and says nothing about the fix under test. Assert the REFUSAL MESSAGE, or construct a case only the fix can catch -- and prove it by reverting that one fix and watching the case fail. The same trap in reverse: a case asserting an OUTCOME (the add lands and every sibling survives) can discriminate where a message cannot, because what a wrong key destroys is the outcome.').
|
||||
work_done('improvise-8tk',bug_fix,'ZZ/:wq quit even when the save failed, silently discarding the model. Save now aborts the effect batch on failure; save_to_current_path/1 returns Result<String,String>; three tests in ui/effect.rs.').
|
||||
work_done('improvise-907',bug_fix,'Import wizard step machine redesign: validation_error() gate at ReviewProposals (no-category and over-limit), history-stack previous() bound to Left on every step, and Done made a real confirmation step holding the built workbook. Superseded improvise-gfq, improvise-o1c, improvise-c6j.').
|
||||
work_done('improvise-6sq',wont_do,'Merging EditOrDrill and EnterEditAtCursorCmd into one EnterEdit{mode} is not safe: they differ in WHEN the cell value is read, not in a mode parameter. Added ui/app.rs::tests::i_and_o_diverge_on_an_empty_records_grid as executable evidence.').
|
||||
@@ -560,6 +561,10 @@ name_char('_').
|
||||
name_shaped(A):-atom_chars(A,B),B=[C|D],lower_or_digit(C),forall(member(E,B),name_char(E)),memberchk('_',D).
|
||||
citation_token(A,B):-(sub_atom(A,C,1,D,')')->true;C=999),(sub_atom(A,E,1,F,',')->true;E=999),G is min(C,E),G>0,G<120,sub_atom(A,0,G,H,B).
|
||||
cited_name(A,B,C):-citation_marker(D,B),sub_atom(A,E,F,G,D),sub_atom(A,H,G,0,I),citation_token(I,C),name_shaped(C).
|
||||
cited_name(A,decision,B):-sub_atom(A,C,D,E,'decision('),sub_atom(A,F,E,0,G),decision_name(G,B),name_shaped(B).
|
||||
citation_resolves(doctrine,A):-doctrine(A,B).
|
||||
citation_resolves(invariant,A):-invariant(A,B).
|
||||
citation_resolves(decision,A):-decision(B,A,C).
|
||||
citation_body(A,B):-sub_atom(A,C,1,D,')'),!,C>0,C<200,sub_atom(A,0,C,E,B).
|
||||
trim_left(A,B):-sub_atom(A,0,1,C,' ')->sub_atom(A,1,D,0,E),trim_left(E,B);B=A.
|
||||
decision_name(A,B):-citation_body(A,C),sub_atom(C,D,1,E,','),!,F is D+1,sub_atom(C,F,G,0,H),trim_left(H,B).
|
||||
|
||||
Reference in New Issue
Block a user