An ask reads at a glance on the phone: a headline, a summary and its points, the rest in a note (issues 383 and 639)

The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold,
no summary. A settings proposal was the same wall of text, its values masked so the approval was blind.

- messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each
  answer does, the expiry, where else it is answered and the asker's Details travel as the message's note
  (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram ·
  <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind
  that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone
  (from the earlier work on issue 383, #178).
- telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in
  a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message
  shown before is still compared by the digest it was kept under.
- desk-channel: the note follows the body.
- mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three
  points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is
  composed again as it was asked (form), so its warrant still holds.
- claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the
  module's), its text in the Details; a question's context is at most 300 characters and its recommendation
  one point. Older proposals and questions keep their form.

The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and
asks.Glance.
This commit is contained in:
jochen
2026-10-13 17:16:50 +02:00
parent 9c166398f2
commit eadf301f9f
46 changed files with 1276 additions and 162 deletions
@@ -2,9 +2,10 @@ package main
// The rules a record is held to (novox/hq ADR 0290 rule 4, to-be 55 §4 and §5).
//
// **29 rules judge one record or its prose**, and `propose` refuses a record that breaks any of them, naming every
// **30 rules judge one record or its prose**, and `propose` refuses a record that breaks any of them, naming every
// one it breaks at once. They are hq's checks (00-META/checks/records.py, cycle.py, index.py, words.py) as they
// judge one record, moved to the write: 25 on its structure, 4 on its prose. Each has an id, said in the refusal,
// judge one record, moved to the write: 25 on its structure, 4 on its prose; and the brief a record that needs
// the operator is asked with (novox/hq issue 639). Each has an id, said in the refusal,
// and one case in the table test (TestEveryPerRecordRuleRefusesAProposal).
//
// **4 rules judge the whole record set** (wholeSetRules) and run after every acceptance, and in the check
@@ -15,6 +16,8 @@ import (
"fmt"
"regexp"
"strings"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Rule is one rule that judges one record: its id and what it holds.
@@ -25,7 +28,7 @@ type Rule struct {
From string
}
// PerRecordRules are the 29, in the order they are judged.
// PerRecordRules are the 30, in the order they are judged.
var PerRecordRules = []Rule{
{"kind", "a record is a decision or a research effort", "cycle.py"},
{"reserved", "its number was reserved for its kind and holds no other record", "cycle.py (duplicate numbers)"},
@@ -56,6 +59,7 @@ var PerRecordRules = []Rule{
{"daemon-line", "it writes no daemon's line", "words.py lines"},
{"deferral", "a paragraph that puts work off names the issue that tracks it", "cycle.py deferrals"},
{"insight-form", "a progressive insight its text mentions is in the dated, marked form", "records.py insights"},
{"brief", "a record that needs the operator carries a brief its ask reads at a glance: a headline, a summary, at most three points, each fit for the phone", "novox/hq issue 639"},
}
// Refusal is one rule a record breaks, and how.
@@ -116,7 +120,7 @@ func allText(r *Record) string {
return b.String()
}
// JudgeRecord holds a proposed record to the 29 rules. held is the entry its number holds (a reservation, the
// JudgeRecord holds a proposed record to the 30 rules. held is the entry its number holds (a reservation, the
// proposal being revised, or the accepted record an insight corrects); lookup reads every other record.
func JudgeRecord(r *Record, held *Entry, lookup Lookup, ref *Reference) []Refusal {
var out []Refusal
@@ -349,6 +353,11 @@ func JudgeRecord(r *Record, held *Entry, lookup Lookup, ref *Reference) []Refusa
refuse("insight-form", "a progressive insight dated %s predates the record (%s)", mk[1], r.Date)
}
}
if r.NeedsOperator || r.Brief != nil {
for _, why := range briefProblems(r.Brief) {
refuse("brief", "%s", why)
}
}
return out
}
@@ -597,6 +606,7 @@ var RuleFrom = map[string]string{
"consequences": "2026-10-11",
"checked": "2026-10-11",
"live-citation": "2026-10-11",
"brief": "2026-10-13",
}
// DecisionFrom overrides RuleFrom for a decision record: hq's words check never read 02-DECISIONS ("a decision
@@ -615,3 +625,35 @@ func Holds(rule, kind, date string) bool {
}
return !dated || !isDay.MatchString(date) || date >= from
}
// briefProblems are the ways a brief would not read at a glance on the operator's phone (novox/hq issue 639):
// missing; a headline empty or longer than an ask's; a summary or a point the router could refuse, or that
// names a path, an address or a long word (shown); more than three points, or one too long (asks.Glance).
func briefProblems(b *Brief) []string {
if b == nil {
return []string{"a record that needs the operator carries a brief (headline, summary, points): what it changes for the operator, " +
"in their words, which its ask shows in place of its clauses"}
}
var out []string
h := strings.TrimSpace(b.Headline)
if h == "" || strings.Contains(h, "\n") || len([]rune(h)) > asks.HeadlineLength {
out = append(out, fmt.Sprintf("the brief's headline is empty, more than one line, or longer than %d characters", asks.HeadlineLength))
}
if s := strings.TrimSpace(b.Summary); s == "" || strings.Contains(s, "\n") {
out = append(out, "the brief's summary is empty or more than one line")
}
for i, p := range b.Points {
if p = strings.TrimSpace(p); p == "" || strings.Contains(p, "\n") {
out = append(out, fmt.Sprintf("the brief's point %d is empty or more than one line", i+1))
}
}
if err := asks.Glance(b.Explanation()); err != nil {
out = append(out, "the brief "+strings.TrimPrefix(err.Error(), "the explanation "))
}
for _, line := range append([]string{h}, strings.Split(b.Explanation(), "\n")...) {
if !shown(line) {
out = append(out, fmt.Sprintf("the brief's line %q names a path, an address, a dotted name or a long word, which the operator's phone is not shown", line))
}
}
return out
}