Risk tiers: take Hexis's tier for a Hexis capability, derive only for local tool rows #162

Closed
claude wants to merge 33 commits from task/523-risk-tiers-take-hexis-s-tier-for-a-hexis into task/449-destructive-confirm-policy-risk-tiers-no
3 changed files with 79 additions and 25 deletions
Showing only changes of commit feca776077 - Show all commits
+3 -3
View File
@@ -65,7 +65,7 @@ var ackKeys = []string{
// 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.
var ackFloor = registerFloor(map[string]string{
var ackFloor = map[string]string{
AckFact: "записала факт.",
AckFactKey: "отметила: {key}",
AckFactValue: "отметила: {key} = {value}",
@@ -90,7 +90,7 @@ var ackFloor = registerFloor(map[string]string{
FailAck: "не получилось отметить.",
FailSnooze: "не получилось отложить.",
FailQuiet: "не получилось переключить тихий режим.",
})
}
// Acks picks a hand-written Russian acknowledgement. Safe for concurrent use.
type Acks struct{ d *deck }
@@ -119,7 +119,7 @@ func LoadAcks(src rand.Source) (*Acks, error) {
// deck reads through a nil *Acks, which is the unloadable-file case.
func (a *Acks) deck() *deck {
if a == nil {
return nil
return floorDeck(ackFloor)
}
return a.d
}
+73 -19
View File
@@ -35,8 +35,8 @@ type deckFile struct {
Entries map[string]deckEntry `json:"entries"`
}
// deck picks a line. Safe for concurrent use. A nil *deck answers from the
// floor, which is what an unloadable file leaves behind.
// 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
@@ -90,7 +90,7 @@ 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, e)
tmpl = d.pick(key, fillable(e.Variants, vars))
}
}
if tmpl == "" {
@@ -129,12 +129,67 @@ func (d *deck) variants() []string {
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, e deckEntry) string {
func (d *deck) pick(key string, variants []string) string {
d.mu.Lock()
defer d.mu.Unlock()
choices := e.Variants
choices := variants
if len(choices) > 1 {
fresh := make([]string, 0, len(choices))
for _, v := range choices {
@@ -151,24 +206,23 @@ func (d *deck) pick(key string, e deckEntry) string {
return got
}
// 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.
// 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 && d.floor != nil {
return d.floor[key]
if d == nil {
return ""
}
return deckFloors[key]
return d.floor[key]
}
// deckFloors — every family's floor literals in one map, so a nil deck still
// finds them. Families register at init; the keys are namespaced by family.
var deckFloors = map[string]string{}
func registerFloor(floor map[string]string) map[string]string {
for k, v := range floor {
deckFloors[k] = v
}
return floor
// 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
+3 -3
View File
@@ -39,12 +39,12 @@ var fbKeys = []string{fbChat, fbQueryUnknown, fbQuerySources, fbWorldGap}
// 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.
var hardFloor = registerFloor(map[string]string{
var hardFloor = map[string]string{
fbChat: "даже не знаю, что сказать.",
fbQueryUnknown: "не знаю.",
fbQuerySources: "вот что я нашла: {sources}",
fbWorldGap: "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу.",
})
}
// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use.
type Fallbacks struct{ d *deck }
@@ -66,7 +66,7 @@ func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
// deck reads through a nil *Fallbacks, which is the unloadable-file case.
func (f *Fallbacks) deck() *deck {
if f == nil {
return nil
return floorDeck(hardFloor)
}
return f.d
}