praxis: an empty attention list is not always an all-clear (V-540)
ECOSYSTEM-SPEC §2.6 requires list_attention to distinguish "nothing needs attention" from "I cannot currently tell", and to say so when a source is failed or stale. Maven said the first one unconditionally: ListAttention decoded into []map[string]any, the word degraded appeared nowhere, and an empty list answered "ничего не требует внимания". A Praxis with every source dead read as calm. Two halves, because the spec's mechanism does not exist server-side yet. The deployed Praxis answers /api/v1/tools/attention with a bare array and no envelope, so praxisAttention now decodes either shape and believes a degraded array when one arrives. Until one does, an empty list triggers one read of /api/v1/sources, and anything that is not reporting health "ok" is named instead of the all-clear. Zero sources is the same answer: a Praxis that polls nothing knows nothing, which is the state of this box today. A sources read that fails is deliberately not a hedge. The attention call succeeded, and not being able to ask about health is not evidence of a fault. Both hedges also cover the entity-scoped digest, where a per-entity all-clear is the more convincing of the two. New keys attention_degraded and attention_no_sources, in acts_ru_v1.json and the floor. The fake Praxis serves one healthy source by default, so the existing attention tests still assert an all-clear on purpose rather than by omission.
This commit is contained in:
@@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An empty attention list used to be answered "ничего не требует внимания"
|
||||||
|
// unconditionally, which is an all-clear Maven had no way to know was true
|
||||||
|
// (ECOSYSTEM-SPEC §2.6, Vikunja #540).
|
||||||
|
func TestAttentionEmptyWithHealthySourcesIsAllClear(t *testing.T) {
|
||||||
|
praxis := newFakePraxisWithSources(t, `[]`, `[{"source_id":"src_ntfy","health":"ok"}]`)
|
||||||
|
h := newPraxisTestHandler(t, praxis)
|
||||||
|
|
||||||
|
reply := h.handlePraxisAct(context.Background(), praxisActDec("list_attention"))
|
||||||
|
if !strings.Contains(reply, "ничего не требует внимания") {
|
||||||
|
t.Fatalf("healthy and quiet should be an all-clear, got %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttentionEmptyWithAFailedSourceHedges(t *testing.T) {
|
||||||
|
praxis := newFakePraxisWithSources(t, `[]`, `[
|
||||||
|
{"source_id":"src_ntfy","health":"ok"},
|
||||||
|
{"source_id":"src_llamacpp","health":"failed"},
|
||||||
|
{"source_id":"src_imap","health":"stale"}
|
||||||
|
]`)
|
||||||
|
h := newPraxisTestHandler(t, praxis)
|
||||||
|
|
||||||
|
reply := h.handlePraxisAct(context.Background(), praxisActDec("list_attention"))
|
||||||
|
if strings.Contains(reply, "ничего не требует внимания") {
|
||||||
|
t.Fatalf("a failed source must not read as all-clear, got %q", reply)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"src_llamacpp", "src_imap"} {
|
||||||
|
if !strings.Contains(reply, want) {
|
||||||
|
t.Errorf("reply names no %s: %q", want, reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(reply, "src_ntfy") {
|
||||||
|
t.Errorf("the healthy source is named as a problem: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Praxis that polls nothing knows nothing, which is the state the box is in.
|
||||||
|
func TestAttentionEmptyWithNoSourcesHedges(t *testing.T) {
|
||||||
|
praxis := newFakePraxisWithSources(t, `[]`, `[]`)
|
||||||
|
h := newPraxisTestHandler(t, praxis)
|
||||||
|
|
||||||
|
reply := h.handlePraxisAct(context.Background(), praxisActDec("list_attention"))
|
||||||
|
if strings.Contains(reply, "ничего не требует внимания") {
|
||||||
|
t.Fatalf("a Praxis with no sources must not answer all-clear, got %q", reply)
|
||||||
|
}
|
||||||
|
if !strings.Contains(reply, "источник") {
|
||||||
|
t.Errorf("reply does not say why she cannot tell: %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The spec's own mechanism, which the deployed Praxis does not send yet: the
|
||||||
|
// envelope's degraded array is believed without a second call.
|
||||||
|
func TestAttentionDegradedEnvelopeIsReadWithoutASourcesCall(t *testing.T) {
|
||||||
|
praxis := newFakePraxisWithSources(t,
|
||||||
|
`{"items":[],"degraded":["src_metrics"]}`,
|
||||||
|
`[{"source_id":"src_ntfy","health":"ok"}]`)
|
||||||
|
h := newPraxisTestHandler(t, praxis)
|
||||||
|
|
||||||
|
reply := h.handlePraxisAct(context.Background(), praxisActDec("list_attention"))
|
||||||
|
if !strings.Contains(reply, "src_metrics") {
|
||||||
|
t.Fatalf("the envelope's degraded source is not named: %q", reply)
|
||||||
|
}
|
||||||
|
for _, r := range praxis.Requests() {
|
||||||
|
if r.Path == "/api/v1/sources" {
|
||||||
|
t.Error("sources was read even though the response carried degraded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sources endpoint that errors is not evidence of a fault: the attention call
|
||||||
|
// itself succeeded, and hedging on it would make her permanently uncertain.
|
||||||
|
func TestAttentionKeepsAllClearWhenSourcesCannotBeRead(t *testing.T) {
|
||||||
|
praxis := newFakePraxisWithSources(t, `[]`, `[]`)
|
||||||
|
praxis.SetRouteFault("/api/v1/sources", 500)
|
||||||
|
h := newPraxisTestHandler(t, praxis)
|
||||||
|
|
||||||
|
reply := h.handlePraxisAct(context.Background(), praxisActDec("list_attention"))
|
||||||
|
if !strings.Contains(reply, "ничего не требует внимания") {
|
||||||
|
t.Fatalf("an unreadable sources list should leave the answer alone, got %q", reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both response shapes decode, because the spec says one and the box sends the
|
||||||
|
// other.
|
||||||
|
func TestPraxisAttentionDecodesBothShapes(t *testing.T) {
|
||||||
|
var bare praxisAttention
|
||||||
|
if err := json.Unmarshal([]byte(`[{"id":"item_1"}]`), &bare); err != nil {
|
||||||
|
t.Fatalf("bare array: %v", err)
|
||||||
|
}
|
||||||
|
if len(bare.Items) != 1 || len(bare.Degraded) != 0 {
|
||||||
|
t.Errorf("bare array decoded as %+v", bare)
|
||||||
|
}
|
||||||
|
|
||||||
|
var env praxisAttention
|
||||||
|
if err := json.Unmarshal([]byte(`{"items":[{"id":"item_2"}],"degraded":["src_a"]}`), &env); err != nil {
|
||||||
|
t.Fatalf("envelope: %v", err)
|
||||||
|
}
|
||||||
|
if len(env.Items) != 1 || len(env.Degraded) != 1 || env.Degraded[0] != "src_a" {
|
||||||
|
t.Errorf("envelope decoded as %+v", env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A source that reports no health at all counts as healthy. A Praxis that never
|
||||||
|
// fills the field would otherwise make every quiet turn a hedge.
|
||||||
|
func TestUnhealthySourcesTreatsAnAbsentHealthFieldAsHealthy(t *testing.T) {
|
||||||
|
praxis := newFakePraxisWithSources(t, `[]`, `[{"source_id":"src_a"},{"id":"src_b","health":"stale"}]`)
|
||||||
|
bad, total, err := newPraxisClient(praxis.URL).UnhealthySources(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UnhealthySources: %v", err)
|
||||||
|
}
|
||||||
|
if total != 2 {
|
||||||
|
t.Errorf("total = %d, want 2", total)
|
||||||
|
}
|
||||||
|
if len(bad) != 1 || bad[0] != "src_b" {
|
||||||
|
t.Errorf("bad = %v, want [src_b]", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
-4
@@ -263,18 +263,85 @@ func (c *praxisClient) getJSON(ctx context.Context, op, path string, out any) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *praxisClient) ListAttention(ctx context.Context, limit int) ([]map[string]any, error) {
|
// praxisAttention — an attention response in either of the two shapes Praxis
|
||||||
var out []map[string]any
|
// may send (Vikunja #540).
|
||||||
|
//
|
||||||
|
// ECOSYSTEM-SPEC §2.6 says the response carries `degraded: [source_ids]` when a
|
||||||
|
// source is failed or stale, and that Maven is required to say so rather than
|
||||||
|
// report all-clear. The deployed Praxis answers with a bare JSON array and no
|
||||||
|
// envelope at all, so both are decoded here: an array is the items, an object is
|
||||||
|
// the spec envelope. This lands the Maven half without waiting on the server,
|
||||||
|
// and the sources read below is what makes the hedge work meanwhile.
|
||||||
|
type praxisAttention struct {
|
||||||
|
Items []map[string]any
|
||||||
|
Degraded []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *praxisAttention) UnmarshalJSON(data []byte) error {
|
||||||
|
trimmed := bytes.TrimSpace(data)
|
||||||
|
if len(trimmed) > 0 && trimmed[0] == '[' {
|
||||||
|
return json.Unmarshal(trimmed, &a.Items)
|
||||||
|
}
|
||||||
|
var env struct {
|
||||||
|
Items []map[string]any `json:"items"`
|
||||||
|
Degraded []string `json:"degraded"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(trimmed, &env); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Items, a.Degraded = env.Items, env.Degraded
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *praxisClient) ListAttention(ctx context.Context, limit int) (praxisAttention, error) {
|
||||||
|
var out praxisAttention
|
||||||
err := c.getJSON(ctx, "attention", fmt.Sprintf("/api/v1/tools/attention?limit=%d", limit), &out)
|
err := c.getJSON(ctx, "attention", fmt.Sprintf("/api/v1/tools/attention?limit=%d", limit), &out)
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// praxisSource — one polled source, as much of it as the hedge needs. The tools
|
||||||
|
// API does not expose sources, so this decodes the plain `/api/v1/sources` rows.
|
||||||
|
type praxisSource struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
SourceID string `json:"source_id"`
|
||||||
|
Health string `json:"health"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s praxisSource) name() string {
|
||||||
|
if s.SourceID != "" {
|
||||||
|
return s.SourceID
|
||||||
|
}
|
||||||
|
return s.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnhealthySources reports which sources cannot be trusted to have reported,
|
||||||
|
// and how many sources Praxis has at all (Vikunja #540).
|
||||||
|
//
|
||||||
|
// Only read when the attention list came back empty, which is the one turn where
|
||||||
|
// an all-clear is at stake. A source whose health field is absent counts as
|
||||||
|
// healthy: a Praxis that never reports health would otherwise make every quiet
|
||||||
|
// turn a hedge, and an unreported field is not evidence of a fault. Everything it
|
||||||
|
// does report other than "ok" — failed, stale, degraded, unknown — counts as
|
||||||
|
// cannot-tell, because none of them mean the source has spoken.
|
||||||
|
func (c *praxisClient) UnhealthySources(ctx context.Context) (bad []string, total int, err error) {
|
||||||
|
var out []praxisSource
|
||||||
|
if err := c.getJSON(ctx, "sources", "/api/v1/sources", &out); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for _, s := range out {
|
||||||
|
if s.Health != "" && s.Health != "ok" {
|
||||||
|
bad = append(bad, s.name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bad, len(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListAttentionForEntity is ListAttention scoped to a single canonical Nexus
|
// ListAttentionForEntity is ListAttention scoped to a single canonical Nexus
|
||||||
// entity, so callers already holding a resolved entity_id (e.g. after
|
// entity, so callers already holding a resolved entity_id (e.g. after
|
||||||
// resolveEntityReference) can ask "what needs attention for this entity"
|
// resolveEntityReference) can ask "what needs attention for this entity"
|
||||||
// instead of filtering the unscoped list client-side.
|
// instead of filtering the unscoped list client-side.
|
||||||
func (c *praxisClient) ListAttentionForEntity(ctx context.Context, entityID string, limit int) ([]map[string]any, error) {
|
func (c *praxisClient) ListAttentionForEntity(ctx context.Context, entityID string, limit int) (praxisAttention, error) {
|
||||||
var out []map[string]any
|
var out praxisAttention
|
||||||
err := c.getJSON(ctx, "attention_for_entity",
|
err := c.getJSON(ctx, "attention_for_entity",
|
||||||
fmt.Sprintf("/api/v1/tools/attention?limit=%d&entity_id=%s", limit, url.QueryEscape(entityID)), &out)
|
fmt.Sprintf("/api/v1/tools/attention?limit=%d&entity_id=%s", limit, url.QueryEscape(entityID)), &out)
|
||||||
return out, err
|
return out, err
|
||||||
|
|||||||
@@ -154,14 +154,18 @@ func (listAttentionCapability) aliases() []string {
|
|||||||
|
|
||||||
func (listAttentionCapability) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, _ router.Decision) string {
|
func (listAttentionCapability) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, _ router.Decision) string {
|
||||||
started := h.now()
|
started := h.now()
|
||||||
items, err := px.ListAttention(ctx, 20)
|
att, err := px.ListAttention(ctx, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ecosystem: praxis attention: %v", err)
|
log.Printf("ecosystem: praxis attention: %v", err)
|
||||||
h.recordEcosystemTrace(ctx, "praxis", "list_attention", traceStatusForError(err),
|
h.recordEcosystemTrace(ctx, "praxis", "list_attention", traceStatusForError(err),
|
||||||
started, traceErrorFields(err))
|
started, traceErrorFields(err))
|
||||||
return phraser.A(phraser.AttentionFail, nil)
|
return phraser.A(phraser.AttentionFail, nil)
|
||||||
}
|
}
|
||||||
|
items := att.Items
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
|
if hedge := h.attentionCannotTell(ctx, px, att.Degraded, started); hedge != "" {
|
||||||
|
return hedge
|
||||||
|
}
|
||||||
return phraser.A(phraser.AttentionNone, nil)
|
return phraser.A(phraser.AttentionNone, nil)
|
||||||
}
|
}
|
||||||
h.recordPraxisTrace(ctx, "list_attention", started, map[string]any{"count": len(items)})
|
h.recordPraxisTrace(ctx, "list_attention", started, map[string]any{"count": len(items)})
|
||||||
@@ -299,14 +303,14 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|||||||
}
|
}
|
||||||
|
|
||||||
queried := h.now()
|
queried := h.now()
|
||||||
items, err := px.ListAttentionForEntity(ctx, entityID, 20)
|
att, err := px.ListAttentionForEntity(ctx, entityID, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ecosystem: praxis attention for %s: %v", entityID, err)
|
log.Printf("ecosystem: praxis attention for %s: %v", entityID, err)
|
||||||
h.recordEcosystemTrace(ctx, "praxis", "entity_attention", traceStatusForError(err),
|
h.recordEcosystemTrace(ctx, "praxis", "entity_attention", traceStatusForError(err),
|
||||||
queried, mergeFields(traceErrorFields(err), map[string]any{"entity_id": entityID}))
|
queried, mergeFields(traceErrorFields(err), map[string]any{"entity_id": entityID}))
|
||||||
return phraser.A(phraser.AttentionFailEntity, map[string]string{"name": displayName})
|
return phraser.A(phraser.AttentionFailEntity, map[string]string{"name": displayName})
|
||||||
}
|
}
|
||||||
items, scoped := scopedToEntity(items, entityID)
|
items, scoped := scopedToEntity(att.Items, entityID)
|
||||||
if !scoped {
|
if !scoped {
|
||||||
// A Praxis old enough to ignore an unknown query parameter answers the
|
// A Praxis old enough to ignore an unknown query parameter answers the
|
||||||
// scoped question with the unscoped list. Reading that back as "по
|
// scoped question with the unscoped list. Reading that back as "по
|
||||||
@@ -339,6 +343,11 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|||||||
parts = append(parts, known)
|
parts = append(parts, known)
|
||||||
}
|
}
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
|
// The scoped list is as exposed to a silent source as the unscoped one,
|
||||||
|
// and a per-entity all-clear is the more convincing of the two (#540).
|
||||||
|
if hedge := h.attentionCannotTell(ctx, px, att.Degraded, queried); hedge != "" {
|
||||||
|
return hedge
|
||||||
|
}
|
||||||
return phraser.A(phraser.AttentionNoneEntity, map[string]string{"name": displayName})
|
return phraser.A(phraser.AttentionNoneEntity, map[string]string{"name": displayName})
|
||||||
}
|
}
|
||||||
return phraser.A(phraser.AttentionListEntity, map[string]string{"name": displayName, "items": strings.Join(parts, "; ")})
|
return phraser.A(phraser.AttentionListEntity, map[string]string{"name": displayName, "items": strings.Join(parts, "; ")})
|
||||||
@@ -783,3 +792,46 @@ func (h *reactiveHandler) hexisBeforeClarify(ctx context.Context, dec router.Dec
|
|||||||
}
|
}
|
||||||
return h.handleHexisAct(ctx, dec)
|
return h.handleHexisAct(ctx, dec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// attentionCannotTell returns the hedge to say instead of an all-clear, or ""
|
||||||
|
// when an empty attention list really does mean nothing needs looking at
|
||||||
|
// (ECOSYSTEM-SPEC §2.6, Vikunja #540).
|
||||||
|
//
|
||||||
|
// "Nothing needs attention" and "I cannot currently tell" are different answers
|
||||||
|
// and only one of them was ever said. The spec's mechanism is a `degraded` array
|
||||||
|
// on the attention response, which the deployed Praxis does not send, so the
|
||||||
|
// source health read is the half that works today. It costs one HTTP call and
|
||||||
|
// only on the empty-list turn, which is the only turn where an all-clear is at
|
||||||
|
// stake.
|
||||||
|
//
|
||||||
|
// A failed sources read is deliberately NOT a hedge. The attention call itself
|
||||||
|
// succeeded, and not being able to ask about health is not evidence of a fault —
|
||||||
|
// hedging on it would turn one flaky endpoint into a permanently uncertain
|
||||||
|
// assistant.
|
||||||
|
func (h *reactiveHandler) attentionCannotTell(ctx context.Context, px *praxisClient, degraded []string, started time.Time) string {
|
||||||
|
if len(degraded) > 0 {
|
||||||
|
h.recordPraxisTrace(ctx, "attention_degraded", started, map[string]any{
|
||||||
|
"degraded": strings.Join(degraded, ","), "source": "response",
|
||||||
|
})
|
||||||
|
return phraser.A(phraser.AttentionDegraded, map[string]string{"items": strings.Join(degraded, ", ")})
|
||||||
|
}
|
||||||
|
bad, total, err := px.UnhealthySources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ecosystem: praxis sources: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
// A Praxis that polls nothing knows nothing, so its silence is not an
|
||||||
|
// all-clear either. This is the state the box is in as of 2026-08-05:
|
||||||
|
// /api/v1/sources answers with an empty array.
|
||||||
|
h.recordPraxisTrace(ctx, "attention_no_sources", started, map[string]any{"sources": 0})
|
||||||
|
return phraser.A(phraser.AttentionNoSources, nil)
|
||||||
|
}
|
||||||
|
if len(bad) > 0 {
|
||||||
|
h.recordPraxisTrace(ctx, "attention_degraded", started, map[string]any{
|
||||||
|
"degraded": strings.Join(bad, ","), "sources": total, "source": "health",
|
||||||
|
})
|
||||||
|
return phraser.A(phraser.AttentionDegraded, map[string]string{"items": strings.Join(bad, ", ")})
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -330,7 +330,17 @@ func newFakeNexus(t *testing.T, resolveBody string) *fakeServer {
|
|||||||
// Maven's praxisClient calls. Every route returns its fixed body until a
|
// Maven's praxisClient calls. Every route returns its fixed body until a
|
||||||
// fault is injected via SetFault.
|
// fault is injected via SetFault.
|
||||||
func newFakePraxis(t *testing.T, attentionBody string) *fakeServer {
|
func newFakePraxis(t *testing.T, attentionBody string) *fakeServer {
|
||||||
|
// One healthy source by default: an empty attention list only means
|
||||||
|
// all-clear when something is actually polling (Vikunja #540), and the
|
||||||
|
// other tests here are about attention rather than about source health.
|
||||||
|
return newFakePraxisWithSources(t, attentionBody, `[{"source_id":"src_ntfy","health":"ok"}]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFakePraxisWithSources is newFakePraxis with the /api/v1/sources body
|
||||||
|
// under the test's control, for the degraded and no-sources hedges.
|
||||||
|
func newFakePraxisWithSources(t *testing.T, attentionBody, sourcesBody string) *fakeServer {
|
||||||
return newFakeServer(t, map[string]http.HandlerFunc{
|
return newFakeServer(t, map[string]http.HandlerFunc{
|
||||||
|
"GET /api/v1/sources": jsonHandler(http.StatusOK, sourcesBody),
|
||||||
"GET /api/v1/tools/attention": jsonHandler(http.StatusOK, attentionBody),
|
"GET /api/v1/tools/attention": jsonHandler(http.StatusOK, attentionBody),
|
||||||
"GET /api/v1/tools/changes": jsonHandler(http.StatusOK, `[]`),
|
"GET /api/v1/tools/changes": jsonHandler(http.StatusOK, `[]`),
|
||||||
"POST /api/v1/tools/surface": jsonHandler(http.StatusOK, `{}`),
|
"POST /api/v1/tools/surface": jsonHandler(http.StatusOK, `{}`),
|
||||||
|
|||||||
@@ -60,13 +60,17 @@ const (
|
|||||||
AttentionNoneEntity = "attention_none_entity"
|
AttentionNoneEntity = "attention_none_entity"
|
||||||
AttentionListEntity = "attention_list_entity"
|
AttentionListEntity = "attention_list_entity"
|
||||||
AttentionFailEntity = "attention_fail_entity"
|
AttentionFailEntity = "attention_fail_entity"
|
||||||
ChangesNone = "changes_none"
|
// AttentionDegraded and AttentionNoSources — the two ways an empty
|
||||||
ChangesList = "changes_list"
|
// attention list is not an all-clear (ECOSYSTEM-SPEC §2.6, Vikunja #540).
|
||||||
ChangesFail = "changes_fail"
|
AttentionDegraded = "attention_degraded"
|
||||||
HomeUnreachable = "home_unreachable"
|
AttentionNoSources = "attention_no_sources"
|
||||||
HomeEmpty = "home_empty"
|
ChangesNone = "changes_none"
|
||||||
HomeOn = "home_on"
|
ChangesList = "changes_list"
|
||||||
HomeDark = "home_dark"
|
ChangesFail = "changes_fail"
|
||||||
|
HomeUnreachable = "home_unreachable"
|
||||||
|
HomeEmpty = "home_empty"
|
||||||
|
HomeOn = "home_on"
|
||||||
|
HomeDark = "home_dark"
|
||||||
)
|
)
|
||||||
|
|
||||||
var actKeys = []string{
|
var actKeys = []string{
|
||||||
@@ -76,6 +80,7 @@ var actKeys = []string{
|
|||||||
EcoDenied, EcoDown, EcoAmbiguous, EcoUnknownEntity, EcoNoNexus, EcoAboutWhat, EcoRecall,
|
EcoDenied, EcoDown, EcoAmbiguous, EcoUnknownEntity, EcoNoNexus, EcoAboutWhat, EcoRecall,
|
||||||
AttentionNone, AttentionList, AttentionFail,
|
AttentionNone, AttentionList, AttentionFail,
|
||||||
AttentionNoneEntity, AttentionListEntity, AttentionFailEntity,
|
AttentionNoneEntity, AttentionListEntity, AttentionFailEntity,
|
||||||
|
AttentionDegraded, AttentionNoSources,
|
||||||
ChangesNone, ChangesList, ChangesFail,
|
ChangesNone, ChangesList, ChangesFail,
|
||||||
HomeUnreachable, HomeEmpty, HomeOn, HomeDark,
|
HomeUnreachable, HomeEmpty, HomeOn, HomeDark,
|
||||||
}
|
}
|
||||||
@@ -113,6 +118,8 @@ var actFloor = map[string]string{
|
|||||||
AttentionNoneEntity: "по «{name}» ничего нет.",
|
AttentionNoneEntity: "по «{name}» ничего нет.",
|
||||||
AttentionListEntity: "по «{name}»: {items}",
|
AttentionListEntity: "по «{name}»: {items}",
|
||||||
AttentionFailEntity: "не могу сейчас узнать, что требует внимания по «{name}».",
|
AttentionFailEntity: "не могу сейчас узнать, что требует внимания по «{name}».",
|
||||||
|
AttentionDegraded: "за всё не отвечу — источники молчат: {items}.",
|
||||||
|
AttentionNoSources: "мне пока нечего смотреть — у Praxis нет источников.",
|
||||||
ChangesNone: "изменений нет.",
|
ChangesNone: "изменений нет.",
|
||||||
ChangesList: "изменения: {items}",
|
ChangesList: "изменения: {items}",
|
||||||
ChangesFail: "не могу сейчас узнать об изменениях.",
|
ChangesFail: "не могу сейчас узнать об изменениях.",
|
||||||
|
|||||||
@@ -114,6 +114,14 @@
|
|||||||
"fixed": true,
|
"fixed": true,
|
||||||
"variants": ["не могу сейчас узнать, что требует внимания по «{name}»."]
|
"variants": ["не могу сейчас узнать, что требует внимания по «{name}»."]
|
||||||
},
|
},
|
||||||
|
"attention_degraded": {
|
||||||
|
"fixed": true,
|
||||||
|
"variants": ["за всё не отвечу — источники молчат: {items}."]
|
||||||
|
},
|
||||||
|
"attention_no_sources": {
|
||||||
|
"fixed": true,
|
||||||
|
"variants": ["мне пока нечего смотреть — у Praxis нет источников."]
|
||||||
|
},
|
||||||
"changes_none": {
|
"changes_none": {
|
||||||
"fixed": true,
|
"fixed": true,
|
||||||
"variants": ["изменений нет."]
|
"variants": ["изменений нет."]
|
||||||
|
|||||||
Reference in New Issue
Block a user