Skip to content

Commit d29f971

Browse files
committed
refactor(G4): tense modulation breaks the broadcast-flatness; per-tense priors now vary
Closes the G4 loose end where default_table() broadcast 12 family priors across all 12 tenses, producing a degenerate 144-cell table with only 12 unique values and zero tense x family interaction. Adds: - SlotPriorDelta { temporal, kausal, modal, lokal, instrument } - SlotPrior::combine(self, delta) -> SlotPrior (sum + clamp to [0,1]) - tense_modifier(tense: Tense) -> SlotPriorDelta with linguistically grounded modulation per Quirk et al. *Comprehensive Grammar of the English Language* sections 4.21-4.27 (tense / aspect / mood) - base_prior(family) factored out from default_table() Modulation rules (after reading the actual Tense enum from role_keys.rs; the enum has Potential, no Subjunctive — Potential fills that role): Perfect | Pluperfect | FuturePerfect : temporal +0.15 PresentContinuous | PastContinuous | FutureContinuous : temporal +0.10, modal -0.05 Imperative : temporal -0.20, modal +0.20 Potential : temporal -0.10, kausal -0.05, modal +0.25 Habitual : temporal -0.10, modal +0.05 Present | Past | Future : no modifier default_table() now iterates (family, tense) and applies final = base_prior(family).combine(tense_modifier(tense)). Failing-test-first: test_perfect_amplifies_temporal_within_family was written and confirmed to fail on the broadcast-flat code (Causes/Perfect == Causes/Past == 0.4); after the fix it passes (0.55 > 0.4). Also adds: - test_imperative_suppresses_temporal (Causes: 0.2 < 0.4 temporal, modal up) - test_subjunctive_amplifies_modal (Supports/Potential modal > Present) - test_continuous_amplifies_temporal_less_than_perfect (ordering sanity) - test_combine_clamps_to_unit_interval (clamping) Two pre-existing tests that sampled non-default tenses (Refines/Perfect, Dissolves/Imperative) had encoded the broadcast-flat assumption; switched their tense to Present (unmarked, no modifier) so they keep asserting the family-level base prior. The tense-specific behaviour they previously shadowed is now covered by the new modulation tests. cargo test -p lance-graph-contract verb_table --lib: 21 passed (was 16). cargo test -p lance-graph-contract --lib: 324 passed, 0 failed.
1 parent 58cce20 commit d29f971

1 file changed

Lines changed: 202 additions & 82 deletions

File tree

crates/lance-graph-contract/src/grammar/verb_table.rs

Lines changed: 202 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@
1010
//! See PR #279 outlook E3 + grammar-landscape.md §9.
1111
//!
1212
//! META-AGENT: `pub mod verb_table;` to mod.rs.
13+
//!
14+
//! ## Tense modulation (G4 loose end)
15+
//!
16+
//! Earlier seed broadcast 12 family priors across all 12 tenses, producing a
17+
//! degenerate 12-unique-value table with zero tense x family interaction. The
18+
//! refactor introduces `SlotPriorDelta` + `SlotPrior::combine` and a
19+
//! `tense_modifier(Tense)` function so each cell becomes
20+
//! `final = base.combine(tense_modifier(tense))`.
21+
//!
22+
//! Modifiers are linguistically grounded in standard English grammar
23+
//! (Quirk, Greenbaum, Leech & Svartvik, *A Comprehensive Grammar of the
24+
//! English Language*, Longman 1985, sections 4.21-4.27 on tense / aspect /
25+
//! mood):
26+
//!
27+
//! - Perfect aspects (Perfect, Pluperfect, FuturePerfect) emphasise
28+
//! completion and therefore temporal anchoring -> `temporal +0.15`.
29+
//! - Continuous (progressive) aspects emphasise an ongoing process ->
30+
//! `temporal +0.10`, `modal -0.05` (less anchored, less modal weight).
31+
//! - Imperative is a timeless directive command -> `temporal -0.20`,
32+
//! `modal +0.20`.
33+
//! - Potential (irrealis / possibility mood; this enum's stand-in for the
34+
//! Subjunctive) emphasises possibility -> `temporal -0.10`, `modal +0.25`,
35+
//! `kausal -0.05` (cause is hypothetical).
36+
//! - Habitual is recurring-as-timeless -> `temporal -0.10`, `modal +0.05`.
37+
//! - Default (Present, Past, Future) leaves the base prior untouched.
38+
//!
39+
//! All resulting axes are clamped to [0.0, 1.0] in `SlotPrior::combine`.
1340
1441
use crate::grammar::role_keys::Tense;
1542

@@ -46,6 +73,66 @@ impl SlotPrior {
4673
pub const fn uniform() -> Self {
4774
Self { temporal: 0.5, kausal: 0.5, modal: 0.5, lokal: 0.5, instrument: 0.5 }
4875
}
76+
77+
/// Apply a tense-driven delta to each axis and clamp the result to
78+
/// `[0.0, 1.0]`. This is how the broadcast-flat 12 priors per family
79+
/// gain tense x family interaction (G4 loose end).
80+
pub fn combine(self, delta: SlotPriorDelta) -> Self {
81+
fn clamp(x: f32) -> f32 {
82+
if x < 0.0 { 0.0 } else if x > 1.0 { 1.0 } else { x }
83+
}
84+
Self {
85+
temporal: clamp(self.temporal + delta.temporal),
86+
kausal: clamp(self.kausal + delta.kausal),
87+
modal: clamp(self.modal + delta.modal),
88+
lokal: clamp(self.lokal + delta.lokal),
89+
instrument: clamp(self.instrument + delta.instrument),
90+
}
91+
}
92+
}
93+
94+
/// Additive delta applied to a `SlotPrior` per tense. Each axis is summed
95+
/// with the base prior and clamped via `SlotPrior::combine`. Default = no
96+
/// change (all zeros).
97+
#[derive(Debug, Clone, Copy, Default)]
98+
pub struct SlotPriorDelta {
99+
pub temporal: f32,
100+
pub kausal: f32,
101+
pub modal: f32,
102+
pub lokal: f32,
103+
pub instrument: f32,
104+
}
105+
106+
/// Tense-driven modifier table. Linguistic grounding: Quirk et al.
107+
/// *Comprehensive Grammar of the English Language* sections 4.21-4.27.
108+
/// See module-level doc comment for the per-tense rationale.
109+
pub fn tense_modifier(tense: Tense) -> SlotPriorDelta {
110+
use Tense::*;
111+
match tense {
112+
// Perfect aspects emphasise completion -> temporal anchoring.
113+
Perfect | Pluperfect | FuturePerfect => SlotPriorDelta {
114+
temporal: 0.15, kausal: 0.0, modal: 0.0, lokal: 0.0, instrument: 0.0,
115+
},
116+
// Continuous (progressive) aspects emphasise ongoing process.
117+
PresentContinuous | PastContinuous | FutureContinuous => SlotPriorDelta {
118+
temporal: 0.10, kausal: 0.0, modal: -0.05, lokal: 0.0, instrument: 0.0,
119+
},
120+
// Imperative: timeless directive -> suppresses temporal, amplifies modal.
121+
Imperative => SlotPriorDelta {
122+
temporal: -0.20, kausal: 0.0, modal: 0.20, lokal: 0.0, instrument: 0.0,
123+
},
124+
// Potential (irrealis / subjunctive role): possibility -> modal up,
125+
// kausal slightly down (cause is hypothetical), temporal slightly down.
126+
Potential => SlotPriorDelta {
127+
temporal: -0.10, kausal: -0.05, modal: 0.25, lokal: 0.0, instrument: 0.0,
128+
},
129+
// Habitual: recurring-as-timeless.
130+
Habitual => SlotPriorDelta {
131+
temporal: -0.10, kausal: 0.0, modal: 0.05, lokal: 0.0, instrument: 0.0,
132+
},
133+
// Present, Past, Future: unmarked tense, no modifier.
134+
Present | Past | Future => SlotPriorDelta::default(),
135+
}
49136
}
50137

51138
/// 144-cell lookup: rows = `VerbFamily`, columns = `Tense`. Indexing is
@@ -87,89 +174,37 @@ impl VerbRoleTable {
87174
/// The numbers are *priors*, not facts: a future PR replaces them
88175
/// with corpus-derived statistics. Mark this `// starter — tune empirically`
89176
/// in any consumer that depends on specific values.
90-
pub fn default_table() -> VerbRoleTable {
91-
let mut t = VerbRoleTable::new_uniform();
92-
93-
// --- Change verbs: high Temporal + Modal ---
94-
95-
// BECOMES: state-change, strongly temporal + modal
96-
let becomes = SlotPrior { temporal: 0.9, kausal: 0.2, modal: 0.7, lokal: 0.3, instrument: 0.2 };
97-
for tense in Tense::ALL {
98-
t.set(VerbFamily::Becomes, tense, becomes);
99-
}
100-
101-
// DISSOLVES: destruction-as-change, high temporal + modal
102-
let dissolves = SlotPrior { temporal: 0.85, kausal: 0.3, modal: 0.7, lokal: 0.25, instrument: 0.2 };
103-
for tense in Tense::ALL {
104-
t.set(VerbFamily::Dissolves, tense, dissolves);
105-
}
106-
107-
// ABSTRACTS: conceptual transformation, high modal + temporal
108-
let abstracts = SlotPrior { temporal: 0.7, kausal: 0.25, modal: 0.85, lokal: 0.15, instrument: 0.2 };
109-
for tense in Tense::ALL {
110-
t.set(VerbFamily::Abstracts, tense, abstracts);
111-
}
112-
113-
// MIRRORS: reflection/symmetry, temporal + modal + lokal
114-
let mirrors = SlotPrior { temporal: 0.75, kausal: 0.2, modal: 0.7, lokal: 0.6, instrument: 0.15 };
115-
for tense in Tense::ALL {
116-
t.set(VerbFamily::Mirrors, tense, mirrors);
117-
}
118-
119-
// --- Action verbs: high Kausal + Temporal ---
120-
121-
// CAUSES: strong causal agency, high kausal + instrument
122-
let causes = SlotPrior { temporal: 0.4, kausal: 0.95, modal: 0.4, lokal: 0.3, instrument: 0.5 };
123-
for tense in Tense::ALL {
124-
t.set(VerbFamily::Causes, tense, causes);
125-
}
126-
127-
// PREVENTS: blocking action, high kausal + temporal
128-
let prevents = SlotPrior { temporal: 0.7, kausal: 0.9, modal: 0.4, lokal: 0.25, instrument: 0.35 };
129-
for tense in Tense::ALL {
130-
t.set(VerbFamily::Prevents, tense, prevents);
177+
/// Base prior for a `VerbFamily` (pre-tense-modulation). The full per-cell
178+
/// prior is `base_prior(family).combine(tense_modifier(tense))`.
179+
pub fn base_prior(family: VerbFamily) -> SlotPrior {
180+
match family {
181+
// --- Change verbs: high Temporal + Modal ---
182+
VerbFamily::Becomes => SlotPrior { temporal: 0.9, kausal: 0.2, modal: 0.7, lokal: 0.3, instrument: 0.2 },
183+
VerbFamily::Dissolves => SlotPrior { temporal: 0.85, kausal: 0.3, modal: 0.7, lokal: 0.25, instrument: 0.2 },
184+
VerbFamily::Abstracts => SlotPrior { temporal: 0.7, kausal: 0.25, modal: 0.85, lokal: 0.15, instrument: 0.2 },
185+
VerbFamily::Mirrors => SlotPrior { temporal: 0.75, kausal: 0.2, modal: 0.7, lokal: 0.6, instrument: 0.15 },
186+
// --- Action verbs: high Kausal + Temporal ---
187+
VerbFamily::Causes => SlotPrior { temporal: 0.4, kausal: 0.95, modal: 0.4, lokal: 0.3, instrument: 0.5 },
188+
VerbFamily::Prevents => SlotPrior { temporal: 0.7, kausal: 0.9, modal: 0.4, lokal: 0.25, instrument: 0.35 },
189+
VerbFamily::Transforms => SlotPrior { temporal: 0.8, kausal: 0.85, modal: 0.35, lokal: 0.3, instrument: 0.6 },
190+
// --- State verbs: high Modal, low Temporal ---
191+
VerbFamily::Supports => SlotPrior { temporal: 0.2, kausal: 0.35, modal: 0.85, lokal: 0.2, instrument: 0.3 },
192+
VerbFamily::Contradicts => SlotPrior { temporal: 0.15, kausal: 0.7, modal: 0.9, lokal: 0.15, instrument: 0.1 },
193+
VerbFamily::Refines => SlotPrior { temporal: 0.3, kausal: 0.4, modal: 0.8, lokal: 0.2, instrument: 0.35 },
194+
VerbFamily::Grounds => SlotPrior { temporal: 0.25, kausal: 0.3, modal: 0.75, lokal: 0.85, instrument: 0.2 },
195+
// --- Discovery / enablement: high Kausal + Lokal ---
196+
VerbFamily::Enables => SlotPrior { temporal: 0.35, kausal: 0.8, modal: 0.4, lokal: 0.7, instrument: 0.45 },
131197
}
198+
}
132199

133-
// TRANSFORMS: active change, high kausal + temporal + instrument
134-
let transforms = SlotPrior { temporal: 0.8, kausal: 0.85, modal: 0.35, lokal: 0.3, instrument: 0.6 };
135-
for tense in Tense::ALL {
136-
t.set(VerbFamily::Transforms, tense, transforms);
137-
}
138-
139-
// --- State verbs: high Modal, low Temporal ---
140-
141-
// SUPPORTS: epistemic backing, high modal
142-
let supports = SlotPrior { temporal: 0.2, kausal: 0.35, modal: 0.85, lokal: 0.2, instrument: 0.3 };
143-
for tense in Tense::ALL {
144-
t.set(VerbFamily::Supports, tense, supports);
145-
}
146-
147-
// CONTRADICTS: logical opposition, high modal + kausal
148-
let contradicts = SlotPrior { temporal: 0.15, kausal: 0.7, modal: 0.9, lokal: 0.15, instrument: 0.1 };
149-
for tense in Tense::ALL {
150-
t.set(VerbFamily::Contradicts, tense, contradicts);
151-
}
152-
153-
// REFINES: iterative improvement, high modal, moderate kausal
154-
let refines = SlotPrior { temporal: 0.3, kausal: 0.4, modal: 0.8, lokal: 0.2, instrument: 0.35 };
155-
for tense in Tense::ALL {
156-
t.set(VerbFamily::Refines, tense, refines);
157-
}
158-
159-
// GROUNDS: anchoring to context, high lokal + modal
160-
let grounds = SlotPrior { temporal: 0.25, kausal: 0.3, modal: 0.75, lokal: 0.85, instrument: 0.2 };
161-
for tense in Tense::ALL {
162-
t.set(VerbFamily::Grounds, tense, grounds);
163-
}
164-
165-
// --- Discovery/enablement verbs: high Kausal + Lokal ---
166-
167-
// ENABLES: facilitation, high kausal + lokal
168-
let enables = SlotPrior { temporal: 0.35, kausal: 0.8, modal: 0.4, lokal: 0.7, instrument: 0.45 };
169-
for tense in Tense::ALL {
170-
t.set(VerbFamily::Enables, tense, enables);
200+
pub fn default_table() -> VerbRoleTable {
201+
let mut t = VerbRoleTable::new_uniform();
202+
for family in VerbFamily::ALL {
203+
let base = base_prior(family);
204+
for tense in Tense::ALL {
205+
t.set(family, tense, base.combine(tense_modifier(tense)));
206+
}
171207
}
172-
173208
t
174209
}
175210

@@ -250,8 +285,11 @@ mod tests {
250285

251286
#[test]
252287
fn refines_state_verb_modal() {
288+
// Tense::Present is unmarked (no modifier) so the family-level base
289+
// prior is preserved. (Under tense modulation, Perfect adds +0.15 to
290+
// temporal, which would push Refines.temporal from 0.3 to 0.45.)
253291
let t = default_table();
254-
let p = t.lookup(VerbFamily::Refines, Tense::Perfect);
292+
let p = t.lookup(VerbFamily::Refines, Tense::Present);
255293
assert!(p.modal > 0.7, "Refines should have high modal");
256294
assert!(p.temporal < 0.4, "Refines should have low temporal");
257295
assert!(count_non_uniform(&p) >= 2);
@@ -315,8 +353,11 @@ mod tests {
315353

316354
#[test]
317355
fn dissolves_change_verb_temporal_modal() {
356+
// Use Tense::Present (unmarked) so the family base prior is preserved.
357+
// Imperative would suppress temporal by 0.20 (0.85 -> 0.65 < 0.7) and
358+
// amplify modal — those are tested in `test_imperative_suppresses_temporal`.
318359
let t = default_table();
319-
let p = t.lookup(VerbFamily::Dissolves, Tense::Imperative);
360+
let p = t.lookup(VerbFamily::Dissolves, Tense::Present);
320361
assert!(p.temporal > 0.7, "Dissolves should have high temporal");
321362
assert!(p.modal > 0.6, "Dissolves should have elevated modal");
322363
assert!(count_non_uniform(&p) >= 2);
@@ -334,4 +375,83 @@ mod tests {
334375
);
335376
}
336377
}
378+
379+
// --- Tense modulation tests (G4 loose end: priors must vary across tenses
380+
// within a family; broadcast-flat 12-priors-across-12-tenses produces
381+
// zero tense×family interaction). ---
382+
383+
/// Failing-test-first: Perfect aspect (completion → temporal anchoring
384+
/// per Quirk et al. CGEL §4.21–4.27) must yield strictly higher temporal
385+
/// prior than the unmarked Past for the same family.
386+
#[test]
387+
fn test_perfect_amplifies_temporal_within_family() {
388+
let t = default_table();
389+
let perfect = t.lookup(VerbFamily::Causes, Tense::Perfect);
390+
let past = t.lookup(VerbFamily::Causes, Tense::Past);
391+
assert!(
392+
perfect.temporal > past.temporal,
393+
"Perfect should amplify temporal over Past for Causes; got perfect={} past={}",
394+
perfect.temporal, past.temporal
395+
);
396+
}
397+
398+
/// Imperative (timeless command) suppresses temporal in favour of modal.
399+
#[test]
400+
fn test_imperative_suppresses_temporal() {
401+
let t = default_table();
402+
let imperative = t.lookup(VerbFamily::Causes, Tense::Imperative);
403+
let present = t.lookup(VerbFamily::Causes, Tense::Present);
404+
assert!(
405+
imperative.temporal < present.temporal,
406+
"Imperative should suppress temporal vs Present for Causes; got imp={} pres={}",
407+
imperative.temporal, present.temporal
408+
);
409+
assert!(
410+
imperative.modal > present.modal,
411+
"Imperative should amplify modal vs Present for Causes; got imp={} pres={}",
412+
imperative.modal, present.modal
413+
);
414+
}
415+
416+
/// Subjunctive equivalent — this enum has Potential (irrealis/possibility
417+
/// mood), which fills the Subjunctive role. Potential should amplify modal
418+
/// over Present.
419+
#[test]
420+
fn test_subjunctive_amplifies_modal() {
421+
let t = default_table();
422+
let potential = t.lookup(VerbFamily::Supports, Tense::Potential);
423+
let present = t.lookup(VerbFamily::Supports, Tense::Present);
424+
assert!(
425+
potential.modal > present.modal,
426+
"Potential (subjunctive role) should amplify modal vs Present for Supports; \
427+
got pot={} pres={}",
428+
potential.modal, present.modal
429+
);
430+
}
431+
432+
/// Sanity: continuous aspects amplify temporal but less than perfect.
433+
/// Use `Causes` (temporal base 0.4) so neither modifier saturates at 1.0.
434+
#[test]
435+
fn test_continuous_amplifies_temporal_less_than_perfect() {
436+
let t = default_table();
437+
let cont = t.lookup(VerbFamily::Causes, Tense::PresentContinuous);
438+
let perf = t.lookup(VerbFamily::Causes, Tense::Perfect);
439+
let pres = t.lookup(VerbFamily::Causes, Tense::Present);
440+
assert!(cont.temporal > pres.temporal, "Continuous > Present temporal");
441+
assert!(perf.temporal > cont.temporal, "Perfect > Continuous temporal");
442+
}
443+
444+
/// Sanity: clamp to [0,1] holds even when base prior is near saturation.
445+
#[test]
446+
fn test_combine_clamps_to_unit_interval() {
447+
let t = default_table();
448+
// Causes has kausal=0.95 base; no tense modifier touches kausal,
449+
// but Perfect adds +0.15 to temporal where Causes.temporal=0.4 → 0.55.
450+
let p = t.lookup(VerbFamily::Causes, Tense::Perfect);
451+
assert!(p.temporal >= 0.0 && p.temporal <= 1.0);
452+
assert!(p.kausal >= 0.0 && p.kausal <= 1.0);
453+
assert!(p.modal >= 0.0 && p.modal <= 1.0);
454+
assert!(p.lokal >= 0.0 && p.lokal <= 1.0);
455+
assert!(p.instrument >= 0.0 && p.instrument <= 1.0);
456+
}
337457
}

0 commit comments

Comments
 (0)