fa98e4722e
The guard answers one question — may this utterance become an executable action — as a three-way policy gate (permissive / blocked / ambiguous) and never decides what the utterance is. Rules are the encoding of the measured slice-20 dev-pool discriminators: 126 capability-question rows are 42/42/42 addressed / bare-ability / bare-future; 127 bare можешь+пожалуйста rows are 100% action; can-you-please is 100% action. Reuses the shipped prohibition parser, morph finiteness and lexicon fillers; reason vocabulary is closed. Slice 21 (task/725, brief after the accepted slice 20).
140 lines
3.3 KiB
Go
140 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/morph"
|
|
)
|
|
|
|
func TestEvaluateFixtures(t *testing.T) {
|
|
for _, fx := range Fixtures {
|
|
got := Evaluate(fx.Utterance)
|
|
if got.Eligibility != fx.Want {
|
|
t.Errorf("%s: want %s got %s (reasons %v)", fx.Utterance, fx.Want, got.Eligibility, got.Reasons)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestMeasuredDiscriminators pins the corpus-verified numbers on the frozen
|
|
// pool. These are the exact measurements the rules were built on, so a change
|
|
// that moves them is a rule regression visible in the slice.
|
|
func loadPool(t *testing.T) []Row {
|
|
t.Helper()
|
|
b, err := os.ReadFile("/tmp/mvn-s21/pool.json")
|
|
if os.IsNotExist(err) {
|
|
t.Skip("pool.json missing; run slice21_emit.py first")
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var rows []Row
|
|
if err := json.Unmarshal(b, &rows); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func TestDevCapabilityProhibition(t *testing.T) {
|
|
if !morph.Available() {
|
|
t.Skip("morph dict unavailable")
|
|
}
|
|
rows := loadPool(t)
|
|
var n, pass, blockedN, ambig int
|
|
for _, r := range rows {
|
|
if !hasTok(r.Tags, "capability_question") {
|
|
continue
|
|
}
|
|
n++
|
|
switch Evaluate(variants(r.NText)[vOrig]).Eligibility {
|
|
case Permissive:
|
|
pass++
|
|
case Blocked:
|
|
blockedN++
|
|
case Ambiguous:
|
|
ambig++
|
|
}
|
|
}
|
|
if n != 126 {
|
|
t.Fatalf("capability-question rows = %d, want 126", n)
|
|
}
|
|
if pass != 0 {
|
|
t.Fatalf("capability-question dangerous pass = %d, want 0", pass)
|
|
}
|
|
if blockedN != 126 && ambig != 0 {
|
|
t.Errorf("blocked=%d ambig=%d, expect all 126 blocked", blockedN, ambig)
|
|
}
|
|
}
|
|
|
|
func TestDevBareCanPoliteIsAction(t *testing.T) {
|
|
if !morph.Available() {
|
|
t.Skip("morph dict unavailable")
|
|
}
|
|
rows := loadPool(t)
|
|
total, action, perm := 0, 0, 0
|
|
for _, r := range rows {
|
|
toks := tokens(r.NText)
|
|
if !hasTok(toks, "можешь") || hasAny(toks, ruAddress) ||
|
|
hasAny(toks, append([]string{}, ruAbilityForms.single...)) {
|
|
continue
|
|
}
|
|
total++
|
|
if r.Route == "action" {
|
|
action++
|
|
}
|
|
if Evaluate(r.NText).Eligibility == Permissive {
|
|
perm++
|
|
}
|
|
}
|
|
if total != 127 || action != 127 {
|
|
t.Fatalf("bare-можешь+polite: n=%d action=%d, want 127/127", total, action)
|
|
}
|
|
if perm != 127 {
|
|
t.Fatalf("bare-можешь+polite permissive=%d, want 127", perm)
|
|
}
|
|
}
|
|
|
|
func TestDevEnglishCanPoliteIsAction(t *testing.T) {
|
|
if !morph.Available() {
|
|
t.Skip("morph dict unavailable")
|
|
}
|
|
rows := loadPool(t)
|
|
total, action, perm := 0, 0, 0
|
|
for _, r := range rows {
|
|
toks := tokens(r.NText)
|
|
if !hasAny(toks, enCanForms) {
|
|
continue
|
|
}
|
|
total++
|
|
if r.Route == "action" {
|
|
action++
|
|
}
|
|
if Evaluate(r.NText).Eligibility == Permissive {
|
|
perm++
|
|
}
|
|
}
|
|
if total != 96 || action != 96 {
|
|
t.Fatalf("can-rows: n=%d action=%d, want 96/96", total, action)
|
|
}
|
|
if perm != 96 {
|
|
t.Fatalf("can-rows permissive=%d, want 96", perm)
|
|
}
|
|
}
|
|
|
|
func TestNoPermissiveRowEndsInQuestion(t *testing.T) {
|
|
// asymmetric posture: the guard must never approve a trailing-? frame --
|
|
// the corpus has 0 action rows ending in "?", and approving any would
|
|
// bet on punctuation the slice has ruled uncertain.
|
|
rows := loadPool(t)
|
|
for _, r := range rows {
|
|
if !strings.HasSuffix(r.NText, "?") {
|
|
continue
|
|
}
|
|
if got := Evaluate(r.NText).Eligibility; got == Permissive {
|
|
t.Errorf("canonical '?': %q approved (%s)", r.Text, got)
|
|
}
|
|
}
|
|
}
|