6b41eec176
Makes the OSHA minimum-distance anchor visible per measure in a project without a DB schema change or re-seed: persisted mitigations store the measure NAME verbatim (not the catalog ID), and measure names are unique across the 578-entry library (pinned by test), so a name→ID resolver bridges the gap. Backend: MeasureIDByName + MinimumDistancesForMeasureName/LinksForMeasureName; /iace/minimum-distances now accepts ?measure_name=; link table enriched with measure_name for one-request UI matching. Frontend: useMinimumDistances loads the link table once and keys it by name; OshaDistanceNote renders the anchor (value/CFR/license/EU-hint/relation) on the matching measure group in the Maßnahmen tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
168 lines
5.3 KiB
Go
168 lines
5.3 KiB
Go
package iace
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// These tests codify the May-built-but-never-verified OSHA minimum-distance
|
|
// library: that its public-domain values convert correctly, that the
|
|
// measure→distance links point at real measures and real distances, and that
|
|
// a "value_source" link's OSHA figure actually appears in the measure prose
|
|
// (the consistency the audit asked for).
|
|
|
|
const inchToMM = 25.4
|
|
|
|
func TestOSHAMinimumDistances_ConversionAndLicense(t *testing.T) {
|
|
dists := GetOSHAMinimumDistances()
|
|
if len(dists) == 0 {
|
|
t.Fatal("OSHA minimum-distance library is empty")
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, d := range dists {
|
|
if seen[d.ID] {
|
|
t.Errorf("duplicate distance ID %q", d.ID)
|
|
}
|
|
seen[d.ID] = true
|
|
|
|
if d.License == "" || d.SourceCFR == "" || d.Context == "" {
|
|
t.Errorf("%s: missing license/source/context: %+v", d.ID, d)
|
|
}
|
|
|
|
// Inch → mm conversions must be mathematically exact (within rounding).
|
|
if d.OriginalUnit == UnitInch {
|
|
checkConv := func(label string, orig, exact float64) {
|
|
if orig == 0 && exact == 0 {
|
|
return
|
|
}
|
|
if math.Abs(orig*inchToMM-exact) > 0.05 {
|
|
t.Errorf("%s: %s conversion off: %.3f in → %.3f mm (expected %.3f)",
|
|
d.ID, label, orig, exact, orig*inchToMM)
|
|
}
|
|
}
|
|
checkConv("value", d.OriginalValue, d.ExactMM)
|
|
checkConv("min", d.OriginalMin, d.ExactMinMM)
|
|
checkConv("max", d.OriginalMax, d.ExactMaxMM)
|
|
}
|
|
|
|
// Safe-side rounding must stay near the exact value (≤5 mm grid).
|
|
if d.ExactMM > 0 && d.RecommendedMM > 0 {
|
|
if math.Abs(float64(d.RecommendedMM)-d.ExactMM) > 5 {
|
|
t.Errorf("%s: recommended %d mm too far from exact %.2f mm",
|
|
d.ID, d.RecommendedMM, d.ExactMM)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMeasureDistanceLinks_Integrity(t *testing.T) {
|
|
measures := map[string]ProtectiveMeasureEntry{}
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measures[m.ID] = m
|
|
}
|
|
|
|
links := AllMeasureDistanceLinks()
|
|
if len(links) == 0 {
|
|
t.Fatal("no measure→distance links declared")
|
|
}
|
|
for _, l := range links {
|
|
if _, ok := measures[l.MeasureID]; !ok {
|
|
t.Errorf("link references unknown measure %q", l.MeasureID)
|
|
}
|
|
if l.Relation != LinkValueSource && l.Relation != LinkCrossRef {
|
|
t.Errorf("link %q has invalid relation %q", l.MeasureID, l.Relation)
|
|
}
|
|
if len(l.DistanceIDs) == 0 {
|
|
t.Errorf("link %q has no distance IDs", l.MeasureID)
|
|
}
|
|
for _, id := range l.DistanceIDs {
|
|
if _, ok := GetMinimumDistanceByID(id); !ok {
|
|
t.Errorf("link %q references unknown distance %q", l.MeasureID, id)
|
|
}
|
|
}
|
|
// The resolver must return exactly the linked distances.
|
|
if got := len(MinimumDistancesForMeasure(l.MeasureID)); got != len(l.DistanceIDs) {
|
|
t.Errorf("resolver for %q returned %d, expected %d", l.MeasureID, got, len(l.DistanceIDs))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMeasureDistanceLinks_ValueSourceProseConsistency(t *testing.T) {
|
|
measures := map[string]ProtectiveMeasureEntry{}
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
measures[m.ID] = m
|
|
}
|
|
|
|
for _, l := range AllMeasureDistanceLinks() {
|
|
if l.Relation != LinkValueSource {
|
|
continue // cross-refs legitimately use an EU value in prose
|
|
}
|
|
m := measures[l.MeasureID]
|
|
text := strings.ToLower(m.Name + " " + m.Description + " " + strings.Join(m.Examples, " "))
|
|
text = strings.ReplaceAll(text, ".", "") // "1.600" → "1600"
|
|
|
|
for _, id := range l.DistanceIDs {
|
|
md, _ := GetMinimumDistanceByID(id)
|
|
if !proseMentionsDistance(text, md) {
|
|
t.Errorf("value_source measure %q does not mention any value of linked distance %q "+
|
|
"— prose has drifted from the OSHA source", l.MeasureID, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The name→ID resolver underpins surfacing OSHA anchors on persisted
|
|
// mitigations (which store the name, not the catalog ID). It is only safe if
|
|
// measure names are unique — pin that, and that the resolver round-trips for
|
|
// every linked measure.
|
|
func TestMeasureNames_UniqueAndResolvable(t *testing.T) {
|
|
idByName := map[string]string{}
|
|
nameByID := map[string]string{}
|
|
for _, m := range GetProtectiveMeasureLibrary() {
|
|
if prev, dup := idByName[m.Name]; dup {
|
|
t.Errorf("duplicate measure name %q (IDs %s, %s) — name→ID resolver unsafe",
|
|
m.Name, prev, m.ID)
|
|
}
|
|
idByName[m.Name] = m.ID
|
|
nameByID[m.ID] = m.Name
|
|
}
|
|
|
|
for _, l := range AllMeasureDistanceLinks() {
|
|
name := nameByID[l.MeasureID]
|
|
if name == "" {
|
|
t.Errorf("linked measure %q not found in library", l.MeasureID)
|
|
continue
|
|
}
|
|
gotID, ok := MeasureIDByName(name)
|
|
if !ok || gotID != l.MeasureID {
|
|
t.Errorf("MeasureIDByName(%q) = %q,%v; want %q", name, gotID, ok, l.MeasureID)
|
|
}
|
|
if len(MinimumDistancesForMeasureName(name)) != len(MinimumDistancesForMeasure(l.MeasureID)) {
|
|
t.Errorf("name vs id resolution mismatch for %q", l.MeasureID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// proseMentionsDistance reports whether the (dot-stripped, lowercased) measure
|
|
// text contains a numeric form of the distance's value, formula or recommended mm.
|
|
func proseMentionsDistance(text string, md MinimumDistance) bool {
|
|
candidates := []int{}
|
|
if md.FormulaMMPerSecond > 0 {
|
|
candidates = append(candidates, int(math.Round(md.FormulaMMPerSecond)))
|
|
}
|
|
if md.RecommendedMM > 0 {
|
|
candidates = append(candidates, md.RecommendedMM)
|
|
}
|
|
if md.RecommendedMinMM > 0 {
|
|
candidates = append(candidates, md.RecommendedMinMM)
|
|
}
|
|
for _, n := range candidates {
|
|
if strings.Contains(text, strconv.Itoa(n)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|