A decision links the milestone that builds it: link kind built-in (milestone 47/G, W15.2)

ADR 0493 section 4 names the links; the knowledge base took no milestone as a target.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

(Joined from 5f4e4d29.)
Builds: design 47 G §318 (KB1)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:48:07 +01:00
parent 6d4ad2c249
commit 4839884d93
3 changed files with 28 additions and 4 deletions
@@ -75,10 +75,12 @@ const (
LinkCites = "cites"
LinkRestsOn = "rests-on"
LinkTouches = "touches"
// LinkBuiltIn is from a decision the milestone that builds it, design/id such as 47/G (ADR 0493 §4, design 47).
LinkBuiltIn = "built-in"
)
// LinkKinds are the kinds a link may be.
var LinkKinds = []string{LinkSupersedes, LinkChanges, LinkExtends, LinkBecame, LinkCites, LinkRestsOn, LinkTouches}
var LinkKinds = []string{LinkSupersedes, LinkChanges, LinkExtends, LinkBecame, LinkCites, LinkRestsOn, LinkTouches, LinkBuiltIn}
// LinkMeanings says what each kind of link means, as the knowledge base's own review is told (hq issue 492): the
// rules it sees list LinkKinds, which propose holds a link to, so the two cannot drift.
@@ -90,12 +92,15 @@ var LinkMeanings = map[string]string{
LinkCites: "any record, issue or design it names",
LinkRestsOn: "from a decision, a decision it stands on, which may not be superseded under it",
LinkTouches: "from a research effort, a record, issue or design its effort bears on",
LinkBuiltIn: "from a decision, the milestone that builds it, named as milestone 47/G",
}
// What a link may point at besides a record of this knowledge base: an issue in the tracker, a design in hq.
const (
TargetIssue = "issue"
TargetDesign = "design"
// TargetMilestone is a milestone of the issue tracker, design/id; its Path holds that.
TargetMilestone = "milestone"
)
// Key is a record's key: its kind and number together.
@@ -166,6 +171,8 @@ func (r Ref) String() string {
return fmt.Sprintf("issue %d", r.Number)
case TargetDesign:
return "design " + r.Path
case TargetMilestone:
return "milestone " + r.Path
}
return r.Kind
}
@@ -174,6 +181,8 @@ var (
refRecord = regexp.MustCompile(`^(?i)(?:(adr|decision|research)\s*)?(\d{1,4})(?:\s*§\s*(\d{1,3}))?$`)
refIssue = regexp.MustCompile(`^(?i)issue\s*(\d{1,5})$`)
refDesign = regexp.MustCompile(`^(?i)(?:design\s+)?(03-DESIGN/[0-9a-z/-]+\.md)$`)
// refMilestone is the tracker's name for a milestone: design/id, such as 47/G.
refMilestone = regexp.MustCompile(`^(?i)milestone\s+([0-9]{1,3}/[A-Za-z0-9][A-Za-z0-9_-]{0,31})$`)
)
// ParseRef reads a link's target as an agent writes it: "0276", "ADR 0276 §4", "research 045", "issue 365",
@@ -187,6 +196,9 @@ func ParseRef(s string) (Ref, error) {
if m := refDesign.FindStringSubmatch(s); m != nil {
return Ref{Kind: TargetDesign, Path: m[1]}, nil
}
if m := refMilestone.FindStringSubmatch(s); m != nil {
return Ref{Kind: TargetMilestone, Path: m[1]}, nil
}
if m := refRecord.FindStringSubmatch(s); m != nil {
kind := KindDecision
if strings.EqualFold(m[1], "research") {
@@ -202,8 +214,8 @@ func ParseRef(s string) (Ref, error) {
}
return Ref{Kind: kind, Number: n, Clause: c}, nil
}
return Ref{}, fmt.Errorf("%q is not a record (0276, ADR 0276 §4, research 045), an issue (issue 365) or a design "+
"(03-DESIGN/01-to-be/55-the-knowledge-base.md)", s)
return Ref{}, fmt.Errorf("%q is not a record (0276, ADR 0276 §4, research 045), an issue (issue 365), a design "+
"(03-DESIGN/01-to-be/55-the-knowledge-base.md) or a milestone (milestone 47/G)", s)
}
// Link is one typed link, held on the record it goes out from and read from both ends. From is the clause it
@@ -248,6 +248,10 @@ func JudgeRecordOn(r *Record, held *Entry, lookup Lookup, ref *Reference, day st
if !decision || l.To.Kind != KindDecision || l.To.Clause > 0 {
refuse("link-fits", "rests-on goes from a decision to a decision it stands on: not %s", l)
}
case l.Kind == LinkBuiltIn:
if !decision || l.To.Kind != TargetMilestone {
refuse("link-fits", "built-in goes from a decision to the milestone that builds it, named as milestone 47/G: not %s", l)
}
case l.Kind == LinkTouches:
if r.Kind != KindResearch || l.To.Clause > 0 {
refuse("link-fits", "touches goes from a research effort to a record, an issue or a design: not %s", l)
@@ -54,6 +54,9 @@ func TestEveryPerRecordRuleRefusesAProposal(t *testing.T) {
{"link-fits", func(p *ProposeAsk, w *world, t *testing.T) {
p.Links = append(p.Links, LinkAsk{Kind: LinkBecame, To: "0276"})
}},
{"link-fits", func(p *ProposeAsk, w *world, t *testing.T) {
p.Links = append(p.Links, LinkAsk{Kind: LinkBuiltIn, To: "0276"}) // built-in names a milestone, not a record
}},
{"self-link", func(p *ProposeAsk, w *world, t *testing.T) {
p.Links = append(p.Links, LinkAsk{Kind: LinkCites, To: itoa4(p.Number)})
}},
@@ -165,11 +168,16 @@ func TestTheValidProposalIsTaken(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@test")
got, err := w.k.Propose("author@test", valid(n))
p := valid(n)
p.Links = append(p.Links, LinkAsk{Kind: LinkBuiltIn, To: "milestone 47/G"}) // a decision links its milestone
got, err := w.k.Propose("author@test", p)
if err != nil {
t.Fatal(err)
}
e := mustGet(t, s, Key{KindDecision, n})
if l := e.Current.Links[len(e.Current.Links)-1]; l.Kind != LinkBuiltIn || l.To != (Ref{Kind: TargetMilestone, Path: "47/G"}) {
t.Fatalf("the milestone link was kept as %+v", l)
}
if got.Status != StatusProposed || e.Current.Status != StatusProposed || len(e.Current.Clauses) != 2 ||
e.Current.Clauses[0].Status != ClauseProposed || e.Current.Author != "author@test" {
t.Fatalf("%+v / %+v", got, e.Current)