Files
mesh-catalog/modules/messenger/cmd/messenger/preview.go
T
jschoubben b7d9590fdc
mesh/merge-gate head b7d9590fdc95 merged into trunk e1c8965ddbf1 — pass: builds messenger, telegram → novox; no bus step; every machine composes with the c…
mesh/repo-check head b7d9590fdc95 merged into trunk e1c8965ddbf1 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
An answered ask keeps its question whole, its answer below it (issue 714)
Issue 372's fix with issue 639's one line edited an answered ask down to
'You chose X at HH:MM · headline', so the operator could not read back what
they approved. The headline stays the title, the summary, points, link and
Details stay the body, and how it ended and who acts on it become the note.
Telegram cuts the body from its end past 4096 characters, never the title or
the note.
2026-10-14 20:06:24 +02:00

138 lines
5.5 KiB
Go

package main
// What a condition's message would say, read before it is sent (novox/hq issue 422). The words a
// condition reaches the operator with are composed here, in this router, from the condition's own plain
// words and the line on the issues that track it (tracking.go). Until this tool, the only way to read
// them was to wait for a send and look at the operator's screen: the channels keep the text of nothing
// they sent (`Sent` holds a key and an outcome, never words), and a notification still on the desk is
// not in the notifier's history. So a wording could not be proven without nagging the operator.
//
// It is the sending path's own walk and nothing beside it: `allowedWords` (holder.go), which composes
// the message, carries the tracking line into it and reduces it where the content rule refuses its
// words — the same function `words` calls for a send, so a preview cannot drift from one. It records no
// refusal, states nothing on the bus, queues nothing, sends nothing and changes no record: the walk runs
// against a copy.
//
// What it does not say, because a channel adds it where a condition carries an open ask (`sayOn`): who
// asks, what each answer does, and the answers themselves. A preview of such a condition says that its
// message carries more, rather than letting the words read as the whole of it.
//
// Two messages are about a moment that has not come. A clearing is composed as if the condition cleared
// now, which the answer says, because a clearing composed from an open record would say "after a moment"
// for a condition open four hours. A silence is refused unless the condition is under one: a silence
// message says until when, and a condition nobody silenced has no such moment to say.
import (
"sort"
"strings"
)
// The messages a condition is said with, as `what` names them through the sending path (holder.go).
var previewable = []string{"raised", "reminder", "quiet-reminder", "escalated", "reopened", "cleared", "silenced"}
// Preview is the message for a condition this router holds, composed as it would be sent, and sent
// nowhere. `what` is which of the messages about it (previewable, default `raised`).
func (h *Holder) Preview(key, what string) (map[string]any, error) {
key = strings.TrimSpace(key)
if key == "" {
return nil, errorf("key is required: the condition's key, as the controller names it — `mesh-controller.conditions` says which are open")
}
what = strings.TrimSpace(what)
if what == "" {
what = "raised"
}
if !previewableWhat(what) {
return nil, errorf("what %q is not one of the messages a condition is said with: %s", what, strings.Join(previewable, ", "))
}
// Asked outside the work, as every tick that composes a message warms it: a preview on a cold
// tracking word would say the tracking could not be checked, which is true of this instant and not
// of the send it is read for.
h.warmTracking()
now := h.Now()
h.mu.Lock()
held, holding := h.open[key]
var r Record
if holding {
r = *held // the walk runs against a copy: a preview changes no record this router holds
}
var conditions []string
for k, rec := range h.open {
if rec.Origin == "condition" {
conditions = append(conditions, k)
}
}
h.mu.Unlock()
if !holding || r.Origin != "condition" {
sort.Strings(conditions)
switch {
case holding && len(conditions) == 0:
return nil, errorf("%s is not a condition but this router's own %s record, which is said as part of an ask, never on its own; it holds no condition",
key, r.Origin)
case holding:
return nil, errorf("%s is not a condition but this router's own %s record, which is said as part of an ask, never on its own; it holds the conditions %s",
key, r.Origin, strings.Join(conditions, ", "))
case len(conditions) == 0:
return nil, errorf("no condition %s is open here, and this router holds none: `mesh-controller.conditions` says what the controller holds open", key)
default:
return nil, errorf("no condition %s is open here; it holds %s", key, strings.Join(conditions, ", "))
}
}
if what == "silenced" && !r.silenced(now) {
return nil, errorf("%s is under no silence, and a silence message says until when it is quiet: there is no such message to read", key)
}
if what == "cleared" {
// As if it cleared now: the clearing says how long it was open, which an open record cannot say.
r.Cleared = now
}
asking := h.openAsk(&r) != nil // askOf takes the same lock, so this is asked outside it
m, line, refusal, ownWords := h.allowedWords(&r, what, now)
if r.Answered != "" {
m = h.answeredWords(&r, m, "")
}
out := map[string]any{
"key": key,
"what": what,
"title": m.Title,
"body": m.Body,
"sent": "nothing: this composes the words and sends nowhere",
}
if m.Urgent {
out["urgent"] = true
}
if m.Quiet {
out["quiet"] = true
}
if line != "" {
out["tracking"] = line
}
if what == "cleared" {
out["as-if"] = "it cleared now: how long it was open is said from this moment"
}
if asking {
out["carries-more"] = "this condition has an open ask, so a channel's message also carries who asks, what each answer does, and the answers themselves"
}
out["allowed"] = ownWords
if !ownWords {
// What stands above is what would leave; its own words would not, and this says why.
out["carried"] = refusal.What
if refusal.Class != "" {
out["class"] = refusal.Class
}
out["instead"] = "its own words may not leave the mesh, so this is the message as reduced for sending"
}
return out, nil
}
func previewableWhat(what string) bool {
for _, w := range previewable {
if w == what {
return true
}
}
return false
}