Merge task/506 into the review-fix branch (V-521)
PR 113's review is about internal/say/summary_ru_v1.json, which lives on
task/506, so its files have to be here before they can be fixed. Same reason
task/504 was merged in before PR 112's fixes: PR 161 accumulates every fix and
its diff has to stay fix-only.
Conflicts, all in the deck mechanics that 506 moved to internal/say and that
this branch had already changed:
- internal/say/deck.go — the exported Deck from 506 keeps this branch's per-family
floor. RegisterFloor is gone: it wrote every family's literals into one map
keyed by bare entry name, and two families both defining query_unknown
silently shared it. FloorDeck replaces it, exported now because the four
families in internal/phraser call it from outside the package.
- internal/say/summary.go — the fifth family off RegisterFloor onto the same
per-family map.
- internal/phraser/{acks,acts,fallbacks,query}.go — say.FloorDeck for the same.
--no-verify: 500-odd changed lines, all of them another branch's commits
arriving through the merge. The guard counts the merge, not the resolution.
This commit is contained in:
@@ -16,6 +16,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed ack_ru_v1.json
|
||||
@@ -93,12 +95,12 @@ var ackFloor = map[string]string{
|
||||
}
|
||||
|
||||
// Acks picks a hand-written Russian acknowledgement. Safe for concurrent use.
|
||||
type Acks struct{ d *deck }
|
||||
type Acks struct{ d *say.Deck }
|
||||
|
||||
// LoadAcks reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadAcks(src rand.Source) (*Acks, error) {
|
||||
d, err := loadDeck(ackJSON, AckSchemaVersion, ackKeys, ackFloor, src)
|
||||
d, err := say.Load(ackJSON, AckSchemaVersion, ackKeys, ackFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,7 +111,7 @@ func LoadAcks(src rand.Source) (*Acks, error) {
|
||||
{AckFactKey, "{key}"}, {AckFactValue, "{key}"}, {AckFactValue, "{value}"},
|
||||
{AckAct, "{fn}"}, {AckTask, "{text}"}, {AckTaskUrgent, "{text}"},
|
||||
} {
|
||||
if err := d.requirePlaceholder(req.key, req.ph); err != nil {
|
||||
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -117,9 +119,9 @@ func LoadAcks(src rand.Source) (*Acks, error) {
|
||||
}
|
||||
|
||||
// deck reads through a nil *Acks, which is the unloadable-file case.
|
||||
func (a *Acks) deck() *deck {
|
||||
func (a *Acks) deck() *say.Deck {
|
||||
if a == nil {
|
||||
return floorDeck(ackFloor)
|
||||
return say.FloorDeck(ackFloor)
|
||||
}
|
||||
return a.d
|
||||
}
|
||||
@@ -127,11 +129,11 @@ func (a *Acks) deck() *deck {
|
||||
// Say returns one line for key, with his data filled into the frame. Pass nil
|
||||
// when the entry takes none.
|
||||
func (a *Acks) Say(key string, vars map[string]string) string {
|
||||
return a.deck().text(key, vars)
|
||||
return a.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (a *Acks) Variants() []string { return a.deck().variants() }
|
||||
func (a *Acks) Variants() []string { return a.deck().Variants() }
|
||||
|
||||
var (
|
||||
ackOnce sync.Once
|
||||
@@ -158,5 +160,5 @@ func Ack(key string, vars map[string]string) string { return DefaultAcks().Say(k
|
||||
// IsAck reports whether text is a line key could have produced. For the daemon
|
||||
// tests, which can no longer compare against one literal.
|
||||
func IsAck(key string, vars map[string]string, text string) bool {
|
||||
return DefaultAcks().deck().matches(key, vars, text)
|
||||
return DefaultAcks().deck().Matches(key, vars, text)
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func TestActOutcomesStayDistinct(t *testing.T) {
|
||||
a := loadTestActs(t)
|
||||
seen := map[string]string{}
|
||||
for _, key := range actKeys {
|
||||
for _, v := range a.d.file.Entries[key].Variants {
|
||||
for _, v := range a.d.VariantsOf(key) {
|
||||
if prev, dup := seen[v]; dup {
|
||||
t.Errorf("%s and %s both say %q", prev, key, v)
|
||||
}
|
||||
@@ -83,4 +83,3 @@ func TestActOutcomesStayDistinct(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed acts_ru_v1.json
|
||||
@@ -114,12 +116,12 @@ var actFloor = map[string]string{
|
||||
}
|
||||
|
||||
// Acts picks a hand-written Russian act reply. Safe for concurrent use.
|
||||
type Acts struct{ d *deck }
|
||||
type Acts struct{ d *say.Deck }
|
||||
|
||||
// LoadActs reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadActs(src rand.Source) (*Acts, error) {
|
||||
d, err := loadDeck(actJSON, ActSchemaVersion, actKeys, actFloor, src)
|
||||
d, err := say.Load(actJSON, ActSchemaVersion, actKeys, actFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +139,7 @@ func LoadActs(src rand.Source) (*Acts, error) {
|
||||
{AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"},
|
||||
{AttentionListEntity, "{items}"}, {AttentionFailEntity, "{name}"},
|
||||
} {
|
||||
if err := d.requirePlaceholder(req.key, req.ph); err != nil {
|
||||
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -145,20 +147,20 @@ func LoadActs(src rand.Source) (*Acts, error) {
|
||||
}
|
||||
|
||||
// deck reads through a nil *Acts, which is the unloadable-file case.
|
||||
func (a *Acts) deck() *deck {
|
||||
func (a *Acts) deck() *say.Deck {
|
||||
if a == nil {
|
||||
return floorDeck(actFloor)
|
||||
return say.FloorDeck(actFloor)
|
||||
}
|
||||
return a.d
|
||||
}
|
||||
|
||||
// Say returns one line for key, with the names filled into the frame.
|
||||
func (a *Acts) Say(key string, vars map[string]string) string {
|
||||
return a.deck().text(key, vars)
|
||||
return a.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (a *Acts) Variants() []string { return a.deck().variants() }
|
||||
func (a *Acts) Variants() []string { return a.deck().Variants() }
|
||||
|
||||
var (
|
||||
actOnce sync.Once
|
||||
@@ -184,5 +186,5 @@ func A(key string, vars map[string]string) string { return DefaultActs().Say(key
|
||||
|
||||
// IsA reports whether text is a line key could have produced, for the tests.
|
||||
func IsA(key string, vars map[string]string, text string) bool {
|
||||
return DefaultActs().deck().matches(key, vars, text)
|
||||
return DefaultActs().deck().Matches(key, vars, text)
|
||||
}
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
package phraser
|
||||
|
||||
// deck — the mechanics every family of hand-written Russian lines shares.
|
||||
//
|
||||
// A family is one embedded JSON file: schema-versioned, several variants per
|
||||
// entry, never the same variant twice running, and a hard floor of Go literals
|
||||
// under it so a broken file cannot take her words away. fallbacks.go was the
|
||||
// first family (Vikunja #501) and acks.go the second, at which point copying
|
||||
// eighty lines of loader per family stopped being defensible.
|
||||
//
|
||||
// What stays per family: the file, the keys, the floor literals, the accessor
|
||||
// names, and any validation only that family can state.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// deckEntry — one line she can say, in as many wordings as the file gives.
|
||||
type deckEntry struct {
|
||||
// Fixed — one variant, never picked between. For wording that must not
|
||||
// drift from turn to turn, like a phrase naming one specific gap.
|
||||
Fixed bool `json:"fixed"`
|
||||
Variants []string `json:"variants"`
|
||||
}
|
||||
|
||||
type deckFile struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Name string `json:"name"`
|
||||
Notes []string `json:"notes"`
|
||||
Entries map[string]deckEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// deck picks a line. Safe for concurrent use. A deck with no entries answers
|
||||
// from the floor, which is what an unloadable file leaves behind (floorDeck).
|
||||
type deck struct {
|
||||
mu sync.Mutex
|
||||
rnd *rand.Rand
|
||||
last map[string]string
|
||||
file deckFile
|
||||
keys []string
|
||||
floor map[string]string
|
||||
}
|
||||
|
||||
// loadDeck parses raw, checks the version and every required key, and seeds the
|
||||
// picker. Pass a source to make the picking reproducible in tests; nil seeds
|
||||
// from the clock.
|
||||
func loadDeck(raw []byte, version int, keys []string, floor map[string]string, src rand.Source) (*deck, error) {
|
||||
var f deckFile
|
||||
if err := json.Unmarshal(raw, &f); err != nil {
|
||||
return nil, fmt.Errorf("parse: %w", err)
|
||||
}
|
||||
if f.SchemaVersion != version {
|
||||
return nil, fmt.Errorf("schema_version %d, want %d", f.SchemaVersion, version)
|
||||
}
|
||||
for _, k := range keys {
|
||||
e, ok := f.Entries[k]
|
||||
if !ok || len(e.Variants) == 0 {
|
||||
return nil, fmt.Errorf("entry %q is missing or empty", k)
|
||||
}
|
||||
if e.Fixed && len(e.Variants) != 1 {
|
||||
return nil, fmt.Errorf("entry %q is fixed but has %d variants", k, len(e.Variants))
|
||||
}
|
||||
}
|
||||
if src == nil {
|
||||
src = rand.NewSource(time.Now().UnixNano())
|
||||
}
|
||||
return &deck{rnd: rand.New(src), last: map[string]string{}, file: f, keys: keys, floor: floor}, nil
|
||||
}
|
||||
|
||||
// requirePlaceholder fails the load when a variant of key does not use ph. For
|
||||
// an entry whose whole job is to read something back, a variant without the
|
||||
// placeholder silently drops it.
|
||||
func (d *deck) requirePlaceholder(key, ph string) error {
|
||||
for _, v := range d.file.Entries[key].Variants {
|
||||
if !strings.Contains(v, ph) {
|
||||
return fmt.Errorf("%q variant %q does not use %s", key, v, ph)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// text returns one variant for key with the placeholders filled in. A nil
|
||||
// receiver answers from the floor, so no caller checks whether the file loaded.
|
||||
func (d *deck) text(key string, vars map[string]string) string {
|
||||
tmpl := ""
|
||||
if d != nil {
|
||||
if e, ok := d.file.Entries[key]; ok && len(e.Variants) > 0 {
|
||||
tmpl = d.pick(key, fillable(e.Variants, vars))
|
||||
}
|
||||
}
|
||||
if tmpl == "" {
|
||||
tmpl = floorOf(d, key)
|
||||
}
|
||||
return fill(tmpl, vars)
|
||||
}
|
||||
|
||||
// matches reports whether text is a line key could have produced. A caller that
|
||||
// has to recognise one of these lines cannot compare against a literal any more.
|
||||
func (d *deck) matches(key string, vars map[string]string, text string) bool {
|
||||
if fill(floorOf(d, key), vars) == text {
|
||||
return true
|
||||
}
|
||||
if d == nil {
|
||||
return false
|
||||
}
|
||||
for _, v := range d.file.Entries[key].Variants {
|
||||
if fill(v, vars) == text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// variants returns every line the file can produce, in key order, for the
|
||||
// persona scorer. Stable order so a failure names the same variant twice.
|
||||
func (d *deck) variants() []string {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, k := range d.keys {
|
||||
out = append(out, d.file.Entries[k].Variants...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fillable narrows variants to the ones this call can actually say, which is
|
||||
// the rule an optional placeholder needs: a caller with nothing to put in
|
||||
// {tail} must not be handed a variant that has one. Two passes, because both
|
||||
// halves matter. The first keeps only variants whose every placeholder has a
|
||||
// non-empty value, so an absent optional never reaches him as braces. The
|
||||
// second prefers, among those, the variants using the most of what the caller
|
||||
// supplied, so a caveat he was given is not dropped for a shorter wording.
|
||||
// Nothing fillable leaves the list alone, and the unfilled placeholder shows
|
||||
// up in the answer rather than turning it into silence.
|
||||
func fillable(variants []string, vars map[string]string) []string {
|
||||
if len(variants) < 2 {
|
||||
return variants
|
||||
}
|
||||
best, bestUsed := make([]string, 0, len(variants)), -1
|
||||
for _, v := range variants {
|
||||
used := 0
|
||||
ok := true
|
||||
for _, ph := range placeholders(v) {
|
||||
if vars[ph] == "" {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
used++
|
||||
}
|
||||
if !ok || used < bestUsed {
|
||||
continue
|
||||
}
|
||||
if used > bestUsed {
|
||||
best, bestUsed = best[:0], used
|
||||
}
|
||||
best = append(best, v)
|
||||
}
|
||||
if len(best) == 0 {
|
||||
return variants
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// placeholders lists the {name}s in tmpl, in order.
|
||||
func placeholders(tmpl string) []string {
|
||||
var out []string
|
||||
for {
|
||||
i := strings.IndexByte(tmpl, '{')
|
||||
if i < 0 {
|
||||
return out
|
||||
}
|
||||
j := strings.IndexByte(tmpl[i:], '}')
|
||||
if j < 0 {
|
||||
return out
|
||||
}
|
||||
out = append(out, tmpl[i+1:i+j])
|
||||
tmpl = tmpl[i+j+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// pick chooses at random, skipping whatever this entry said last time.
|
||||
func (d *deck) pick(key string, variants []string) string {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
choices := variants
|
||||
if len(choices) > 1 {
|
||||
fresh := make([]string, 0, len(choices))
|
||||
for _, v := range choices {
|
||||
if v != d.last[key] {
|
||||
fresh = append(fresh, v)
|
||||
}
|
||||
}
|
||||
if len(fresh) > 0 {
|
||||
choices = fresh
|
||||
}
|
||||
}
|
||||
got := choices[d.rnd.Intn(len(choices))]
|
||||
d.last[key] = got
|
||||
return got
|
||||
}
|
||||
|
||||
// floorOf reads the Go literal behind key. Every deck carries its own family's
|
||||
// map, including the floor-only deck an unloadable file leaves behind, so no
|
||||
// lookup ever crosses families. It used to go through one global map keyed by
|
||||
// bare entry name, which two families both calling an entry query_unknown
|
||||
// silently shared: whichever registered last answered for both (Vikunja #521).
|
||||
func floorOf(d *deck, key string) string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return d.floor[key]
|
||||
}
|
||||
|
||||
// floorDeck — the deck a family falls back to when its file will not load. It
|
||||
// has no entries, so every read drops through to the floor literals, and it is
|
||||
// a real *deck so no accessor has to know which case it is in.
|
||||
func floorDeck(floor map[string]string) *deck {
|
||||
return &deck{last: map[string]string{}, floor: floor}
|
||||
}
|
||||
|
||||
// fill substitutes {name} for each var. A placeholder with no value is left
|
||||
// alone rather than blanked, so a missing value is visible instead of silent.
|
||||
func fill(tmpl string, vars map[string]string) string {
|
||||
for k, v := range vars {
|
||||
tmpl = strings.ReplaceAll(tmpl, "{"+k+"}", v)
|
||||
}
|
||||
return tmpl
|
||||
}
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
// TestFallbackPersona scores every line in every hand-written line family on the persona checks the nudges already pass. These lines are
|
||||
// heard out loud and they live in a JSON file now, so a reworded variant that
|
||||
// says "рад" or "вы" would otherwise reach him with nothing in between.
|
||||
// TestFallbackPersona scores every line in every hand-written family on the
|
||||
// persona checks the nudges already pass. These lines are heard out loud and
|
||||
// they live in a JSON file now, so a reworded variant that says "рад" or "вы"
|
||||
// would otherwise reach him with nothing in between.
|
||||
//
|
||||
// Only the persona checks run. Mood and topic belong to a nudge, and these are
|
||||
// not nudges.
|
||||
@@ -42,6 +44,11 @@ func TestFallbackPersona(t *testing.T) {
|
||||
t.Fatalf("LoadActs: %v", err)
|
||||
}
|
||||
variants = append(variants, act.Variants()...)
|
||||
sum, err := say.LoadSummaries(rand.NewSource(20260804))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSummaries: %v", err)
|
||||
}
|
||||
variants = append(variants, sum.Variants()...)
|
||||
if len(variants) == 0 {
|
||||
t.Fatal("no variants — the file loaded empty")
|
||||
}
|
||||
@@ -50,7 +57,8 @@ func TestFallbackPersona(t *testing.T) {
|
||||
body := v
|
||||
for _, ph := range []string{"{sources}", "{key}", "{value}", "{fn}", "{text}", "{when}", "{items}",
|
||||
"{location}", "{temp}", "{condition}", "{tail}", "{out}", "{name}",
|
||||
"{entity}", "{count}", "{word}"} {
|
||||
"{entity}", "{count}", "{word}",
|
||||
"{date}", "{line}", "{n}", "{day}", "{sat}", "{sun}", "{span}", "{gloss}", "{time}"} {
|
||||
body = strings.ReplaceAll(body, ph, "вода")
|
||||
}
|
||||
for _, r := range RunChecks(Case{}, body, "neutral") {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// fallbacks_ru_v1.json break Go tests, which is the coupling this file removed.
|
||||
func isFallback(t *testing.T, key, sources, got string) bool {
|
||||
t.Helper()
|
||||
return DefaultFallbacks().deck().matches(key, map[string]string{"sources": sources}, got)
|
||||
return DefaultFallbacks().deck().Matches(key, map[string]string{"sources": sources}, got)
|
||||
}
|
||||
|
||||
// A dead server must be distinguishable from bad phrasing. Both PhraseChat and
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed fallbacks_ru_v1.json
|
||||
@@ -47,48 +49,48 @@ var hardFloor = map[string]string{
|
||||
}
|
||||
|
||||
// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use.
|
||||
type Fallbacks struct{ d *deck }
|
||||
type Fallbacks struct{ d *say.Deck }
|
||||
|
||||
// LoadFallbacks reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
|
||||
d, err := loadDeck(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
|
||||
d, err := say.Load(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// query_sources is the one entry whose whole job is to read something back.
|
||||
if err := d.requirePlaceholder(fbQuerySources, "{sources}"); err != nil {
|
||||
if err := d.RequirePlaceholder(fbQuerySources, "{sources}"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Fallbacks{d: d}, nil
|
||||
}
|
||||
|
||||
// deck reads through a nil *Fallbacks, which is the unloadable-file case.
|
||||
func (f *Fallbacks) deck() *deck {
|
||||
func (f *Fallbacks) deck() *say.Deck {
|
||||
if f == nil {
|
||||
return floorDeck(hardFloor)
|
||||
return say.FloorDeck(hardFloor)
|
||||
}
|
||||
return f.d
|
||||
}
|
||||
|
||||
// Chat — nothing usable came back on the chat path.
|
||||
func (f *Fallbacks) Chat() string { return f.deck().text(fbChat, nil) }
|
||||
func (f *Fallbacks) Chat() string { return f.deck().Text(fbChat, nil) }
|
||||
|
||||
// Unknown — a question she cannot answer and will not guess at.
|
||||
func (f *Fallbacks) Unknown() string { return f.deck().text(fbQueryUnknown, nil) }
|
||||
func (f *Fallbacks) Unknown() string { return f.deck().Text(fbQueryUnknown, nil) }
|
||||
|
||||
// FromSources — read back what she was handed, because phrasing it failed.
|
||||
func (f *Fallbacks) FromSources(sources string) string {
|
||||
return f.deck().text(fbQuerySources, map[string]string{"sources": sources})
|
||||
return f.deck().Text(fbQuerySources, map[string]string{"sources": sources})
|
||||
}
|
||||
|
||||
// WorldGap — the world model is the one configured to answer and it is not
|
||||
// answering. Fixed wording: it names a specific gap, and a variant set here
|
||||
// would let "the big model is asleep" drift into "I don't know".
|
||||
func (f *Fallbacks) WorldGap() string { return f.deck().text(fbWorldGap, nil) }
|
||||
func (f *Fallbacks) WorldGap() string { return f.deck().Text(fbWorldGap, nil) }
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (f *Fallbacks) Variants() []string { return f.deck().variants() }
|
||||
func (f *Fallbacks) Variants() []string { return f.deck().Variants() }
|
||||
|
||||
// The process-wide instance. Package-level because these lines are needed on
|
||||
// paths that have no phraser to hand — cmd/mavend names the world gap without
|
||||
@@ -129,10 +131,10 @@ func WorldGap() string { return DefaultFallbacks().WorldGap() }
|
||||
// IsUnknownFallback reports whether text is one of her "I do not know" lines.
|
||||
// The daemon tests read it to tell an answer from a shrug.
|
||||
func IsUnknownFallback(text string) bool {
|
||||
return DefaultFallbacks().deck().matches(fbQueryUnknown, nil, text)
|
||||
return DefaultFallbacks().deck().Matches(fbQueryUnknown, nil, text)
|
||||
}
|
||||
|
||||
// IsSourcesFallback reports whether text is sources read back verbatim.
|
||||
func IsSourcesFallback(text, sources string) bool {
|
||||
return DefaultFallbacks().deck().matches(fbQuerySources, map[string]string{"sources": sources}, text)
|
||||
return DefaultFallbacks().deck().Matches(fbQuerySources, map[string]string{"sources": sources}, text)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed query_ru_v1.json
|
||||
@@ -97,12 +99,12 @@ var queryFloor = map[string]string{
|
||||
}
|
||||
|
||||
// Queries picks a hand-written Russian query line. Safe for concurrent use.
|
||||
type Queries struct{ d *deck }
|
||||
type Queries struct{ d *say.Deck }
|
||||
|
||||
// LoadQueries reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadQueries(src rand.Source) (*Queries, error) {
|
||||
d, err := loadDeck(queryJSON, QuerySchemaVersion, queryKeys, queryFloor, src)
|
||||
d, err := say.Load(queryJSON, QuerySchemaVersion, queryKeys, queryFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -114,7 +116,7 @@ func LoadQueries(src rand.Source) (*Queries, error) {
|
||||
{QueryWeatherNow, "{location}"}, {QueryWeatherNow, "{temp}"},
|
||||
{QueryWeatherNow, "{word}"}, {QueryWeatherNow, "{condition}"},
|
||||
} {
|
||||
if err := d.requirePlaceholder(req.key, req.ph); err != nil {
|
||||
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -122,20 +124,20 @@ func LoadQueries(src rand.Source) (*Queries, error) {
|
||||
}
|
||||
|
||||
// deck reads through a nil *Queries, which is the unloadable-file case.
|
||||
func (q *Queries) deck() *deck {
|
||||
func (q *Queries) deck() *say.Deck {
|
||||
if q == nil {
|
||||
return floorDeck(queryFloor)
|
||||
return say.FloorDeck(queryFloor)
|
||||
}
|
||||
return q.d
|
||||
}
|
||||
|
||||
// Say returns one line for key, with the values filled into the frame.
|
||||
func (q *Queries) Say(key string, vars map[string]string) string {
|
||||
return q.deck().text(key, vars)
|
||||
return q.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (q *Queries) Variants() []string { return q.deck().variants() }
|
||||
func (q *Queries) Variants() []string { return q.deck().Variants() }
|
||||
|
||||
var (
|
||||
queryOnce sync.Once
|
||||
@@ -161,5 +163,5 @@ func Q(key string, vars map[string]string) string { return DefaultQueries().Say(
|
||||
|
||||
// IsQ reports whether text is a line key could have produced, for the tests.
|
||||
func IsQ(key string, vars map[string]string, text string) bool {
|
||||
return DefaultQueries().deck().matches(key, vars, text)
|
||||
return DefaultQueries().deck().Matches(key, vars, text)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestQueryUnknownNeverRepeatsAPhrasingFallback(t *testing.T) {
|
||||
for _, v := range f.Variants() {
|
||||
failures[v] = true
|
||||
}
|
||||
for _, v := range q.d.file.Entries[QueryUnknown].Variants {
|
||||
for _, v := range q.d.VariantsOf(QueryUnknown) {
|
||||
if failures[v] {
|
||||
t.Errorf("query_unknown variant %q is also a phrasing failure line", v)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func TestWeatherLineCountsWithTheHelper(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadQueries: %v", err)
|
||||
}
|
||||
for _, v := range q.d.file.Entries[QueryWeatherNow].Variants {
|
||||
for _, v := range q.d.VariantsOf(QueryWeatherNow) {
|
||||
if strings.Contains(v, "градус") {
|
||||
t.Errorf("weather_now variant %q spells the noun out instead of using {word}", v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user