mesh/merge-gate pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
A changes link onto a drafted clause was refused, so records that really change such a rule could only cite it (ADR 0294, 0301, 0320); and return renumbered ADR 0281's clauses, pointing other records' links at other clauses. The changed drafted clause keeps its number and stays unserved; return refuses a record whose clauses another record changed or links by number. Issue 494, ADR 0321.
783 lines
25 KiB
Go
783 lines
25 KiB
Go
package main
|
|
|
|
// The answers (novox/hq ADR 0290 rule 6, to-be 55 §2 and §3): ask, get, holds, links and history, each the shape
|
|
// an agent needs. ask, get and holds stay under a bound they state, 4 KiB unless the caller asks for more, and say
|
|
// when they were cut and how to read the rest. A drafted clause is marked as such and never served as in force.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// MostQuestion is the most characters a question to ask may hold.
|
|
const MostQuestion = 500
|
|
|
|
// The bound of an answer.
|
|
const (
|
|
DefaultBound = 4 << 10
|
|
MostBound = 64 << 10
|
|
LeastBound = 1 << 10
|
|
)
|
|
|
|
func boundOf(b int) int {
|
|
switch {
|
|
case b <= 0:
|
|
return DefaultBound
|
|
case b < LeastBound:
|
|
return LeastBound
|
|
case b > MostBound:
|
|
return MostBound
|
|
}
|
|
return b
|
|
}
|
|
|
|
func size(v any) int {
|
|
raw, _ := json.Marshal(v)
|
|
return len(raw)
|
|
}
|
|
|
|
// fit is the largest n (from 0 to total) whose answer stays within the bound.
|
|
func fit(total, bound int, build func(n int) any) (any, int) {
|
|
lo, hi := 0, total
|
|
for lo < hi {
|
|
mid := (lo + hi + 1) / 2
|
|
if size(build(mid)) <= bound {
|
|
lo = mid
|
|
} else {
|
|
hi = mid - 1
|
|
}
|
|
}
|
|
return build(lo), lo
|
|
}
|
|
|
|
func clip(s string, n int) string {
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
if len([]rune(s)) <= n {
|
|
return s
|
|
}
|
|
return string([]rune(s)[:n-1]) + "…"
|
|
}
|
|
|
|
// held says, for a clause, whether it holds and what changed it, in the words an agent reads.
|
|
func held(r *Record, c Clause) string {
|
|
switch {
|
|
case c.Source == SourceDrafted:
|
|
s := "drafted, not confirmed: not the record's word, never in force"
|
|
if c.Unconfirmed != "" {
|
|
s += " (the second judgement did not confirm it: " + c.Unconfirmed + ")"
|
|
}
|
|
if len(c.ChangedBy) > 0 {
|
|
if c.Status == ClauseSuperseded {
|
|
s += "; superseded by " + strings.Join(c.ChangedBy, ", ")
|
|
} else {
|
|
s += "; changed by " + strings.Join(c.ChangedBy, ", ") + ": read both"
|
|
}
|
|
}
|
|
return s
|
|
case r.Status == StatusProposed || c.Status == ClauseProposed:
|
|
return "proposed: not in force"
|
|
case c.Status == ClauseChanged:
|
|
return "changed by " + strings.Join(c.ChangedBy, ", ") + ": read both"
|
|
case c.Status == ClauseSuperseded:
|
|
if len(c.ChangedBy) > 0 {
|
|
return "superseded by " + strings.Join(c.ChangedBy, ", ")
|
|
}
|
|
return "superseded"
|
|
case c.Source == SourceConfirmed:
|
|
return "in force (drafted from prose, confirmed)"
|
|
}
|
|
return "in force"
|
|
}
|
|
|
|
// Served says whether a clause may be served as holding: in force or changed, and not drafted.
|
|
func Served(r *Record, c Clause) bool {
|
|
return r.Accepted() && c.Source != SourceDrafted && (c.Status == ClauseInForce || c.Status == ClauseChanged)
|
|
}
|
|
|
|
// AnswerClause is one clause in an answer.
|
|
type AnswerClause struct {
|
|
N int `json:"n"`
|
|
Text string `json:"text"`
|
|
Holds string `json:"holds"`
|
|
Source string `json:"source,omitempty"`
|
|
}
|
|
|
|
// AnswerRecord is one record in an answer to ask.
|
|
type AnswerRecord struct {
|
|
Record string `json:"record"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
Clauses []AnswerClause `json:"clauses,omitempty"`
|
|
Note string `json:"note,omitempty"`
|
|
Since []string `json:"changed-since,omitempty"`
|
|
Read string `json:"read"`
|
|
}
|
|
|
|
// Asked is the answer to ask.
|
|
type Asked struct {
|
|
Question string `json:"question"`
|
|
Records []AnswerRecord `json:"records"`
|
|
Cut string `json:"cut,omitempty"`
|
|
Bound int `json:"bound"`
|
|
Says string `json:"says,omitempty"`
|
|
}
|
|
|
|
func readHow(k Key) string {
|
|
return fmt.Sprintf("get {\"kind\": %q, \"number\": %d, \"reasoning\": true}", k.Kind, k.Number)
|
|
}
|
|
|
|
// since is what changed a record since it was accepted: the accepted records that supersede or change it.
|
|
func (k *KB) since(ctx context.Context, key Key) []string {
|
|
in, err := k.Store.Inbound(ctx, key)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for _, x := range in {
|
|
if x.Link.Kind != LinkSupersedes && x.Link.Kind != LinkChanges {
|
|
continue
|
|
}
|
|
from, err := k.Store.Get(ctx, x.From)
|
|
if err != nil || from == nil || !from.Current.Accepted() {
|
|
continue
|
|
}
|
|
s := x.From.String()
|
|
if x.Link.From > 0 {
|
|
s += fmt.Sprintf(" §%d", x.Link.From)
|
|
}
|
|
switch {
|
|
case x.Link.Kind == LinkSupersedes:
|
|
s += " supersedes it"
|
|
case x.Link.To.Clause > 0:
|
|
s += fmt.Sprintf(" changes §%d", x.Link.To.Clause)
|
|
default:
|
|
s += " changes it in part; which clause is not judged yet"
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Ask is the records and clauses bearing on a question, ranked, each saying whether it holds or what changed it.
|
|
func (k *KB) Ask(question string, bound int) (*Asked, error) {
|
|
ctx := context.Background()
|
|
question = strings.TrimSpace(question)
|
|
if question == "" {
|
|
return nil, fmt.Errorf("ask needs a question, in plain words")
|
|
}
|
|
if len([]rune(question)) > MostQuestion {
|
|
return nil, fmt.Errorf("a question is at most %d characters; this one is %d: ask it in fewer words", MostQuestion, len([]rune(question)))
|
|
}
|
|
bound = boundOf(bound)
|
|
hits, err := k.Store.Search(ctx, question, 80)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the knowledge base cannot be searched (%v)", err)
|
|
}
|
|
type group struct {
|
|
key Key
|
|
clauses []int
|
|
record bool
|
|
// score is the record's own rank, its best clause's, and a little of each other clause's.
|
|
score, best, rest float64
|
|
}
|
|
var order []*group
|
|
byKey := map[Key]*group{}
|
|
for _, h := range hits {
|
|
g := byKey[h.Key]
|
|
if g == nil {
|
|
g = &group{key: h.Key}
|
|
byKey[h.Key] = g
|
|
order = append(order, g)
|
|
}
|
|
if h.Clause > 0 {
|
|
g.clauses = append(g.clauses, h.Clause)
|
|
if h.Rank > g.best {
|
|
g.rest += g.best
|
|
g.best = h.Rank
|
|
} else {
|
|
g.rest += h.Rank
|
|
}
|
|
} else {
|
|
g.record = true
|
|
g.score += h.Rank
|
|
}
|
|
}
|
|
for _, g := range order {
|
|
g.score += g.best + g.rest/4
|
|
}
|
|
sort.SliceStable(order, func(i, j int) bool { return order[i].score > order[j].score })
|
|
var records []AnswerRecord
|
|
for _, g := range order {
|
|
e, err := k.Store.Get(ctx, g.key)
|
|
if err != nil || e == nil {
|
|
continue
|
|
}
|
|
r := e.Current
|
|
ar := AnswerRecord{Record: g.key.String(), Title: r.Title, Status: r.Status, Read: readHow(g.key), Since: k.since(ctx, g.key)}
|
|
sort.Ints(g.clauses)
|
|
for _, n := range g.clauses {
|
|
if c := r.Clause(n); c != nil {
|
|
ar.Clauses = append(ar.Clauses, AnswerClause{N: n, Text: clip(c.Text, 220), Holds: held(r, *c), Source: c.Source})
|
|
}
|
|
}
|
|
if len(ar.Clauses) == 0 {
|
|
drafted := 0
|
|
for _, c := range r.Clauses {
|
|
if c.Source == SourceDrafted {
|
|
drafted++
|
|
}
|
|
}
|
|
switch {
|
|
case len(r.Clauses) == 0 && r.Kind == KindDecision:
|
|
ar.Note = "its decision is written as prose and its clauses are not drafted yet: read its Decision section"
|
|
case drafted > 0:
|
|
ar.Note = fmt.Sprintf("%d of its clauses are drafted and not confirmed: none of them is served as in force", drafted)
|
|
}
|
|
}
|
|
records = append(records, ar)
|
|
}
|
|
out := &Asked{Question: question, Bound: bound}
|
|
if len(records) == 0 {
|
|
out.Records = []AnswerRecord{}
|
|
out.Says = "nothing the knowledge base holds bears on that; ask in other words"
|
|
return out, nil
|
|
}
|
|
// Within the bound: the most records that fit; when not even one fits whole, its clauses shortened.
|
|
build := func(n int) any {
|
|
a := *out
|
|
a.Records = records[:n]
|
|
if n < len(records) {
|
|
names := []string{}
|
|
for _, r := range records[n:min(len(records), n+6)] {
|
|
names = append(names, r.Record)
|
|
}
|
|
a.Cut = fmt.Sprintf("%d more record(s) bear on it and are not shown within %d bytes (%s…): ask with a larger bound, or narrower words",
|
|
len(records)-n, bound, strings.Join(names, ", "))
|
|
}
|
|
return &a
|
|
}
|
|
v, n := fit(len(records), bound, build)
|
|
if n == 0 {
|
|
first := records[0]
|
|
for i := range first.Clauses {
|
|
first.Clauses[i].Text = clip(first.Clauses[i].Text, 80)
|
|
}
|
|
records[0] = first
|
|
v, _ = fit(len(records), bound, build)
|
|
}
|
|
return v.(*Asked), nil
|
|
}
|
|
|
|
// ---- get -----------------------------------------------------------------------------------------------------
|
|
|
|
// GetAsk is what get takes.
|
|
type GetAsk struct {
|
|
Kind string
|
|
Number int
|
|
Reasoning bool
|
|
Section string
|
|
Version int
|
|
Bound int
|
|
}
|
|
|
|
// Got is the answer to get.
|
|
type Got struct {
|
|
Record string `json:"record"`
|
|
Title string `json:"title"`
|
|
Status string `json:"status"`
|
|
Topic string `json:"topic,omitempty"`
|
|
Date string `json:"date,omitempty"`
|
|
Deciders []string `json:"deciders,omitempty"`
|
|
Version int `json:"version"`
|
|
Versions int `json:"versions"`
|
|
Clauses []AnswerClause `json:"clauses,omitempty"`
|
|
Links []string `json:"links,omitempty"`
|
|
LinkedFrom []string `json:"linked-from,omitempty"`
|
|
Since []string `json:"changed-since,omitempty"`
|
|
Sections []string `json:"sections,omitempty"`
|
|
Reasoning []Section `json:"reasoning,omitempty"`
|
|
Insights []Insight `json:"insights,omitempty"`
|
|
Pending string `json:"pending,omitempty"`
|
|
Waiting string `json:"waiting,omitempty"`
|
|
Reviews []Review `json:"reviews,omitempty"`
|
|
Drafting *Drafting `json:"drafting,omitempty"`
|
|
Review string `json:"review,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Reserved string `json:"reserved,omitempty"`
|
|
NeedsOperat bool `json:"needs-operator,omitempty"`
|
|
Cut string `json:"cut,omitempty"`
|
|
Bound int `json:"bound"`
|
|
Read string `json:"read,omitempty"`
|
|
|
|
// Reconstructed, Amended and Touches are the record's fields of those names; Fields, any other it holds as
|
|
// written (hq issue 487).
|
|
Reconstructed *bool `json:"reconstructed,omitempty"`
|
|
Amended []string `json:"amended,omitempty"`
|
|
Touches []string `json:"touches,omitempty"`
|
|
Fields map[string]string `json:"fields,omitempty"`
|
|
}
|
|
|
|
// Get is one record: its clauses, its links both ways, what changed it since; its reasoning only when asked.
|
|
func (k *KB) Get(a GetAsk) (*Got, error) {
|
|
ctx := context.Background()
|
|
if a.Kind == "" {
|
|
a.Kind = KindDecision
|
|
}
|
|
key := Key{a.Kind, a.Number}
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the knowledge base cannot be read (%v)", err)
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
|
}
|
|
r, version := e.Current, e.CurrentVersion
|
|
if a.Version > 0 {
|
|
vs, err := k.Store.Versions(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if a.Version > len(vs) {
|
|
return nil, fmt.Errorf("%s has %d version(s); there is no version %d", key, len(vs), a.Version)
|
|
}
|
|
r, version = vs[a.Version-1].Record, a.Version
|
|
}
|
|
bound := boundOf(a.Bound)
|
|
g := &Got{Record: key.String(), Title: r.Title, Status: r.Status, Topic: r.Topic, Date: r.Date, Deciders: r.Deciders,
|
|
Version: version, Versions: e.Versions, Insights: r.Insights, Reviews: r.Reviews, Drafting: r.Drafting, Source: r.Source,
|
|
NeedsOperat: r.NeedsOperator, Bound: bound, Since: k.since(ctx, key),
|
|
Reconstructed: r.Reconstructed, Amended: r.Amended, Touches: r.Touches, Fields: r.Extra}
|
|
if r.Status == StatusReserved || r.Status == StatusWithdrawn {
|
|
g.Reserved = fmt.Sprintf("reserved by %s at %s for %q", r.ReservedBy, r.ReservedAt.Format("2006-01-02 15:04 UTC"), r.ReservedFor)
|
|
}
|
|
if e.Pending != nil && a.Version == 0 {
|
|
g.Pending = fmt.Sprintf("version %d, a progressive insight, waits for its review: history shows what it changes", e.PendingVersion)
|
|
}
|
|
if r.Ask != nil {
|
|
g.Waiting = "the operator's ask: " + r.Ask.Said
|
|
}
|
|
if a.Version == 0 {
|
|
g.Review = reviewSays(e.Working())
|
|
}
|
|
for _, c := range r.Clauses {
|
|
g.Clauses = append(g.Clauses, AnswerClause{N: c.N, Text: c.Text, Holds: held(r, c), Source: c.Source})
|
|
}
|
|
for _, l := range r.Links {
|
|
g.Links = append(g.Links, l.String())
|
|
}
|
|
if in, err := k.Store.Inbound(ctx, key); err == nil {
|
|
for _, x := range in {
|
|
s := x.From.String() + " "
|
|
if x.Link.From > 0 {
|
|
s += fmt.Sprintf("§%d ", x.Link.From)
|
|
}
|
|
s += x.Link.Kind
|
|
if x.Link.To.Clause > 0 {
|
|
s += fmt.Sprintf(" §%d", x.Link.To.Clause)
|
|
}
|
|
g.LinkedFrom = append(g.LinkedFrom, s)
|
|
}
|
|
}
|
|
for _, s := range r.Sections {
|
|
g.Sections = append(g.Sections, s.Name)
|
|
if a.Reasoning || (a.Section != "" && strings.EqualFold(a.Section, s.Name)) {
|
|
g.Reasoning = append(g.Reasoning, s)
|
|
}
|
|
}
|
|
if a.Section != "" && len(g.Reasoning) == 0 {
|
|
return nil, fmt.Errorf("%s has no section %q; it has %s", key, a.Section, strings.Join(g.Sections, ", "))
|
|
}
|
|
if !a.Reasoning && a.Section == "" {
|
|
g.Read = "its reasoning: " + readHow(key) + ", or one section with section"
|
|
}
|
|
if size(g) <= bound {
|
|
return g, nil
|
|
}
|
|
// Cut: the reasoning first, section by section from the end, then the reviews but the last, then the links
|
|
// from the end, then the clauses' text, then clauses; each cut is said. Room is kept for saying it.
|
|
room := bound - 600
|
|
var cut []string
|
|
if len(g.Reasoning) > 0 {
|
|
reasoning := g.Reasoning
|
|
v, n := fit(len(reasoning), room, func(n int) any { c := *g; c.Reasoning = reasoning[:n]; return &c })
|
|
*g = *(v.(*Got))
|
|
if n < len(reasoning) {
|
|
left := []string{}
|
|
for _, s := range reasoning[n:] {
|
|
left = append(left, s.Name)
|
|
}
|
|
cut = append(cut, fmt.Sprintf("the sections %s are not shown within %d bytes: ask for one with section, or raise bound", strings.Join(left, ", "), bound))
|
|
if spare := room - size(g); n == 0 && spare > 400 {
|
|
s := reasoning[0]
|
|
s.Text = clip(s.Text, spare/2)
|
|
g.Reasoning = []Section{s}
|
|
cut[len(cut)-1] = fmt.Sprintf("the section %s is shortened, and %s not shown: ask for one with section, or raise bound",
|
|
s.Name, strings.Join(append([]string{}, left[1:]...), ", "))
|
|
}
|
|
}
|
|
}
|
|
if size(g) > room && len(g.Reviews) > 1 {
|
|
cut = append(cut, fmt.Sprintf("%d earlier review(s) are not shown: history lists every version", len(g.Reviews)-1))
|
|
g.Reviews = g.Reviews[len(g.Reviews)-1:]
|
|
}
|
|
for _, which := range []string{"linked-from", "links"} {
|
|
if size(g) <= room {
|
|
break
|
|
}
|
|
list := func(c *Got) *[]string {
|
|
if which == "links" {
|
|
return &c.Links
|
|
}
|
|
return &c.LinkedFrom
|
|
}
|
|
full := *list(g)
|
|
v, n := fit(len(full), room, func(n int) any { c := *g; *list(&c) = full[:n]; return &c })
|
|
if n < len(full) {
|
|
*g = *(v.(*Got))
|
|
cut = append(cut, fmt.Sprintf("%d of its %s are not shown: links lists them all", len(full)-n, which))
|
|
}
|
|
}
|
|
if size(g) > room {
|
|
for i := range g.Clauses {
|
|
g.Clauses[i].Text = clip(g.Clauses[i].Text, 160)
|
|
}
|
|
cut = append(cut, "the clauses are shortened: raise bound to read them whole")
|
|
}
|
|
if size(g) > room {
|
|
clauses := g.Clauses
|
|
v, n := fit(len(clauses), room, func(n int) any { c := *g; c.Clauses = clauses[:n]; return &c })
|
|
*g = *(v.(*Got))
|
|
cut = append(cut, fmt.Sprintf("%d clause(s) are not shown: raise bound", len(clauses)-n))
|
|
}
|
|
g.Cut = strings.Join(cut, "; ")
|
|
return g, nil
|
|
}
|
|
|
|
// ---- holds ---------------------------------------------------------------------------------------------------
|
|
|
|
// HeldClause is one clause in force, with the record it comes from.
|
|
type HeldClause struct {
|
|
Record string `json:"record"`
|
|
N int `json:"n"`
|
|
Text string `json:"text"`
|
|
Holds string `json:"holds"`
|
|
}
|
|
|
|
// Holding is the answer to holds.
|
|
type Holding struct {
|
|
Of string `json:"of"`
|
|
Clauses []HeldClause `json:"clauses"`
|
|
NotYet string `json:"not-served,omitempty"`
|
|
Cut string `json:"cut,omitempty"`
|
|
Bound int `json:"bound"`
|
|
}
|
|
|
|
// Holds is the clauses in force for a topic or a record: never a drafted one, and a confirmed one marked.
|
|
func (k *KB) Holds(topic string, key *Key, bound int) (*Holding, error) {
|
|
ctx := context.Background()
|
|
bound = boundOf(bound)
|
|
var records []*Record
|
|
of := ""
|
|
switch {
|
|
case key != nil:
|
|
e, err := k.Store.Get(ctx, *key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
|
}
|
|
records, of = []*Record{e.Current}, key.String()
|
|
case strings.TrimSpace(topic) != "":
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, e := range all {
|
|
if e.Current.Topic == topic {
|
|
records = append(records, e.Current)
|
|
}
|
|
}
|
|
of = "the topic " + topic
|
|
if len(records) == 0 {
|
|
return nil, fmt.Errorf("no record has the topic %q", topic)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("holds takes a topic or a record's number")
|
|
}
|
|
out := &Holding{Of: of, Bound: bound, Clauses: []HeldClause{}}
|
|
drafted, prose := 0, 0
|
|
for _, r := range records {
|
|
if r.Kind == KindDecision && r.Accepted() && len(r.Clauses) == 0 {
|
|
prose++
|
|
}
|
|
for _, c := range r.Clauses {
|
|
if c.Source == SourceDrafted && r.Accepted() && c.Status != ClauseSuperseded {
|
|
drafted++
|
|
}
|
|
if Served(r, c) {
|
|
out.Clauses = append(out.Clauses, HeldClause{Record: r.Key().String(), N: c.N, Text: c.Text, Holds: held(r, c)})
|
|
}
|
|
}
|
|
}
|
|
var notes []string
|
|
if drafted > 0 {
|
|
notes = append(notes, fmt.Sprintf("%d drafted clause(s) are not served: they are not confirmed as the record's word", drafted))
|
|
}
|
|
if prose > 0 {
|
|
notes = append(notes, fmt.Sprintf("%d accepted decision(s) write their decision as prose and have no clauses yet", prose))
|
|
}
|
|
out.NotYet = strings.Join(notes, "; ")
|
|
clauses := out.Clauses
|
|
v, n := fit(len(clauses), bound, func(n int) any {
|
|
c := *out
|
|
c.Clauses = clauses[:n]
|
|
if n < len(clauses) {
|
|
c.Cut = fmt.Sprintf("%d more clause(s) are in force and not shown within %d bytes: raise bound, or ask holds of one record", len(clauses)-n, bound)
|
|
}
|
|
return &c
|
|
})
|
|
if n == 0 && len(clauses) > 0 {
|
|
for i := range clauses {
|
|
clauses[i].Text = clip(clauses[i].Text, 160)
|
|
}
|
|
v, _ = fit(len(clauses), bound, func(n int) any {
|
|
c := *out
|
|
c.Clauses = clauses[:n]
|
|
c.Cut = fmt.Sprintf("the clauses are shortened, and %d not shown within %d bytes: raise bound", len(clauses)-n, bound)
|
|
return &c
|
|
})
|
|
}
|
|
return v.(*Holding), nil
|
|
}
|
|
|
|
// ---- links ---------------------------------------------------------------------------------------------------
|
|
|
|
// Linked is the answer to links.
|
|
type Linked struct {
|
|
Record string `json:"record"`
|
|
Out []LinkRow `json:"out"`
|
|
In []LinkRow `json:"in"`
|
|
}
|
|
|
|
// LinkRow is one link, read from either end.
|
|
type LinkRow struct {
|
|
Kind string `json:"kind"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Source string `json:"source,omitempty"`
|
|
Unresolved bool `json:"unresolved,omitempty"`
|
|
}
|
|
|
|
// Links is a record's links, both ways.
|
|
func (k *KB) Links(key Key) (*Linked, error) {
|
|
ctx := context.Background()
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
|
}
|
|
out := &Linked{Record: key.String(), Out: []LinkRow{}, In: []LinkRow{}}
|
|
from := func(k Key, clause int) string {
|
|
if clause > 0 {
|
|
return k.String() + " §" + strconv.Itoa(clause)
|
|
}
|
|
return k.String()
|
|
}
|
|
for _, l := range e.Current.Links {
|
|
out.Out = append(out.Out, LinkRow{Kind: l.Kind, From: from(key, l.From), To: l.To.String(), Source: l.Source, Unresolved: l.Unresolved})
|
|
}
|
|
in, err := k.Store.Inbound(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, x := range in {
|
|
out.In = append(out.In, LinkRow{Kind: x.Link.Kind, From: from(x.From, x.Link.From), To: x.Link.To.String(), Source: x.Link.Source})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ---- history -------------------------------------------------------------------------------------------------
|
|
|
|
// VersionRow is one version in a history.
|
|
type VersionRow struct {
|
|
N int `json:"n"`
|
|
By string `json:"by"`
|
|
At string `json:"at"`
|
|
Why string `json:"why"`
|
|
Status string `json:"status"`
|
|
Insight bool `json:"insight,omitempty"`
|
|
Served bool `json:"served,omitempty"`
|
|
Pending bool `json:"pending,omitempty"`
|
|
}
|
|
|
|
// Historied is the answer to history.
|
|
type Historied struct {
|
|
Record string `json:"record"`
|
|
Versions []VersionRow `json:"versions"`
|
|
From int `json:"from,omitempty"`
|
|
To int `json:"to,omitempty"`
|
|
Diff []string `json:"diff,omitempty"`
|
|
Same bool `json:"same,omitempty"`
|
|
Cut string `json:"cut,omitempty"`
|
|
Bound int `json:"bound,omitempty"`
|
|
}
|
|
|
|
// History is a record's versions, and the difference between two of them, line by line.
|
|
func (k *KB) History(key Key, from, to, bound int) (*Historied, error) {
|
|
ctx := context.Background()
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
|
}
|
|
vs, err := k.Store.Versions(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := &Historied{Record: key.String()}
|
|
for _, v := range vs {
|
|
out.Versions = append(out.Versions, VersionRow{N: v.N, By: v.By, At: v.At.UTC().Format("2006-01-02 15:04:05 UTC"), Why: v.Why,
|
|
Status: v.Record.Status, Insight: v.Insight, Served: v.N == e.CurrentVersion, Pending: v.N == e.PendingVersion})
|
|
}
|
|
if from == 0 && to == 0 {
|
|
out.Bound = boundOf(bound)
|
|
versions := out.Versions
|
|
v, m := fit(len(versions), out.Bound-300, func(m int) any { c := *out; c.Versions = versions[len(versions)-m:]; return &c })
|
|
*out = *(v.(*Historied))
|
|
if m < len(versions) {
|
|
out.Cut = fmt.Sprintf("the %d oldest versions are not listed within %d bytes: raise bound", len(versions)-m, out.Bound)
|
|
}
|
|
return out, nil
|
|
}
|
|
if from < 1 || to < 1 || from > len(vs) || to > len(vs) {
|
|
return nil, fmt.Errorf("%s has versions 1 to %d; from and to name two of them", key, len(vs))
|
|
}
|
|
out.From, out.To, out.Bound = from, to, boundOf(bound)
|
|
diff := Diff(Render(vs[from-1].Record), Render(vs[to-1].Record))
|
|
out.Same = len(diff) == 0
|
|
// Within the bound: the most of the difference's lines that fit, cut at a hunk's line and said; and when the
|
|
// versions alone do not fit, the oldest of them left out first.
|
|
versions := out.Versions
|
|
v, n := fit(len(diff), out.Bound-300, func(n int) any { c := *out; c.Diff = diff[:n]; return &c })
|
|
*out = *(v.(*Historied))
|
|
var cut []string
|
|
if n < len(diff) {
|
|
cut = append(cut, fmt.Sprintf("%d of the difference's %d lines are not shown within %d bytes: raise bound, or compare nearer versions",
|
|
len(diff)-n, len(diff), out.Bound))
|
|
}
|
|
if size(out) > out.Bound-300 {
|
|
v, m := fit(len(versions), out.Bound-300, func(m int) any { c := *out; c.Versions = versions[len(versions)-m:]; return &c })
|
|
*out = *(v.(*Historied))
|
|
cut = append(cut, fmt.Sprintf("the %d oldest versions are not listed", len(versions)-m))
|
|
}
|
|
out.Cut = strings.Join(cut, "; ")
|
|
return out, nil
|
|
}
|
|
|
|
// MostDiffWork bounds the work of a difference: the lines of the two texts' changed middles multiplied. Past it,
|
|
// the middle is given whole as removed and added — still exact, only less narrow.
|
|
const MostDiffWork = 4_000_000
|
|
|
|
// Diff is the difference of two texts, line by line: each hunk headed "@@ -a,n +b,m @@", its lines "-" removed
|
|
// and "+" added, by the longest common subsequence of their lines.
|
|
func Diff(a, b string) []string {
|
|
x, y := strings.Split(a, "\n"), strings.Split(b, "\n")
|
|
// The common head and tail cost nothing to find; only the middle between them is compared.
|
|
head := 0
|
|
for head < len(x) && head < len(y) && x[head] == y[head] {
|
|
head++
|
|
}
|
|
tail := 0
|
|
for tail < len(x)-head && tail < len(y)-head && x[len(x)-1-tail] == y[len(y)-1-tail] {
|
|
tail++
|
|
}
|
|
x, y = x[head:len(x)-tail], y[head:len(y)-tail]
|
|
n, m := len(x), len(y)
|
|
if n*m > MostDiffWork {
|
|
var out []string
|
|
if n+m > 0 {
|
|
out = append(out, fmt.Sprintf("@@ -%d,%d +%d,%d @@", head+1, n, head+1, m))
|
|
}
|
|
for _, l := range x {
|
|
out = append(out, "-"+l)
|
|
}
|
|
for _, l := range y {
|
|
out = append(out, "+"+l)
|
|
}
|
|
return out
|
|
}
|
|
lcs := make([][]int, n+1)
|
|
for i := range lcs {
|
|
lcs[i] = make([]int, m+1)
|
|
}
|
|
for i := n - 1; i >= 0; i-- {
|
|
for j := m - 1; j >= 0; j-- {
|
|
if x[i] == y[j] {
|
|
lcs[i][j] = lcs[i+1][j+1] + 1
|
|
} else {
|
|
lcs[i][j] = max(lcs[i+1][j], lcs[i][j+1])
|
|
}
|
|
}
|
|
}
|
|
var out []string
|
|
var hunk []string
|
|
ha, hb, ca, cb := 0, 0, 0, 0
|
|
flush := func() {
|
|
if len(hunk) > 0 {
|
|
out = append(out, fmt.Sprintf("@@ -%d,%d +%d,%d @@", ha, ca, hb, cb))
|
|
out = append(out, hunk...)
|
|
hunk = nil
|
|
}
|
|
}
|
|
i, j := 0, 0
|
|
for i < n || j < m {
|
|
switch {
|
|
case i < n && j < m && x[i] == y[j]:
|
|
flush()
|
|
i++
|
|
j++
|
|
case i < n && (j == m || lcs[i+1][j] >= lcs[i][j+1]):
|
|
if len(hunk) == 0 {
|
|
ha, hb, ca, cb = head+i+1, head+j+1, 0, 0
|
|
}
|
|
hunk = append(hunk, "-"+x[i])
|
|
ca++
|
|
i++
|
|
default:
|
|
if len(hunk) == 0 {
|
|
ha, hb, ca, cb = head+i+1, head+j+1, 0, 0
|
|
}
|
|
hunk = append(hunk, "+"+y[j])
|
|
cb++
|
|
j++
|
|
}
|
|
}
|
|
flush()
|
|
return out
|
|
}
|
|
|
|
// reviewSays is where a proposed version stands in the knowledge base's own review, in words; "" when nothing waits.
|
|
func reviewSays(r *Record) string {
|
|
j := r.Judging
|
|
if j == nil || r.Status != StatusProposed {
|
|
return ""
|
|
}
|
|
switch j.State {
|
|
case JudgingDue:
|
|
return "waiting for the knowledge base's review"
|
|
case JudgingUnproven:
|
|
return "not reviewed: " + j.Reason
|
|
}
|
|
// When it is asked again is said, so a pause is not taken for a review lost (novox/hq issue 497).
|
|
return fmt.Sprintf("not reviewed: %s (%s; no judgement that could not be had counts as an approval", j.Reason, j.owedSays())
|
|
}
|