go/asks: a message carries its note apart, and an explanation is held to a glance (hq issues 639 and 383)
mesh/merge-gate head 9588871d6629 merged into trunk a0fc82fdcd7c — pass, with a note: builds mesh-sdk (+40 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head 9588871d6629 merged into trunk a0fc82fdcd7c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 9588871d6629 merged into trunk a0fc82fdcd7c — pass, with a note: builds mesh-sdk (+40 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head 9588871d6629 merged into trunk a0fc82fdcd7c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery superseded: a newer head of the same pull request
The operator will not read a Telegram ask of thirty plain lines. The secondary words (who asked, what each answer does, the expiry, the fingerprint, how to read it whole) travel as Message.Note, so a channel can show them small or collapsed; an ask's Details go there. Glance names the bound of an explanation: a summary of at most 300 characters, then at most three points of at most 160.
This commit is contained in:
+52
-4
@@ -202,11 +202,55 @@ type Ask struct {
|
|||||||
// The asker withholds a value shaped like a secret in it, and the router refuses the ask when one is left;
|
// The asker withholds a value shaped like a secret in it, and the router refuses the ask when one is left;
|
||||||
// Explanation stays under the whole content rule everywhere else. Empty, Explanation is shown everywhere.
|
// Explanation stays under the whole content rule everywhere else. Empty, Explanation is shown everywhere.
|
||||||
Whole string `json:"whole,omitempty"`
|
Whole string `json:"whole,omitempty"`
|
||||||
// Details is what the Details answer on the ask's message shows, line by line under the content rule: a
|
// Details are the ask's secondary words, line by line under the content rule: who proposed it, a
|
||||||
// fingerprint, how to read the proposal whole at the terminal. Empty, an ask's own message offers no Details.
|
// fingerprint, how to read it whole. The router shows them in the note of the ask's own message
|
||||||
|
// (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq
|
||||||
|
// issue 639).
|
||||||
Details string `json:"details,omitempty"`
|
Details string `json:"details,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary
|
||||||
|
// of two or three short sentences on its first line — what changes, why, what it costs or risks — then at
|
||||||
|
// most three points, a line each, only where they add something. Whole is held to the same. Who asked, when,
|
||||||
|
// the fingerprint and how to read it whole are Details, never the explanation.
|
||||||
|
const (
|
||||||
|
SummaryLength = 300
|
||||||
|
MostPoints = 3
|
||||||
|
PointLength = 160
|
||||||
|
)
|
||||||
|
|
||||||
|
// Glance says why an explanation does not read at a glance, or nil: its first line, the summary, longer than
|
||||||
|
// SummaryLength; more than MostPoints lines after it; a point longer than PointLength. Blank lines are not
|
||||||
|
// counted. An asker holds its asks to it in its own tests; the router says, without refusing, an ask that
|
||||||
|
// does not hold to it.
|
||||||
|
func Glance(explanation string) error {
|
||||||
|
var lines []string
|
||||||
|
for _, l := range strings.Split(explanation, "\n") {
|
||||||
|
if strings.TrimSpace(l) != "" {
|
||||||
|
lines = append(lines, strings.TrimSpace(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var problems []string
|
||||||
|
switch {
|
||||||
|
case len(lines) == 0:
|
||||||
|
problems = append(problems, "it has no summary")
|
||||||
|
case len([]rune(lines[0])) > SummaryLength:
|
||||||
|
problems = append(problems, fmt.Sprintf("its summary is %d characters, more than %d", len([]rune(lines[0])), SummaryLength))
|
||||||
|
}
|
||||||
|
if len(lines) > 1+MostPoints {
|
||||||
|
problems = append(problems, fmt.Sprintf("it has %d points after its summary, more than %d", len(lines)-1, MostPoints))
|
||||||
|
}
|
||||||
|
for i, l := range lines {
|
||||||
|
if i > 0 && len([]rune(l)) > PointLength {
|
||||||
|
problems = append(problems, fmt.Sprintf("its point %d is %d characters, more than %d", i, len([]rune(l)), PointLength))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(problems) > 0 {
|
||||||
|
return errors.New("the explanation does not read at a glance: " + strings.Join(problems, "; "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// The bounds of an ask (novox/hq ADR 0234 §8, ADR 0259 §4).
|
// The bounds of an ask (novox/hq ADR 0234 §8, ADR 0259 §4).
|
||||||
const (
|
const (
|
||||||
MostOptions = 4
|
MostOptions = 4
|
||||||
@@ -435,8 +479,12 @@ type Message struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body"`
|
||||||
Buttons []Button `json:"buttons,omitempty"`
|
Buttons []Button `json:"buttons,omitempty"`
|
||||||
Urgent bool `json:"urgent,omitempty"`
|
// Note is the message's secondary words, after its body and its buttons: who asked and when, what each
|
||||||
Silent bool `json:"silent,omitempty"`
|
// answer does, when it expires, and the ask's Details. A channel shows it smaller, collapsed or set apart
|
||||||
|
// where it can, and after the body where it cannot; it is never left out (novox/hq issue 639).
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
Urgent bool `json:"urgent,omitempty"`
|
||||||
|
Silent bool `json:"silent,omitempty"`
|
||||||
// Reply is the Choice or LinkAsked this answers, by its ID: the channel shows it where that was made.
|
// Reply is the Choice or LinkAsked this answers, by its ID: the channel shows it where that was made.
|
||||||
Reply string `json:"reply,omitempty"`
|
Reply string `json:"reply,omitempty"`
|
||||||
// To is the account linked as the operator on this kind, for a channel that verifies its sender: where
|
// To is the account linked as the operator on this kind, for a channel that verifies its sender: where
|
||||||
|
|||||||
@@ -279,3 +279,41 @@ func TestAWarrantForAnAskWithNoExpiryIsRefused(t *testing.T) {
|
|||||||
t.Error("a warrant for an ask with no expiry was taken")
|
t.Error("a warrant for an ask with no expiry was taken")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An explanation that reads at a glance passes; a wall of text says each way it is one (novox/hq issue 639).
|
||||||
|
func TestAnExplanationReadsAtAGlance(t *testing.T) {
|
||||||
|
if err := Glance("Your agents' memory follows you to every machine. A file changed on two keeps both.\n\n• Only the memory folders are carried\n• A conflict reaches you as a warning"); err != nil {
|
||||||
|
t.Fatalf("a summary and two points: %v", err)
|
||||||
|
}
|
||||||
|
var wall strings.Builder
|
||||||
|
wall.WriteString("ADR 0343 was proposed by fix-367 and approved by the knowledge base's own review.")
|
||||||
|
for i := 1; i <= 13; i++ {
|
||||||
|
wall.WriteString("\n" + strings.Repeat("A clause lead. ", 3))
|
||||||
|
}
|
||||||
|
err := Glance(wall.String())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "13 points") {
|
||||||
|
t.Fatalf("thirteen clause lines pass as a glance: %v", err)
|
||||||
|
}
|
||||||
|
if err := Glance(strings.Repeat("word ", 70)); err == nil || !strings.Contains(err.Error(), "summary is 349") {
|
||||||
|
t.Fatalf("a long summary passes: %v", err)
|
||||||
|
}
|
||||||
|
if err := Glance("short.\n" + strings.Repeat("p", PointLength+1)); err == nil || !strings.Contains(err.Error(), "point 1") {
|
||||||
|
t.Fatalf("a long point passes: %v", err)
|
||||||
|
}
|
||||||
|
if err := Glance(" \n"); err == nil {
|
||||||
|
t.Fatal("an empty explanation passes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A message's note travels beside its body, and is left out of the encoding when empty, so a channel older
|
||||||
|
// than it reads every message it read before.
|
||||||
|
func TestAMessageCarriesItsNoteApart(t *testing.T) {
|
||||||
|
raw, _ := json.Marshal(Message{Handle: "h", Title: "t", Body: "b", Note: "Asked by x."})
|
||||||
|
if !strings.Contains(string(raw), `"note":"Asked by x."`) {
|
||||||
|
t.Fatalf("the note is not encoded: %s", raw)
|
||||||
|
}
|
||||||
|
raw, _ = json.Marshal(Message{Handle: "h", Title: "t"})
|
||||||
|
if strings.Contains(string(raw), "note") {
|
||||||
|
t.Fatalf("an empty note is encoded: %s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user