say: move the copy deck into a package memory can import (V-506)

The summaries family is spoken by internal/memory, internal/morning and
internal/tasks. internal/phraser already imports internal/memory, so the
deck cannot stay in phraser without a cycle.

internal/say is a leaf: embed, json, math/rand, strings, sync. The four
phraser families keep their files and their floors and now call say.Load,
*say.Deck, Text, Matches, Variants, RequirePlaceholder and RegisterFloor.
No copy changed and no behaviour changed.
This commit is contained in:
2026-08-04 01:42:20 +04:00
parent f3c0540b42
commit c35979d9f9
7 changed files with 74 additions and 62 deletions
+10 -8
View File
@@ -16,6 +16,8 @@ import (
"log" "log"
"math/rand" "math/rand"
"sync" "sync"
"github.com/kami/maven/internal/say"
) )
//go:embed ack_ru_v1.json //go:embed ack_ru_v1.json
@@ -65,7 +67,7 @@ var ackKeys = []string{
// ackFloor — the literal each key falls back to when the file is unusable. // ackFloor — the literal each key falls back to when the file is unusable.
// These are the exact strings that lived in Go before this file existed. // These are the exact strings that lived in Go before this file existed.
var ackFloor = registerFloor(map[string]string{ var ackFloor = say.RegisterFloor(map[string]string{
AckFact: "записала факт.", AckFact: "записала факт.",
AckFactKey: "отметила: {key}", AckFactKey: "отметила: {key}",
AckFactValue: "отметила: {key} = {value}", AckFactValue: "отметила: {key} = {value}",
@@ -93,12 +95,12 @@ var ackFloor = registerFloor(map[string]string{
}) })
// Acks picks a hand-written Russian acknowledgement. Safe for concurrent use. // 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 // LoadAcks reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock. // reproducible in tests; nil seeds from the clock.
func LoadAcks(src rand.Source) (*Acks, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -109,7 +111,7 @@ func LoadAcks(src rand.Source) (*Acks, error) {
{AckFactKey, "{key}"}, {AckFactValue, "{key}"}, {AckFactValue, "{value}"}, {AckFactKey, "{key}"}, {AckFactValue, "{key}"}, {AckFactValue, "{value}"},
{AckAct, "{fn}"}, {AckTask, "{text}"}, {AckTaskUrgent, "{text}"}, {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 return nil, err
} }
} }
@@ -117,7 +119,7 @@ func LoadAcks(src rand.Source) (*Acks, error) {
} }
// deck reads through a nil *Acks, which is the unloadable-file case. // 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 { if a == nil {
return nil return nil
} }
@@ -127,11 +129,11 @@ func (a *Acks) deck() *deck {
// Say returns one line for key, with his data filled into the frame. Pass nil // Say returns one line for key, with his data filled into the frame. Pass nil
// when the entry takes none. // when the entry takes none.
func (a *Acks) Say(key string, vars map[string]string) string { 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. // 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 ( var (
ackOnce sync.Once 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 // IsAck reports whether text is a line key could have produced. For the daemon
// tests, which can no longer compare against one literal. // tests, which can no longer compare against one literal.
func IsAck(key string, vars map[string]string, text string) bool { 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)
} }
+10 -8
View File
@@ -16,6 +16,8 @@ import (
"log" "log"
"math/rand" "math/rand"
"sync" "sync"
"github.com/kami/maven/internal/say"
) )
//go:embed acts_ru_v1.json //go:embed acts_ru_v1.json
@@ -74,7 +76,7 @@ var actKeys = []string{
// actFloor — the literal each key falls back to when the file is unusable. // actFloor — the literal each key falls back to when the file is unusable.
// These are the exact strings that lived in Go before this file existed. // These are the exact strings that lived in Go before this file existed.
var actFloor = registerFloor(map[string]string{ var actFloor = say.RegisterFloor(map[string]string{
ActDone: "готово.", ActDone: "готово.",
ActDoneOut: "готово: {out}", ActDoneOut: "готово: {out}",
ActDoneEntity: "команда выполнена для {name}.", ActDoneEntity: "команда выполнена для {name}.",
@@ -112,12 +114,12 @@ var actFloor = registerFloor(map[string]string{
}) })
// Acts picks a hand-written Russian act reply. Safe for concurrent use. // 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 // LoadActs reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock. // reproducible in tests; nil seeds from the clock.
func LoadActs(src rand.Source) (*Acts, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -133,7 +135,7 @@ func LoadActs(src rand.Source) (*Acts, error) {
{AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"}, {AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"},
{AttentionListEntity, "{items}"}, {AttentionFailEntity, "{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 return nil, err
} }
} }
@@ -141,7 +143,7 @@ func LoadActs(src rand.Source) (*Acts, error) {
} }
// deck reads through a nil *Acts, which is the unloadable-file case. // 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 { if a == nil {
return nil return nil
} }
@@ -150,11 +152,11 @@ func (a *Acts) deck() *deck {
// Say returns one line for key, with the names filled into the frame. // Say returns one line for key, with the names filled into the frame.
func (a *Acts) Say(key string, vars map[string]string) string { 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. // 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 ( var (
actOnce sync.Once actOnce sync.Once
@@ -180,5 +182,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. // 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 { 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)
} }
+4 -3
View File
@@ -8,9 +8,10 @@ import (
"github.com/kami/maven/internal/phraser" "github.com/kami/maven/internal/phraser"
) )
// TestFallbackPersona scores every line in every hand-written line family on the persona checks the nudges already pass. These lines are // TestFallbackPersona scores every line in every hand-written family on the
// heard out loud and they live in a JSON file now, so a reworded variant that // persona checks the nudges already pass. These lines are heard out loud and
// says "рад" or "вы" would otherwise reach him with nothing in between. // 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 // Only the persona checks run. Mood and topic belong to a nudge, and these are
// not nudges. // not nudges.
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// fallbacks_ru_v1.json break Go tests, which is the coupling this file removed. // fallbacks_ru_v1.json break Go tests, which is the coupling this file removed.
func isFallback(t *testing.T, key, sources, got string) bool { func isFallback(t *testing.T, key, sources, got string) bool {
t.Helper() 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 // A dead server must be distinguishable from bad phrasing. Both PhraseChat and
+14 -12
View File
@@ -15,6 +15,8 @@ import (
"log" "log"
"math/rand" "math/rand"
"sync" "sync"
"github.com/kami/maven/internal/say"
) )
//go:embed fallbacks_ru_v1.json //go:embed fallbacks_ru_v1.json
@@ -39,7 +41,7 @@ var fbKeys = []string{fbChat, fbQueryUnknown, fbQuerySources, fbWorldGap}
// hardFloor — the literal each key falls back to when the file is unusable. // hardFloor — the literal each key falls back to when the file is unusable.
// These are the exact strings that lived in Go before this file existed. // These are the exact strings that lived in Go before this file existed.
var hardFloor = registerFloor(map[string]string{ var hardFloor = say.RegisterFloor(map[string]string{
fbChat: "даже не знаю, что сказать.", fbChat: "даже не знаю, что сказать.",
fbQueryUnknown: "не знаю.", fbQueryUnknown: "не знаю.",
fbQuerySources: "вот что я нашла: {sources}", fbQuerySources: "вот что я нашла: {sources}",
@@ -47,24 +49,24 @@ var hardFloor = registerFloor(map[string]string{
}) })
// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use. // 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 // LoadFallbacks reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock. // reproducible in tests; nil seeds from the clock.
func LoadFallbacks(src rand.Source) (*Fallbacks, error) { 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 { if err != nil {
return nil, err return nil, err
} }
// query_sources is the one entry whose whole job is to read something back. // 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 nil, err
} }
return &Fallbacks{d: d}, nil return &Fallbacks{d: d}, nil
} }
// deck reads through a nil *Fallbacks, which is the unloadable-file case. // 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 { if f == nil {
return nil return nil
} }
@@ -72,23 +74,23 @@ func (f *Fallbacks) deck() *deck {
} }
// Chat — nothing usable came back on the chat path. // 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. // 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. // FromSources — read back what she was handed, because phrasing it failed.
func (f *Fallbacks) FromSources(sources string) string { 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 // 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 // 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". // 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. // 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 // 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 // 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. // 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. // The daemon tests read it to tell an answer from a shrug.
func IsUnknownFallback(text string) bool { 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. // IsSourcesFallback reports whether text is sources read back verbatim.
func IsSourcesFallback(text, sources string) bool { 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)
} }
+10 -8
View File
@@ -16,6 +16,8 @@ import (
"log" "log"
"math/rand" "math/rand"
"sync" "sync"
"github.com/kami/maven/internal/say"
) )
//go:embed query_ru_v1.json //go:embed query_ru_v1.json
@@ -65,7 +67,7 @@ var queryKeys = []string{
// queryFloor — the literal each key falls back to when the file is unusable. // queryFloor — the literal each key falls back to when the file is unusable.
// These are the exact strings that lived in Go before this file existed. // These are the exact strings that lived in Go before this file existed.
var queryFloor = registerFloor(map[string]string{ var queryFloor = say.RegisterFloor(map[string]string{
QueryUnknown: "не знаю.", QueryUnknown: "не знаю.",
QueryOtherDay: "про другой день так не отвечу — спроси целиком.", QueryOtherDay: "про другой день так не отвечу — спроси целиком.",
QueryPersonalNone: "не знаю — не нашла у тебя такой записи.", QueryPersonalNone: "не знаю — не нашла у тебя такой записи.",
@@ -95,12 +97,12 @@ var queryFloor = registerFloor(map[string]string{
}) })
// Queries picks a hand-written Russian query line. Safe for concurrent use. // 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 // LoadQueries reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock. // reproducible in tests; nil seeds from the clock.
func LoadQueries(src rand.Source) (*Queries, error) { 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 { if err != nil {
return nil, err return nil, err
} }
@@ -111,7 +113,7 @@ func LoadQueries(src rand.Source) (*Queries, error) {
{QueryFound, "{text}"}, {QueryPageText, "{text}"}, {QueryFeedsNew, "{items}"}, {QueryFound, "{text}"}, {QueryPageText, "{text}"}, {QueryFeedsNew, "{items}"},
{QueryWeatherNow, "{location}"}, {QueryWeatherNow, "{temp}"}, {QueryWeatherNow, "{condition}"}, {QueryWeatherNow, "{location}"}, {QueryWeatherNow, "{temp}"}, {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 return nil, err
} }
} }
@@ -119,7 +121,7 @@ func LoadQueries(src rand.Source) (*Queries, error) {
} }
// deck reads through a nil *Queries, which is the unloadable-file case. // 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 { if q == nil {
return nil return nil
} }
@@ -128,11 +130,11 @@ func (q *Queries) deck() *deck {
// Say returns one line for key, with the values filled into the frame. // Say returns one line for key, with the values filled into the frame.
func (q *Queries) Say(key string, vars map[string]string) string { 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. // 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 ( var (
queryOnce sync.Once queryOnce sync.Once
@@ -158,5 +160,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. // 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 { 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)
} }
@@ -1,6 +1,9 @@
package phraser package say
// deck — the mechanics every family of hand-written Russian lines shares. // Package say holds the mechanics every family of hand-written Russian lines
// shares. The families themselves live next to the code that speaks them.
//
// Deck — the mechanics every family of hand-written Russian lines shares.
// //
// A family is one embedded JSON file: schema-versioned, several variants per // 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 // entry, never the same variant twice running, and a hard floor of Go literals
@@ -20,28 +23,28 @@ import (
"time" "time"
) )
// deckEntry — one line she can say, in as many wordings as the file gives. // Entry — one line she can say, in as many wordings as the file gives.
type deckEntry struct { type Entry struct {
// Fixed — one variant, never picked between. For wording that must not // Fixed — one variant, never picked between. For wording that must not
// drift from turn to turn, like a phrase naming one specific gap. // drift from turn to turn, like a phrase naming one specific gap.
Fixed bool `json:"fixed"` Fixed bool `json:"fixed"`
Variants []string `json:"variants"` Variants []string `json:"variants"`
} }
type deckFile struct { type file struct {
SchemaVersion int `json:"schema_version"` SchemaVersion int `json:"schema_version"`
Name string `json:"name"` Name string `json:"name"`
Notes []string `json:"notes"` Notes []string `json:"notes"`
Entries map[string]deckEntry `json:"entries"` Entries map[string]Entry `json:"entries"`
} }
// deck picks a line. Safe for concurrent use. A nil *deck answers from the // deck picks a line. Safe for concurrent use. A nil *Deck answers from the
// floor, which is what an unloadable file leaves behind. // floor, which is what an unloadable file leaves behind.
type deck struct { type Deck struct {
mu sync.Mutex mu sync.Mutex
rnd *rand.Rand rnd *rand.Rand
last map[string]string last map[string]string
file deckFile file file
keys []string keys []string
floor map[string]string floor map[string]string
} }
@@ -49,8 +52,8 @@ type deck struct {
// loadDeck parses raw, checks the version and every required key, and seeds the // 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 // picker. Pass a source to make the picking reproducible in tests; nil seeds
// from the clock. // from the clock.
func loadDeck(raw []byte, version int, keys []string, floor map[string]string, src rand.Source) (*deck, error) { func Load(raw []byte, version int, keys []string, floor map[string]string, src rand.Source) (*Deck, error) {
var f deckFile var f file
if err := json.Unmarshal(raw, &f); err != nil { if err := json.Unmarshal(raw, &f); err != nil {
return nil, fmt.Errorf("parse: %w", err) return nil, fmt.Errorf("parse: %w", err)
} }
@@ -69,13 +72,13 @@ func loadDeck(raw []byte, version int, keys []string, floor map[string]string, s
if src == nil { if src == nil {
src = rand.NewSource(time.Now().UnixNano()) src = rand.NewSource(time.Now().UnixNano())
} }
return &deck{rnd: rand.New(src), last: map[string]string{}, file: f, keys: keys, floor: floor}, nil 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 // 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 // an entry whose whole job is to read something back, a variant without the
// placeholder silently drops it. // placeholder silently drops it.
func (d *deck) requirePlaceholder(key, ph string) error { func (d *Deck) RequirePlaceholder(key, ph string) error {
for _, v := range d.file.Entries[key].Variants { for _, v := range d.file.Entries[key].Variants {
if !strings.Contains(v, ph) { if !strings.Contains(v, ph) {
return fmt.Errorf("%q variant %q does not use %s", key, v, ph) return fmt.Errorf("%q variant %q does not use %s", key, v, ph)
@@ -86,7 +89,7 @@ func (d *deck) requirePlaceholder(key, ph string) error {
// text returns one variant for key with the placeholders filled in. A 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. // receiver answers from the floor, so no caller checks whether the file loaded.
func (d *deck) text(key string, vars map[string]string) string { func (d *Deck) Text(key string, vars map[string]string) string {
tmpl := "" tmpl := ""
if d != nil { if d != nil {
if e, ok := d.file.Entries[key]; ok && len(e.Variants) > 0 { if e, ok := d.file.Entries[key]; ok && len(e.Variants) > 0 {
@@ -101,7 +104,7 @@ func (d *deck) text(key string, vars map[string]string) string {
// matches reports whether text is a line key could have produced. A caller that // 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. // 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 { func (d *Deck) Matches(key string, vars map[string]string, text string) bool {
if fill(floorOf(d, key), vars) == text { if fill(floorOf(d, key), vars) == text {
return true return true
} }
@@ -118,7 +121,7 @@ func (d *deck) matches(key string, vars map[string]string, text string) bool {
// variants returns every line the file can produce, in key order, for the // 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. // persona scorer. Stable order so a failure names the same variant twice.
func (d *deck) variants() []string { func (d *Deck) Variants() []string {
if d == nil { if d == nil {
return nil return nil
} }
@@ -130,7 +133,7 @@ func (d *deck) variants() []string {
} }
// pick chooses at random, skipping whatever this entry said last time. // pick chooses at random, skipping whatever this entry said last time.
func (d *deck) pick(key string, e deckEntry) string { func (d *Deck) pick(key string, e Entry) string {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@@ -153,7 +156,7 @@ func (d *deck) pick(key string, e deckEntry) string {
// floorOf reads the Go literal behind key, and works on a nil deck because that // floorOf reads the Go literal behind key, and works on a nil deck because that
// is exactly the case it exists for. The per-family map is the source of truth. // is exactly the case it exists for. The per-family map is the source of truth.
func floorOf(d *deck, key string) string { func floorOf(d *Deck, key string) string {
if d != nil && d.floor != nil { if d != nil && d.floor != nil {
return d.floor[key] return d.floor[key]
} }
@@ -164,7 +167,7 @@ func floorOf(d *deck, key string) string {
// finds them. Families register at init; the keys are namespaced by family. // finds them. Families register at init; the keys are namespaced by family.
var deckFloors = map[string]string{} var deckFloors = map[string]string{}
func registerFloor(floor map[string]string) map[string]string { func RegisterFloor(floor map[string]string) map[string]string {
for k, v := range floor { for k, v := range floor {
deckFloors[k] = v deckFloors[k] = v
} }