suite check: направление ссылок внутри темы проверяется во всём документе
- ссылка на слой своей темы с объявленной осью — ошибка где угодно, а не только в норме: гарантированно присутствует в копии один базовый слой - ссылки на чужую тему вне нормы остаются разрешёнными по META-20
This commit is contained in:
@@ -160,7 +160,22 @@ func TestLayeredChecks(t *testing.T) {
|
|||||||
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.",
|
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.",
|
||||||
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z, как требует GTIM-1.", 1)
|
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z, как требует GTIM-1.", 1)
|
||||||
},
|
},
|
||||||
want: "a layer of its own topic but not the base one",
|
want: "points at a layer of this topic that is not the base one",
|
||||||
|
}, {
|
||||||
|
name: "rationale of the base layer references a layer above it",
|
||||||
|
setup: func(f files) {
|
||||||
|
f["conventions/arch/time.md"] = strings.Replace(archTime,
|
||||||
|
"**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.",
|
||||||
|
"**ПОЧЕМУ.** Без явного смещения не видно зоны; в Go это выражает GTIM-1.", 1)
|
||||||
|
},
|
||||||
|
want: "points at a layer of this topic that is not the base one",
|
||||||
|
}, {
|
||||||
|
name: "a section outside any rule references a layer above",
|
||||||
|
setup: func(f files) {
|
||||||
|
f["conventions/arch/time.md"] = archTime +
|
||||||
|
"\n## Связано\n\nРеализация на Go — GTIM-1.\n"
|
||||||
|
},
|
||||||
|
want: "points at a layer of this topic that is not the base one",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -220,7 +235,7 @@ func TestNoFalsePositives(t *testing.T) {
|
|||||||
name: "a stub of a retired rule with a date and a reason",
|
name: "a stub of a retired rule with a date and a reason",
|
||||||
setup: func(f files) {
|
setup: func(f files) {
|
||||||
f["conventions/arch/time.md"] = archTime +
|
f["conventions/arch/time.md"] = archTime +
|
||||||
"\n### TIME-2. Ширина строки фиксируется\n\n**СНЯТО 2026-07-26.** Правило переехало в GTIM-1.\n"
|
"\n### TIME-2. Ширина строки фиксируется\n\n**СНЯТО 2026-07-26.** Ширина следует из TIME-1 и отдельного правила не требует.\n"
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "a scenario block in the suite's own vocabulary",
|
name: "a scenario block in the suite's own vocabulary",
|
||||||
@@ -235,6 +250,26 @@ func TestNoFalsePositives(t *testing.T) {
|
|||||||
"**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.",
|
"**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.",
|
||||||
"**ПОЧЕМУ.** Условие `WHERE a AND b OR c` сортировку не спасает.", 1)
|
"**ПОЧЕМУ.** Условие `WHERE a AND b OR c` сортировку не спасает.", 1)
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
name: "a rationale references a foreign topic, which META-20 allows",
|
||||||
|
setup: func(f files) {
|
||||||
|
f["conventions/lang/go/time.md"] = strings.Replace(goTime,
|
||||||
|
"**ПОЧЕМУ.** Единая точка даёт гарантированный UTC.",
|
||||||
|
"**ПОЧЕМУ.** Единая точка даёт гарантированный UTC; тот же довод стоит за SLOG-1.", 1)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "a section outside any rule references a foreign topic",
|
||||||
|
setup: func(f files) {
|
||||||
|
f["conventions/lang/go/time.md"] = goTime +
|
||||||
|
"\n## Связано\n\nКонвенция logging, правило SLOG-1.\n"
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "a layer references the base layer of its topic outside a norm",
|
||||||
|
setup: func(f files) {
|
||||||
|
f["conventions/lang/go/time.md"] = strings.Replace(goTime,
|
||||||
|
"**ПОЧЕМУ.** Единая точка даёт гарантированный UTC.",
|
||||||
|
"**ПОЧЕМУ.** Единая точка даёт гарантированный UTC, чего и требует TIME-1.", 1)
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
name: "the single document without a topic is the one the suite governs itself by",
|
name: "the single document without a topic is the one the suite governs itself by",
|
||||||
setup: func(f files) {
|
setup: func(f files) {
|
||||||
|
|||||||
+45
-18
@@ -19,7 +19,8 @@ func checkSpread(s *suite.Suite, d *doc.Document, rep *Report) {
|
|||||||
checkExtends(s, d, rep)
|
checkExtends(s, d, rep)
|
||||||
checkMechanized(s, d, rep)
|
checkMechanized(s, d, rep)
|
||||||
checkCanonPaths(s, d, rep)
|
checkCanonPaths(s, d, rep)
|
||||||
checkForeignTopicPrefix(s, d, rep)
|
checkForeignTopicInNorm(s, d, rep)
|
||||||
|
checkOwnTopicLayerRefs(s, d, rep)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkTopic reconciles the topic from the front matter with the manifest
|
// checkTopic reconciles the topic from the front matter with the manifest
|
||||||
@@ -151,13 +152,16 @@ func checkCanonPaths(s *suite.Suite, d *doc.Document, rep *Report) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkForeignTopicPrefix looks for the prefix of a foreign topic inside a norm
|
// checkForeignTopicInNorm looks for the prefix of a foreign topic inside a norm
|
||||||
// block (META-20). The norm of a rule must be executable holding this one file:
|
// block (META-20). The norm of a rule must be executable holding this one file:
|
||||||
// a repository subscribes to an arbitrary subset of the conventions, and it has
|
// a repository subscribes to an arbitrary subset of the conventions, and it has
|
||||||
// no dependency graph by construction. The prefix of the base layer of its own
|
// no dependency graph by construction.
|
||||||
// topic is allowed there (META-24) — an assembled file starts with that layer
|
//
|
||||||
// whatever language and stack were chosen.
|
// Outside a norm such a reference is lawful and stays unchecked. A rationale
|
||||||
func checkForeignTopicPrefix(s *suite.Suite, d *doc.Document, rep *Report) {
|
// that loses its addressee degrades honestly — the cross-check goes, the
|
||||||
|
// meaning stays — while a norm missing a neighbouring file becomes unenforceable
|
||||||
|
// in silence.
|
||||||
|
func checkForeignTopicInNorm(s *suite.Suite, d *doc.Document, rep *Report) {
|
||||||
own := s.Prefix(d)
|
own := s.Prefix(d)
|
||||||
for _, r := range d.Rules {
|
for _, r := range d.Rules {
|
||||||
for _, norm := range r.Norms() {
|
for _, norm := range r.Norms() {
|
||||||
@@ -166,21 +170,44 @@ func checkForeignTopicPrefix(s *suite.Suite, d *doc.Document, rep *Report) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
target, ok := s.ByPrefix[ref.Prefix]
|
target, ok := s.ByPrefix[ref.Prefix]
|
||||||
if !ok {
|
if !ok || target.Front.Topic == d.Front.Topic {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if target.Front.Topic != d.Front.Topic {
|
rep.Errorf(Spread, d.Path, ref.Line,
|
||||||
rep.Errorf(Spread, d.Path, ref.Line,
|
"the norm of %s refers to %s from the foreign topic %q: only the rationale looks outward",
|
||||||
"the norm of %s refers to %s from the foreign topic %q: only the rationale looks outward",
|
r.ID(), ref.Text, target.Front.Topic)
|
||||||
r.ID(), ref.Text, target.Front.Topic)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if target.Front.Axis() {
|
|
||||||
rep.Errorf(Spread, d.Path, ref.Line,
|
|
||||||
"the norm of %s refers to %s, a layer of its own topic but not the base one: that layer reaches the copy through the manifest, so there is no guarantee it stands nearby",
|
|
||||||
r.ID(), ref.Text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkOwnTopicLayerRefs holds the direction of references inside one topic:
|
||||||
|
// only the base layer may be pointed at, and from anywhere in the document
|
||||||
|
// rather than from the norm alone.
|
||||||
|
//
|
||||||
|
// Guaranteed to stand in the copy is exactly one layer of a topic — the base
|
||||||
|
// one; it goes in whatever language and stack were chosen (META-24). The order
|
||||||
|
// of assembly, base then language then stack, is the order of concatenation and
|
||||||
|
// not a chain of dependency: a consumer is free to take stack=htmx with
|
||||||
|
// lang=python, so a stack layer has no claim on a language layer either.
|
||||||
|
//
|
||||||
|
// The reason reaches past dangling links. The base layer is the text that has
|
||||||
|
// to hold for every language and every stack; a rationale of its that needs a
|
||||||
|
// rule of the Go layer to explain itself is the specific leaking into the
|
||||||
|
// shared, and the reasoning belongs in that layer instead. The base layer is
|
||||||
|
// therefore left no way to point at a layer above it at all — by design.
|
||||||
|
func checkOwnTopicLayerRefs(s *suite.Suite, d *doc.Document, rep *Report) {
|
||||||
|
own := s.Prefix(d)
|
||||||
|
for _, ref := range refsIn(d, d.Body, d.Len()) {
|
||||||
|
if ref.Prefix == own || strings.HasPrefix(ref.Prefix, "X") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
target, ok := s.ByPrefix[ref.Prefix]
|
||||||
|
if !ok || target.Front.Topic != d.Front.Topic || !target.Front.Axis() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rep.Errorf(Spread, d.Path, ref.Line,
|
||||||
|
"the reference %s points at a layer of this topic that is not the base one: only the base layer is guaranteed to stand in a copy, and a rule that needs this one belongs in that layer",
|
||||||
|
ref.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user