mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
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
hq's words check never read a decision record, so a correction to any decision of October 7 to 10 was refused for words it was allowed to write. The links an accepted record holds are kept by its correction without being judged again.
578 lines
22 KiB
Go
578 lines
22 KiB
Go
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
|
|
// 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,
|
|
// 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
|
|
// process every few minutes: a failure is kept as a finding, which the module's health answers, so the mesh
|
|
// raises a condition naming the rule and the records. It does not undo the acceptance.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Rule is one rule that judges one record: its id and what it holds.
|
|
type Rule struct {
|
|
ID string
|
|
Holds string
|
|
// From is the hq check it comes from.
|
|
From string
|
|
}
|
|
|
|
// PerRecordRules are the 29, 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)"},
|
|
{"title", "it has a title, one line of at most 200 characters", "records.py numbering"},
|
|
{"topic", "a decision's topic is one of the reading order's", "records.py topics, index.py"},
|
|
{"date", "its date is a day, YYYY-MM-DD", "records.py insights, cycle.py deferrals"},
|
|
{"deciders", "a decision names who decided it", "records.py (deciders on every record)"},
|
|
{"research-status", "a research effort is active, graduated or abandoned", "cycle.py research"},
|
|
{"has-clause", "a decision has at least one clause", "records.py clauses"},
|
|
{"clause-numbering", "its clauses are numbered from 1, in order, each once", "records.py clauses"},
|
|
{"clause-lead", "every clause opens with a bold lead", "records.py clauses"},
|
|
{"clause-size", "every clause says something, in at most 2000 bytes", "records.py clauses"},
|
|
{"context", "a decision has a Context section", "the decision record's form"},
|
|
{"consequences", "a decision has a Consequences section", "the decision record's form"},
|
|
{"checked", "a decision says how it is checked", "AGENTS.md: a rule states how it is checked"},
|
|
{"overview", "a research effort has its overview", "cycle.py research"},
|
|
{"link-kind", "every link is of a known kind and goes out from the record or one of its clauses", "records.py links"},
|
|
{"link-target", "every link names a record, and a clause, that exist", "records.py links, rests-on"},
|
|
{"link-fits", "a link's kind fits its ends", "cycle.py research became, records.py supersession"},
|
|
{"self-link", "no link points at the record itself", "records.py supersession"},
|
|
{"rests-on", "a record it extends is accepted or proposed, never superseded or withdrawn", "records.py rests-on"},
|
|
{"supersedes-accepted", "a record it supersedes or changes is accepted", "records.py supersession, partial-supersession"},
|
|
{"changes-in-force", "a clause it changes is in force or changed, never superseded, proposed or drafted", "records.py partial-supersession"},
|
|
{"live-citation", "a superseded record it cites is cited beside the record that replaced it", "records.py live-citation"},
|
|
{"became", "a graduated research effort names what it became", "cycle.py research"},
|
|
{"text-links", "every link its text writes to a decision record or research effort names one the knowledge base holds", "records.py links"},
|
|
{"retired-word", "its prose uses no word the glossary retired", "words.py retired"},
|
|
{"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"},
|
|
}
|
|
|
|
// Refusal is one rule a record breaks, and how.
|
|
type Refusal struct {
|
|
Rule string `json:"rule"`
|
|
Says string `json:"says"`
|
|
}
|
|
|
|
func (r Refusal) String() string { return r.Rule + ": " + r.Says }
|
|
|
|
// Lookup is the served version of a record the knowledge base holds, or nil.
|
|
type Lookup func(Key) *Record
|
|
|
|
// MostClauseBytes is the most one clause may hold.
|
|
const MostClauseBytes = 2000
|
|
|
|
var (
|
|
isDay = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
|
deferral = regexp.MustCompile(`(?i)until (?:it is |they are |the code (?:is )?)?renamed|until the code rename|` +
|
|
`\ba later rename|not renamed here|identifier to rename|left (?:for|to) (?:a )?later|not decided here|` +
|
|
`\bpostponed\b|future work|\bdeferred\b|later work|not built here|\ba later change`)
|
|
issueRef = regexp.MustCompile(`(?i)tracked (?:by|in|as)\s+\[?issue (\d{3,5})|\[?issue (\d{3,5})\]?(?:\([^)]*\))?\s+tracks\b`)
|
|
insightPhrase = regexp.MustCompile(`(?i)progressive insights?`)
|
|
insightMarker = regexp.MustCompile(`\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2}-\d{2})[^*\n]*\*\*`)
|
|
)
|
|
|
|
// decisionSection is the text of a record's Decision section.
|
|
func decisionSection(r *Record) (string, bool) {
|
|
for _, s := range r.Sections {
|
|
if s.Name == "Decision" || strings.HasPrefix(s.Name, "Decision ") || strings.HasPrefix(s.Name, "Decision:") {
|
|
return s.Text, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func hasSection(r *Record, names ...string) bool {
|
|
for _, s := range r.Sections {
|
|
for _, n := range names {
|
|
if strings.EqualFold(strings.TrimSpace(s.Name), n) && strings.TrimSpace(s.Text) != "" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// allText is every word of a record an agent reads: its title, clauses and sections.
|
|
func allText(r *Record) string {
|
|
var b strings.Builder
|
|
b.WriteString(r.Title + "\n\n")
|
|
for _, c := range r.Clauses {
|
|
b.WriteString(c.Text + "\n\n")
|
|
}
|
|
for _, s := range r.Sections {
|
|
b.WriteString(s.Text + "\n\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// JudgeRecord holds a proposed record to the 29 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
|
|
refuse := func(rule, format string, a ...any) {
|
|
if Holds(rule, r.Kind, r.Date) {
|
|
out = append(out, Refusal{Rule: rule, Says: fmt.Sprintf(format, a...)})
|
|
}
|
|
}
|
|
decision := r.Kind == KindDecision
|
|
if r.Kind != KindDecision && r.Kind != KindResearch {
|
|
refuse("kind", "%q is neither decision nor research", r.Kind)
|
|
}
|
|
switch {
|
|
case held == nil:
|
|
refuse("reserved", "%s was never reserved: reserve gives the number", r.Key())
|
|
case held.Current.Status == StatusWithdrawn:
|
|
refuse("reserved", "%s was reserved and withdrawn: reserve a new number", r.Key())
|
|
}
|
|
title := strings.TrimSpace(r.Title)
|
|
if title == "" || strings.Contains(title, "\n") || len([]rune(title)) > 200 {
|
|
refuse("title", "the title is empty, more than one line, or longer than 200 characters")
|
|
}
|
|
if decision && !contains(ref.Topics, r.Topic) {
|
|
refuse("topic", "%q is not a topic of the reading order: %s", r.Topic, strings.Join(ref.Topics, ", "))
|
|
}
|
|
if !isDay.MatchString(r.Date) {
|
|
refuse("date", "%q is not a day written YYYY-MM-DD", r.Date)
|
|
}
|
|
if decision && len(r.Deciders) == 0 {
|
|
refuse("deciders", "a decision names who decided it")
|
|
}
|
|
if !decision && !contains(ResearchStatuses, r.Effort) {
|
|
refuse("research-status", "%q is not active, graduated or abandoned", r.Effort)
|
|
}
|
|
if decision && len(r.Clauses) == 0 {
|
|
refuse("has-clause", "a decision is made of clauses, and this one has none")
|
|
}
|
|
for i, c := range r.Clauses {
|
|
if c.N != i+1 {
|
|
refuse("clause-numbering", "clause %d stands %d. in order: clauses are numbered 1, 2, 3 … each once", c.N, i+1)
|
|
}
|
|
if c.Lead() == "" {
|
|
refuse("clause-lead", "clause %d does not open with a bold lead (**What is decided.** …)", c.N)
|
|
}
|
|
if strings.TrimSpace(c.Text) == "" || len(c.Text) > MostClauseBytes {
|
|
refuse("clause-size", "clause %d is empty or longer than %d bytes", c.N, MostClauseBytes)
|
|
}
|
|
}
|
|
if decision && !hasSection(r, "Context") {
|
|
refuse("context", "a decision has a Context section: why it was decided")
|
|
}
|
|
if decision && !hasSection(r, "Consequences") {
|
|
refuse("consequences", "a decision has a Consequences section: what follows from it, and what got harder")
|
|
}
|
|
if decision && !hasSection(r, "How it is checked") {
|
|
refuse("checked", "a decision has a section \"How it is checked\": a rule says how it is verified")
|
|
}
|
|
if !decision && !hasSection(r, "00-overview.md", "overview") {
|
|
refuse("overview", "a research effort has its overview, a section named 00-overview.md")
|
|
}
|
|
self := r.Key()
|
|
superseders := map[Key]bool{}
|
|
for _, l := range r.Links {
|
|
if to, ok := l.To.Record(); ok && l.Kind == LinkSupersedes {
|
|
superseders[to] = true
|
|
}
|
|
}
|
|
cited := map[Key]bool{}
|
|
for _, l := range r.Links {
|
|
if to, ok := l.To.Record(); ok {
|
|
cited[to] = true
|
|
}
|
|
}
|
|
for _, l := range r.Links {
|
|
if !contains(LinkKinds, l.Kind) {
|
|
refuse("link-kind", "%q is not a kind of link: %s", l.Kind, strings.Join(LinkKinds, ", "))
|
|
continue
|
|
}
|
|
if l.From > 0 && r.Clause(l.From) == nil {
|
|
refuse("link-kind", "the link %s goes out from clause %d, which the record does not have", l, l.From)
|
|
}
|
|
to, isRecord := l.To.Record()
|
|
if isRecord && to == self {
|
|
refuse("self-link", "the link %s points at the record itself", l)
|
|
continue
|
|
}
|
|
var target *Record
|
|
if isRecord {
|
|
target = lookup(to)
|
|
if target == nil || target.Status == StatusReserved || target.Status == StatusWithdrawn {
|
|
refuse("link-target", "the link %s names %s, which the knowledge base does not hold", l, to)
|
|
continue
|
|
}
|
|
if l.To.Clause > 0 && target.Clause(l.To.Clause) == nil {
|
|
refuse("link-target", "the link %s names clause %d, which %s does not have", l, l.To.Clause, to)
|
|
continue
|
|
}
|
|
}
|
|
kept := held != nil && held.Current.Accepted() && hasLink(held.Current, l)
|
|
switch {
|
|
case kept:
|
|
// An insight keeps what the accepted record linked, as hq wrote it: a change hq named by record waits
|
|
// for its clause to be judged.
|
|
case l.Kind == LinkBecame:
|
|
if r.Kind != KindResearch || !(l.To.Kind == KindDecision || l.To.Kind == TargetDesign) || l.To.Clause > 0 {
|
|
refuse("link-fits", "became goes from a research effort to a decision record or a design: not %s", l)
|
|
}
|
|
case l.Kind == LinkChanges:
|
|
if !decision || l.To.Kind != KindDecision || l.To.Clause == 0 {
|
|
refuse("link-fits", "changes goes from a decision (or a clause of it) to a clause of another decision, named as 0276 §4: not %s", l)
|
|
}
|
|
case l.Kind == LinkSupersedes || l.Kind == LinkExtends:
|
|
if r.Kind != l.To.Kind || !isRecord || l.To.Clause > 0 {
|
|
refuse("link-fits", "%s goes from a record to a record of its kind: not %s", l.Kind, l)
|
|
}
|
|
}
|
|
if target == nil {
|
|
continue
|
|
}
|
|
if held != nil && held.Current.Accepted() && hasLink(held.Current, l) {
|
|
// An insight keeps what the accepted record linked: judged when it was accepted.
|
|
continue
|
|
}
|
|
switch l.Kind {
|
|
case LinkExtends:
|
|
if target.Status == StatusSuperseded && !superseders[to] {
|
|
refuse("rests-on", "%s extends %s, which is superseded", self, to)
|
|
}
|
|
case LinkSupersedes:
|
|
if !target.Accepted() || target.Status == StatusSuperseded {
|
|
refuse("supersedes-accepted", "%s supersedes %s, which is %s, not accepted", self, to, target.Status)
|
|
}
|
|
case LinkChanges:
|
|
if !target.Accepted() || target.Status == StatusSuperseded {
|
|
refuse("supersedes-accepted", "%s changes %s, which is %s, not accepted", self, to, target.Status)
|
|
} else if c := target.Clause(l.To.Clause); c != nil {
|
|
if (c.Status != ClauseInForce && c.Status != ClauseChanged) || c.Source == SourceDrafted {
|
|
refuse("changes-in-force", "%s changes %s §%d, which is %s (%s)", self, to, l.To.Clause, c.Status, c.Source)
|
|
}
|
|
}
|
|
case LinkCites:
|
|
if target.Status == StatusSuperseded {
|
|
named := false
|
|
for _, by := range supersedersOf(target) {
|
|
named = named || cited[by]
|
|
}
|
|
if !named {
|
|
refuse("live-citation", "%s cites %s, which is superseded, and not the record that replaced it", self, to)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if r.Kind == KindResearch && r.Effort == StatusGraduated {
|
|
became := false
|
|
for _, l := range r.Links {
|
|
became = became || l.Kind == LinkBecame
|
|
}
|
|
if !became {
|
|
refuse("became", "a graduated research effort names what it became, as a became link")
|
|
}
|
|
}
|
|
for _, s := range r.Sections {
|
|
for _, m := range mdLink.FindAllStringSubmatch(s.Text, -1) {
|
|
target := m[1]
|
|
var k Key
|
|
switch {
|
|
case researchFolder.MatchString(target):
|
|
n := atoi(researchFolder.FindStringSubmatch(target)[1])
|
|
k = Key{KindResearch, n}
|
|
case adrFile.MatchString(target) && !strings.Contains(target, "04-ISSUES") && !strings.Contains(target, "03-DESIGN"):
|
|
k = Key{KindDecision, atoi(adrFile.FindStringSubmatch(target)[1])}
|
|
default:
|
|
continue
|
|
}
|
|
if k == self {
|
|
continue
|
|
}
|
|
if t := lookup(k); t == nil || t.Status == StatusReserved || t.Status == StatusWithdrawn {
|
|
refuse("text-links", "the section %s links %s, which the knowledge base does not hold", s.Name, k)
|
|
}
|
|
}
|
|
}
|
|
text := allText(r)
|
|
if words := ref.RetiredIn(text); len(words) > 0 {
|
|
refuse("retired-word", "it uses %s, retired by the glossary", quoteAll(unique(words)))
|
|
}
|
|
if lines := ref.LinesIn(text); len(lines) > 0 {
|
|
refuse("daemon-line", "it writes %s, a daemon's line: the operator types nox", quoteAll(unique(lines)))
|
|
}
|
|
for _, s := range r.Sections {
|
|
if strings.Contains(strings.ToLower(s.Name), "option") || strings.Contains(strings.ToLower(s.Name), "rejected") {
|
|
continue
|
|
}
|
|
for _, p := range paragraphsOf(s.Text) {
|
|
if m := deferral.FindString(p); m != "" && !issueRef.MatchString(p) {
|
|
refuse("deferral", "the section %s puts work off (%q) and names no issue that tracks it (\"tracked by issue N\")", s.Name, m)
|
|
}
|
|
}
|
|
}
|
|
for _, c := range r.Clauses {
|
|
if m := deferral.FindString(c.Text); m != "" && !issueRef.MatchString(c.Text) {
|
|
refuse("deferral", "clause %d puts work off (%q) and names no issue that tracks it (\"tracked by issue N\")", c.N, m)
|
|
}
|
|
}
|
|
marked := insightMarker.FindAllStringSubmatchIndex(text, -1)
|
|
for _, loc := range insightPhrase.FindAllStringIndex(text, -1) {
|
|
inside := false
|
|
for _, mk := range marked {
|
|
inside = inside || (mk[0] <= loc[0] && loc[1] <= mk[1])
|
|
}
|
|
line := lineAround(text, loc[0])
|
|
if inside || strings.Contains(line, "](") || strings.HasPrefix(strings.TrimSpace(line), "#") {
|
|
continue
|
|
}
|
|
refuse("insight-form", "it mentions a progressive insight outside the dated marked form **Progressive insight — YYYY-MM-DD.**")
|
|
break
|
|
}
|
|
for _, mk := range insightMarker.FindAllStringSubmatch(text, -1) {
|
|
if isDay.MatchString(r.Date) && mk[1] < r.Date {
|
|
refuse("insight-form", "a progressive insight dated %s predates the record (%s)", mk[1], r.Date)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hasLink(r *Record, l Link) bool {
|
|
for _, x := range r.Links {
|
|
if x.Kind == l.Kind && x.From == l.From && x.To == l.To {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func lineAround(text string, at int) string {
|
|
start := strings.LastIndex(text[:at], "\n") + 1
|
|
end := strings.Index(text[at:], "\n")
|
|
if end < 0 {
|
|
return text[start:]
|
|
}
|
|
return text[start : at+end]
|
|
}
|
|
|
|
// paragraphsOf cuts a text at blank lines and a list's items.
|
|
func paragraphsOf(text string) []string {
|
|
var out []string
|
|
for _, chunk := range strings.Split(text, "\n\n") {
|
|
for _, item := range regexp.MustCompile(`\n(?:[-*] |\d+\. )`).Split(chunk, -1) {
|
|
if strings.TrimSpace(item) != "" {
|
|
out = append(out, item)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// supersedersOf are the records a record says superseded it: its superseded-by, a file name as hq wrote it or
|
|
// "ADR NNNN" as an acceptance here writes it.
|
|
func supersedersOf(r *Record) []Key {
|
|
var out []Key
|
|
for _, by := range strings.Split(r.Extra["superseded-by"], ",") {
|
|
by = strings.TrimSpace(by)
|
|
if m := adrFile.FindStringSubmatch(by); m != nil {
|
|
out = append(out, Key{KindDecision, atoi(m[1])})
|
|
} else if ref, err := ParseRef(by); err == nil {
|
|
if k, ok := ref.Record(); ok {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func atoi(s string) int {
|
|
n := 0
|
|
for _, c := range s {
|
|
n = n*10 + int(c-'0')
|
|
}
|
|
return n
|
|
}
|
|
|
|
func contains(list []string, s string) bool {
|
|
for _, x := range list {
|
|
if x == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func unique(list []string) []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, s := range list {
|
|
if !seen[strings.ToLower(s)] {
|
|
seen[strings.ToLower(s)] = true
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func quoteAll(list []string) string {
|
|
q := make([]string, len(list))
|
|
for i, s := range list {
|
|
q[i] = fmt.Sprintf("%q", s)
|
|
}
|
|
return strings.Join(q, ", ")
|
|
}
|
|
|
|
// ---- the whole record set ----------------------------------------------------------------------------------
|
|
|
|
// WholeSetRules are the 4 rules that judge the whole record set.
|
|
var WholeSetRules = []Rule{
|
|
{"cited", "a decision accepted here is cited by another record or names a home in hq", "cycle.py decisions"},
|
|
{"supersession", "a superseded record is named by the record that superseded it, and every record an accepted record supersedes is superseded", "records.py supersession"},
|
|
{"partial", "a changed clause is changed by an accepted record's changes link, and every clause an accepted record changes is changed or superseded", "records.py partial-supersession"},
|
|
{"rests-on-live", "no accepted record extends a superseded one, but the record that superseded it", "records.py rests-on"},
|
|
}
|
|
|
|
// JudgeSet holds the served versions of every record to the 4 whole-set rules. An imported record was held to
|
|
// hq's own checks at the commit it was read at, so the first rule judges only a record accepted here.
|
|
func JudgeSet(records []*Record) []Finding {
|
|
byKey := map[Key]*Record{}
|
|
for _, r := range records {
|
|
byKey[r.Key()] = r
|
|
}
|
|
inbound := map[Key][]struct {
|
|
from *Record
|
|
l Link
|
|
}{}
|
|
for _, r := range records {
|
|
if !r.Accepted() && r.Status != StatusSuperseded {
|
|
continue
|
|
}
|
|
for _, l := range r.Links {
|
|
if to, ok := l.To.Record(); ok {
|
|
inbound[to] = append(inbound[to], struct {
|
|
from *Record
|
|
l Link
|
|
}{r, l})
|
|
}
|
|
}
|
|
}
|
|
var out []Finding
|
|
add := func(rule, says string, keys ...Key) {
|
|
names := make([]string, len(keys))
|
|
for i, k := range keys {
|
|
names[i] = k.String()
|
|
}
|
|
out = append(out, Finding{Rule: rule, Records: names, Says: says})
|
|
}
|
|
for _, r := range records {
|
|
k := r.Key()
|
|
if r.Kind == KindDecision && r.Status == StatusAccepted && !r.AcceptedAt.IsZero() && len(r.Homes) == 0 {
|
|
cited := false
|
|
for _, in := range inbound[k] {
|
|
cited = cited || in.from.Key() != k
|
|
}
|
|
if !cited {
|
|
add("cited", fmt.Sprintf("%s is accepted and nothing cites it: no record links it and it names no home", k), k)
|
|
}
|
|
}
|
|
if r.Status == StatusSuperseded {
|
|
by := false
|
|
for _, in := range inbound[k] {
|
|
// The record that superseded it may since have been superseded itself, as hq's 0125 by 0127 by 0131.
|
|
by = by || in.l.Kind == LinkSupersedes
|
|
}
|
|
if !by {
|
|
add("supersession", fmt.Sprintf("%s is superseded and no record says it supersedes it", k), k)
|
|
}
|
|
}
|
|
if r.Accepted() {
|
|
for _, l := range r.Links {
|
|
to, ok := l.To.Record()
|
|
target := byKey[to]
|
|
if !ok || target == nil {
|
|
continue
|
|
}
|
|
switch l.Kind {
|
|
case LinkSupersedes:
|
|
if target.Status != StatusSuperseded {
|
|
add("supersession", fmt.Sprintf("%s supersedes %s, which is %s", k, to, target.Status), k, to)
|
|
}
|
|
case LinkChanges:
|
|
if l.To.Clause == 0 {
|
|
continue
|
|
}
|
|
if c := target.Clause(l.To.Clause); c != nil && c.Status != ClauseChanged && c.Status != ClauseSuperseded {
|
|
add("partial", fmt.Sprintf("%s changes %s §%d, which is %s", k, to, l.To.Clause, c.Status), k, to)
|
|
}
|
|
case LinkExtends:
|
|
if target.Status == StatusSuperseded {
|
|
superseder := false
|
|
for _, in := range inbound[to] {
|
|
superseder = superseder || (in.l.Kind == LinkSupersedes && in.from.Key() == k)
|
|
}
|
|
if !superseder {
|
|
add("rests-on-live", fmt.Sprintf("%s extends %s, which is superseded", k, to), k, to)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, c := range r.Clauses {
|
|
if c.Status != ClauseChanged {
|
|
continue
|
|
}
|
|
by := false
|
|
for _, in := range inbound[k] {
|
|
by = by || (in.l.Kind == LinkChanges && in.l.To.Clause == c.N && in.from.Accepted())
|
|
}
|
|
if !by {
|
|
add("partial", fmt.Sprintf("%s §%d is changed and no accepted record changes it", k, c.N), k)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// RuleFrom is the day each dated rule began to hold a record, as hq's checks hold it: a record dated earlier was
|
|
// never held to it, and a dated correction of it (a progressive insight, which keeps the record's date) is not.
|
|
// words.py held the retired words from 2026-10-07 and the daemons' lines from 2026-10-09; cycle.py the deferrals
|
|
// from 2026-10-09; records.py the clauses from 2026-10-11 (ADR 0290 rule 3). The decision record's sections, and
|
|
// a superseded record cited alone, were held by no check of hq's (words.py and live-citation never read decision
|
|
// records): they hold from the day the knowledge base takes the write, 2026-10-11. Every other rule holds every
|
|
// record, as hq's checks held it.
|
|
var RuleFrom = map[string]string{
|
|
"retired-word": "2026-10-07",
|
|
"daemon-line": "2026-10-09",
|
|
"deferral": "2026-10-09",
|
|
"has-clause": "2026-10-11",
|
|
"clause-numbering": "2026-10-11",
|
|
"clause-lead": "2026-10-11",
|
|
"clause-size": "2026-10-11",
|
|
"context": "2026-10-11",
|
|
"consequences": "2026-10-11",
|
|
"checked": "2026-10-11",
|
|
"live-citation": "2026-10-11",
|
|
}
|
|
|
|
// DecisionFrom overrides RuleFrom for a decision record: hq's words check never read 02-DECISIONS ("a decision
|
|
// record keeps the words it was written with"), so its retired words and daemons' lines hold a decision only from
|
|
// the day the knowledge base takes the write.
|
|
var DecisionFrom = map[string]string{
|
|
"retired-word": "2026-10-11",
|
|
"daemon-line": "2026-10-11",
|
|
}
|
|
|
|
// Holds says whether a rule holds a record of this kind and date. A record with no day is held to every rule.
|
|
func Holds(rule, kind, date string) bool {
|
|
from, dated := RuleFrom[rule]
|
|
if d, ok := DecisionFrom[rule]; ok && kind == KindDecision {
|
|
from = d
|
|
}
|
|
return !dated || !isDay.MatchString(date) || date >= from
|
|
}
|