diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index e35574614..f904b9214 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,11 @@ +## 2026-10-16 (5) — ce64-grammar-wiring-v1 — every CE64 upper-half field of a parsed triple from its source → `.claude/plans/ce64-grammar-wiring-v1.md` + +**Status:** D-CGW-0 in PR; D-CGW-1..12 PROPOSAL, test-first. + +- Ships the `deepnsm-v2::wire` field map, the Gadamer gate in front of counting, the W arc, the Haspelmath orientation and the peer loci. +- First gate: wire edges carry readings of 43..45 and 46..49 that existing readers interpret differently (pathology triad, inference mantissa); they stay in the probe until a versioned binding exists (D-CGW-1). +- Then: frequency default with antecedent-tested promotion (D-CGW-2), case-driven argument order (D-CGW-3), concessive resistance (D-CGW-5), counterfactual lanes to `Causes` (D-CGW-4), cross-cohort arcs (D-CGW-6), layout widenings behind the envelope audit (D-CGW-7/8). + ## 2026-10-16 (4) — catch-up: 14 plans of 2026-10-12..16 and 59 imported plans had no index entry **Status:** INDEX CATCH-UP; no plan status changes. The 14 dated plans below were read at their header and status line; the 59 imported ones were not read. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index bbf615eb6..8f0da59c2 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,8 @@ +## 2026-10-16 — CE64 grammar wiring (branch `ccr-9f3b8075-74u47l-ce64-grammar-wiring`, in PR) + +- `deepnsm-v2::wire` writes every CausalEdge64 field of a parsed triple from a named source, with the Gadamer gate (`contract::revision`) deciding before anything is counted; W is the statement's Markov arc in a `WitnessTable<64>`; 43..45 is the Haspelmath dative-map function; loci carry Antecedent, TEKAMOLO, Quorum and Contradiction pointers. Probe: `lance-graph-quack/examples/wired_reasoning_probe.rs`. Plan: `ce64-grammar-wiring-v1.md` (D-CGW-0..12). +- Contract inventory delta: none (consumes `revision`, `witness_table`, `causal_witness`, `epistemic_state5`). `deepnsm-v2` gains `wire` and `DativeFunction::ALL`; `causal-edge` gains `activation` / `novelty_wisdom` accessors. + ## 2026-10-16 — board catch-up for merged PRs #1388–#1588 (in PR) - **`PR_ARC_INVENTORY.md`:** 115 merged PRs between #1388 and #1588 had no entry; each now has one, written from its description and file list, in date order. Hygiene-only PRs (#1527, #1528, #1563, #1568, #1583) get none, per the termination clause. diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index bfa9aba7c..c36037185 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,10 @@ +## 2026-10-16 — lance-graph PR #1590 (merged `2a6d502d`, branch `ccr-9f3b8075-74u47l-violation-banners`, head `d43a6ac5`, 1 commit) — board: mark the gather designs as violations; point at D-HOLO H14c/H14d + +- **Added:** capital-letter VIOLATION banners on plans `wcoj-closing-atoms-v1`, `quack-multitable-bind-v1`, `metta-datalog-assimilation-v1-council-v3`, the D-CMR-5 row, the joins line of `relalg-v1`; entries `wcoj-closing-atoms-council`, `wcoj-probes-1-3`, `coresearch-wcoj-leapfrog`, `asm-accepted-proposals`; STATUS rows D-QMB-0..5, D-ASM-11/11A/11B, D-CMR-5, D-RA-4; both 2026-10-16 INTEGRATION_PLANS entries; `TD-NO-BOUNDED-U32-GATHER-1`. Each names `d-holo-h14c-thought-first-v1.md` and `d-holo-h14d-logic-mask-read-v1.md` as the correct plans. +- **Locked:** a read at a position taken from another lane's values (gather, foreign lanes, the preimage hop) is a violation; nothing is deleted. +- **Deferred / open:** the gather surface that already exists in code is a separate change. +- **Confidence:** n/a (board). + ## 2026-10-16 — lance-graph PR #1588 (merged `399c8558`, branch `ccr-9f3b8075-74u47l-cats-validity`, head `ba4d0b19`, 3 commits) — sap: optional CATS code fields bind three-valued through validity planes - **Added:** `CatsBatch::bind` builds one validity plane per optional `U32` field (`OPTIONAL_CODE_FIELDS = [6, 7, 8, 16, 20, 21]`, unit test pins it against `FIELDS`); `CatsBatch::masks(live)` returns live first, then the validity planes; `CatsBinder::field` binds optional fields with `validity: Some(validity_mask(..))`. Files: `lance-graph-sap` `bind.rs`, `binder.rs`, `tests/binder.rs`; two hypothesis notes in `lance-graph-glove-parity` `basis.rs`; `LATEST_STATE.md`, `PR_ARC_INVENTORY.md`. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 3ebe128c3..8dfda1d48 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -1,3 +1,25 @@ +## D-CGW — CE64 grammar wiring: every upper-half field of a parsed triple from its source (2026-10-16) + +Plan: `.claude/plans/ce64-grammar-wiring-v1.md`. Entry: `entries/2026-10-16-grammar-coordinates-annotation-wiring.md` §7–§11. Related: D-SMD-2 (grammar topology from Haspelmath maps), D-SAG-CF-1 (counterfactual contract), D-LXC-29 (8 rails), EW64 OD-5. + +| D-id | scope | status | gate / falsifier | +|---|---|---|---| +| **D-CGW-0** | `deepnsm-v2::wire`: CE64 0..63 from named sources; Gadamer gate before counting; W = Markov arc in `WitnessTable<64>`; activation = update-caused frequency change; 43..45 = Haspelmath function; Antecedent / TEKAMOLO / Quorum / Contradiction loci; qualia register; `wired_reasoning_probe` | In PR | 229 tests; probe 741 rows, 5 rules, 0 violations; disable runs red per entry §7–§11 | +| **D-CGW-1** | one global meaning per CE64 field, held in the `causal-edge` contract (no per-tenant readings; superseded the same day); amend 43..45 to orientation with all triad readers migrated | Queued | one code table shared by accessor, writer and readers; field isolation 43..49. **Gate:** wire edges stay in the probe until this lands | +| **D-CGW-2** | frequency-ranked default reading (COCA, DeReKo) + antecedent-tested promotion of the alternative (relative pronoun) with agreement | Queued | `der Mann, der …` promoted; `der Mann gab der Frau …` silent; English `that` promoted without a COCA Rel tag | +| **D-CGW-3** | case morphology decides argument order in the parser (`Den Hund biss der Mann`) | Queued | OVS flips, SVO unchanged, syncretic case unresolved; UD German GSD dev measured | +| **D-CGW-4** | counterfactual lanes: `CausalCandidate` → `Causes` only with `IncreaseEligible` ∧ `Necessary` | Queued | necessary / dispensable / not-run triple | +| **D-CGW-5** | concessive markers as gate resistance and refuted implication | Queued | marked vs unmarked contest | +| **D-CGW-6** | witness arcs across 63-row cohorts (196/741 probe rows broke) | Queued | arc walk across cohorts; `arc_breaks` 0 for the fixture | +| **D-CGW-7** | loci 24 → 32 × i4 over the full 16-byte lane | Queued | envelope audit; field isolation over 32 slots | +| **D-CGW-8** | S/P/O identity as 8 × u8:u8 rails beside the u8 palette | Queued | decision: palette stays in 0..23 | +| **D-CGW-9** | 46..49 stays the ISA inference mantissa (law); activation moves out of the edge until the contract gives it a field | Queued | every wire edge decodes through the mantissa decoder as the inference it states | +| **D-CGW-10** | `ce64-semantic-upper-half.md`: the grammar reading of 43..45 declared beside the triad | Queued | doc only | +| **D-CGW-11** | Judicantis and multifunctional forms without a lane: measure before adding context rules | Queued | measurement on a corpus with attested forms | +| **D-CGW-12** | word → qualia index source (WordNet × Jina) | Deferred (operator) | — | +| **D-CGW-13** | all oracles (Java OpenNARS, ONA, MeTTa, OpenCog, MORK) read out on the same CE64 fields | Queued | D-ORACLE readout | +| **D-CGW-14** | OpenPSI modulators as a qualia device profile into the #1571 PCS (demands stay goals) | Queued | — | + ## Catch-up (2026-10-16) — D-ids named by merged PRs that had no row Found by the PR-arc catch-up over #1388–#1588. HubSPO-rs D-ids (D-IAM-2, D-IAM-3, D-QRY-1) belong to that repo's board; D-K16 and D-V3M were renamed in their own PRs (to D-KEY16, D-HPR). diff --git a/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md new file mode 100644 index 000000000..5732a4920 --- /dev/null +++ b/.claude/board/entries/2026-10-16-grammar-coordinates-annotation-wiring.md @@ -0,0 +1,275 @@ +# 2026-10-16 — Grammar on coordinates: annotations, TEKAMOLO × Antecedent loci, AriGraph episodic vs epistemic, CE64 bits 42 / 52 / 59..63 + +**Status:** OPEN. A breadcrumb ledger for one design thread; it feeds the +loose ends of `2026-10-16-board-catchup-open-items.md` (#1589). It decides +nothing. Every claim cites the file it was read from (read in full, or through +a Haiku substrate map whose items were spot-checked against source); every +open point says what would close it. + +## 0. What landed in this thread (local commits, not pushed) + +| commit | what | measured (v4, tier printed by the program) | +|---|---|---| +| lance-graph `b0046b30` | `lance-graph-quack/examples/ruff_taint_probe.rs`: ruff_metta_spo's interpreter-only fixpoint recomputed as `Any(And(Plane(E), Plane(T_rep)))` per head extent | Hyperon 147 heads / PeTTa 1005 heads: 0 mismatches vs ruff; zero steps mismatch by exactly the call-tainted heads (9 / 71); dropping `T_rep` from the AND goes red | +| lance-graph `b4a35521` | `lance-graph-quack/examples/semantic_map_probe.rs`: Haspelmath (2003) grams as a gram × function plane over a function × function plane; contiguity = the same closure restricted to the gram's row | all 18 grams of Figs. 8.1/8.6/8.9/8.12 contiguous; (4b) broken by French à, (4c) by German zu; Turkish old present (23) caught; both disables red | +| ruff `4513c50af9` | `ruff_metta_spo/examples/metta_census.rs --dump` writes `heads.tsv` / `calls.tsv` | input of the taint probe | + +**Prior art missed, recorded so it is not repeated:** +`crates/deepnsm-v2/src/haspelmath.rs` already implements Fig. 8.1 continuity +(`continuity`, `FormAttestation` with `evidence_root`, `MapVerdict::Incomplete` +for incomplete coverage) at the FSM → hydrate → CE64 seam. The quack probe is +the same check on planes and covers Figs. 8.6–8.12 and (4)/(23), which +haspelmath.rs does not; it has no `Incomplete` verdict. OPEN: fold one into the +other (haspelmath.rs is the in-tree home; the probe's extra maps become its +fixtures) — not done. + +## 1. Datalog vs NAL vs ONA vs relalg as annotation readings of one fact + +| reading | where it lives (read) | what it adds to a fact at a coordinate | +|---|---|---| +| relalg / Datalog | `lance-graph-relalg/src/ir.rs` (`Rel`, `Atom`, `Measure`; no string by const assert); plan `relalg-v1.md` D-RA-3 (rules as algebra values), D-RA-4 (bounded μ) | set membership only; no truth, no stamp. μ and joins are "not built" (`lib.rs` doc, plan §4) | +| NAL | `lance-graph-nal/src/lib.rs`: key = NodeGuid read as `nal-statement-v1` `[classid, S, copula, P, id, 0, 0]` (lib.rs:7); f/c are value lanes beside the key; `HORIZON = 1` (lib.rs:216), `MAX_CONFIDENCE_CODE = 252` (lib.rs:220); revision refuses evidence without a stamp, `NalError::MissingProvenance` (lib.rs:515, nal.rs:298) | truth (f, c) + evidential stamp, revision gated on independence | +| ONA | oracle only (rules: no NARS clone); the NAL crate's constants are named after it | external answers to compare against | +| MeTTa | ruff `ruff_metta_spo` (harvest + classify) | which heads lower, call graph | + +Working model (not decided): one fact = one coordinate; each reading is an +**annotation plane or lane beside the key**, never a second store and never a +string. relalg supplies membership, NAL supplies (f, c, stamp) lanes, ONA and +Hyperon are oracles. Gap to watch: AriGraph's `TripletGraph::revise_with_evidence` +(`lance-graph/src/graph/arigraph/triplet_graph.rs:904`) revises on an exact +string match with **no stamp**, so it cannot tell independent evidence from a +repeat — the exact case the NAL crate refuses (and the rule "FirstOfRun is not +evidential independence"). + +## 2. TEKAMOLO × relativPronomen (Antecedent) — the 24 × i4 loci in DeepNSM-v2 + +Read in full: `deepnsm-v2/src/tekamolo.rs`, `deepnsm-v2/src/loci.rs`, +`lance-graph-contract/src/causal_witness.rs` (`Locus`), and the fsm.rs map. + +- The register: `CausalWitnessFacet`, 12 bytes = 24 signed i4 loci (−8..+7 + event offsets); 16 are named (`causal_witness.rs:116`): Temporal 0, Kausal 1, + Modal 2, Lokal 3, S/P/O meaning 4–6, **Antecedent 7** ("relativPronomen → its + antecedent"), BasinAnchor 8, SupportedBy 9, Supports 10, RunbookEvidence 11, + QualiaReference 12, MeaningLevel 13, Quorum 14, Contradiction 15. 8 slots are + unnamed. +- Who writes loci today: only `loci.rs` (`loci_from_triples`), and only + `Locus::Antecedent`, only for **pronoun subjects**, by recency or selectional + fit, refusing thin margins and anything beyond −8 (a basin edge, not an + overflow). A test pins that no other locus is ever written. +- **Gap G1 — relative-clause antecedent is computed but not exported.** The FSM's + one-level relative-clause sub-machine holds the antecedent in a register + (`fsm.rs:146` field `antecedent`, set at `fsm.rs:277`) and emits it inside the + SPO triple (object-relative: `Spo::new(es, verb, antecedent)`, `fsm.rs:232`). + The module doc calls it "the FSM-side feeder for the ±8 antecedent pointer in + `wave`" (`fsm.rs:32-33`, `:62-63`), but no code writes `Locus::Antecedent` + from it. Closing it: the parser reports, per emitted triple, the stream index + of the triple whose subject is the antecedent; loci writes `-(i - j)` when it + is within reach. +- **Gap G2 — TEKAMOLO lanes are filled, TEKAMOLO loci are not.** `tekamolo.rs` + writes the `ValueTenant::Tekamolo` facet (`LaneAddress {relation, rank}`, what + kind of circumstance); `LaneHypothesis.at` (`tekamolo.rs:162`) records the + TOKEN index that stated it. Loci 0–3 are EVENT offsets in the triple stream + (`loci.rs`: "one event per triple, at version = its stream index"). Unit + mismatch: a token index is not an event offset, so `at` cannot be copied into + a locus; it needs the token → triple-index map from the parse. +- The two meet: a relative clause ("der Mann, **der** gestern kam") carries + both an Antecedent pointer and its own TEKAMOLO lanes; the circumstance + belongs to the embedded event, the pointer to the matrix one. Which event a + lane attaches to is open. + +## 3. AriGraph episodic vs epistemic + +- **Episodic** exists: `EpisodicMemory` (`episodic.rs:64`), episodes with + `step`, observation text and `triplets: Vec` (`episodic.rs:50`) — + strings as content, against the no-strings rule; `EpisodicBasins::rail_rows` + (`episodic.rs:215`) moves basins onto `ValueTenant::EpisodicBasin` (D-ACR-6); + `episodic_search` = AriGraph Eq. 1. +- **Epistemic** has no AriGraph type. It lives in two places outside it: CE64 + bits 59..63 (`EpistemicState5`, `causal-edge/src/layout.rs:102-115`; meaning + per `(classid, rail, generation)` in `lance-graph-contract/src/epistemic_state5.rs`) + and the planner's read gate `EpistemicMode` (`lance-graph-planner/src/temporal.rs:77`, + Strict / Aware / Retro by rung). +- Working model: episodic = WHEN and IN WHICH EXPERIENCE (versions, steps, + basins); epistemic = HOW WELL KNOWN (topology × certification). Neither should + be inferred from the other. OPEN: AriGraph triplets carry `TruthValue` f32 and + a `timestamp`, but no epistemic state and no stamp. + +## 4. CE64 bits 42, 52, 59..63 (`causal-edge/src/layout.rs`, `pearl.rs`, `plasticity.rs`, `isa.rs`) + +| bits | field (code) | what the named bit means | +|---|---|---| +| 40..42 | Pearl `CausalMask`, 3 plane bits | **bit 42 = the S plane bit** (`CausalMask::S = 0b100`): set in S, SO (association), SP (confounder detection), SPO (counterfactual); clear in O, P, PO (intervention) | +| 50..52 | Plasticity, per plane hot/frozen (`isa.rs:360` `Field::Plasticity`) | **bit 52 = O-plane hot** (`PlasticityState::O_HOT = 0b100`) | +| 59..63 | `EpistemicState5`, one 5-bit field = topology (59-60) | certification << 2 (61-63) | D-EPI-CANON-0 | + +- **Two readings of 50..52 disagree.** The code names them plasticity; session + rule S7 (2026-10-12) records them as carrying the Staunen/witness ladder plus + a free-energy follow-up signal. #1589's N1 adds: `syllogize` writes bits 50..52 + as `0b111` and zeroes witness and epistemic bits. OPEN: which reading is the + contract; until decided, nothing new writes 50..52. +- `haspelmath.rs:10-13` keeps CE64 bits 24..63 at ZERO for grammar-only edges + ("syntax is not NARS confidence, Pearl causation, evidence independence or + epistemic maturity"). Any wiring of annotations into CE64 must keep that + boundary: a grammatical fit never sets Pearl, plasticity or epistemic bits. + +## 5. Open decisions (for the operator) + +| # | Decision | Recommendation | Why | Caveats | Needs from you | +|---|---|---|---|---|---| +| 1 | Annotation shape | lanes/planes beside the key, one per reading | one fact, many readings, no second store | relalg has no truth; NAL needs stamps the parser does not produce | OK on the model | +| 2 | G1 (relative antecedent → `Locus::Antecedent`) | build first, test-first, in deepnsm-v2 | the value is already computed; only the export is missing | needs triple-index provenance from `parse_readings` | go / no-go | +| 3 | G2 (TEKAMOLO → loci 0..3) | after G1, with the token → event map | lanes say what, loci say where | unit mismatch token vs event | go / no-go | +| 4 | CE64 bits 50..52 | settle the contract before any writer | code and rule S7 disagree; N1 shows a writer already clobbers it | — | ruling | +| 5 | Meaning of "42 52" in this request | treat as S-plane presence (42) and O-plane plasticity (52) per code | that is what the code says | may have meant a range | confirm | +| 6 | Haspelmath duplication | fold the quack probe's maps into `haspelmath.rs` fixtures | one home | haspelmath.rs is deepnsm-v2, the probe is quack | go / no-go | +| 7 | AriGraph revision without stamps | route revision through the NAL stamp gate | independence is otherwise unknown | AriGraph triplets are strings | go / no-go | + +## 6. Not searched + +ONA source (oracle only, not opened here); Hyperon's own grammar of +contexts beyond ruff's harvest; the 8 unnamed loci's intended use; any plan +that may already assign them (grep `Locus` in `.claude/plans` not run). + +## 7. Wiring landed (2026-10-16, later the same day) + +`crates/deepnsm-v2/src/wire.rs` fills every field of the CE64 semantic +upper half on each parsed triple, each from one named source (module doc +has the table). `run_harness` is unchanged and still emits the grammar-only +edge; `wire` is the annotation layer above it. + +| bits | field | source | +|---|---|---| +| 24..39 | F/C | `lance-graph-arm-discovery::arm_to_truth_u8`, rule `(S,P) → O` over independent verses, k = 1 | +| 40..42 | Pearl | TEKAMOLO Kausal lane on the event → `PO`, else `SO` | +| 43..45 | orientation | clause path (`fsm::parse_to_spo_traced`): 1 Main, 2 Carried, 3 SubjRelative, 4 ObjRelative, 0 untraced | +| 46..49 | activation | pressure `pos − neg`, clamped i4 (`CausalEdge64::activation`, same bits as `inference_mantissa`) | +| 50..52 | novelty → wisdom | per-statement Markov rung, 1 Staunen ..= 7 wisdom (`novelty_wisdom`, same bits as plasticity) | +| 53..58 | belief update | W1/W2/W3 window of the last three updates: 1 corroborate, 2 contest, 3 dependent | +| 59..63 | epistemic | topology from the clause path; Associated → Related (≥2 independent, pressure > 0) → CausalCandidate (≥2 independent Kausal verses); never Causes | + +Closes G1 (relative antecedent → `Locus::Antecedent`) and G2 (TEKAMOLO → +loci 0..3, via `ClauseRead::at` and the triple's `closed_at`). + +Tests: 10 in `wire`, each guard disable-verified red on exactly its named +test (rival reset, dependent kind, resolved gate, pressure gate, two causal +verses, Kausal → PO, orientation code, antecedent locus, contest shift, map +link). `f_and_c_against_nal_revision_of_units` pins that arm `f` equals NAL +revision of unit evidence, and arm `c` equals it only when uncontested. + +Reasoning AST: `lance-graph-quack/examples/wired_reasoning_probe.rs` states +five field rules as Datalog (R1 `pearl_po ↔ kausal`, R2 `cert_rel ↔ +support2 ∧ press_pos`, R3 `candidate → cert_rel`, R4 `climbed → support2`, +R5 `direct ↔ main`) and runs each as a quack filter over presence planes of +541 wired rows: 0 violations, counts equal to a row oracle, every plane +non-trivial, one flipped output bit breaks exactly its rule. + +Not done, recorded so it is not lost: + +- The orientation map's contiguity check has no consumer: over the two + relative functions it is always true, so a per-relativizer probe would be + vacuous. +- `deepnsm_v2::belief::BeliefArena` and `lance-graph-nal` do not consume + wired rows yet. The arena's `Stamp` folds sources modulo 64 (false + overlap past 64 verses); `lance-graph-nal` has no relational copula for + `(S, verb, O)` (R10). +- AriGraph `revise_with_evidence` still revises without stamps. +- The 144 verbs (`contract/src/grammar/verb_table.rs`, 12×12 in a 16×16 + space with TEKAMOLO slot priors) are not connected to the P code or the + Pearl rung. + +## 8. Kausal claims through the belief arena; qualia separation measured + +- `wire` now returns `Wired { rows, report, facts, causes, events }`. Every + row is a unit observation `S –Rel(P)→ O` in a facts `BeliefArena`; every + `weil` claim a unit observation `cause ⇒ effect` (event terms) in a causes + arena, both stamped `Stamp::source(verse)`. `CausalCandidate` now requires + the implication to rest on ≥2 disjoint stamps; the per-statement + causal-verse counter is gone. Five disables red (causes stamp, facts stamp, + two sources, no self-implication, cause = stating event); the two-sources + test was vacuous until its fixture put the plain verse first. +- Reasoning probe on 741 rows: 0 violations on R1–R5, 72 candidates, 201 + implications, 138 pooled. +- Qualia palette (#1571, 219 patches, PCS hundredths): nearest-neighbour L1 + min 25, p10 50, median 90, max 250; no duplicates; 95 / 219 nearest + neighbours in the same family. So any PCS reading within L1 12 of a patch + decodes to that patch uniquely. A "3σ" statement needs the noise σ of a + reading, which is the unmeasured A2B side (`ISS-QUALIA-A2B-CLUT-UNMEASURED`). + +## 9. Qualia register (approved proposals #1, #2, #4) + +- `WiredRow::qualia` is a `FacetCascade` (class id 0) read `G6D2`, six + `u8:u8` pairs: 0..2 = `(operand code : qualia index)` for S, P, O; pair 3 = + `(verb_table::morton_cell : 1)`, or `(0 : 0)` when the verb does not + classify. An adjective's index lands on its noun's S/O pair, an adverb's + on its event's P pair: one index, two meanings, told apart by the pair. +- Deviation from the proposal, found while building: `morton_cell(Becomes, + Present) == 0`, a real verb, so the cell cannot share a pair whose zero + means "absent", and a reserved cell may not be borrowed as a sentinel. The + cell got its own pair with a presence byte. +- `saw` stays unclassified (`verb_lexicon` refuses the homograph); German + surfaces never classify (the lexicon is English). +- Five disables red. Two tests were vacuous on their first fixture and were + fixed until the disable bit: the single-implication case (plain verse + first) and the sentence-bound adjective (only bites when the next + sentence's noun also occurs in the current sentence's triples). +- OPEN (#3, deferred by the operator): the word → qualia index source. + Candidate for later: WordNet senses measured against the 219 patches' + Jina embeddings (`data/qualia/calibrated_219.bin1024`). Until then the + caller supplies indices and every index defaults to 0. + +## 10. W is the Markov arc; activation is what the update caused (`e31cd087`) + +- W (53..58) indexes a per-cohort `WitnessTable<64>` (63 rows per cohort, + slot 0 = no prior). Each row's entry is `(verse + 1, facts-arena id)`; its + W points at the entry of the PRIOR state of the same statement, so walking + W from the newest row visits that statement's verses newest to oldest and + skips rival rows. A prior in an earlier cohort starts a new arc + (`arc_breaks`). The earlier 3-slot window (`shift_window`) is gone: it + ignored the existing `WitnessTable` contract. +- Activation (46..49) = the change of the statement's arm frequency over the + prior state, in eighths (`128` = unknown prior). A repeat of a settled + belief activates 0. The ladder and certification read the support balance + (`pos > neg`) instead. +- Tests: `w_walks_the_statement_arc_backwards`, + `a_cohort_boundary_breaks_the_arc`, plus re-pinned activation values. Four + disables red (activation as pos − neg, cohort check removed, balance + always true, W = previous row). +- Probe R2 now reads `2·support > antecedent`; 741 rows, 0 violations. +- OPEN: bits 46..49 are also `InferenceType`'s mantissa, and + `contract::counterfactual::deposit_counterfactual` writes −6 there to mark + the road not taken. Activation and that marker cannot share the bits. + +## 11. Gadamer gate, Haspelmath orientation, peer loci (`18c2363e`, `c9a3e13b`) + +- **Gate first (G5).** Each `(S, P)` group is an `InterpretiveHorizon` + (claims = objects, roots = independent verses, 256 bits each). Every + observation is passed through `GadamerRevision`; only `IncreaseEligible` + observations are counted, reach the facts arena, or climb. Claims are never + withdrawn, so a contest no longer resets the rival's rung (G3). A rival from + a verse already counted is `ContradictionPreserved`: suspended, no fact. + Rows carry the gate's kind and `contested`. +- **43..45 = Haspelmath.** The dative-map function the relation is read in + (Fig. 8.1): 0 core argument, 1..7 Direction, Recipient, Experiencer, + Purpose, Beneficiary, Predicative possessor, External possessor. Source: + the forms attested in the clause span (bounded by the sentence start), + narrowed by TEKAMOLO lanes (Kausal → Purpose, Lokal → Direction). Several + functions left → 0 and the row is unresolved. Judicantis has no code. The + clause path now feeds only the topology. The earlier clause-path code was + wrong. +- **Loci.** The antecedent is tested in both directions within −8..+7, + nearest first, as matrix subject or object (right-corner relatives were + missed before). Every row points at its nearest Quorum and Contradiction + peer in the whole row stream. +- Probe corpus (741 rows): 593 fusions, 8 suspended, 132 echoes, 423 quorum + and 609 contradiction pointers, 0 violations. `arc_breaks` 196: cohorts of + 63 rows cut many arcs. +- Nine disables red (four gate, five orientation/loci). +- Measured (DeReKo-2014, local only, CC BY-NC): the relative-pronoun reading + is a minority of every article form (`der` 4.6 %, `die` 13 %, `das` 9.6 %, + `den` 1.2 %); COCA has no relativizer tag at all (`that` = c 8.3 M / d + 5.0 M). Frequency gives a default only; the antecedent test promotes. +- OPEN: frequency-ranked promotion of an alternative reading in the parser; + case morphology for argument order (*Den Hund biss der Mann*); the + counterfactual lanes for `CausalCandidate` → `Causes`; concessive markers + as resistance; 32 × i4 loci over the full 16-byte lane; S/P/O as 16-byte + identities (8 × u8:u8) instead of the u8 palette. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 4cb428a34..fe5a4e894 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -359 entries, 2026-08-06 .. 2026-10-16. +360 entries, 2026-08-06 .. 2026-10-16. | date | entry id | finding | file | |---|---|---|---| @@ -36,6 +36,7 @@ exactly one of them, never both. | 2026-10-16 | `D-ASM-1` | | [2026-10-16-metta-datalog-assimilation-council-v3.md](2026-10-16-metta-datalog-assimilation-council-v3.md) | | 2026-10-16 | `ledger-update-dedup-1579` | | [2026-10-16-ledger-update-dedup-1579.md](2026-10-16-ledger-update-dedup-1579.md) | | 2026-10-16 | `ledger-revision-after-1576` | | [2026-10-16-ledger-revision-after-1576.md](2026-10-16-ledger-revision-after-1576.md) | +| 2026-10-16 | `grammar-coordinates-annotation-wiring` | | [2026-10-16-grammar-coordinates-annotation-wiring.md](2026-10-16-grammar-coordinates-annotation-wiring.md) | | 2026-10-16 | `D-HOLO-H14d` | | [2026-10-16-d-holo-h14d-logic-mask-read.md](2026-10-16-d-holo-h14d-logic-mask-read.md) | | 2026-10-16 | `coresearch-wcoj-leapfrog` | | [2026-10-16-coresearch-wcoj-leapfrog.md](2026-10-16-coresearch-wcoj-leapfrog.md) | | 2026-10-16 | `board-catchup-open-items` | | [2026-10-16-board-catchup-open-items.md](2026-10-16-board-catchup-open-items.md) | diff --git a/.claude/plans/ce64-grammar-wiring-v1.md b/.claude/plans/ce64-grammar-wiring-v1.md new file mode 100644 index 000000000..4498f405e --- /dev/null +++ b/.claude/plans/ce64-grammar-wiring-v1.md @@ -0,0 +1,297 @@ +# CE64 grammar wiring v1 — every upper-half field of a parsed triple from its source + +**Status:** D-CGW-0 SHIPPED IN PR (this branch); D-CGW-1..12 PROPOSAL, test-first, +nothing built. Board entry: `entries/2026-10-16-grammar-coordinates-annotation-wiring.md` +§7–§11 (what landed, with measurements and disable runs). + +**Scope.** `crates/deepnsm-v2/src/wire.rs` and the readers of the edges it writes. +A parsed verse becomes rows; each row's `CausalEdge64` bits 0..63 are written from +one named source. The probe `crates/lance-graph-quack/examples/wired_reasoning_probe.rs` +checks the field rules as Datalog over presence planes against a row oracle. + +**Reading order.** The CE64 field meanings follow +`docs/architecture/ce64-semantic-upper-half.md` (WORKING-MODEL) and the frozen +revision invariants G1–G7 (`entries/2026-10-14-revision-layer-invariants.md`). +Where this plan and those disagree, they win and this plan is corrected. + +## 0. What D-CGW-0 delivers (this PR) + +| bits | field | source in `wire.rs` | +|---|---|---| +| 0..23 | S, P, O | the frozen operand ABI of `run_harness` | +| 24..31, 32..39 | F, C | `arm_to_truth_u8`, rule `(S, P) → O`, counted only after the Gadamer gate admits | +| 40..42 | Pearl ladder | `PO` when a Kausal lane is stated on the event, else `SO` | +| 43..45 | orientation | the Haspelmath dative-map function (0 core argument, 1..7 Direction, Recipient, Experiencer, Purpose, Beneficiary, Predicative possessor, External possessor) from the forms attested in the clause span, narrowed by TEKAMOLO lanes | +| 46..49 | activation | the change of the statement's arm frequency this sentence caused, in eighths | +| 50..52 | novelty → wisdom | rung 1 Staunen ..= 7; climbs on an admitted, resolved observation with positive support balance; a contest never resets it | +| 53..58 | W | slot into the cohort's `WitnessTable<64>`: the witness of the same statement's prior state (the Markov arc) | +| 59..63 | epistemic state | topology from the clause path, certification from support balance and the causes arena | + +Besides the edge, every row carries `CausalWitnessFacet` loci (Antecedent, TEKAMOLO, +Quorum, Contradiction, all −8..+7 over the row stream), the qualia register +(`FacetCascade`, `G6D2`), the gate's `RevisionKind` and `contested`. + +Verification: 229 deepnsm-v2 tests; clippy `-D warnings` on the lib; the probe +(741 rows, 5 rules, 0 violations, each rule broken by a one-bit flip); disable runs +red for every guard listed in the board entry (§7–§11). + +## 1. Gate: nothing below leaves the probe until D-CGW-1 lands + +Two fields written by `wire.rs` already have other readers with other meanings: + +| bits | wire writes | existing readers read | +|---|---|---| +| 43..45 | Haspelmath function code | the per-plane pathology triad: `CausalEdge64::{s,p,o}_pathological`, `concern_level`, `network.rs` Simpson's-paradox detection, `compose` (`edge.rs:808`), `moore_nars16_probe`, `pearl_ladder_probe`, `witness_angle_probe` | +| 46..49 | activation (signed change) | the inference mantissa (+6 Intervention, −6 Counterfactual, NARS rule index): `pearl.rs`, `chain_counterfactual.rs`, `contract::counterfactual::deposit_counterfactual`, `cognitive-shader-driver/src/mailbox_soa.rs` (production) | + +So a wire edge handed to `CausalNetwork` would read code 1 (Direction) as +"S pathological", and one handed to the mailbox would read activation +4 as a NARS +rule. Today wire edges go only to the probe. **They must not go anywhere else +before D-CGW-1.** Under the contract rule (D-CGW-1) this is not a reading mismatch +to bind around: these are contract violations by `wire.rs`, to be fixed by +amending the contract (43..45) or by `wire.rs` conforming (46..49). + +## 2. Work items + +Each item lists: goal; tests to write first (can fire + stays silent, non-trivial +inputs); files; dependencies; the decision it needs, if any. + +### D-CGW-1 — one global meaning per CE64 field, held in the contract + +> ⊘ **Superseded 2026-10-16, same day:** the first version of this item was a +> per-tenant "versioned reading binding" for 43..45 and 46..49. That is wrong. +> CE64 is an ISA wire format, like TCP/IP or BF16: every field has one meaning +> everywhere, fixed by the contract, and nothing interprets it per tenant or per +> edge. The mantissa bits are law. Like GMT, the edge is the fixed point every +> reading is measured against, so it carries no qualia of its own: qualia and +> other readings sit beside it (the row's qualia register), never inside it. + +- **Rule.** The meaning of each field lives once, in the `causal-edge` contract + (`layout.rs`, the `edge.rs` accessors, and `docs/architecture/ce64-semantic-upper-half.md`). + Every writer conforms to it, `wire.rs` included. A field changes meaning only by + amending the contract once, with every reader migrated in the same change + (`I-LEGACY-API-FEATURE-GATED`). +- **Where `wire.rs` breaks it today.** + - 43..45: the contract says S/P/O pathology triad; `wire.rs` writes the + Haspelmath function. The operator ruled 43..45 = orientation / syntax + (Haspelmath for text). So the contract is amended to orientation, and the + triad's readers (`*_pathological`, `concern_level`, `network.rs` Simpson + check, `compose`) migrate or move their signal elsewhere. + - 46..49: the contract says inference mantissa, which the ISA executes + (+6 Intervention, −6 Counterfactual, NARS rule index). `wire.rs` writes + activation there. Because the mantissa is law, activation cannot share these + bits; see D-CGW-9. +- **Tests first.** For each amended field: the contract accessor, a writer and + every migrated reader agree on one table of codes; the field-isolation matrix + over 43..49; a reader of the old meaning that is not migrated fails to compile + (the accessor is renamed), never silently misreads. +- **Files.** `causal-edge/src/{layout.rs,edge.rs,network.rs,v2_layout_tests.rs}`, + the readers listed in §1, `ce64-semantic-upper-half.md`. +- **Open.** Moore for non-verbal Cartesian data and Haspelmath for text are two + code tables for 43..45. Under one global meaning they must be one table, or + orientation needs a code space both share. + +### D-CGW-2 — frequency-ranked default, antecedent-tested promotion of the alternative reading + +- **Goal.** Corpus frequency gives each ambiguous token a default reading (a + best-effort median, never a decision). When the readings contradict, the + alternative reading is promoted as a constituent of a different parse if the + grammar test passes: the relative pronoun's antecedent within −8..+7, with + morphological agreement. +- **Measured basis (2026-10-16, DeReKo-2014 local only, CC BY-NC, never committed).** + The relative-pronoun reading is a minority of every article form: `der` ART + 241.4 M / PRELS 11.5 M (4.6 %), `die` 13 %, `das` PDS 11.6 M / PRELS 6.3 M (9.6 %), `den` + 1.2 %, `dem` 3.3 %; `welche` PRELS dominant. COCA has no relativizer tag: `that` + c 8.3 M / d 5.0 M / r 0.1 M, `which` d only, `who` p only. So the most frequent + reading is wrong exactly at a relative clause, and in English frequency cannot + even propose it. +- **Tests first.** + - `der Mann, der den Hund sah, ...`: the second `der` (frequency default ART) + is promoted to Rel because an antecedent noun of matching gender/number lies + within −8..−1 and the following clause lacks a subject. Stays silent: `der + Mann gab der Frau das Buch` (no comma clause, no gap) keeps ART. + - English `the man that saw the dog bit the cat`: `that` promoted to Rel by the + antecedent test although COCA has no Rel reading; silent on `he said that the + dog bit the cat` (no antecedent noun directly before). + - A promotion is recorded as a contradiction resolved by the grammar (the gate's + resistance), never as a silent re-tag. +- **Files.** `deepnsm-v2/src/{fsm.rs,lexical.rs,coca.rs}`; a DeReKo adapter beside + `coca.rs` (reads a local path, like `examples/dereko_profile.rs`; nothing + committed); `wire.rs` only consumes the result. +- **Dependencies.** None for English; the DeReKo adapter for German. +- **Decision.** Agreement features: STTS has no gender/case, so agreement needs UD + features (`Gender`, `Number`, `Case`) or a small article table. Recommendation: + UD features when present, else the closed German article paradigm as a table. + +### D-CGW-3 — case morphology decides argument order + +- **Goal.** `Den Hund biss der Mann` yields `SPO(Mann, biss, Hund)`: when case + contradicts position, case wins, and the triple is built from it in the parser + (the role decision is the FSM's, not a post-hoc swap in `wire.rs`). +- **Tests first.** OVS with unambiguous case flips roles; SVO with agreeing case is + unchanged; syncretic case (`die Frau sah die Katze`) keeps position and marks the + token ambiguous (the row is not resolved, nothing climbs). +- **Files.** `deepnsm-v2/src/fsm.rs` (`Reading` gains an optional case reading); + `wire.rs` (none beyond reading the parse). +- **Measure.** On UD German GSD dev (fetched locally, 15,041 lines, 5,604 `Case=` + features): share of transitive clauses where case and position disagree; parse + agreement with UD `nsubj`/`obj` before and after. +- **Dependency.** D-CGW-2's agreement source. + +### D-CGW-4 — counterfactual lanes: `CausalCandidate` → `Causes` + +- **Goal.** A row is certified `Causes` only when `RevisionVerdict::is_acceptable` + holds: the gate said `IncreaseEligible` AND a counterfactual said `Necessary`. + The counterfactual removes the cause's contributing verses from the causes arena + and the effect's arc and re-evaluates the effect's support (W0 vs W_X, D-SAG-CF-1 + §0). Candidates run in parallel, one per hypothesis lane (64-lane budget per + batch; 72 candidates in the probe corpus → two batches). +- **Tests first.** A cause stated by two independent verses whose effect is + supported only through it → `Necessary` → `Causes`; the same cause where the + effect has an independent third source → `Dispensable` → stays `CausalCandidate`; + a candidate never attacked stays `CausalCandidate` (`NotRun`, G6). +- **Files.** `wire.rs` (record contributors per candidate); the replay over the + arenas; `contract::revision` (no change; consumes `CounterfactualVerdict`). +- **Constraints.** A counterfactual belief is never written as observed truth; the + −6 tag does not go into 46..49 of a grammar-bound edge (D-CGW-1). Single cuts + miss overdetermination (D-SAG-CF-1 §1): joint cuts are a follow-up. + +### D-CGW-5 — concessive markers as resistance and as a refuted implication + +- **Goal.** `obwohl X, Y` / `X, aber Y`: the writer marks a contradiction on + purpose. The clash enters the gate as `resistance` (it ranks above an unmarked + one) and `obwohl X, Y` seeds the counterfactual test `X ⇒ ¬Y` refuted. +- **Tests first.** Marked contest → resistance set, kind unchanged where G1–G7 + decide it; unmarked contest → no resistance. `obwohl` adds one refuted-implication + observation to the causes arena with the verse stamp. +- **Files.** `tekamolo.rs` (`GrammarHeuristics` gains a concessive row class), + `wire.rs` (`Group::encounter` passes resistance). +- **Decision.** Concessive is not one of the four TEKAMOLO lanes. Options: a fifth + lane in the heuristics table, or a separate marker table. Recommendation: a + separate marker table, so the four lane addresses stay unchanged. + +### D-CGW-6 — witness arcs across cohorts + +- **Problem.** A cohort is 63 rows, so 196 of 741 probe rows started a new arc + (`arc_breaks`). Whole books break arcs at every boundary. +- **Options to measure.** (a) the first row of a statement in a new cohort writes a + back-link entry whose `spo_fact_ref` names the same facts-arena id, so the arc is + continued by fact identity, not by slot; (b) cohorts per statement group instead + of per reading position. +- **Tests first.** A statement seen in rows 10, 70, 130 walks back 130 → 70 → 10 + under the chosen option; `arc_breaks` = 0 for that statement. +- **Measure.** `arc_breaks` on the probe corpus and on a KJV book. + +### D-CGW-7 — loci: 24 → 32 × i4 over the full 16-byte lane + +- **Goal.** Tenant 14 reads `G24N4` (24 loci in the 12-byte payload of a 4+12 + facet, slots 16..23 reserved). With the classid already in the row key + (D-LXC-29), all 16 bytes can hold 32 loci. +- **Needs.** `v3-envelope-auditor` review; `ENVELOPE_LAYOUT_VERSION` decision; + EW64 OD-5 (`ew64-witness-unification-v1.md`). +- **Tests first.** Field isolation over 32 slots; slots 16..31 read 0 until set; + the existing 16 named loci keep their offsets. + +### D-CGW-8 — S/P/O as 16-byte identities (8 × u8:u8) instead of the u8 palette + +- **Goal.** The frozen operand ABI caps a verse at 256 operands (8:8:8 in bits + 0..23). The row already has a 16-byte identity and a 16-byte edge facet; reading + them as 8 rails of `u8:u8` (D-LXC-29 measured 8 rails: one tenant fits 99.41 % of + KJV verses vs 95.66 % with 6) removes the cap. +- **Decision.** Whether the edge's 0..23 stays the palette (hot, 3 bytes) with the + identity beside it, or the identity replaces it. Recommendation: keep 0..23 as the + palette and resolve through the identity rails; changing 0..23 breaks every ISA + reader. + +### D-CGW-9 — bits 46..49: the inference mantissa is law; where does activation go? + +- **Contract.** 46..49 is the inference mantissa the ISA executes. It keeps that + meaning globally (D-CGW-1). +- **Consequence for `wire.rs`.** Writing activation into 46..49 contradicts the + contract. Until the contract gives activation a field, `wire.rs` must write the + mantissa (or nothing) there and keep activation outside the edge (the row, or a + lane beside it). +- **Supersedes** the row-6 direction of the 2026-10-16 table ("activation owns the + bits for grammar-bound edges"), which assumed per-tenant readings. +- **Tests first.** Every edge `wire.rs` emits decodes through the ISA mantissa + decoder as the inference it states; activation is read from its own home and + equals the frequency change. +- **Decision.** Where activation lives: a row field beside the edge, or an amended + contract field. Recommendation: a row field until the contract is amended, + because no 4-bit field of the edge is free. + +### D-CGW-10 — the clause-path → orientation correction downstream + +- **Goal.** The board entry §7 table and the plan text that named 43..45 as the + clause path are corrected (done in §11 of the entry); `ce64-semantic-upper-half.md` + gains the grammar reading of 43..45 as a declared reading beside the triad. +- **Files.** `docs/architecture/ce64-semantic-upper-half.md` only. + +### D-CGW-11 — Judicantis and multifunctional forms + +- **Open.** Judicantis has no code (seven states named, one core-argument state). + A form allowing several functions with no TEKAMOLO lane leaves 0 and the row + unresolved. Measure how often on a real corpus with attested forms before + deciding whether a context rule beyond Kausal → Purpose / Lokal → Direction is + needed. + +### D-CGW-12 — word → qualia index source (deferred) + +- **Deferred by the operator.** Candidate: WordNet senses measured against the 219 + patches' Jina embeddings (`data/qualia/calibrated_219.bin1024`). Until then the + caller supplies indices. + +### D-CGW-13 — every oracle reads out on the same CE64 fields + +- **Why now.** Because each CE64 field has one global meaning (D-CGW-1), the + answers of independent reasoners can be compared on the same register: F and C + (24..39), the Pearl question (40..42), orientation (43..45), the inference + mantissa (46..49) and the certification ladder (59..63). This is the readout of + the existing oracle plan `d-oracle-fidelity-matrix-v1.md` (D-ORACLE), not a new + harness: Java OpenNARS (Pei Wang's lineage, `opennars/opennars`), ONA, MeTTa + (Hyperon and PeTTa, golden corpus #48), OpenCog (temporary oracle only, entry + `2026-10-16-opencog-oracle-scope.md`) and MORK (D-ASM-10). +- **Tests first.** One fixture, all oracles, each native answer recorded + verbatim; then a per-oracle projection onto the CE64 fields, where a field the + oracle has no notion of is written as "not comparable", never as 0. Falsifier: + two oracles that disagree in their native output must not project to equal + edges unless the disagreement lies in a field marked not comparable. +- **Fences.** Native outputs stay separate (D-ORACLE F9: non-parity does not mean + one is wrong). Oracle code runs outside the workspace; only inputs we generated + and recorded answers are committed. No blended score. +- **Needs.** D-CGW-1 (the contract amendment), D-ORACLE runners pinned. + +### D-CGW-14 — OpenPSI motivation as a qualia device (#1571) + +- **Shape.** OpenPSI is another *device* in the ICC sense of + `contract::qualia_profile` (#1571, D-QPROF-0), beside the engine telemetry and + the Ekman observer: its modulators (activation/arousal, resolution level, + selection threshold, securing rate) get a device → PCS profile, measured, not + declared. Arousal and resolution meet the PCS axes `arousal`, + `resolution_hunger` and `clarity` by name; every other mapping is fitted from + probe pairs, like the A2B CLUT the module says it does not ship yet. +- **What does not move.** The edge carries no qualia (D-CGW-1: the edge is the + fixed point). OpenPSI *demands* are goals, not felt states: they are never + revised by counting and stay with the Rubikon planning/evaluation path + (revision invariants G1–G7); only the modulators enter the qualia profile. + D-ORACLE row D already marks the OpenPSI ↔ Rubikon correspondence as a + hypothesis. +- **Tests first.** Drive one OpenPSI scenario as an oracle (outside the + workspace), record its modulator trace, fit the shared-axis transform, and + check it with a held-out trace; a guard that maps every trace to the same + palette index fails. +- **Source.** Written from the published PSI/MicroPsi model and OpenPSI's + observed behaviour, never from its source. + +## 3. Order + +D-CGW-1 → D-CGW-9 (same contract amendment) → D-CGW-2 → D-CGW-3 → D-CGW-5 → D-CGW-4 → +D-CGW-6 → D-CGW-7 / D-CGW-8 (layout, need the envelope audit) → D-CGW-10 / 11. +D-CGW-12 waits for the operator. D-CGW-13 follows D-CGW-1; D-CGW-14 follows +D-CGW-13's OpenPSI runner. + +## 4. Not in scope + +- No new opcode, interpreter or executor; the probe's rules run as quack filters. +- No production write path for wire rows. +- No change to `contract::revision` (G1–G7 are frozen). diff --git a/crates/causal-edge/src/edge.rs b/crates/causal-edge/src/edge.rs index fee1b1cf4..9f9aab322 100644 --- a/crates/causal-edge/src/edge.rs +++ b/crates/causal-edge/src/edge.rs @@ -1103,6 +1103,54 @@ impl CausalEdge64 { Self((self.0 & !W_MASK) | (((w as u64) & BITS6_MASK) << W_SHIFT)) } + /// Rung on the novelty → wisdom ladder over bits 50..52: `0` = not on the + /// ladder (unaddressed, the zero-fallback), `1` = Staunen (first sighting, + /// unresolved) ..= `7` = wisdom (settled). A rung is climbed one step at a + /// time and only while the step below holds (the Maslow shape); the + /// semantic pressure in bits 46..49 ([`Self::activation`]) is what moves it. + /// + /// These are the same three bits [`Self::plasticity`] reads. The CE64 + /// semantic upper-half contract (2026-10-16: 40..42 Pearl ladder, 43..45 + /// orientation/syntax, 46..49 activation i4, 50..52 entropy/novelty, 53..58 + /// belief-state update witness, 59..63 epistemic state) names this field + /// entropy/novelty; per-plane plasticity is the older reading of it. One + /// field, two vocabularies, no bits moved. A writer picks one vocabulary + /// per class and says which. + #[inline(always)] + pub fn novelty_wisdom(self) -> u8 { + ((self.0 >> crate::layout::PLAST_SHIFT) & crate::layout::BITS3_MASK) as u8 + } + + /// Return new edge with the novelty → wisdom rung (bits 50..52) set. + /// Values above 7 saturate to 7 rather than wrapping to a lower rung. + #[inline] + pub fn with_novelty_wisdom(self, level: u8) -> Self { + use crate::layout::{BITS3_MASK, PLAST_MASK, PLAST_SHIFT}; + let level = u64::from(level.min(7)); + Self((self.0 & !PLAST_MASK) | ((level & BITS3_MASK) << PLAST_SHIFT)) + } + + /// Semantic pressure over bits 46..49, a signed i4 (`+7` excitation, `0` + /// neutral, `-8` inhibition): the net evidence pushing a statement up or + /// down the [`Self::novelty_wisdom`] ladder. + /// + /// These are the same four bits [`Self::inference_mantissa`] reads. The + /// semantic upper-half contract (2026-10-16) names the field activation; + /// the ISA reads it as a signed NARS rule index. One field, two + /// vocabularies: an edge written with a pressure must not be decoded as an + /// inference rule ([`Self::inference_type`]), and the reverse. + #[inline(always)] + pub fn activation(self) -> i8 { + self.inference_mantissa() + } + + /// Return new edge with the semantic pressure (bits 46..49) set, clamped + /// to `-8..=7` rather than wrapping to the opposite sign. + #[inline] + pub fn with_activation(self, pressure: i8) -> Self { + self.with_inference_mantissa(pressure.clamp(-8, 7)) + } + /// Return new edge with bits 59..63 set to the 5-bit `EpistemicState5` /// code `code` (`0..=31`). The ONE canonical writer of the field: all five /// bits move together, nothing else moves. @@ -1331,6 +1379,22 @@ impl CausalEdge64 { pub fn with_w_slot(self, _w: u8) -> Self { self } + #[inline(always)] + pub fn novelty_wisdom(self) -> u8 { + 0 + } + #[inline] + pub fn with_novelty_wisdom(self, _level: u8) -> Self { + self + } + #[inline(always)] + pub fn activation(self) -> i8 { + 0 + } + #[inline] + pub fn with_activation(self, _pressure: i8) -> Self { + self + } #[deprecated( since = "0.2.0", note = "writes bits 59..63 under a reading that is not the declared EpistemicState5 one (TrustTexture, historical ReasoningBand, or untyped spare); its bits would then be consumed as canonical without a declaration. Write a state from `lance_graph_contract::epistemic_state5` with `with_epistemic_raw5`, or the topology factor with `with_topology`" @@ -1414,6 +1478,40 @@ impl std::fmt::Debug for CausalEdge64 { mod tests { use super::*; + /// Novelty reads and writes bits 50..52 only, saturates at 7, and is the + /// same register plasticity reads (one field, two vocabularies). + #[cfg(feature = "causal-edge-v2-layout")] + #[test] + fn novelty_wisdom_owns_bits_50_to_52_only() { + let base = CausalEdge64(0xFFFF_FFFF_FFFF_FFFF ^ (0b111 << 50)); + for level in 0..=7u8 { + let e = base.with_novelty_wisdom(level); + assert_eq!(e.novelty_wisdom(), level); + assert_eq!(e.0 & !(0b111 << 50), base.0, "no other bit moved"); + assert_eq!(e.plasticity().bits(), level, "same three bits"); + } + assert_eq!( + CausalEdge64::ZERO.with_novelty_wisdom(200).novelty_wisdom(), + 7 + ); + } + + /// Activation reads and writes bits 46..49 only, as a signed i4, clamps + /// instead of wrapping, and is the register the inference mantissa reads. + #[cfg(feature = "causal-edge-v2-layout")] + #[test] + fn activation_owns_bits_46_to_49_only() { + let base = CausalEdge64(0xFFFF_FFFF_FFFF_FFFF ^ (0b1111 << 46)); + for p in -8..=7i8 { + let e = base.with_activation(p); + assert_eq!(e.activation(), p); + assert_eq!(e.0 & !(0b1111 << 46), base.0, "no other bit moved"); + assert_eq!(e.inference_mantissa(), p, "same four bits"); + } + assert_eq!(CausalEdge64::ZERO.with_activation(100).activation(), 7); + assert_eq!(CausalEdge64::ZERO.with_activation(-100).activation(), -8); + } + /// The canonical byte form is little-endian and round-trips every bit, /// including bits 59..63 (byte 7, bits 3..7). #[test] diff --git a/crates/deepnsm-v2/Cargo.toml b/crates/deepnsm-v2/Cargo.toml index fed14a490..80d2baf71 100644 --- a/crates/deepnsm-v2/Cargo.toml +++ b/crates/deepnsm-v2/Cargo.toml @@ -30,6 +30,9 @@ lance-graph-contract = { path = "../lance-graph-contract", features = ["guid-v3- # The Pearl 2³ ladder: `fsm::answered_questions` returns its `CausalMask`, the # same eight masks CausalEdge64 packs at bits [40:42]. Zero-dependency crate. causal-edge = { path = "../causal-edge" } +# F/C of wired edges: arm-discovery's `arm_to_truth_u8` (the OpenCog/learn- +# adjacent rule miner). Zero dependencies by default. +lance-graph-arm-discovery = { path = "../lance-graph-arm-discovery" } # `test = true` makes `cargo test` run the example's own `#[cfg(test)]` tests # (the D-LXC-1 tagger). Cargo only compiles an example by default; its `main()` diff --git a/crates/deepnsm-v2/src/fsm.rs b/crates/deepnsm-v2/src/fsm.rs index 3eff8e4ee..fe56b9666 100644 --- a/crates/deepnsm-v2/src/fsm.rs +++ b/crates/deepnsm-v2/src/fsm.rs @@ -371,6 +371,151 @@ pub fn answered_questions(tokens: &[Tagged]) -> Vec { out } +/// How a triple's slots came off the surface: the clause path that emitted it. +/// +/// This is what `CausalEdge64`'s orientation field (bits 43..45) reads — which +/// way the relation is read — and what the relative-clause antecedent pointer +/// (`Locus::Antecedent`) is derived from. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Origin { + /// Matrix clause, subject taken from the clause's own surface subject. + Main, + /// Matrix clause, subject carried over from the previous triple's object + /// (serial-verb chain: "gave **him** the charge"). + Carried, + /// Object-relative clause ("rat that cat bit"): the OBJECT is the + /// relativizer's antecedent, read before the embedded subject and verb. + ObjRelative, + /// Subject-relative clause ("dog that chased cat"): the SUBJECT is the + /// relativizer's antecedent. + SubjRelative, +} + +impl Origin { + /// Is the triple emitted inside a relative clause? + #[must_use] + pub const fn is_relative(self) -> bool { + matches!(self, Origin::ObjRelative | Origin::SubjRelative) + } +} + +/// One triple with the clause path that emitted it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Traced { + /// The triple, exactly as [`parse_to_spo`] emits it. + pub spo: Spo, + /// Which clause path emitted it. + pub origin: Origin, + /// Sentence ordinal within the input (incremented at every `Stop`). + pub sentence: u32, + /// Input index of the token that closed the triple. + pub closed_at: usize, +} + +/// [`parse_to_spo`] with each triple's [`Origin`]. Steps the SAME transition +/// table; the triples are identical to [`parse_to_spo`]'s, in the same order. +#[must_use] +pub fn parse_to_spo_traced(tokens: &[Tagged]) -> Vec { + let mut out = Vec::new(); + let mut core = Core::START; + let mut sentence = 0u32; + for (i, &t) in tokens.iter().enumerate() { + if t.pos == Pos::Stop { + core.state = State::Start; + core.rel = Rel::None; + sentence += 1; + continue; + } + // The path is decided by the registers BEFORE the closing token. + let origin = match core.rel { + Rel::ObjSubject(_) => Origin::ObjRelative, + Rel::HaveVerb { .. } => Origin::SubjRelative, + Rel::Open | Rel::None if core.subject_carried => Origin::Carried, + Rel::Open | Rel::None => Origin::Main, + }; + if let Some(spo) = core.step(t) { + out.push(Traced { + spo, + origin, + sentence, + closed_at: i, + }); + } + } + out +} + +#[cfg(test)] +mod traced_tests { + use super::*; + + fn n(id: WordId) -> Tagged { + Tagged::new(id, Pos::Noun) + } + fn v(id: WordId) -> Tagged { + Tagged::new(id, Pos::Verb) + } + fn det() -> Tagged { + Tagged::new(0, Pos::Det) + } + fn rel() -> Tagged { + Tagged::new(0, Pos::Rel) + } + fn stop() -> Tagged { + Tagged::new(0, Pos::Stop) + } + + /// The traced parse emits exactly `parse_to_spo`'s triples, in order, on + /// inputs that exercise all four paths. + #[test] + fn traced_triples_equal_parse_to_spo() { + let streams: Vec> = vec![ + vec![det(), n(1), rel(), det(), n(2), v(3), v(4), det(), n(5)], + vec![det(), n(1), rel(), v(2), det(), n(3), v(4), det(), n(5)], + vec![n(1), v(2), n(3), v(4), n(5), stop(), n(6), v(7), n(8)], + ]; + for s in streams { + let traced: Vec = parse_to_spo_traced(&s).iter().map(|t| t.spo).collect(); + assert_eq!(traced, parse_to_spo(&s)); + assert!(!traced.is_empty(), "anti-vacuity: the stream emits"); + } + } + + /// Each path is named: object-relative, subject-relative, main, carried. + /// Two-sided: every stream carries at least two different origins. + #[test] + fn each_clause_path_is_named() { + let obj = parse_to_spo_traced(&[det(), n(1), rel(), det(), n(2), v(3), v(4), det(), n(5)]); + assert_eq!( + obj.iter().map(|t| t.origin).collect::>(), + vec![Origin::ObjRelative, Origin::Main] + ); + assert_eq!( + obj[0].spo, + Spo::new(2, 3, 1), + "the antecedent is the object" + ); + + let subj = parse_to_spo_traced(&[det(), n(1), rel(), v(2), det(), n(3), v(4), det(), n(5)]); + assert_eq!( + subj.iter().map(|t| t.origin).collect::>(), + vec![Origin::SubjRelative, Origin::Main] + ); + assert_eq!(subj[0].spo.subject, 1, "the antecedent is the subject"); + + let chain = parse_to_spo_traced(&[n(1), v(2), n(3), v(4), n(5), stop(), n(6), v(7), n(8)]); + assert_eq!( + chain + .iter() + .map(|t| (t.origin, t.sentence)) + .collect::>(), + vec![(Origin::Main, 0), (Origin::Carried, 0), (Origin::Main, 1)] + ); + assert_eq!(chain[0].closed_at, 2); + assert_eq!(chain[2].closed_at, 8); + } +} + // ───────────────────────────────────────────────────────────────────────────── // Multi-reading input (D-LXC-2) // ───────────────────────────────────────────────────────────────────────────── diff --git a/crates/deepnsm-v2/src/haspelmath.rs b/crates/deepnsm-v2/src/haspelmath.rs index 60cce105e..4280f49ad 100644 --- a/crates/deepnsm-v2/src/haspelmath.rs +++ b/crates/deepnsm-v2/src/haspelmath.rs @@ -36,6 +36,18 @@ pub enum DativeFunction { } impl DativeFunction { + /// All eight functions in ordinal order. + pub const ALL: [Self; 8] = [ + Self::Direction, + Self::Recipient, + Self::Experiencer, + Self::Purpose, + Self::Beneficiary, + Self::PredicativePossessor, + Self::ExternalPossessor, + Self::Judicantis, + ]; + #[must_use] pub const fn bit(self) -> u8 { 1 << (self as u8) diff --git a/crates/deepnsm-v2/src/lib.rs b/crates/deepnsm-v2/src/lib.rs index b0de36863..77ec24c1d 100644 --- a/crates/deepnsm-v2/src/lib.rs +++ b/crates/deepnsm-v2/src/lib.rs @@ -57,6 +57,7 @@ pub mod tekamolo; pub mod toc; pub mod vocab; pub mod wave; +pub mod wire; use lance_graph_contract::temporal_pov::{TemporalPov, VersionRange}; diff --git a/crates/deepnsm-v2/src/tekamolo.rs b/crates/deepnsm-v2/src/tekamolo.rs index ab0f08721..11e14c269 100644 --- a/crates/deepnsm-v2/src/tekamolo.rs +++ b/crates/deepnsm-v2/src/tekamolo.rs @@ -467,6 +467,10 @@ pub struct ClauseRead { pub abstained: usize, /// Lanes whose winner was an ambiguous form. pub ambiguous_wins: usize, + /// Per committed lane, the token index of the winning form — WHERE the + /// circumstance was stated, in Te-Ka-Mo-Lo order. `None` exactly where + /// [`Self::lanes`] is `None`. + pub at: [Option; 4], } /// Read ONE clause's tokens under `params`. @@ -546,6 +550,7 @@ pub fn read_clause(h: &GrammarHeuristics, tokens: &[&str], params: ReadParams) - out.ambiguous_wins += 1; } out.lanes[li] = Some(best.addr); + out.at[li] = Some(best.at); } out } @@ -653,6 +658,12 @@ mod tests { "right corner adjudicates to `damit` (rank 1) on the evidence" ); assert_ne!(left.lanes[k], right.lanes[k]); + // WHERE each corner found its Kausal form: `weil` at 0, `damit` at 3. + assert_eq!(left.at[k], Some(0)); + assert_eq!(right.at[k], Some(3)); + // Unaddressed lanes report no position. + let t = TekamoloRole::Temporal as usize; + assert_eq!((right.lanes[t], right.at[t]), (None, None)); } /// An ambiguous form may win only where the style admits it — and the diff --git a/crates/deepnsm-v2/src/wire.rs b/crates/deepnsm-v2/src/wire.rs new file mode 100644 index 000000000..c79162a15 --- /dev/null +++ b/crates/deepnsm-v2/src/wire.rs @@ -0,0 +1,1982 @@ +//! `wire` — every `CausalEdge64` field of a parsed triple, filled from the one +//! source that grounds it, at the address [`crate::hydrate`] gives it. +//! +//! The field contract is the CE64 semantic upper half (2026-10-16). Each row of +//! the table names the field's source. A field whose source says nothing stays +//! at its zero-fallback and the [`WireReport`] counts it; nothing is guessed to +//! make a field non-zero. [`crate::haspelmath::run_harness`] still emits the +//! grammar-only edge (bits 24..63 zero). This module is the layer above it that +//! adds annotations, each from a named source and never from the grammatical +//! fit. +//! +//! | bits | field | source | +//! |---|---|---| +//! | 0..23 | S, P, O codes | `run_harness`'s frozen operand ABI | +//! | 24..31 | frequency | `lance-graph-arm-discovery` [`arm_to_truth_u8`], rule `(S, P) → O` over independent verses: `f = cooccur / antecedent` | +//! | 32..39 | confidence | the same: `c = cooccur / (cooccur + k)`, `k = 1` (the NAL horizon) | +//! | 40..42 | Pearl ladder | the TEKAMOLO read: a Kausal lane stated on this event → `PO` (the claim sits at the intervention rung); otherwise `SO` (association) | +//! | 43..45 | orientation / syntax | the Haspelmath function the relation is read in ([`orientation_code`]): the dative-map functions the clause's attested forms allow, narrowed by its TEKAMOLO lanes; `0` = plain core argument or undecided. The clause path ([`Origin`]) goes to the topology in 59..63 | +//! | 46..49 | activation i4 | what this sentence's update caused: the change of the statement's arm frequency over the prior state, in eighths, clamped to `-8..=7`; `0` when the update changes nothing | +//! | 50..52 | novelty → wisdom | the statement's rung after this update, `1` Staunen ..= `7` wisdom | +//! | 53..58 | belief-state update | W slot into the cohort's [`WitnessTable`]: the entry that witnessed this statement's PRIOR state; `0` = first sighting or the prior is in another cohort | +//! | 59..63 | epistemic state | `EpistemicState5`: topology from the clause path, certification from the belief state and the causes arena | +//! +//! # The belief arenas +//! +//! Every row is also offered to [`crate::belief::BeliefArena`] as one unit +//! observation, stamped with its verse: the stamp is the independence gate, +//! so disjoint verses pool and a repeat inside one verse is CHOICE, never +//! counted twice. Two arenas, two term spaces: +//! +//! - **facts**: `S –Rel(P)→ O` over word ids. `Rel` never transits. +//! - **causes**: `cause ⇒ effect` over event terms. The cause is the event +//! whose clause the Kausal marker (`weil`) opens; every matrix event of the +//! same verse is an effect. `Impl` does not auto-transit. +//! +//! A cause event is certified `CausalCandidate` only when its `cause ⇒ +//! effect` belief rests on two or more disjoint verse stamps (and the triple +//! itself is Related). Text never certifies `Causes`. The arena's stamps are +//! a 64-source bitset that folds by modulo: past 64 verses two sources can +//! collide, which turns a revision into a CHOICE (an undercount, never a +//! double count). +//! +//! # The belief state is a Markov chain per statement +//! +//! A statement `(S, P, O)` has one state: `pos`, its independent supporting +//! verses; `tot`, the independent verses that gave `(S, P)` at all (so +//! `neg = tot - pos` gave another object); and its rung. Each +//! observation updates the states it touches and nothing else, so the next +//! state depends only on the current state and the observation. +//! +//! # The Gadamer gate decides; counting follows +//! +//! Each `(S, P)` group is an interpretive horizon +//! (`contract::revision::GadamerRevision`): its claims are the objects met, +//! its roots the independent verses. Every observation is an encounter, and +//! the gate's verdict decides whether anything is counted: +//! +//! - a new verse for the only object so far is `IndependentConfirmation` (or +//! `HorizonExpansion` for a first sighting): counted; +//! - a new verse while another object is in view is `HorizonFusion`: counted, +//! and both objects join the group's tension; +//! - a verse already counted is `Echo`: nothing counted; +//! - a rival object from a verse already counted is `ContradictionPreserved`: +//! suspended, nothing counted, the tension kept. +//! +//! Claims are never withdrawn and the tension only grows (G3): a contest +//! never resets the rival. Only admitted observations reach the facts arena. +//! +//! Reading order is the chain. Each row records its statement's state right +//! after its own update, and registers one [`WitnessEntry`] (its verse, its +//! facts-arena belief) in the cohort's [`WitnessTable`]. Its W slot points at +//! the entry of the statement's PREVIOUS row, as the contract says +//! (`witness_table`: "each edge's W-slot resolves to the entry that witnessed +//! the prior state"). Walking W from a row and resolving each entry back to +//! its row walks the statement's updates newest → oldest: W1 prior, W2, W3 are +//! the last three hops. A cohort is 63 consecutive rows in reading order +//! (slots `1..=63`, slot 0 = none); a prior in an earlier cohort cannot be +//! addressed by 6 bits, so W is 0 there and the report counts an arc break. +//! +//! # The ladder (bits 50..52) +//! +//! A first sighting sits at rung 1 (Staunen). A corroboration climbs one rung +//! only while the observation is resolved and the support balance stays +//! positive (`pos > neg`): a +//! rung is reached only when the one below holds. Resolved means single +//! readings, traced, no incomplete Haspelmath map, no TEKAMOLO abstention and, +//! for a relative clause, a bound antecedent. A contest leaves the rival's +//! rung as it is. Six resolved, admitted observations with a positive balance +//! after the first sighting reach wisdom. +//! +//! # F/C against NAL revision +//! +//! `f` equals NAL revision of the unit observations (`pos` positive and `neg` +//! negative units at horizon 1). `c` equals it only for an uncontested +//! statement: arm-discovery counts confidence over the co-occurrence mass +//! (`c = pos / (pos + 1)`), NAL over all evidence (`c = tot / (tot + 1)`). +//! `f_and_c_against_nal_revision_of_units` pins both halves. +//! +//! # The orientation map (bits 43..45) +//! +//! A deterministic code on a semantic map of the syntactic functions the FSM +//! decides, in the Haspelmath sense: nodes are functions, and links say which +//! functions may share a form. The links follow the relativization +//! accessibility order (subject above object, Keenan & Comrie 1977): +//! `Main – Carried`, `Main – SubjRelative`, `SubjRelative – ObjRelative`. +//! Haspelmath's dative map is not encoded here: its eight functions would use +//! all eight codes and leave no unaddressed value, and [`crate::haspelmath`] +//! keeps its ordinals harness-local. + +use std::collections::{HashMap, HashSet}; + +use causal_edge::{CausalEdge64, CausalMask}; +use lance_graph_arm_discovery::{arm_to_truth_u8, CandidateRule, Item}; +use lance_graph_contract::causal_witness::{CausalWitnessFacet, Locus}; +use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, EpistemicState5, Topology2}; +use lance_graph_contract::exploration::NarsTruth; +use lance_graph_contract::facet::{CascadeShape, FacetCascade}; +use lance_graph_contract::grammar::verb_lexicon::classify_verb; +use lance_graph_contract::grammar::verb_table::morton_cell; +use lance_graph_contract::hhtl::NiblePath; +use lance_graph_contract::revision::{ + BasisView, CodebookId, EncounterEvidence, EvidenceMask, EvidentialEffect, GadamerRevision, + GrammarId, HorizonId, InterpretiveHorizon, LanguageId, LensId, QuestionId, RevisionDelta, + RevisionKind, RevisionPolicy, +}; +use lance_graph_contract::witness_table::{WitnessEntry, WitnessTable}; + +use crate::belief::{BeliefArena, CStmt, Copula, ReviseOutcome, Stamp}; +use crate::fsm::{parse_readings, parse_to_spo_traced, Origin, Pos, Reading, Tagged, Traced}; +use crate::haspelmath::{ + continuity, run_harness, DativeFunction, FormAttestation, FrozenOperandAbi, HarnessError, + MapVerdict, +}; +use crate::spo::Spo; +use crate::tekamolo::{read_clause, GrammarHeuristics, LaneAddress, ReadParams}; + +/// The TEKAMOLO lane index of Kausal, in Te-Ka-Mo-Lo order. +pub const KAUSAL_LANE: usize = 1; + +/// The orientation code of a Haspelmath function (bits 43..45): the +/// function the relation is read in, on the dative semantic map +/// (Haspelmath 2003, Fig. 8.1). `0` is the plain core argument (no dative-map +/// function); `1..=7` are the seven functions below. `Judicantis` has no code. +#[must_use] +pub const fn orientation_code(f: DativeFunction) -> Option { + match f { + DativeFunction::Direction => Some(1), + DativeFunction::Recipient => Some(2), + DativeFunction::Experiencer => Some(3), + DativeFunction::Purpose => Some(4), + DativeFunction::Beneficiary => Some(5), + DativeFunction::PredicativePossessor => Some(6), + DativeFunction::ExternalPossessor => Some(7), + DativeFunction::Judicantis => None, + } +} + +/// The function an orientation code names; `None` for `0` (core argument) +/// and anything above 7. +#[must_use] +pub const fn orientation_function(code: u8) -> Option { + match code { + 1 => Some(DativeFunction::Direction), + 2 => Some(DativeFunction::Recipient), + 3 => Some(DativeFunction::Experiencer), + 4 => Some(DativeFunction::Purpose), + 5 => Some(DativeFunction::Beneficiary), + 6 => Some(DativeFunction::PredicativePossessor), + 7 => Some(DativeFunction::ExternalPossessor), + _ => None, + } +} + +/// TEKAMOLO lanes that select a dative-map function when a form is +/// multifunctional: Kausal (final reading) → Purpose, Lokal → Direction. +const LANE_FUNCTION: [u8; 4] = [ + 0, + DativeFunction::Purpose.bit(), + 0, + DativeFunction::Direction.bit(), +]; + +/// Rows per witness cohort: slots `1..=63` of a `WitnessTable<64>`. +pub const COHORT_ROWS: usize = 63; + +/// One verse as the caller has it. +#[derive(Debug, Clone, Default)] +pub struct VerseIn<'a> { + /// The verse's tokens with their admissible readings (no trailing stop). + pub readings: Vec, + /// The surface forms, one per reading, for the TEKAMOLO read. Empty means + /// "no surface text": the TEKAMOLO lanes and loci stay unaddressed. + pub surface: Vec<&'a str>, + /// Haspelmath form attestations cited for this verse. + pub forms: Vec, + /// Qualia palette index per token (`contract::qualia_profile`, `0` = + /// unnamed), aligned with `readings`; empty = none. Where the index comes + /// from is the caller's: this crate holds no word → qualia lexicon. + pub qualia: Vec, +} + +/// One wired triple. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct WiredRow { + /// The verse node's address. + pub path: NiblePath, + /// Corpus-order index (the Markov index of [`crate::hydrate`]). + pub order: u32, + /// Which input verse it was read in. + pub verse: usize, + /// The triple. + pub spo: Spo, + /// The 64-bit edge, every field set from its source. + pub edge: CausalEdge64, + /// The 24 × i4 loci. + pub loci: CausalWitnessFacet, + /// TEKAMOLO lane addresses, on the event that stated them only. + pub tekamolo: [Option; 4], + /// The clause path, when the verse could be traced. + pub origin: Option, + /// Independent verses supporting the statement so far (`pos`). + pub support: u32, + /// Independent verses that gave `(S, P)` so far (`tot`). + pub antecedent: u32, + /// The witness cohort this row's W slot is read in. + pub cohort: u32, + /// The object is part of its `(S, P)` group's preserved tension: another + /// object was met for the same `(S, P)` and neither was withdrawn. + pub contested: bool, + /// What the Gadamer gate made of this observation (`None`: the group ran + /// out of gate bits and the observation was not admitted). + pub revision: Option, + /// The qualia register, read under [`QUALIA_SHAPE`]: pairs 0..2 are + /// `(operand code : qualia)` for S, P, O; pair 3 is `(verb cell : 1)` when + /// the verb classifies, `(0 : 0)` when not. Class id 0 (default class). + pub qualia: FacetCascade, +} + +/// The carving the qualia register is read under: six `u8:u8` pairs. +pub const QUALIA_SHAPE: CascadeShape = CascadeShape::G6D2; + +/// Pair index of the subject in the qualia register. +pub const Q_SUBJECT: u8 = 0; +/// Pair index of the predicate. +pub const Q_PREDICATE: u8 = 1; +/// Pair index of the object. +pub const Q_OBJECT: u8 = 2; +/// Pair index of the verb cell (`verb_table::morton_cell`, flag byte). +pub const Q_VERB_CELL: u8 = 3; + +/// The qualia index on pair `pair` (level 1 of the `u8:u8`). +#[must_use] +pub fn qualia_at(f: FacetCascade, pair: u8) -> u8 { + f.cascade_byte(QUALIA_SHAPE, pair, 1) +} + +/// The operand code on pair `pair` (level 0 of the `u8:u8`). +#[must_use] +pub fn operand_at(f: FacetCascade, pair: u8) -> u8 { + f.cascade_byte(QUALIA_SHAPE, pair, 0) +} + +/// The verb cell, when the verb classified. Cell `0` is a real verb +/// (`Becomes`, `Present`), so presence is the flag byte, never the cell. +#[must_use] +pub fn verb_cell(f: FacetCascade) -> Option { + (f.cascade_byte(QUALIA_SHAPE, Q_VERB_CELL, 1) == 1) + .then(|| f.cascade_byte(QUALIA_SHAPE, Q_VERB_CELL, 0)) +} + +/// What the wiring did, so a zero field is never mistaken for a reading. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct WireReport { + /// Rows written. + pub rows: usize, + /// Verses with a multi-reading token or a parse mismatch: orientation 0. + pub untraced_verses: usize, + /// Relative-clause rows whose antecedent event was found within reach. + pub antecedent_bound: usize, + /// Relative-clause rows whose antecedent event was not found. + pub antecedent_unbound: usize, + /// TEKAMOLO loci written (a pointer to another event of the verse). + pub tekamolo_loci: usize, + /// TEKAMOLO lanes committed on a stating event. + pub tekamolo_lanes: usize, + /// Rows at the intervention rung (a Kausal lane stated on the event). + pub intervention_rows: usize, + /// Contest updates applied to other statements. + pub contests: usize, + /// Dependent observations (a verse already counted). + pub dependent: usize, + /// Rows whose orientation names a dative-map function. + pub orientation_coded: usize, + /// Rows left at 0 because the forms allow several functions. + pub orientation_ambiguous: usize, + /// Rows whose one function has no code (Judicantis). + pub orientation_unrepresented: usize, + /// Rows whose Quorum locus points at an agreeing row. + pub quorum_bound: usize, + /// Rows whose Contradiction locus points at a disagreeing row. + pub contradiction_bound: usize, + /// Observations the gate suspended: a contradiction with no new + /// independent root (`ContradictionPreserved`). Nothing is counted. + pub suspended: usize, + /// Observations not admitted because a group had more than 256 distinct + /// verses or objects. + pub gate_overflow: usize, + /// Rows certified `CausalCandidate`. + pub causal_candidates: usize, + /// `cause ⇒ effect` observations offered to the causes arena. + pub implications: usize, + /// Of those, observations the arena pooled (disjoint stamps). + pub implications_revised: usize, + /// Rows whose prior update sits in an earlier witness cohort (W = 0). + pub arc_breaks: usize, + /// Events not interned because the u16 event-term space was full. + pub event_overflow: usize, + /// Qualia tokens attached to a subject or object (adjectival). + pub qualia_adjectival: usize, + /// Qualia tokens attached to an event (adverbial). + pub qualia_adverbial: usize, + /// Qualia tokens with no slot to attach to (or in an untraced verse). + pub qualia_unattached: usize, + /// A second, different qualia for an already-qualified slot; the first is + /// kept. + pub qualia_conflicts: usize, + /// Triples whose verb surface classified to a 144-cell. + pub verb_classified: usize, + /// Triples whose verb did not classify (unknown, non-English, or a + /// homograph like `saw` the lexicon leaves unresolved). + pub verb_unclassified: usize, +} + +/// What [`wire`] produced: the rows, the report, and the two belief arenas +/// the rows were fed into. +#[derive(Debug)] +pub struct Wired { + /// One row per certain triple, in stream order. + pub rows: Vec, + /// What happened. + pub report: WireReport, + /// Every row as `S –Rel(P)→ O`, one unit observation per verse, stamped + /// with the verse. `Rel` never transits. + pub facts: BeliefArena, + /// Every Kausal claim as `cause ⇒ effect` over EVENT terms (indices into + /// [`Self::events`]), one unit observation per verse, stamped with the + /// verse. `Impl` does not auto-transit (belief.rs V0). + pub causes: BeliefArena, + /// Event term `i` is the triple `events[i]`. + pub events: Vec, + /// One `WitnessTable<64>` per cohort of [`COHORT_ROWS`] rows, in reading + /// order. Entry = `(verse + 1, Some(facts-arena belief id))`. + pub witnesses: Vec>, +} + +/// One unit of positive evidence at horizon 1. +fn unit() -> NarsTruth { + NarsTruth::new(1.0, 0.5) +} + +const REACH_BACK: i64 = -8; +const REACH_FWD: i64 = 7; + +fn offset(from: usize, to: usize) -> Option { + let d = to as i64 - from as i64; + (d != 0 && (REACH_BACK..=REACH_FWD).contains(&d)).then_some(d as i8) +} + +/// The traced clause paths of one verse, or `None` when a token has more than +/// one reading or the traced parse disagrees with the multi-reading one. +fn trace(readings: &[Reading], certain: &[Spo]) -> Option> { + let mut tagged = Vec::with_capacity(readings.len() + 1); + for r in readings { + if r.pos.len() != 1 { + return None; + } + tagged.push(Tagged::new(r.id, r.pos.iter().next()?)); + } + tagged.push(Tagged::new(0, Pos::Stop)); + let traced = parse_to_spo_traced(&tagged); + let same = + traced.len() == certain.len() && traced.iter().zip(certain).all(|(t, c)| t.spo == *c); + same.then_some(traced) +} + +/// The state of one statement `(S, P, O)`. +#[derive(Debug, Clone, Default)] +struct StmtState { + verses: HashSet, + rung: u8, + /// `(cohort, slot)` of this statement's latest row. + last: Option<(u32, u8)>, +} + +/// The gate's mask width: group-local roots and claims, 256 each. +type GateMask = [u64; 4]; + +/// Bit `i` of a [`GateMask`]. +fn gate_bit(i: u16) -> GateMask { + let mut m = [0u64; 4]; + m[usize::from(i / 64)] = 1 << (i % 64); + m +} + +const GATE_BITS: u16 = 256; + +struct Group { + verses: HashSet, + objects: HashMap, + /// The `(S, P)` interpretive horizon: claims are this group's objects, + /// roots its independent verses (group-local bit indices). + horizon: InterpretiveHorizon<(), GateMask>, + roots: HashMap, + claims: HashMap, +} + +impl Default for Group { + fn default() -> Self { + let empty = GateMask::empty(); + Self { + verses: HashSet::new(), + objects: HashMap::new(), + horizon: InterpretiveHorizon { + id: HorizonId(0), + awareness: (), + question: QuestionId(0), + language: LanguageId(0), + grammar: GrammarId(0), + codebook: CodebookId(0), + lens: LensId(0), + projected_claims: empty, + independent_roots: empty, + inherited_roots: empty, + unresolved_tension: empty, + revision_index: 0, + }, + roots: HashMap::new(), + claims: HashMap::new(), + } + } +} + +impl Group { + /// Pass one observation of `(S, P, object)` from verse `vi` through the + /// Gadamer gate and adopt the resulting horizon. `None` when the group has + /// run out of root or claim bits (the observation is not admitted). + fn encounter(&mut self, vi: usize, object: u16) -> Option> { + let next_root = self.roots.len() as u16; + let next_claim = self.claims.len() as u16; + let root = *self.roots.entry(vi).or_insert(next_root); + let claim = *self.claims.entry(object).or_insert(next_claim); + if root >= GATE_BITS || claim >= GATE_BITS { + return None; + } + let prior = &self.horizon; + let own = gate_bit(claim); + // Claims are never withdrawn: the working whole keeps every object + // the group has met, so a rival stays in view as live tension. + let proposed = prior.projected_claims.union(&own); + let others = prior.projected_claims.difference(&own); + let contradictions = if others.is_empty() { + GateMask::empty() + } else { + others.union(&own) + }; + let encounter = EncounterEvidence { + proposed_claims: proposed, + independent_roots: gate_bit(root), + inherited_roots: GateMask::empty(), + resistance: GateMask::empty(), + contradictions, + affected_parts: contradictions, + }; + let ancestry = BasisView { + ancestry_independent_roots: prior.independent_roots, + ancestry_derived_roots: GateMask::empty(), + ancestor_claims: prior.projected_claims, + closes_cycle: false, + }; + let delta = GadamerRevision.revise(prior, &encounter, &ancestry); + self.horizon = delta.resulting.clone(); + Some(delta) + } + + /// Whether `object` is part of this group's preserved tension. + fn contested(&self, object: u16) -> bool { + self.claims.get(&object).is_some_and(|&c| { + c < GATE_BITS && self.horizon.unresolved_tension.intersects(&gate_bit(c)) + }) + } +} + +/// Fill the qualia register of one traced verse's rows. +/// +/// - The operand codes come from the edge (bits 0..23). +/// - A qualia on an adjective attaches to the next noun of its sentence, on +/// every triple of that sentence where the noun is the subject or object. +/// - A qualia on any other skipped token (an adverb) attaches to an event: +/// the preceding triple of the sentence when no noun or verb stands +/// between its close and the token, otherwise the next triple, otherwise +/// the last of the sentence. +/// - The verb cell is `morton_cell(classify_verb(surface))` of the verb token +/// that closed into the triple's predicate. +fn attach_qualia(verse: &VerseIn<'_>, t: &[Traced], rows: &mut [WiredRow], rep: &mut WireReport) { + let r = &verse.readings; + let pos_of = |i: usize| r[i].pos.iter().next(); + let mut sentence = vec![0u32; r.len()]; + let mut s = 0; + for (i, slot) in sentence.iter_mut().enumerate() { + *slot = s; + if pos_of(i) == Some(Pos::Stop) { + s += 1; + } + } + // Pair g sits at bytes 4 + 2g (lo = qualia) and 5 + 2g (hi = operand). + let mut bytes: Vec<[u8; 16]> = rows + .iter() + .map(|row| { + let mut b = [0u8; 16]; + b[5] = row.edge.s_idx(); + b[7] = row.edge.p_idx(); + b[9] = row.edge.o_idx(); + b + }) + .collect(); + let content = |i: usize| matches!(pos_of(i), Some(Pos::Noun | Pos::Verb)); + for (i, &q) in verse.qualia.iter().enumerate() { + if q == 0 || i >= r.len() { + continue; + } + let sent = sentence[i]; + let in_sent: Vec = (0..t.len()).filter(|&k| t[k].sentence == sent).collect(); + let mut put = |k: usize, pair: u8| { + let at = 4 + 2 * pair as usize; + if bytes[k][at] != 0 && bytes[k][at] != q { + rep.qualia_conflicts += 1; + } else { + bytes[k][at] = q; + } + }; + let attached = match pos_of(i) { + Some(Pos::Adj) => { + let noun = (i + 1..r.len()) + .take_while(|&j| sentence[j] == sent) + .find(|&j| pos_of(j) == Some(Pos::Noun)); + let mut hit = false; + if let Some(n) = noun { + let w = r[n].id; + for &k in &in_sent { + if t[k].spo.subject == w { + put(k, Q_SUBJECT); + hit = true; + } + if t[k].spo.object == w { + put(k, Q_OBJECT); + hit = true; + } + } + } + hit.then_some(true) + } + Some(Pos::Adv | Pos::Other) => { + let prev = in_sent.iter().rev().copied().find(|&k| t[k].closed_at < i); + let next = in_sent.iter().copied().find(|&k| t[k].closed_at >= i); + let target = match (prev, next) { + (Some(p), _) if !(t[p].closed_at + 1..i).any(content) => Some(p), + (_, Some(n)) => Some(n), + (p, None) => p, + }; + target.map(|k| { + put(k, Q_PREDICATE); + false + }) + } + _ => None, + }; + match attached { + Some(true) => rep.qualia_adjectival += 1, + Some(false) => rep.qualia_adverbial += 1, + None => rep.qualia_unattached += 1, + } + } + for (k, tr) in t.iter().enumerate() { + let last = tr.closed_at.min(r.len().saturating_sub(1)); + let verb = (0..=last) + .rev() + .take_while(|&j| sentence[j] == tr.sentence) + .find(|&j| pos_of(j) == Some(Pos::Verb) && r[j].id == tr.spo.predicate); + let cell = verb + .and_then(|j| verse.surface.get(j)) + .and_then(|w| classify_verb(w)) + .map(|(f, tense)| morton_cell(f, tense)); + match cell { + Some(c) => { + bytes[k][10] = 1; + bytes[k][11] = c; + rep.verb_classified += 1; + } + None => rep.verb_unclassified += 1, + } + } + for (row, b) in rows.iter_mut().zip(bytes) { + row.qualia = FacetCascade::from_bytes(&b); + } +} + +/// A frequency change in `u8` codes (`-255..=255`) as eighths, rounded half +/// away from zero, clamped to the i4 range. +fn eighths(delta: i32) -> i8 { + let r = if delta >= 0 { + (delta * 8 + 127) / 255 + } else { + (delta * 8 - 127) / 255 + }; + r.clamp(-8, 7) as i8 +} + +/// The arm-discovery rule `(S, P) → O` over the counts so far. +fn rule(spo: Spo, pos: u32, tot: u32, window: u32) -> CandidateRule { + CandidateRule { + antecedent: vec![ + Item::new(0, u32::from(spo.subject)), + Item::new(1, u32::from(spo.predicate)), + ], + consequent: vec![Item::new(2, u32::from(spo.object))], + cooccur: pos, + antecedent_count: tot, + window, + } +} + +/// Wire every certain triple of the corpus. +/// +/// # Errors +/// +/// Exactly what [`run_harness`] refuses: unaligned verses, missing evidence +/// roots, a disconnected complete distribution, parser overflow, unaddressed +/// triples, operands outside the frozen ABI. +pub fn wire( + markers: &[(u16, u16)], + verses: &[VerseIn<'_>], + basin: u8, + abi: &FrozenOperandAbi, + heuristics: Option<&GrammarHeuristics>, + params: ReadParams, +) -> Result { + let readings: Vec> = verses.iter().map(|v| v.readings.clone()).collect(); + let forms: Vec> = verses.iter().map(|v| v.forms.clone()).collect(); + let harness = run_harness(markers, &readings, &forms, basin, abi)?; + + let mut rep = WireReport::default(); + let mut rows: Vec = Vec::with_capacity(harness.encoded.len()); + let mut groups: HashMap<(u16, u16), Group> = HashMap::new(); + let mut facts = BeliefArena::new(); + let mut fact_ids: HashMap = HashMap::new(); + let mut causes = BeliefArena::new(); + let mut events: Vec = Vec::new(); + let mut event_id: HashMap = HashMap::new(); + let mut witnesses: Vec> = Vec::new(); + let mut next = 0usize; + + for (vi, verse) in verses.iter().enumerate() { + let mut input = verse.readings.clone(); + input.push(Reading::stop()); + let parsed = parse_readings(&input); + let n = parsed.certain.len(); + if n == 0 { + continue; + } + let traced = trace(&verse.readings, &parsed.certain); + if traced.is_none() { + rep.untraced_verses += 1; + } + let incomplete = verse + .forms + .iter() + .any(|f| continuity(f.functions, f.coverage_complete) == MapVerdict::Incomplete); + let ambiguous = !parsed.ambiguous.is_empty(); + + // TEKAMOLO: one read over the verse; each committed lane lands on the + // event whose clause holds the stating token. + let clause = match heuristics { + Some(h) if !verse.surface.is_empty() && verse.surface.len() == verse.readings.len() => { + Some(read_clause(h, &verse.surface, params)) + } + _ => None, + }; + let abstained = clause.is_some_and(|c| c.abstained > 0); + let stating: [Option; 4] = std::array::from_fn(|lane| { + let at = clause?.at[lane]?; + let t = traced.as_ref()?; + Some( + t.iter() + .position(|x| x.closed_at >= at) + .unwrap_or(t.len() - 1), + ) + }); + + let first = rows.len(); + for k in 0..n { + let h = harness.hydration.triples[next]; + let enc = harness.encoded[next]; + next += 1; + rows.push(WiredRow { + path: h.path, + order: h.order, + verse: vi, + spo: h.spo, + edge: enc.word, + loci: CausalWitnessFacet::ZERO, + tekamolo: [None; 4], + origin: traced.as_ref().map(|t| t[k].origin), + support: 0, + antecedent: 0, + cohort: 0, + contested: false, + revision: None, + qualia: FacetCascade::default(), + }); + } + + // Relative-clause antecedent: the matrix event of the same sentence + // whose subject is the antecedent word. + let mut unbound = vec![false; n]; + if let Some(t) = traced.as_ref() { + for (k, tr) in t.iter().enumerate() { + if !tr.origin.is_relative() { + continue; + } + let ante_word = match tr.origin { + Origin::SubjRelative => tr.spo.subject, + _ => tr.spo.object, + }; + // Test every matrix event of the sentence within the i4 reach, + // before or after the relative clause, nearest first (the + // earlier one on a tie); the antecedent noun may be the + // matrix subject or object. + let matrix = t + .iter() + .enumerate() + .filter(|(j, m)| { + *j != k + && m.sentence == tr.sentence + && !m.origin.is_relative() + && (m.spo.subject == ante_word || m.spo.object == ante_word) + && offset(k, *j).is_some() + }) + .min_by_key(|(j, _)| (j.abs_diff(k), *j > k)); + match matrix.and_then(|(j, _)| offset(k, j)) { + Some(o) => { + rows[first + k].loci = rows[first + k].loci.with(Locus::Antecedent, o); + rep.antecedent_bound += 1; + } + None => { + unbound[k] = true; + rep.antecedent_unbound += 1; + } + } + } + } + + // TEKAMOLO lanes on the stating event, loci elsewhere in the verse. + for (lane, at) in stating.iter().enumerate() { + let (Some(j), Some(c)) = (*at, clause) else { + continue; + }; + rows[first + j].tekamolo[lane] = c.lanes[lane]; + rep.tekamolo_lanes += 1; + for k in 0..n { + if let Some(o) = offset(k, j) { + rows[first + k].loci = rows[first + k].loci.with(Locus::ALL[lane], o); + rep.tekamolo_loci += 1; + } + } + } + + // Orientation: the dative-map functions of the forms attested inside + // each traced clause span, narrowed by the row's TEKAMOLO lanes. + let mut undecided = vec![false; n]; + if let Some(t) = traced.as_ref() { + for (k, tr) in t.iter().enumerate() { + // The clause span: after the previous triple of the same + // sentence, never before the sentence's own start. + let sentence_start = verse.readings[..tr.closed_at] + .iter() + .rposition(|r| r.pos.contains(Pos::Stop)) + .map_or(0, |i| i + 1); + let start = t[..k] + .iter() + .rev() + .find(|p| p.sentence == tr.sentence) + .map_or(sentence_start, |p| (p.closed_at + 1).max(sentence_start)); + let span = &verse.readings + [start.min(tr.closed_at)..=tr.closed_at.min(verse.readings.len() - 1)]; + let mut f = 0u8; + for r in span { + for a in verse.forms.iter().filter(|a| a.word == r.id) { + f |= a.functions; + } + } + let lanes = rows[first + k] + .tekamolo + .iter() + .enumerate() + .fold( + 0u8, + |m, (l, a)| { + if a.is_some() { + m | LANE_FUNCTION[l] + } else { + m + } + }, + ); + if f & lanes != 0 { + f &= lanes; + } + let code = match f.count_ones() { + 0 => 0, + 1 => { + let func = DativeFunction::ALL[f.trailing_zeros() as usize]; + orientation_code(func).unwrap_or_else(|| { + rep.orientation_unrepresented += 1; + 0 + }) + } + _ => { + rep.orientation_ambiguous += 1; + undecided[k] = true; + 0 + } + }; + if code != 0 { + rep.orientation_coded += 1; + } + rows[first + k].edge.set_direction(code); + } + } + + if let Some(t) = traced.as_ref() { + attach_qualia(verse, t, &mut rows[first..], &mut rep); + } else { + rep.qualia_unattached += verse.qualia.iter().filter(|&&q| q != 0).count(); + } + + // The belief-state update, row by row in stream order. + let mut admitted_rows = vec![false; n]; + for (k, row) in rows[first..].iter_mut().enumerate() { + let spo = row.spo; + let causal = row.tekamolo[KAUSAL_LANE].is_some(); + let resolved = traced.is_some() + && !ambiguous + && !incomplete + && !abstained + && !unbound[k] + && !undecided[k]; + + let group = groups.entry((spo.subject, spo.predicate)).or_default(); + // The gate decides whether and in which sense this observation + // revises the (S, P) horizon; counting runs only if it admits. + let delta = group.encounter(vi, spo.object); + let effect = delta.as_ref().map(|d| d.evidential_effect); + match delta.as_ref().map(|d| d.kind) { + None => rep.gate_overflow += 1, + Some(RevisionKind::HorizonFusion) => rep.contests += 1, + Some(RevisionKind::ContradictionPreserved) => rep.suspended += 1, + Some(RevisionKind::Echo) => rep.dependent += 1, + Some(_) => {} + } + let admitted = effect == Some(EvidentialEffect::IncreaseEligible); + let tot_prior = group.verses.len() as u32; + if admitted { + group.verses.insert(vi); + } + let tot = group.verses.len() as u32; + let contested = group.contested(spo.object); + let st = group.objects.entry(spo.object).or_default(); + let pos_prior = st.verses.len() as u32; + let new_stmt = st.rung == 0; + if admitted { + st.verses.insert(vi); + } + let pos = st.verses.len() as u32; + // The support balance: more independent verses for this object than + // for every other object of the same (S, P). + let balance_pos = 2 * pos > tot; + // The activation this sentence causes: the change of the arm + // frequency over the prior state (unknown prior = arm's own 128). + let f_prior = arm_to_truth_u8(&rule(spo, pos_prior, tot_prior, vi as u32), 1).frequency; + let f_now = arm_to_truth_u8(&rule(spo, pos, tot, (vi + 1) as u32), 1).frequency; + let activation = eighths(i32::from(f_now) - i32::from(f_prior)); + if new_stmt { + st.rung = 1; + } else if admitted && resolved && balance_pos { + st.rung = (st.rung + 1).min(7); + } + + let truth = arm_to_truth_u8(&rule(spo, pos, tot, (vi + 1) as u32), 1); + let pearl = if causal { + rep.intervention_rows += 1; + CausalMask::PO + } else { + CausalMask::SO + }; + let topology = match row.origin { + None => Topology2::Unknown, + Some(Origin::Main) => Topology2::Direct, + Some(_) if unbound[k] => Topology2::IndirectUnknown, + Some(_) => Topology2::IndirectKnown, + }; + // The facts arena counts, so it sees only admitted observations; + // a suspended or echoed row points at the fact as it stands (none + // for a claim never admitted). + let stmt = CStmt { + s: spo.subject, + cop: Copula::Rel(spo.predicate), + p: spo.object, + }; + let fact = if admitted { + let id = match facts.observe(stmt, unit(), Stamp::source(vi as u32)) { + ReviseOutcome::Admitted { id } + | ReviseOutcome::Revised { id, .. } + | ReviseOutcome::Chosen { id, .. } => id, + }; + fact_ids.insert(stmt, id); + Some(id) + } else { + fact_ids.get(&stmt).copied() + }; + // Register this row's witness; W points at the prior row's entry. + let row_ix = first + k; + let (cohort, slot) = ( + (row_ix / COHORT_ROWS) as u32, + (1 + row_ix % COHORT_ROWS) as u8, + ); + if witnesses.len() <= cohort as usize { + witnesses.push(WitnessTable::new()); + } + witnesses[cohort as usize] + .set( + slot, + WitnessEntry { + mailbox_ref: vi as u32 + 1, + spo_fact_ref: fact.map(u64::from), + }, + ) + .expect("slot is 1..=63"); + let w_prior = match st.last { + Some((c, s)) if c == cohort => s, + Some(_) => { + rep.arc_breaks += 1; + 0 + } + None => 0, + }; + st.last = Some((cohort, slot)); + row.cohort = cohort; + // CausalCandidate is decided after the Kausal claims are counted. + let certification = if pos >= 2 && balance_pos { + Certification3::Related + } else { + Certification3::Associated + }; + let state = EpistemicState5::new(Epi5Gen::V1, topology, certification); + + let mut edge = row.edge; + edge.set_frequency_u8(truth.frequency); + edge.set_confidence_u8(truth.confidence); + edge.set_causal_mask(pearl); + row.edge = edge + .with_activation(activation) + .with_novelty_wisdom(st.rung) + .with_w_slot(w_prior) + .with_epistemic_raw5(state.raw()); + row.support = pos; + row.antecedent = tot; + row.contested = contested; + row.revision = delta.map(|d| d.kind); + admitted_rows[k] = admitted; + } + + // Kausal claims: the stating event is the cause (the clause `weil` + // opens), every matrix event of the verse is an effect. Each pair is + // one unit observation of `cause ⇒ effect`, stamped with the verse, so + // the arena pools independent verses and never double counts one. + // It counts, so it runs after the gate: a pair is observed only when + // both the cause row and the effect row were admitted (G5). + if let (Some(j), Some(t)) = (stating[KAUSAL_LANE], traced.as_ref()) { + let mut cause_sources = 0u32; + let mut intern = |spo: Spo, rep: &mut WireReport| -> Option { + if let Some(&id) = event_id.get(&spo) { + return Some(id); + } + let Ok(id) = u16::try_from(events.len()) else { + rep.event_overflow += 1; + return None; + }; + events.push(spo); + event_id.insert(spo, id); + Some(id) + }; + for (e, tr) in t.iter().enumerate() { + if e == j || tr.origin != Origin::Main || !admitted_rows[j] || !admitted_rows[e] { + continue; + } + let (Some(c), Some(f)) = (intern(t[j].spo, &mut rep), intern(tr.spo, &mut rep)) + else { + continue; + }; + let stmt = CStmt { + s: c, + cop: Copula::Impl, + p: f, + }; + rep.implications += 1; + if matches!( + causes.observe(stmt, unit(), Stamp::source(vi as u32)), + ReviseOutcome::Revised { .. } + ) { + rep.implications_revised += 1; + } + cause_sources = + cause_sources.max(causes.get(stmt).map_or(0, |b| b.stamp.0.count_ones())); + } + // Two independent implication sources promote a Related cause. + let row = &mut rows[first + j]; + let state = EpistemicState5::decode(Epi5Gen::V1, row.edge.epistemic_raw5()) + .expect("written above as V1"); + if cause_sources >= 2 && state.certification() == Certification3::Related { + rep.causal_candidates += 1; + let up = EpistemicState5::new( + Epi5Gen::V1, + state.topology(), + Certification3::CausalCandidate, + ); + row.edge = row.edge.with_epistemic_raw5(up.raw()); + } + } + + rep.rows = rows.len(); + } + // Quorum and Contradiction loci over the whole row stream (a text is one + // Markov chain): the nearest row within the i4 reach with the same + // (S, P) and the same object agrees, with another object disagrees. The + // contradiction is kept as a pointer, never dropped. + for i in 0..rows.len() { + let me = rows[i].spo; + let near = |same: bool| { + (i.saturating_sub(8)..(i + 8).min(rows.len())) + .filter(|&j| j != i && offset(i, j).is_some()) + .filter(|&j| { + let o = rows[j].spo; + o.subject == me.subject + && o.predicate == me.predicate + && (o.object == me.object) == same + }) + .min_by_key(|&j| (j.abs_diff(i), j > i)) + }; + let (agree, disagree) = (near(true), near(false)); + if let Some(j) = agree { + rows[i].loci = rows[i].loci.with(Locus::Quorum, offset(i, j).unwrap()); + rep.quorum_bound += 1; + } + if let Some(j) = disagree { + rows[i].loci = rows[i] + .loci + .with(Locus::Contradiction, offset(i, j).unwrap()); + rep.contradiction_bound += 1; + } + } + Ok(Wired { + rows, + report: rep, + facts, + causes, + events, + witnesses, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fsm::PosSet; + use lance_graph_contract::exploration::NarsTruth; + + const DOG: u16 = 10; + const MAN: u16 = 11; + const CAT: u16 = 12; + const BIT: u16 = 20; + const SAW: u16 = 21; + const THE: u16 = 30; + const WHO: u16 = 31; + const TO: u16 = 32; + const THERE: u16 = 33; + + fn one(id: u16, pos: Pos) -> Reading { + Reading::new(id, PosSet::single(pos)) + } + + /// `the S V the O`. + fn svo(s: u16, v: u16, o: u16) -> Vec { + vec![ + one(THE, Pos::Det), + one(s, Pos::Noun), + one(v, Pos::Verb), + one(THE, Pos::Det), + one(o, Pos::Noun), + ] + } + + fn abi() -> FrozenOperandAbi { + FrozenOperandAbi::new(1, &[DOG, MAN, CAT, BIT, SAW, THE, WHO, TO, THERE]).unwrap() + } + + fn run(verses: &[Vec]) -> (Vec, WireReport) { + let markers: Vec<(u16, u16)> = (0..verses.len() as u16).map(|i| (1, i + 1)).collect(); + let ins: Vec> = verses + .iter() + .map(|r| VerseIn { + readings: r.clone(), + ..VerseIn::default() + }) + .collect(); + let w = wire(&markers, &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap(); + (w.rows, w.report) + } + + fn epi(e: CausalEdge64) -> EpistemicState5 { + EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()).unwrap() + } + + /// The seven listed functions round-trip through codes 1..=7; code 0 is + /// the plain core argument; Judicantis has no code. + #[test] + fn orientation_codes_round_trip() { + for f in DativeFunction::ALL { + match orientation_code(f) { + Some(c) => assert_eq!(orientation_function(c), Some(f)), + None => assert_eq!(f, DativeFunction::Judicantis), + } + } + let codes: Vec = DativeFunction::ALL + .iter() + .filter_map(|&f| orientation_code(f)) + .collect(); + assert_eq!( + codes, + [1, 2, 3, 4, 5, 6, 7], + "all seven non-zero states used" + ); + assert_eq!(orientation_function(0), None); + } + + #[test] + fn independent_corroboration_climbs_the_ladder() { + let v = vec![svo(DOG, BIT, MAN); 3]; + let (rows, rep) = run(&v); + assert_eq!(rep.rows, 3); + let last = rows[2].edge; + assert_eq!(last.frequency_u8(), 255); + assert_eq!(last.confidence_u8(), (3 * 255 / 4) as u8); + let acts: Vec = rows.iter().map(|r| r.edge.activation()).collect(); + assert_eq!( + acts, + [4, 0, 0], + "a repeat of a settled belief changes nothing" + ); + assert_eq!(last.novelty_wisdom(), 3); + assert_eq!(epi(last).certification(), Certification3::Related); + assert_eq!(epi(last).topology(), Topology2::Direct); + assert_eq!(last.direction(), 0, "no attested form: plain core argument"); + assert_eq!(last.causal_mask(), CausalMask::SO); + // The rows of one statement are its time series. + let rungs: Vec = rows.iter().map(|r| r.edge.novelty_wisdom()).collect(); + assert_eq!(rungs, [1, 2, 3]); + assert_eq!( + epi(rows[0].edge).certification(), + Certification3::Associated + ); + } + + /// Stay silent: a repeat inside ONE verse is dependent — no new support, + /// no climb, no certification beyond Associated. + #[test] + fn a_repeat_in_one_verse_is_dependent() { + let mut v = svo(DOG, BIT, MAN); + v.push(Reading::stop()); + v.extend(svo(DOG, BIT, MAN)); + let (rows, rep) = run(&[v]); + assert_eq!(rows.len(), 2); + assert_eq!(rep.dependent, 1); + let e = rows[1].edge; + assert_eq!(rows[1].support, 1); + assert_eq!(e.novelty_wisdom(), 1, "no climb on dependent evidence"); + assert_eq!(e.activation(), 0, "dependent evidence moves no frequency"); + assert_eq!(epi(e).certification(), Certification3::Associated); + } + + /// A contest drops the rival back to Staunen; the activation is the change + /// of the row's own frequency, and the climb waits for a positive balance. + #[test] + fn a_contest_is_counted_and_the_balance_decides_the_climb() { + let v = vec![ + svo(DOG, BIT, MAN), + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, CAT), + svo(DOG, BIT, CAT), + ]; + let (rows, rep) = run(&v); + assert_eq!( + rep.contests, 3, + "each later verse contests the existing rival" + ); + let cat = rows[4].edge; + assert_eq!(cat.frequency_u8(), (3 * 255 / 5) as u8); + let acts: Vec = rows.iter().map(|r| r.edge.activation()).collect(); + // man 128→255; man stays; cat 0→85; 85→127; 127→153. + assert_eq!(acts, [4, 0, 3, 1, 1]); + assert_eq!(rows[2].edge.novelty_wisdom(), 1); + // pos 2 vs 2: no positive balance, no climb. + assert_eq!( + rows[3].edge.novelty_wisdom(), + 1, + "no climb without a positive balance" + ); + assert_eq!(rows[4].edge.novelty_wisdom(), 2, "3 vs 2 climbs"); + } + + /// A contest does not null the rival: `man` reached rung 2, a `cat` verse + /// contests it, and the next `man` verse climbs on to 3. Both objects stay + /// in the group's preserved tension. + #[test] + fn a_contested_rival_keeps_its_climb() { + let v = vec![ + svo(DOG, BIT, MAN), + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, MAN), + ]; + let (rows, rep) = run(&v); + assert_eq!(rows[1].edge.novelty_wisdom(), 2); + assert_eq!(rows[3].edge.activation(), 1, "170 → 191"); + assert_eq!(rows[3].edge.novelty_wisdom(), 3, "no reset, one climb"); + assert_eq!(rows[2].revision, Some(RevisionKind::HorizonFusion)); + assert_eq!(rows[3].revision, Some(RevisionKind::HorizonFusion)); + assert!(!rows[1].contested, "no rival yet"); + assert!(rows[2].contested && rows[3].contested); + assert_eq!(rep.contests, 2); + // The man arc skips the cat row: W points at the man row of verse 1. + assert_eq!(rows[3].edge.w_slot(), 2); + } + + /// A contradiction with no new independent root is suspended, not + /// counted: one verse states `dog bit man` and `dog bit cat`. The cat row + /// earns no support and no fact; the tension is kept. + #[test] + fn a_contradiction_from_one_source_is_suspended() { + let mut v = svo(DOG, BIT, MAN); + v.push(Reading::stop()); + v.extend(svo(DOG, BIT, CAT)); + let w = run_wired(&[v, svo(DOG, BIT, MAN)]); + assert_eq!(w.report.suspended, 1); + let cat = &w.rows[1]; + assert_eq!(cat.revision, Some(RevisionKind::ContradictionPreserved)); + assert_eq!(cat.support, 0); + assert_eq!(cat.antecedent, 1, "the group counts its one source once"); + assert!(cat.contested); + assert_eq!(w.witnesses[0].get(2).unwrap().spo_fact_ref, None); + let cat_stmt = CStmt { + s: DOG, + cop: Copula::Rel(BIT), + p: CAT, + }; + assert!( + w.facts.get(cat_stmt).is_none(), + "the arena never counted it" + ); + // A later independent man verse fuses (the tension is still live). + assert_eq!(w.rows[2].revision, Some(RevisionKind::HorizonFusion)); + assert_eq!(w.rows[2].support, 2); + } + + fn run_wired(verses: &[Vec]) -> Wired { + let markers: Vec<(u16, u16)> = (0..verses.len() as u16).map(|i| (1, i + 1)).collect(); + let ins: Vec> = verses + .iter() + .map(|r| VerseIn { + readings: r.clone(), + ..VerseIn::default() + }) + .collect(); + wire(&markers, &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap() + } + + /// The W slot is a Markov arc: from the newest row of a statement, each + /// W resolves to the witness of the prior state of the SAME statement, + /// skipping the rival's rows, and the walk ends at W = 0. + #[test] + fn w_walks_the_statement_arc_backwards() { + let v = vec![ + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, MAN), + svo(DOG, BIT, CAT), + svo(DOG, BIT, MAN), + ]; + let w = run_wired(&v); + let table = &w.witnesses[0]; + let mut verses = vec![w.rows[4].verse]; + let mut facts = Vec::new(); + let mut slot = w.rows[4].edge.w_slot(); + while slot != 0 { + let e = table.get(slot).unwrap(); + let vi = (e.mailbox_ref - 1) as usize; + verses.push(vi); + facts.push(e.spo_fact_ref.unwrap()); + let prior = w + .rows + .iter() + .find(|r| r.verse == vi && r.spo.object == MAN) + .unwrap(); + slot = prior.edge.w_slot(); + } + assert_eq!(verses, [4, 2, 0], "newest to oldest, cat rows skipped"); + assert!(facts.windows(2).all(|p| p[0] == p[1]), "one statement"); + let cat_fact = table + .get(w.rows[3].edge.w_slot()) + .unwrap() + .spo_fact_ref + .unwrap(); + assert_ne!(cat_fact, facts[0], "the rival is its own arc"); + assert_eq!(w.report.arc_breaks, 0); + } + + /// A cohort holds 63 rows; a statement whose prior lies in the previous + /// cohort starts a new arc (W = 0) and the break is counted. + #[test] + fn a_cohort_boundary_breaks_the_arc() { + let v = vec![svo(DOG, BIT, MAN); COHORT_ROWS + 2]; + let w = run_wired(&v); + assert_eq!(w.witnesses.len(), 2); + assert_eq!( + w.rows[COHORT_ROWS - 1].edge.w_slot(), + (COHORT_ROWS - 1) as u8 + ); + assert_eq!(w.rows[COHORT_ROWS].cohort, 1); + assert_eq!(w.rows[COHORT_ROWS].edge.w_slot(), 0, "prior is in cohort 0"); + assert_eq!(w.rows[COHORT_ROWS + 1].edge.w_slot(), 1); + assert_eq!(w.report.arc_breaks, 1); + } + + /// F/C stays arm-discovery's law. `f` equals NAL revision of the unit + /// observations; `c` equals it only while uncontested, because + /// arm-discovery counts confidence over the co-occurrence mass. + #[test] + fn f_and_c_against_nal_revision_of_units() { + let pos_u = NarsTruth::new(1.0, 0.5); + let neg_u = NarsTruth::new(0.0, 0.5); + for (pos, neg) in [(1u32, 0u32), (3, 0), (3, 2), (1, 4)] { + let mut t = pos_u; + for _ in 1..pos { + t = t.revision(&pos_u); + } + for _ in 0..neg { + t = t.revision(&neg_u); + } + let arm = arm_to_truth_u8(&rule(Spo::new(1, 2, 3), pos, pos + neg, 1), 1); + let nal_f = (t.frequency * 255.0).floor() as i32; + assert!( + (i32::from(arm.frequency) - nal_f).abs() <= 1, + "f({pos},{neg}): arm {} nal {nal_f}", + arm.frequency + ); + let nal_c = (t.confidence * 255.0).floor() as i32; + let agree = (i32::from(arm.confidence) - nal_c).abs() <= 1; + assert_eq!(agree, neg == 0, "c agrees iff uncontested ({pos},{neg})"); + } + } + + /// A subject relative gets an Antecedent pointer to the matrix event; + /// topology IndirectKnown. With no attested form, orientation stays 0. + #[test] + fn subject_relative_points_at_its_matrix_event() { + // the dog who bit the man saw the cat + let v = vec![ + one(THE, Pos::Det), + one(DOG, Pos::Noun), + one(WHO, Pos::Rel), + one(BIT, Pos::Verb), + one(THE, Pos::Det), + one(MAN, Pos::Noun), + one(SAW, Pos::Verb), + one(THE, Pos::Det), + one(CAT, Pos::Noun), + ]; + let (rows, rep) = run(&[v]); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].origin, Some(Origin::SubjRelative)); + assert_eq!(rows[0].edge.direction(), 0); + assert_eq!(rows[0].loci.at(Locus::Antecedent), 1); + assert_eq!(rep.antecedent_bound, 1); + assert_eq!(epi(rows[0].edge).topology(), Topology2::IndirectKnown); + assert_eq!( + rows[1].loci.at(Locus::Antecedent), + 0, + "matrix event has no pointer" + ); + } + + /// Right corner: `the cat bit the man who saw the dog`. The antecedent + /// `man` is the matrix OBJECT and the matrix event comes FIRST, so the + /// pointer goes back (-1). + #[test] + fn an_antecedent_behind_the_relative_is_found() { + let v = vec![ + one(THE, Pos::Det), + one(CAT, Pos::Noun), + one(BIT, Pos::Verb), + one(THE, Pos::Det), + one(MAN, Pos::Noun), + one(WHO, Pos::Rel), + one(SAW, Pos::Verb), + one(THE, Pos::Det), + one(DOG, Pos::Noun), + ]; + let (rows, rep) = run(&[v]); + assert_eq!(rows.len(), 2); + assert_eq!(rows[0].spo, Spo::new(CAT, BIT, MAN)); + assert!(rows[1].origin.is_some_and(Origin::is_relative)); + assert_eq!(rows[1].loci.at(Locus::Antecedent), -1); + assert_eq!((rep.antecedent_bound, rep.antecedent_unbound), (1, 0)); + } + + fn form(word: u16, fs: &[DativeFunction]) -> FormAttestation { + FormAttestation { + word, + functions: fs.iter().fold(0, |m, f| m | f.bit()), + coverage_complete: true, + evidence_root: 7, + } + } + + /// `the dog gave to the man`: the form `to` inside the clause decides the + /// orientation when it allows one function, abstains (0, undecided) when + /// it allows several, and the code stays out of a clause that lacks it. + #[test] + fn the_attested_form_sets_the_orientation() { + let gave_to = |extra: Vec| { + let mut v = vec![ + one(THE, Pos::Det), + one(DOG, Pos::Noun), + one(BIT, Pos::Verb), + one(TO, Pos::Other), + ]; + v.extend([one(THE, Pos::Det), one(MAN, Pos::Noun)]); + v.extend(extra); + v + }; + let run_forms = |v: Vec, forms: Vec| { + wire( + &[(1, 1)], + &[VerseIn { + readings: v, + forms, + ..VerseIn::default() + }], + 1, + &abi(), + None, + ReadParams::RIGHT_CORNER, + ) + .unwrap() + }; + let w = run_forms( + gave_to(vec![]), + vec![form(TO, &[DativeFunction::Recipient])], + ); + assert_eq!(w.rows[0].edge.direction(), 2, "Recipient"); + assert_eq!(w.report.orientation_coded, 1); + let w = run_forms( + gave_to(vec![]), + vec![form( + TO, + &[DativeFunction::Direction, DativeFunction::Recipient], + )], + ); + assert_eq!(w.rows[0].edge.direction(), 0); + assert_eq!(w.report.orientation_ambiguous, 1); + // A second clause without the form stays at 0. + let mut second = vec![Reading::stop()]; + second.extend(svo(CAT, SAW, DOG)); + let w = run_forms( + gave_to(second), + vec![form(TO, &[DativeFunction::Recipient])], + ); + assert_eq!(w.rows.len(), 2); + assert_eq!( + w.rows[1].edge.direction(), + 0, + "the form is in the other clause" + ); + } + + /// A Lokal lane on the clause narrows a multifunctional form to + /// Direction; without the lane the same form stays undecided. + #[test] + fn a_lokal_lane_narrows_the_form_to_direction() { + let h = + GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nLokal\tdort\tadvmod\t10\n"); + let readings = vec![ + one(THE, Pos::Det), + one(DOG, Pos::Noun), + one(BIT, Pos::Verb), + one(TO, Pos::Other), + one(THE, Pos::Det), + one(MAN, Pos::Noun), + one(THERE, Pos::Other), + ]; + let surface = vec!["the", "dog", "bit", "to", "the", "man", "dort"]; + let forms = vec![form( + TO, + &[DativeFunction::Direction, DativeFunction::Recipient], + )]; + let ins = [VerseIn { + readings, + surface, + forms, + ..VerseIn::default() + }]; + let w = wire( + &[(1, 1)], + &ins, + 1, + &abi(), + Some(&h), + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert!(w.rows[0].tekamolo[3].is_some(), "the Lokal lane landed"); + assert_eq!(w.rows[0].edge.direction(), 1, "Direction"); + let w = wire(&[(1, 1)], &ins, 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap(); + assert_eq!(w.rows[0].edge.direction(), 0, "no lane: undecided"); + } + + /// Quorum points at the nearest agreeing row, Contradiction at the + /// nearest disagreeing one; across verses, the earlier one on a tie. + #[test] + fn quorum_and_contradiction_point_at_peers() { + let v = vec![svo(DOG, BIT, MAN), svo(DOG, BIT, CAT), svo(DOG, BIT, MAN)]; + let (rows, rep) = run(&v); + assert_eq!(rows[2].loci.at(Locus::Quorum), -2); + assert_eq!(rows[2].loci.at(Locus::Contradiction), -1); + assert_eq!(rows[1].loci.at(Locus::Contradiction), -1, "tie: the prior"); + assert_eq!(rows[1].loci.at(Locus::Quorum), 0, "no other cat row"); + assert_eq!(rows[0].loci.at(Locus::Quorum), 2); + assert_eq!((rep.quorum_bound, rep.contradiction_bound), (2, 3)); + } + + /// An ambiguous token leaves the verse untraced: orientation 0, topology + /// Unknown, and no climb. + #[test] + fn an_untraced_verse_claims_no_path() { + // A token both readings skip: the triple is certain, the verse is not + // single-reading, so it cannot be traced. + let mut amb = vec![Reading::new(WHO, PosSet::single(Pos::Det).with(Pos::Other))]; + amb.extend(svo(DOG, BIT, MAN)); + let (rows, rep) = run(&[svo(DOG, BIT, MAN), amb]); + assert_eq!(rep.untraced_verses, 1); + let r = rows + .get(1) + .expect("the certain triple survives the ambiguity"); + assert_eq!(r.edge.direction(), 0); + assert_eq!(epi(r.edge).topology(), Topology2::Unknown); + assert_eq!(r.edge.novelty_wisdom(), 1, "unresolved: no climb"); + } + + fn heuristics() -> GrammarHeuristics { + GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\n") + } + + /// `der Hund biss den Mann. weil die Katze sah den Hund`: effect clause, + /// then the cause clause `weil` opens. + fn weil_verse() -> VerseIn<'static> { + let mut r = svo(DOG, BIT, MAN); + r.push(Reading::stop()); + r.push(one(WHO, Pos::Other)); // `weil`, skipped by the FSM + r.extend(svo(CAT, SAW, DOG)); + let surface = vec![ + "der", "hund", "biss", "den", "mann", ".", "weil", "die", "katze", "sah", "den", "hund", + ]; + VerseIn { + readings: r, + surface, + forms: vec![], + qualia: vec![], + } + } + + fn run_in(ins: &[VerseIn<'_>]) -> Wired { + let markers: Vec<(u16, u16)> = (0..ins.len() as u16).map(|i| (1, i + 1)).collect(); + wire( + &markers, + ins, + 1, + &abi(), + Some(&heuristics()), + ReadParams::RIGHT_CORNER, + ) + .unwrap() + } + + fn impl_stmt(w: &Wired, cause: Spo, effect: Spo) -> CStmt { + let id = |x: Spo| w.events.iter().position(|e| *e == x).unwrap() as u16; + CStmt { + s: id(cause), + cop: Copula::Impl, + p: id(effect), + } + } + + /// Can fire: two independent `weil` verses pool `cause ⇒ effect` in the + /// causes arena (two disjoint stamps), the cause event sits at the + /// intervention rung and is certified CausalCandidate; the effect is not. + #[test] + fn weil_feeds_the_causes_arena() { + let w = run_in(&[weil_verse(), weil_verse()]); + assert_eq!(w.report.implications, 2); + assert_eq!(w.report.implications_revised, 1); + let (cause, effect) = (Spo::new(CAT, SAW, DOG), Spo::new(DOG, BIT, MAN)); + let b = w.causes.get(impl_stmt(&w, cause, effect)).unwrap(); + assert_eq!(b.stamp.0.count_ones(), 2); + assert!(b.truth.confidence > 0.6, "two units pool to c = 2/3"); + let row = |v: usize, x: Spo| w.rows.iter().find(|r| r.verse == v && r.spo == x).unwrap(); + assert_eq!(row(1, cause).edge.causal_mask(), CausalMask::PO); + assert_eq!( + epi(row(1, cause).edge).certification(), + Certification3::CausalCandidate + ); + assert_eq!(row(1, effect).edge.causal_mask(), CausalMask::SO); + assert_eq!( + epi(row(1, effect).edge).certification(), + Certification3::Related + ); + assert_eq!(w.report.causal_candidates, 1); + } + + /// Stay silent: the cause clause seen twice, but the causal claim only + /// once — the cause is Related (two supports), not a candidate. + #[test] + fn one_causal_claim_is_not_a_candidate() { + let mut plain = svo(CAT, SAW, DOG); + plain.push(Reading::stop()); + plain.extend(svo(DOG, BIT, MAN)); + // The plain verse first, so the weil verse's cause row already has two + // supports and a positive balance: only the single implication + // source keeps it from CausalCandidate. + let w = run_in(&[ + VerseIn { + readings: plain, + ..VerseIn::default() + }, + weil_verse(), + ]); + assert_eq!(w.report.implications, 1); + assert_eq!(w.report.implications_revised, 0); + assert_eq!(w.report.causal_candidates, 0); + let cause = w + .rows + .iter() + .find(|r| r.verse == 1 && r.spo == Spo::new(CAT, SAW, DOG)) + .unwrap(); + assert_eq!(cause.support, 2); + assert!(2 * cause.support > cause.antecedent, "positive balance"); + assert_eq!(epi(cause.edge).certification(), Certification3::Related); + assert_eq!( + cause.edge.causal_mask(), + CausalMask::PO, + "the claim is causal" + ); + } + + /// A `weil` clause with no matrix event states a cause of nothing: the + /// claim sits at the intervention rung, but no implication is observed. + #[test] + fn weil_without_an_effect_observes_no_implication() { + let mut r = svo(DOG, BIT, MAN); + r.push(one(WHO, Pos::Other)); + let v = VerseIn { + readings: r, + surface: vec!["der", "hund", "biss", "den", "mann", "weil"], + forms: vec![], + qualia: vec![], + }; + let w = run_in(&[v.clone(), v]); + assert_eq!(w.report.intervention_rows, 2); + assert_eq!(w.report.implications, 0); + assert!(w.causes.entries().is_empty()); + assert_eq!(epi(w.rows[1].edge).certification(), Certification3::Related); + } + + /// The facts arena pools independent verses exactly as arm-discovery's + /// confidence does for an uncontested statement, and one verse repeated + /// is CHOICE, never pooled. + #[test] + fn facts_arena_agrees_with_arm_and_refuses_a_repeat() { + let (dog_bit_man, w) = ( + CStmt { + s: DOG, + cop: Copula::Rel(BIT), + p: MAN, + }, + run_in(&vec![ + VerseIn { + readings: svo(DOG, BIT, MAN), + ..VerseIn::default() + }; + 3 + ]), + ); + let b = w.facts.get(dog_bit_man).unwrap(); + let arm = w.rows[2].edge.confidence_u8(); + assert!(((b.truth.confidence * 255.0) as i32 - i32::from(arm)).abs() <= 1); + assert_eq!(b.stamp.0.count_ones(), 3); + + let mut twice = svo(DOG, BIT, MAN); + twice.push(Reading::stop()); + twice.extend(svo(DOG, BIT, MAN)); + let w = run_in(&[VerseIn { + readings: twice, + ..VerseIn::default() + }]); + let b = w.facts.get(dog_bit_man).unwrap(); + assert_eq!(b.stamp.0.count_ones(), 1, "one verse is one source"); + assert!((b.truth.confidence - 0.5).abs() < 1e-6, "not pooled"); + } + + const QUICK: u8 = 42; + + /// One verse with surfaces and per-token qualia. + fn qverse(tokens: &[(u16, Pos, &'static str, u8)]) -> VerseIn<'static> { + VerseIn { + readings: tokens.iter().map(|&(id, p, _, _)| one(id, p)).collect(), + surface: tokens.iter().map(|&(_, _, w, _)| w).collect(), + forms: vec![], + qualia: tokens.iter().map(|&(_, _, _, q)| q).collect(), + } + } + + fn qrun(v: VerseIn<'static>) -> (WiredRow, WireReport) { + let w = wire(&[(1, 1)], &[v], 1, &abi(), None, ReadParams::RIGHT_CORNER).unwrap(); + (w.rows[0], w.report) + } + + /// The same qualia index is adjectival on the subject and adverbial on the + /// event: the pair carrying it is the distinction, not a tag. + #[test] + fn one_qualia_index_adjective_or_adverb_by_pair() { + let (adj, ra) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (WHO, Pos::Adj, "quick", QUICK), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ])); + assert_eq!(adj.spo, Spo::new(DOG, BIT, MAN)); + assert_eq!(qualia_at(adj.qualia, Q_SUBJECT), QUICK); + assert_eq!(qualia_at(adj.qualia, Q_PREDICATE), 0); + assert_eq!(qualia_at(adj.qualia, Q_OBJECT), 0); + assert_eq!(ra.qualia_adjectival, 1); + + let (adv, rv) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Other, "quickly", QUICK), + ])); + assert_eq!(qualia_at(adv.qualia, Q_PREDICATE), QUICK); + assert_eq!(qualia_at(adv.qualia, Q_SUBJECT), 0); + assert_eq!(rv.qualia_adverbial, 1); + // The operand codes sit beside their qualia, equal to the edge's. + assert_eq!(operand_at(adv.qualia, Q_SUBJECT), adv.edge.s_idx()); + assert_eq!(operand_at(adv.qualia, Q_OBJECT), adv.edge.o_idx()); + } + + /// A token the lexicon tags as an adverb (`Pos::Adv`, COCA `r`) is + /// adverbial like a skipped `Other` token (Codex review on #1591). + #[test] + fn a_pos_adv_token_is_adverbial() { + let (adv, rv) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Adv, "quickly", QUICK), + ])); + assert_eq!(qualia_at(adv.qualia, Q_PREDICATE), QUICK); + assert_eq!((rv.qualia_adverbial, rv.qualia_unattached), (1, 0)); + } + + /// A Kausal claim whose cause row the gate suspended is not counted: the + /// verse states `dog bit man` and then `weil dog bit cat`, a rival from a + /// source already counted. Twice. Nothing reaches the causes arena (Codex + /// review on #1591). + #[test] + fn a_suspended_cause_is_not_counted() { + let mut r = svo(DOG, BIT, MAN); + r.push(Reading::stop()); + r.push(one(WHO, Pos::Other)); + r.extend(svo(DOG, BIT, CAT)); + let surface = vec![ + "der", "hund", "biss", "den", "mann", ".", "weil", "der", "hund", "biss", "die", + "katze", + ]; + let v = VerseIn { + readings: r, + surface, + forms: vec![], + qualia: vec![], + }; + let w = run_in(&[v.clone(), v]); + let cause = w + .rows + .iter() + .find(|x| x.spo == Spo::new(DOG, BIT, CAT)) + .unwrap(); + assert!( + cause.tekamolo[KAUSAL_LANE].is_some(), + "the Kausal lane landed" + ); + assert_eq!(cause.revision, Some(RevisionKind::ContradictionPreserved)); + assert_eq!(w.report.implications, 0); + assert!(w.causes.entries().is_empty()); + assert_eq!(w.report.causal_candidates, 0); + } + + /// An adjective on the object lands on the object pair; an adverb before + /// the verb lands on the event; no qualia leaves every qualia byte 0. + #[test] + fn object_adjective_preverbal_adverb_and_silence() { + let (obj, _) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (WHO, Pos::Adj, "quick", QUICK), + (MAN, Pos::Noun, "man", 0), + ])); + assert_eq!(qualia_at(obj.qualia, Q_OBJECT), QUICK); + assert_eq!(qualia_at(obj.qualia, Q_SUBJECT), 0); + let (pre, _) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (WHO, Pos::Other, "quickly", QUICK), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ])); + assert_eq!(qualia_at(pre.qualia, Q_PREDICATE), QUICK); + let (none, r) = qrun(qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ])); + for pair in [Q_SUBJECT, Q_PREDICATE, Q_OBJECT] { + assert_eq!(qualia_at(none.qualia, pair), 0); + } + assert_eq!( + r.qualia_adjectival + r.qualia_adverbial + r.qualia_unattached, + 0 + ); + } + + /// An adverb after the first clause's object attaches to that clause, not + /// to the next one: no noun or verb stands between. + #[test] + fn a_trailing_adverb_stays_with_its_clause() { + let w = wire( + &[(1, 1)], + &[qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Other, "quickly", QUICK), + (THE, Pos::Other, "and", 0), + (THE, Pos::Det, "the", 0), + (CAT, Pos::Noun, "cat", 0), + (SAW, Pos::Verb, "saw", 0), + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + ])], + 1, + &abi(), + None, + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert_eq!(w.rows.len(), 2); + assert_eq!(qualia_at(w.rows[0].qualia, Q_PREDICATE), QUICK); + assert_eq!(qualia_at(w.rows[1].qualia, Q_PREDICATE), 0); + } + + /// An adjective with no noun after it in its sentence stays unattached; + /// it never reaches the next sentence's subject. + #[test] + fn an_adjective_does_not_cross_a_sentence() { + let w = wire( + &[(1, 1)], + &[qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, "caused", 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + (WHO, Pos::Adj, "quick", QUICK), + (0, Pos::Stop, ".", 0), + // The next sentence opens with `dog`, the subject of the + // first: an unbounded noun search would qualify it there. + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (SAW, Pos::Verb, "saw", 0), + (THE, Pos::Det, "the", 0), + (CAT, Pos::Noun, "cat", 0), + ])], + 1, + &abi(), + None, + ReadParams::RIGHT_CORNER, + ) + .unwrap(); + assert_eq!(w.rows.len(), 2); + for row in &w.rows { + assert_eq!(qualia_at(row.qualia, Q_SUBJECT), 0); + } + assert_eq!(w.report.qualia_unattached, 1); + assert_eq!(w.report.qualia_adjectival, 0); + } + + /// The verb cell is the 144-table byte; cell 0 (Becomes, Present) is real + /// and told apart from "unclassified" by the flag; `saw` stays + /// unclassified (a homograph the lexicon refuses). + #[test] + fn verb_cell_carries_a_presence_flag() { + use lance_graph_contract::grammar::role_keys::Tense; + use lance_graph_contract::grammar::verb_table::VerbFamily; + let verse = |verb: &'static str| { + qverse(&[ + (THE, Pos::Det, "the", 0), + (DOG, Pos::Noun, "dog", 0), + (BIT, Pos::Verb, verb, 0), + (THE, Pos::Det, "the", 0), + (MAN, Pos::Noun, "man", 0), + ]) + }; + let (caused, _) = qrun(verse("caused")); + assert_eq!( + verb_cell(caused.qualia), + Some(morton_cell(VerbFamily::Causes, Tense::Past)) + ); + let (becomes, _) = qrun(verse("becomes")); + assert_eq!(verb_cell(becomes.qualia), Some(0), "cell 0 is a real verb"); + let (saw, r) = qrun(verse("saw")); + assert_eq!(verb_cell(saw.qualia), None); + assert_eq!(r.verb_unclassified, 1); + } + + #[test] + fn plain_rows_stay_at_association() { + let (plain, _) = run(&[svo(DOG, BIT, MAN), svo(DOG, BIT, MAN)]); + assert_eq!(plain[1].edge.causal_mask(), CausalMask::SO); + assert_eq!(epi(plain[1].edge).certification(), Certification3::Related); + } +} diff --git a/crates/lance-graph-quack/Cargo.toml b/crates/lance-graph-quack/Cargo.toml index 590bb4178..8c512c885 100644 --- a/crates/lance-graph-quack/Cargo.toml +++ b/crates/lance-graph-quack/Cargo.toml @@ -25,3 +25,6 @@ lance-graph-arm-discovery = { path = "../lance-graph-arm-discovery" } # canonical `CausalEdge64` accessors, never against a re-derived bit layout. # Zero-dep, example-only. causal-edge = { path = "../causal-edge" } +# `examples/wired_reasoning_probe.rs`: the wired CE64 rows of deepnsm-v2, +# checked as Datalog rules over presence planes. Example-only. +deepnsm-v2 = { path = "../deepnsm-v2" } diff --git a/crates/lance-graph-quack/examples/ruff_taint_probe.rs b/crates/lance-graph-quack/examples/ruff_taint_probe.rs new file mode 100644 index 000000000..b7d4b168b --- /dev/null +++ b/crates/lance-graph-quack/examples/ruff_taint_probe.rs @@ -0,0 +1,220 @@ +//! Dog-food probe: ruff's MeTTa "lowerable / interpreter-only" verdict, +//! recomputed as presence-bit algebra through quack and mask-risc. +//! +//! `ruff_metta_spo` classifies every rule head of a MeTTa corpus: a head is +//! interpreter-only when its own body uses a native (or a variable head), or +//! when it calls an interpreter-only head; the second rule runs to a fixpoint +//! over the call graph. ruff computes that fixpoint in Rust over maps. Here +//! the same fixpoint runs over our own substrate, and ruff's verdict is the +//! oracle. +//! +//! # Layout +//! +//! - Every rule head gets one coordinate: its index in sorted order. Names are +//! labels, read once at ingest and never seen by the algebra. +//! - The call graph is ONE presence plane `E` over `n · S` addresses: the bit +//! at `h · S + g` says head `h` calls head `g`. `S` is `n` rounded up to whole +//! words, so every row starts on a word. +//! - The taint plane `T` (one bit per head) is tiled into every row segment, +//! a word copy, giving `T_rep` with bit `h · S + g` = `T[g]`. +//! - One step: head `h` becomes tainted when `And(Plane(E), Plane(T_rep))` +//! has `Any` bit in row `h`'s extent. The callee bit and the taint bit of +//! callee `g` sit at the same coordinate, so the step reads nothing at a +//! position taken from another lane's values. +//! - Iterate until `T` stops changing. +//! +//! # Seed (stated, because it is borrowed) +//! +//! The direct taint (a head's own body uses a native) is not recomputed: it is +//! read from ruff's dump (`direct = 1`). What this probe tests is the CLOSURE. +//! The disable arm below proves the closure is load-bearing. +//! +//! # Input +//! +//! A directory written by +//! `cargo run --manifest-path crates/ruff_metta_spo/Cargo.toml --example metta_census -- --dump ` +//! in the ruff repository (`heads.tsv`, `calls.tsv`). +//! +//! ```text +//! cargo run -p lance-graph-quack --example ruff_taint_probe -- ... +//! ``` + +use std::collections::BTreeMap; +use std::path::Path; + +use lance_graph_mask_risc::exec::{execute_extent, Scratch}; +use lance_graph_mask_risc::{words_for, Foreign, Out, Planes, Program, Value}; +use lance_graph_quack::{lower, Agg, Filter, Mask, Query}; + +struct Dump { + /// Sorted head labels; the index is the coordinate. + labels: Vec, + direct: Vec, + lowerable: Vec, + /// `(caller, callee)` coordinates. + calls: Vec<(usize, usize)>, +} + +fn read_dump(dir: &Path) -> Dump { + let heads = std::fs::read_to_string(dir.join("heads.tsv")).expect("heads.tsv"); + let mut rows: Vec<(String, bool, bool)> = heads + .lines() + .skip(1) + .map(|l| { + let f: Vec<&str> = l.split('\t').collect(); + assert_eq!(f.len(), 3, "heads.tsv line {l:?}"); + (f[0].to_string(), f[1] == "1", f[2] == "1") + }) + .collect(); + rows.sort(); + let labels: Vec = rows.iter().map(|r| r.0.clone()).collect(); + let coord: BTreeMap<&str, usize> = labels + .iter() + .enumerate() + .map(|(i, l)| (l.as_str(), i)) + .collect(); + let calls_text = std::fs::read_to_string(dir.join("calls.tsv")).expect("calls.tsv"); + let calls = calls_text + .lines() + .skip(1) + .map(|l| { + let f: Vec<&str> = l.split('\t').collect(); + assert_eq!(f.len(), 2, "calls.tsv line {l:?}"); + (coord[f[0]], coord[f[1]]) + }) + .collect(); + Dump { + direct: rows.iter().map(|r| r.1).collect(), + lowerable: rows.iter().map(|r| r.2).collect(), + labels, + calls, + } +} + +fn set(words: &mut [u64], i: usize) { + words[i / 64] |= 1 << (i % 64); +} + +fn get(words: &[u64], i: usize) -> bool { + words[i / 64] >> (i % 64) & 1 == 1 +} + +/// The closure over `iterations` steps (`None` = to the fixpoint). Returns the +/// taint plane and the number of steps that changed it. +fn closure(d: &Dump, seed: &[bool], iterations: Option) -> (Vec, usize) { + let n = d.labels.len(); + let w = words_for(n); + let stride = w * 64; + let n_rows = n * stride; + + // E: the call presence plane. Built once, at ingest. + let mut e = vec![0u64; words_for(n_rows)]; + for &(h, g) in &d.calls { + set(&mut e, h * stride + g); + } + + let mut t = vec![0u64; w]; + for (i, &s) in seed.iter().enumerate() { + if s { + set(&mut t, i); + } + } + + // And(E, T_rep), Any — lowered once, run per row extent. + let program: Program = lower(&Query { + filter: Filter::And(vec![Filter::Plane(Mask(0)), Filter::Plane(Mask(1))]), + agg: Agg::Any, + }) + .expect("lowers"); + + let mut changed_steps = 0; + let mut t_rep = vec![0u64; words_for(n_rows)]; + loop { + if iterations.is_some_and(|k| changed_steps >= k) { + break; + } + // Tile T into every row segment: a positional word copy. + for h in 0..n { + t_rep[h * w..(h + 1) * w].copy_from_slice(&t); + } + let masks: [&[u64]; 2] = [&e, &t_rep]; + let planes = Planes { + n_rows, + masks: &masks, + lanes: &[], + }; + let mut next = t.clone(); + for h in 0..n { + if get(&t, h) { + continue; + } + let mut s = Scratch::for_program(&program, n_rows).expect("scratch"); + let v = execute_extent( + &program, + &planes, + &Foreign::NONE, + &mut s, + Out::None, + h * stride..(h + 1) * stride, + ) + .expect("runs"); + if matches!(v, Value::Bool(true)) { + set(&mut next, h); + } + } + if next == t { + break; + } + t = next; + changed_steps += 1; + } + (t, changed_steps) +} + +fn mismatches(d: &Dump, t: &[u64]) -> usize { + (0..d.labels.len()) + .filter(|&h| get(t, h) == d.lowerable[h]) + .count() +} + +fn main() { + let tier = if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else { + "not v4" + }; + println!("tier: {tier}"); + let dirs: Vec = std::env::args().skip(1).collect(); + assert!(!dirs.is_empty(), "pass one or more dump directories"); + for dir in dirs { + let d = read_dump(Path::new(&dir)); + let n = d.labels.len(); + let by_calls = (0..n).filter(|&h| !d.direct[h] && !d.lowerable[h]).count(); + + // 1. To the fixpoint: must equal ruff's verdict on every head. + let (t, steps) = closure(&d, &d.direct, None); + let tainted = (0..n).filter(|&h| get(&t, h)).count(); + let miss = mismatches(&d, &t); + println!( + "{dir}: heads {n}, call edges {}, seed {}, tainted at fixpoint {tainted} after {steps} steps, mismatches vs ruff {miss}", + d.calls.len(), + d.direct.iter().filter(|&&b| b).count() + ); + assert_eq!(miss, 0, "closure disagrees with ruff"); + + // 2. Disable: no closure. The heads tainted only through a call must + // now disagree — exactly those, and there must be some. + let (t0, _) = closure(&d, &d.direct, Some(0)); + let miss0 = mismatches(&d, &t0); + println!( + " disable (0 steps): mismatches {miss0}, heads tainted only through calls {by_calls}" + ); + assert!(by_calls > 0, "fixture cannot tell closure from seed"); + assert_eq!(miss0, by_calls); + + // 3. Silence: an empty seed taints nothing. + let (te, _) = closure(&d, &vec![false; n], None); + assert!((0..n).all(|h| !get(&te, h)), "closure invented taint"); + println!(" silence (empty seed): nothing tainted"); + } +} diff --git a/crates/lance-graph-quack/examples/semantic_map_probe.rs b/crates/lance-graph-quack/examples/semantic_map_probe.rs new file mode 100644 index 000000000..be3e702ba --- /dev/null +++ b/crates/lance-graph-quack/examples/semantic_map_probe.rs @@ -0,0 +1,555 @@ +//! Cartesian addressing of grammar: Haspelmath's semantic maps as two +//! presence planes, checked through quack and mask-risc. +//! +//! Source: M. Haspelmath, "The Geometry of Grammatical Meaning: Semantic Maps +//! and Cross-Linguistic Comparison", in Tomasello (ed.), *The New Psychology +//! of Language*, vol. 2, 2003, pp. 211–242. +//! +//! A semantic map is a graph over grammatical FUNCTIONS (direction, +//! recipient, purpose, …). Its one testable claim: every gram of every +//! language (English `to`, French `à`, …) covers a CONTIGUOUS region of the +//! map. A gram that covers a disconnected set falsifies the map. +//! +//! # Layout +//! +//! - Every function has one coordinate; every gram has one coordinate. Names +//! are labels, used only to build the planes and print results. +//! - `G` (gram × function): bit `g · S + f` = gram `g` is used in function `f`. +//! A parsed occurrence — an AST node of gram `g` in function `f` — sets +//! exactly that bit; the paper's numbered examples are such occurrences. +//! - `E` (function × function): bit `h · S + f` = the map links `h` and `f` +//! (both directions; the map is undirected). +//! - Contiguity of gram `g`: seed the lowest function of `g`'s row, then +//! repeatedly add every function `h` of the row with +//! `Any(And(Plane(E), Plane(R_rep)))` over row `h`, where `R` is the set +//! reached so far, tiled into every row segment. Contiguous iff the fixpoint +//! equals the gram's row. Every compared pair sits at one coordinate. +//! +//! ```text +//! cargo run -p lance-graph-quack --example semantic_map_probe +//! ``` + +use lance_graph_mask_risc::exec::{execute_extent, Scratch}; +use lance_graph_mask_risc::{words_for, Foreign, Out, Planes, Program, Value}; +use lance_graph_quack::{lower, Agg, Filter, Mask, Query}; + +fn set(words: &mut [u64], i: usize) { + words[i / 64] |= 1 << (i % 64); +} + +fn get(words: &[u64], i: usize) -> bool { + words[i / 64] >> (i % 64) & 1 == 1 +} + +/// One semantic map: function labels and its links. +struct Map { + name: &'static str, + functions: &'static [&'static str], + links: &'static [(&'static str, &'static str)], +} + +/// A gram: its language-particular label and the functions it covers. +struct Gram { + label: &'static str, + functions: &'static [&'static str], +} + +impl Map { + fn coord(&self, f: &str) -> usize { + self.functions + .iter() + .position(|x| *x == f) + .unwrap_or_else(|| panic!("{}: unknown function {f:?}", self.name)) + } +} + +/// The two planes for one map and a set of grams. +struct Space { + n: usize, + w: usize, + stride: usize, + e: Vec, + g: Vec, + program: Program, +} + +impl Space { + fn new(map: &Map, grams: &[Gram]) -> Self { + let n = map.functions.len(); + let w = words_for(n); + let stride = w * 64; + let mut e = vec![0u64; words_for(n * stride)]; + for &(a, b) in map.links { + let (a, b) = (map.coord(a), map.coord(b)); + set(&mut e, a * stride + b); + set(&mut e, b * stride + a); + } + let mut g = vec![0u64; words_for(grams.len() * stride).max(1)]; + for (gi, gram) in grams.iter().enumerate() { + for f in gram.functions { + set(&mut g, gi * stride + map.coord(f)); + } + } + let program = lower(&Query { + filter: Filter::And(vec![Filter::Plane(Mask(0)), Filter::Plane(Mask(1))]), + agg: Agg::Any, + }) + .expect("lowers"); + Space { + n, + w, + stride, + e, + g, + program, + } + } + + /// The gram's row, `w` words. + fn row(&self, gi: usize) -> Vec { + self.g[gi * self.w..(gi + 1) * self.w].to_vec() + } + + /// Reachable part of the gram's row from its lowest function, staying + /// inside the row. `steps = None` runs to the fixpoint. + fn reach(&self, gi: usize, steps: Option) -> Vec { + let row = self.row(gi); + let mut r = vec![0u64; self.w]; + if let Some(first) = (0..self.n).find(|&f| get(&row, f)) { + set(&mut r, first); + } + let n_rows = self.n * self.stride; + let mut r_rep = vec![0u64; words_for(n_rows)]; + let mut done = 0; + loop { + if steps.is_some_and(|k| done >= k) { + break; + } + for h in 0..self.n { + r_rep[h * self.w..(h + 1) * self.w].copy_from_slice(&r); + } + let masks: [&[u64]; 2] = [&self.e, &r_rep]; + let planes = Planes { + n_rows, + masks: &masks, + lanes: &[], + }; + let mut next = r.clone(); + for h in 0..self.n { + if !get(&row, h) || get(&r, h) { + continue; + } + let mut s = Scratch::for_program(&self.program, n_rows).expect("scratch"); + let v = execute_extent( + &self.program, + &planes, + &Foreign::NONE, + &mut s, + Out::None, + h * self.stride..(h + 1) * self.stride, + ) + .expect("runs"); + if matches!(v, Value::Bool(true)) { + set(&mut next, h); + } + } + if next == r { + break; + } + r = next; + done += 1; + } + r + } + + fn contiguous(&self, gi: usize) -> bool { + self.reach(gi, None) == self.row(gi) + } +} + +/// Returns the labels of the grams that are NOT contiguous on the map. +fn check(map: &Map, grams: &[Gram]) -> Vec<&'static str> { + let s = Space::new(map, grams); + grams + .iter() + .enumerate() + .filter(|(gi, _)| !s.contiguous(*gi)) + .map(|(_, g)| g.label) + .collect() +} + +// ── Fig. 8.1: typical dative functions ──────────────────────────────────── + +const DATIVE: Map = Map { + name: "dative (Fig. 8.1)", + functions: &[ + "direction", + "purpose", + "recipient", + "experiencer", + "predicative possessor", + "external possessor", + "beneficiary", + "judicantis", + ], + links: &[ + ("direction", "recipient"), + ("direction", "purpose"), + ("recipient", "experiencer"), + ("recipient", "predicative possessor"), + ("recipient", "beneficiary"), + ("predicative possessor", "external possessor"), + ("beneficiary", "external possessor"), + ("beneficiary", "judicantis"), + ], +}; + +const DATIVE_GRAMS: &[Gram] = &[ + // (2a–d), Fig. 8.1 + Gram { + label: "English to", + functions: &["direction", "recipient", "experiencer", "purpose"], + }, + // Fig. 8.2: no purpose, but predicative possession (Ce chien est à moi) + Gram { + label: "French à", + functions: &[ + "direction", + "recipient", + "experiencer", + "predicative possessor", + ], + }, + // Fig. 8.5 and its text: no direction, no predicative possession + Gram { + label: "French dative clitics", + functions: &[ + "recipient", + "experiencer", + "beneficiary", + "external possessor", + "judicantis", + ], + }, +]; + +// ── Fig. 8.6: indefinite pronoun functions ─────────────────────────────── + +const INDEFINITE: Map = Map { + name: "indefinites (Fig. 8.6)", + functions: &[ + "specific known", + "specific unknown", + "irrealis non-specific", + "question", + "conditional", + "indirect negation", + "comparative", + "direct negation", + "free choice", + ], + links: &[ + ("specific known", "specific unknown"), + ("specific unknown", "irrealis non-specific"), + ("irrealis non-specific", "question"), + ("irrealis non-specific", "conditional"), + ("question", "indirect negation"), + ("conditional", "comparative"), + ("indirect negation", "comparative"), + ("indirect negation", "direct negation"), + ("comparative", "free choice"), + ], +}; + +const INDEFINITE_GRAMS: &[Gram] = &[ + // Fig. 8.7 + Gram { + label: "English some-", + functions: &[ + "specific known", + "specific unknown", + "irrealis non-specific", + "question", + "conditional", + ], + }, + Gram { + label: "English any-", + functions: &[ + "question", + "conditional", + "indirect negation", + "comparative", + "direct negation", + "free choice", + ], + }, + // Fig. 8.8 and (8)–(11): -nibud' in irrealis, questions, conditionals; + // not in negation, not free choice + Gram { + label: "Russian -nibud'", + functions: &["irrealis non-specific", "question", "conditional"], + }, +]; + +// ── Fig. 8.9: reflexive and middle functions ───────────────────────────── + +const MIDDLE: Map = Map { + name: "reflexive/middle (Fig. 8.9)", + functions: &[ + "full reflexive", + "grooming/body motion", + "naturally reciprocal", + "anticausative", + "deobjective", + "potential passive", + "passive", + ], + links: &[ + ("full reflexive", "grooming/body motion"), + ("grooming/body motion", "naturally reciprocal"), + ("grooming/body motion", "anticausative"), + ("anticausative", "deobjective"), + ("anticausative", "potential passive"), + ("potential passive", "passive"), + ], +}; + +const MIDDLE_GRAMS: &[Gram] = &[ + // Fig. 8.10, (13) + Gram { + label: "French se", + functions: &[ + "full reflexive", + "grooming/body motion", + "naturally reciprocal", + "anticausative", + "potential passive", + ], + }, + // Fig. 8.10, (14)–(15) + Gram { + label: "Russian -sja", + functions: &[ + "grooming/body motion", + "naturally reciprocal", + "anticausative", + "deobjective", + "potential passive", + "passive", + ], + }, + // Fig. 8.11 + Gram { + label: "Turkish -In", + functions: &["grooming/body motion"], + }, + Gram { + label: "Turkish -Iş", + functions: &["naturally reciprocal"], + }, + Gram { + label: "Turkish -Il", + functions: &["anticausative", "potential passive", "passive"], + }, +]; + +// ── Fig. 8.12: instrumental and related functions ──────────────────────── + +const INSTRUMENTAL: Map = Map { + name: "instrumental (Fig. 8.12)", + functions: &[ + "recipient", + "beneficiary", + "cause", + "co-agent", + "conjunctive", + "comitative", + "instrumental", + "passive", + "source", + ], + links: &[ + ("recipient", "beneficiary"), + ("beneficiary", "cause"), + ("recipient", "co-agent"), + ("co-agent", "comitative"), + ("conjunctive", "comitative"), + ("comitative", "instrumental"), + ("instrumental", "passive"), + ("passive", "source"), + ("cause", "passive"), + ("cause", "source"), + ], +}; + +const INSTRUMENTAL_GRAMS: &[Gram] = &[ + // Fig. 8.13 + Gram { + label: "English with", + functions: &["co-agent", "comitative", "instrumental"], + }, + Gram { + label: "French par", + functions: &["passive", "cause"], + }, + Gram { + label: "German aus", + functions: &["source", "cause"], + }, + Gram { + label: "German von", + functions: &["passive", "source"], + }, + // Fig. 8.14, (21a–g) + Gram { + label: "Seychelles Creole ek", + functions: &[ + "instrumental", + "comitative", + "conjunctive", + "passive", + "source", + "cause", + "recipient", + "co-agent", + ], + }, + // Fig. 8.15 + Gram { + label: "Russian instrumental", + functions: &["instrumental", "passive"], + }, + Gram { + label: "Russian s", + functions: &["co-agent", "comitative", "conjunctive"], + }, +]; + +// ── (4): choosing between three arrangements of one sub-map ────────────── + +const SUB_FUNCTIONS: &[&str] = &["direction", "recipient", "purpose"]; + +const ARRANGE_A: Map = Map { + name: "(4a) purpose – direction – recipient", + functions: SUB_FUNCTIONS, + links: &[("purpose", "direction"), ("direction", "recipient")], +}; +const ARRANGE_B: Map = Map { + name: "(4b) direction – purpose – recipient", + functions: SUB_FUNCTIONS, + links: &[("direction", "purpose"), ("purpose", "recipient")], +}; +const ARRANGE_C: Map = Map { + name: "(4c) direction – recipient – purpose", + functions: SUB_FUNCTIONS, + links: &[("direction", "recipient"), ("recipient", "purpose")], +}; +/// Fig. 8.4: the vacuous map, every function linked to every other. +const VACUOUS: Map = Map { + name: "Fig. 8.4 vacuous", + functions: SUB_FUNCTIONS, + links: &[ + ("direction", "recipient"), + ("direction", "purpose"), + ("recipient", "purpose"), + ], +}; + +const SUB_GRAMS: &[Gram] = &[ + Gram { + label: "English to", + functions: &["direction", "recipient", "purpose"], + }, + // à Adam, à Leipzig; not purpose + Gram { + label: "French à", + functions: &["direction", "recipient"], + }, + // Anna ging zum Spielen; Ich gehe zu Anna; not recipient + Gram { + label: "German zu", + functions: &["direction", "purpose"], + }, +]; + +// ── (23): the Turkish old present as a doughnut ────────────────────────── + +const TENSE: Map = Map { + name: "(23) habitual – progressive – future", + functions: &["habitual", "progressive", "future"], + links: &[("habitual", "progressive"), ("progressive", "future")], +}; + +const TENSE_GRAMS: &[Gram] = &[ + Gram { + label: "German Present", + functions: &["habitual", "progressive", "future"], + }, + Gram { + label: "English Progressive", + functions: &["progressive", "future"], + }, + // ousted from progressive by -Iyor: the hole in the doughnut + Gram { + label: "Turkish old present", + functions: &["habitual", "future"], + }, +]; + +fn main() { + let tier = if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else { + "not v4" + }; + println!("tier: {tier}"); + + // 1. Every gram the chapter draws lies in one contiguous region. + for (map, grams) in [ + (&DATIVE, DATIVE_GRAMS), + (&INDEFINITE, INDEFINITE_GRAMS), + (&MIDDLE, MIDDLE_GRAMS), + (&INSTRUMENTAL, INSTRUMENTAL_GRAMS), + ] { + let broken = check(map, grams); + println!( + "{}: {} grams, non-contiguous {broken:?}", + map.name, + grams.len() + ); + assert!(broken.is_empty(), "{} is falsified by {broken:?}", map.name); + } + + // 2. The arrangement argument of (4): French and German eliminate (4b) + // and (4c); (4a) survives; the vacuous map admits everything. + let mut survivors = vec![]; + for map in [&ARRANGE_A, &ARRANGE_B, &ARRANGE_C, &VACUOUS] { + let broken = check(map, SUB_GRAMS); + println!("{}: non-contiguous {broken:?}", map.name); + if broken.is_empty() { + survivors.push(map.name); + } + } + assert_eq!(survivors, [ARRANGE_A.name, VACUOUS.name]); + assert_eq!(check(&ARRANGE_B, SUB_GRAMS), ["French à"]); + assert_eq!(check(&ARRANGE_C, SUB_GRAMS), ["German zu"]); + + // 3. A gram with a hole is caught: the Turkish old present. + let broken = check(&TENSE, TENSE_GRAMS); + println!("{}: non-contiguous {broken:?}", TENSE.name); + assert_eq!(broken, ["Turkish old present"]); + + // 4. Disable: with no closure steps only the seed is reached, so every + // gram that covers more than one function must read as broken. + let s = Space::new(&DATIVE, DATIVE_GRAMS); + let broken0 = (0..DATIVE_GRAMS.len()) + .filter(|&gi| s.reach(gi, Some(0)) != s.row(gi)) + .count(); + println!( + "disable (0 steps) on {}: {broken0} of {} broken", + DATIVE.name, + DATIVE_GRAMS.len() + ); + assert_eq!(broken0, DATIVE_GRAMS.len()); +} diff --git a/crates/lance-graph-quack/examples/wired_reasoning_probe.rs b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs new file mode 100644 index 000000000..34b7222db --- /dev/null +++ b/crates/lance-graph-quack/examples/wired_reasoning_probe.rs @@ -0,0 +1,302 @@ +//! The field rules of `deepnsm_v2::wire` as Datalog over presence planes, +//! executed through quack and mask-risc, against a row oracle. +//! +//! One row = one wired triple. Every input fact and every output field is a +//! presence plane over the rows; a rule is a quack filter over those planes, +//! so the rule set is the reasoning AST and mask-risc executes it. Names are +//! labels used only to print. +//! +//! ```text +//! R1 pearl_po(E) <-> kausal(E) (Pearl rung from TEKAMOLO) +//! R2 cert_rel(E) <-> support2(E), press_pos(E) (Related or better) +//! R3 candidate(E) -> cert_rel(E) +//! R4 climbed(E) -> support2(E) (a rung above Staunen needs evidence) +//! R5 direct(E) <-> main(E) (topology from the clause path) +//! ``` +//! +//! An equivalence holds when `Any(Or(And(A, Not B), And(B, Not A)))` is false, +//! an implication when `Any(And(A, Not B))` is false. Each side is checked to +//! be non-trivial (some rows, not all) so no rule passes on an empty plane. +//! The falsifier arm corrupts one output bit and expects exactly that rule to +//! fail. +//! +//! ```text +//! cargo run -p lance-graph-quack --example wired_reasoning_probe +//! ``` + +use causal_edge::CausalMask; +use deepnsm_v2::fsm::{Origin, Pos, PosSet, Reading}; +use deepnsm_v2::haspelmath::FrozenOperandAbi; +use deepnsm_v2::tekamolo::{GrammarHeuristics, ReadParams}; +use deepnsm_v2::wire::{wire, VerseIn, WiredRow, KAUSAL_LANE}; +use lance_graph_contract::epistemic_state5::{Certification3, Epi5Gen, EpistemicState5, Topology2}; +use lance_graph_mask_risc::exec::{execute_extent, Scratch}; +use lance_graph_mask_risc::{words_for, Foreign, Out, Planes, Value}; +use lance_graph_quack::{lower, Agg, Filter, Mask, Query}; + +const NOUNS: [u16; 6] = [10, 11, 12, 13, 14, 15]; +const VERBS: [u16; 3] = [20, 21, 22]; +const THE: u16 = 30; +const WHO: u16 = 31; +const WEIL: u16 = 32; + +fn one(id: u16, pos: Pos) -> Reading { + Reading::new(id, PosSet::single(pos)) +} + +struct Lcg(u64); +impl Lcg { + fn next(&mut self, n: u64) -> u64 { + self.0 = self + .0 + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + (self.0 >> 33) % n + } +} + +/// A synthetic corpus: SVO and subject-relative clauses, a `weil` marker on +/// some verses, few objects per (S, P) so statements are both corroborated +/// and contested. +fn corpus(n: usize) -> (Vec>, Vec>) { + let mut g = Lcg(0x5EED); + let (mut rs, mut ss) = (Vec::new(), Vec::new()); + for _ in 0..n { + let s = NOUNS[g.next(2) as usize]; + let v = VERBS[g.next(2) as usize]; + let o = NOUNS[2 + g.next(2) as usize]; + let mut r = vec![one(THE, Pos::Det), one(s, Pos::Noun)]; + let mut w = vec!["der", "x"]; + if g.next(3) == 0 { + r.extend([ + one(WHO, Pos::Rel), + one(v, Pos::Verb), + one(THE, Pos::Det), + one(o, Pos::Noun), + ]); + w.extend(["der", "v", "den", "y"]); + let o2 = NOUNS[4 + g.next(2) as usize]; + r.extend([one(v, Pos::Verb), one(THE, Pos::Det), one(o2, Pos::Noun)]); + w.extend(["v", "den", "z"]); + } else { + r.extend([one(v, Pos::Verb), one(THE, Pos::Det), one(o, Pos::Noun)]); + w.extend(["v", "den", "y"]); + } + if g.next(2) == 0 { + // `. weil `: the cause event the Kausal lane marks. + let (cs, co) = (NOUNS[g.next(2) as usize], NOUNS[2 + g.next(2) as usize]); + r.extend([ + Reading::stop(), + one(WEIL, Pos::Other), + one(THE, Pos::Det), + one(cs, Pos::Noun), + one(VERBS[2], Pos::Verb), + one(THE, Pos::Det), + one(co, Pos::Noun), + ]); + w.extend([".", "weil", "die", "x", "v", "den", "y"]); + } + rs.push(r); + ss.push(w); + } + (rs, ss) +} + +fn set(w: &mut [u64], i: usize) { + w[i / 64] |= 1 << (i % 64); +} + +fn get(w: &[u64], i: usize) -> bool { + w[i / 64] >> (i % 64) & 1 == 1 +} + +const INPUTS: [&str; 5] = ["kausal", "support2", "press_pos", "main", "climbed"]; +const OUTPUTS: [&str; 4] = ["pearl_po", "cert_rel", "candidate", "direct"]; + +/// Plane index by label: inputs first, then outputs. +fn ix(label: &str) -> usize { + INPUTS + .iter() + .chain(OUTPUTS.iter()) + .position(|x| *x == label) + .unwrap() +} + +fn planes_of(rows: &[WiredRow]) -> Vec> { + let n = rows.len(); + let mut p = vec![vec![0u64; words_for(n)]; INPUTS.len() + OUTPUTS.len()]; + for (i, r) in rows.iter().enumerate() { + let e = r.edge; + let st = EpistemicState5::decode(Epi5Gen::V1, e.epistemic_raw5()).unwrap(); + let cert = st.certification(); + let facts = [ + ("kausal", r.tekamolo[KAUSAL_LANE].is_some()), + ("support2", r.support >= 2), + ("press_pos", 2 * r.support > r.antecedent), + ("main", r.origin == Some(Origin::Main)), + ("climbed", e.novelty_wisdom() > 1), + ("pearl_po", e.causal_mask() == CausalMask::PO), + ( + "cert_rel", + matches!( + cert, + Certification3::Related | Certification3::CausalCandidate + ), + ), + ("candidate", cert == Certification3::CausalCandidate), + ("direct", st.topology() == Topology2::Direct), + ]; + for (label, on) in facts { + if on { + set(&mut p[ix(label)], i); + } + } + } + p +} + +fn pl(label: &str) -> Filter { + Filter::Plane(Mask(ix(label) as u16)) +} + +fn not(f: Filter) -> Filter { + Filter::Not(Box::new(f)) +} + +fn and(a: Filter, b: Filter) -> Filter { + Filter::And(vec![a, b]) +} + +/// The violation filter of each rule: rows that break it. +fn rules() -> Vec<(&'static str, Filter)> { + let iff = + |a: Filter, b: Filter| Filter::Or(vec![and(a.clone(), not(b.clone())), and(b, not(a))]); + vec![ + ("R1 pearl_po <-> kausal", iff(pl("pearl_po"), pl("kausal"))), + ( + "R2 cert_rel <-> support2, press_pos", + iff(pl("cert_rel"), and(pl("support2"), pl("press_pos"))), + ), + ( + "R3 candidate -> cert_rel", + and(pl("candidate"), not(pl("cert_rel"))), + ), + ( + "R4 climbed -> support2", + and(pl("climbed"), not(pl("support2"))), + ), + ("R5 direct <-> main", iff(pl("direct"), pl("main"))), + ] +} + +fn run(filter: &Filter, agg: Agg, planes: &[Vec], n: usize) -> Value { + let program = lower(&Query { + filter: filter.clone(), + agg, + }) + .expect("lowers"); + let masks: Vec<&[u64]> = planes.iter().map(Vec::as_slice).collect(); + let p = Planes { + n_rows: n, + masks: &masks, + lanes: &[], + }; + let mut s = Scratch::for_program(&program, n).expect("scratch"); + execute_extent(&program, &p, &Foreign::NONE, &mut s, Out::None, 0..n).expect("runs") +} + +/// Row oracle: the violation count of each rule, by a plain loop. +fn oracle(planes: &[Vec], n: usize) -> Vec { + let b = |l: &str, i: usize| get(&planes[ix(l)], i); + let mut v = vec![0; 5]; + for i in 0..n { + v[0] += usize::from(b("pearl_po", i) != b("kausal", i)); + v[1] += usize::from(b("cert_rel", i) != (b("support2", i) && b("press_pos", i))); + v[2] += usize::from(b("candidate", i) && !b("cert_rel", i)); + v[3] += usize::from(b("climbed", i) && !b("support2", i)); + v[4] += usize::from(b("direct", i) != b("main", i)); + } + v +} + +fn check(planes: &[Vec], n: usize) -> Vec { + let want = oracle(planes, n); + let mut got = Vec::new(); + for ((name, f), w) in rules().iter().zip(&want) { + let Value::Count(c) = run(f, Agg::Count, planes, n) else { + panic!("{name}: count") + }; + let any = matches!(run(f, Agg::Any, planes, n), Value::Bool(true)); + assert_eq!(c, *w, "{name}: quack vs row oracle"); + assert_eq!(any, *w > 0, "{name}: Any vs Count"); + got.push(c); + } + got +} + +fn main() { + let n_verses = 400; + let (readings, surfaces) = corpus(n_verses); + let markers: Vec<(u16, u16)> = (0..n_verses) + .map(|i| (1 + (i / 50) as u16, 1 + (i % 50) as u16)) + .collect(); + let ins: Vec> = readings + .iter() + .zip(&surfaces) + .map(|(r, s)| VerseIn { + readings: r.clone(), + surface: s.clone(), + forms: vec![], + qualia: vec![], + }) + .collect(); + let mut ops: Vec = NOUNS.to_vec(); + ops.extend(VERBS); + ops.extend([THE, WHO, WEIL]); + let abi = FrozenOperandAbi::new(1, &ops).unwrap(); + let h = GrammarHeuristics::parse("# lane\tlemma\trelation\tcount\nKausal\tweil\tmark\t273\n"); + let w = wire(&markers, &ins, 1, &abi, Some(&h), ReadParams::RIGHT_CORNER).unwrap(); + let (rows, rep) = (w.rows, w.report); + let n = rows.len(); + println!("rows {n} report {rep:?}"); + + let planes = planes_of(&rows); + for (k, label) in INPUTS.iter().chain(OUTPUTS.iter()).enumerate() { + let c = (0..n).filter(|&i| get(&planes[k], i)).count(); + println!(" {label:10} {c:5} / {n}"); + assert!( + c > 0 && c < n, + "{label}: plane must be non-trivial (got {c})" + ); + } + + let v = check(&planes, n); + for ((name, _), c) in rules().iter().zip(&v) { + println!("{name:40} violations {c}"); + } + assert!( + v.iter().all(|&c| c == 0), + "every rule holds on the wired rows" + ); + + // Falsifier: flip one output bit per rule and expect exactly that rule to + // fail (R1..R5 read pearl_po, cert_rel, candidate, climbed, direct). + let flips = ["pearl_po", "cert_rel", "candidate", "climbed", "direct"]; + for (r, label) in flips.iter().enumerate() { + let mut bad = planes.clone(); + let p = &mut bad[ix(label)]; + // A row where flipping must violate: for implications, a row where + // the consequent is false; otherwise any row. + let target = match *label { + "candidate" => (0..n).find(|&i| !get(&planes[ix("cert_rel")], i)), + "climbed" => (0..n).find(|&i| !get(&planes[ix("support2")], i)), + _ => Some(0), + } + .expect("a row to flip"); + p[target / 64] ^= 1 << (target % 64); + let v = check(&bad, n); + assert!(v[r] > 0, "flipping {label} must break rule {}", r + 1); + println!("flip {label:10} -> rule {} violations {}", r + 1, v[r]); + } + println!("ok"); +}