feat(iace): wire OSHA minimum-distance library into measures + endpoint
The May-built OSHA distance library (minimum_distances.go, 29 CFR 1910, US public domain) was dead code — zero callers, no route, no test, while the mm values that actually appear in measures are independent hand-prose (some carrying ISO 13854/13857 values, not OSHA). This surfaces it without touching the measures response contract: - GET /iace/minimum-distances (+ ?measure_id=) returns the distances, the curated measure→distance link table and the licensing note. - AllMeasureDistanceLinks/MinimumDistancesForMeasure resolve only the defensible links (M600 value_source; M254/M065 public-domain crossref to ISO), with the relation made explicit so the join stays honest. - architecture.go lists the OSHA library so it shows in the audit explainer. - Tests: inch→mm conversion + license completeness, link integrity, and a consistency test pinning that a value_source measure's prose still matches the OSHA source (codifies the audit finding as a regression gate). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListMinimumDistances handles GET /minimum-distances.
|
||||||
|
// Read-only OSHA safety-distance reference (29 CFR 1910, US public domain)
|
||||||
|
// plus the curated measure→distance link table, so an auditor can see WHERE a
|
||||||
|
// measure's mm figure comes from. Optional ?measure_id= returns only the
|
||||||
|
// distances (and links) tied to that protective measure.
|
||||||
|
func (h *IACEHandler) ListMinimumDistances(c *gin.Context) {
|
||||||
|
if mid := c.Query("measure_id"); mid != "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"measure_id": mid,
|
||||||
|
"distances": iace.MinimumDistancesForMeasure(mid),
|
||||||
|
"links": iace.LinksForMeasure(mid),
|
||||||
|
"note": iace.MinimumDistanceNote,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"distances": iace.GetOSHAMinimumDistances(),
|
||||||
|
"links": iace.AllMeasureDistanceLinks(),
|
||||||
|
"note": iace.MinimumDistanceNote,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) {
|
|||||||
iaceRoutes.GET("/operational-states", h.ListOperationalStates)
|
iaceRoutes.GET("/operational-states", h.ListOperationalStates)
|
||||||
iaceRoutes.GET("/component-library", h.ListComponentLibrary)
|
iaceRoutes.GET("/component-library", h.ListComponentLibrary)
|
||||||
iaceRoutes.GET("/energy-sources", h.ListEnergySources)
|
iaceRoutes.GET("/energy-sources", h.ListEnergySources)
|
||||||
|
iaceRoutes.GET("/minimum-distances", h.ListMinimumDistances)
|
||||||
iaceRoutes.GET("/tags", h.ListTags)
|
iaceRoutes.GET("/tags", h.ListTags)
|
||||||
iaceRoutes.GET("/hazard-patterns", h.ListHazardPatterns)
|
iaceRoutes.GET("/hazard-patterns", h.ListHazardPatterns)
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func BuildArchitecture() Architecture {
|
|||||||
{Name: "Domänen-Capability-Gates", Count: distinctDomainGates(), SourceFile: "pattern_domain_gates.go", Description: "dom_*-Tags, die domänenspezifische Muster auf ihre echte Maschine begrenzen (Leak-Schutz)."},
|
{Name: "Domänen-Capability-Gates", Count: distinctDomainGates(), SourceFile: "pattern_domain_gates.go", Description: "dom_*-Tags, die domänenspezifische Muster auf ihre echte Maschine begrenzen (Leak-Schutz)."},
|
||||||
{Name: "Kontaktmodus-Tiers", Count: len(contactModeTable), SourceFile: "risk_estimation.go", Description: "Verletzungsmechanismen mit W/P/S-Tiers (ESAW-verankert, GT-kalibriert)."},
|
{Name: "Kontaktmodus-Tiers", Count: len(contactModeTable), SourceFile: "risk_estimation.go", Description: "Verletzungsmechanismen mit W/P/S-Tiers (ESAW-verankert, GT-kalibriert)."},
|
||||||
{Name: "Kontaktmodus-Evidenz", Count: len(contactModeEvidence), SourceFile: "risk_data_sources.go", Description: "Belegte öffentliche Statistik-Quoten (ESAW) als Zitat-/Audit-Schicht."},
|
{Name: "Kontaktmodus-Evidenz", Count: len(contactModeEvidence), SourceFile: "risk_data_sources.go", Description: "Belegte öffentliche Statistik-Quoten (ESAW) als Zitat-/Audit-Schicht."},
|
||||||
|
{Name: "OSHA-Mindestabstände", Count: len(GetOSHAMinimumDistances()), SourceFile: "minimum_distances.go", Description: "OSHA 29 CFR 1910 Sicherheitsabstände (Public Domain) + Maßnahmen-Verknüpfung; EU-Normen nur referenziert."},
|
||||||
},
|
},
|
||||||
DataSources: []ArchDataSource{
|
DataSources: []ArchDataSource{
|
||||||
{Name: "Eurostat ESAW (Kontaktmodus-Unfallstatistik)", License: "CC BY 4.0", Usage: "Anker für Wahrscheinlichkeits-Tiers (W) + zitierbare Quoten", Status: "verwendet"},
|
{Name: "Eurostat ESAW (Kontaktmodus-Unfallstatistik)", License: "CC BY 4.0", Usage: "Anker für Wahrscheinlichkeits-Tiers (W) + zitierbare Quoten", Status: "verwendet"},
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ package iace
|
|||||||
type MinimumDistanceUnit string
|
type MinimumDistanceUnit string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UnitInch MinimumDistanceUnit = "inch"
|
UnitInch MinimumDistanceUnit = "inch"
|
||||||
UnitFoot MinimumDistanceUnit = "foot"
|
UnitFoot MinimumDistanceUnit = "foot"
|
||||||
UnitMeter MinimumDistanceUnit = "meter"
|
UnitMeter MinimumDistanceUnit = "meter"
|
||||||
UnitMM MinimumDistanceUnit = "mm"
|
UnitMM MinimumDistanceUnit = "mm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MinimumDistance is the data contract for a single safety-distance rule.
|
// MinimumDistance is the data contract for a single safety-distance rule.
|
||||||
@@ -36,16 +36,16 @@ const (
|
|||||||
type MinimumDistance struct {
|
type MinimumDistance struct {
|
||||||
ID string `json:"id"` // MD_OSHA_001
|
ID string `json:"id"` // MD_OSHA_001
|
||||||
// Source identifier — full CFR citation or norm reference.
|
// Source identifier — full CFR citation or norm reference.
|
||||||
SourceCFR string `json:"source_cfr,omitempty"` // "29 CFR §1910.217(c)(1)(i)"
|
SourceCFR string `json:"source_cfr,omitempty"` // "29 CFR §1910.217(c)(1)(i)"
|
||||||
SourceTable string `json:"source_table,omitempty"` // "Table O-10"
|
SourceTable string `json:"source_table,omitempty"` // "Table O-10"
|
||||||
License string `json:"license"` // "US Federal Public Domain"
|
License string `json:"license"` // "US Federal Public Domain"
|
||||||
LicenseRule int `json:"license_rule"` // 1 / 2 / 3 (see LICENSE_RULES.md)
|
LicenseRule int `json:"license_rule"` // 1 / 2 / 3 (see LICENSE_RULES.md)
|
||||||
|
|
||||||
// Original verbatim value in the source's own unit.
|
// Original verbatim value in the source's own unit.
|
||||||
OriginalUnit MinimumDistanceUnit `json:"original_unit"`
|
OriginalUnit MinimumDistanceUnit `json:"original_unit"`
|
||||||
OriginalValue float64 `json:"original_value,omitempty"`
|
OriginalValue float64 `json:"original_value,omitempty"`
|
||||||
OriginalMin float64 `json:"original_min,omitempty"`
|
OriginalMin float64 `json:"original_min,omitempty"`
|
||||||
OriginalMax float64 `json:"original_max,omitempty"`
|
OriginalMax float64 `json:"original_max,omitempty"`
|
||||||
|
|
||||||
// Exact conversion to mm — no engineering rounding.
|
// Exact conversion to mm — no engineering rounding.
|
||||||
ExactMM float64 `json:"exact_mm,omitempty"`
|
ExactMM float64 `json:"exact_mm,omitempty"`
|
||||||
@@ -55,18 +55,18 @@ type MinimumDistance struct {
|
|||||||
// Engineering-recommended metric value with safe-side rounding.
|
// Engineering-recommended metric value with safe-side rounding.
|
||||||
// For minimum distances: rounded up. For maximum opening widths:
|
// For minimum distances: rounded up. For maximum opening widths:
|
||||||
// rounded down.
|
// rounded down.
|
||||||
RecommendedMM int `json:"recommended_mm,omitempty"`
|
RecommendedMM int `json:"recommended_mm,omitempty"`
|
||||||
RecommendedMinMM int `json:"recommended_min_mm,omitempty"`
|
RecommendedMinMM int `json:"recommended_min_mm,omitempty"`
|
||||||
RecommendedMaxMM int `json:"recommended_max_mm,omitempty"`
|
RecommendedMaxMM int `json:"recommended_max_mm,omitempty"`
|
||||||
RoundingNote string `json:"rounding_note,omitempty"`
|
RoundingNote string `json:"rounding_note,omitempty"`
|
||||||
|
|
||||||
// Optional formula constant (e.g. OSHA hand-speed 63 in/s).
|
// Optional formula constant (e.g. OSHA hand-speed 63 in/s).
|
||||||
FormulaInchPerSecond float64 `json:"formula_inch_per_second,omitempty"`
|
FormulaInchPerSecond float64 `json:"formula_inch_per_second,omitempty"`
|
||||||
FormulaMMPerSecond float64 `json:"formula_mm_per_second,omitempty"`
|
FormulaMMPerSecond float64 `json:"formula_mm_per_second,omitempty"`
|
||||||
FormulaDescription string `json:"formula_description,omitempty"`
|
FormulaDescription string `json:"formula_description,omitempty"`
|
||||||
|
|
||||||
Context string `json:"context"` // "Point of Operation Guarding mechanical presses"
|
Context string `json:"context"` // "Point of Operation Guarding mechanical presses"
|
||||||
BodyPart string `json:"body_part,omitempty"` // "finger" / "hand" / "head" / "foot" / "body"
|
BodyPart string `json:"body_part,omitempty"` // "finger" / "hand" / "head" / "foot" / "body"
|
||||||
HazardTags []string `json:"hazard_tags,omitempty"` // [crush_point, cutting_part, ...]
|
HazardTags []string `json:"hazard_tags,omitempty"` // [crush_point, cutting_part, ...]
|
||||||
|
|
||||||
// EU norm cross-reference — IDENTIFIER ONLY, no values reproduced.
|
// EU norm cross-reference — IDENTIFIER ONLY, no values reproduced.
|
||||||
@@ -77,8 +77,8 @@ type MinimumDistance struct {
|
|||||||
// any value or text from it. The DINComparisonNote is a human-curated
|
// any value or text from it. The DINComparisonNote is a human-curated
|
||||||
// qualitative judgement (stricter / equivalent / looser) — not a copy.
|
// qualitative judgement (stricter / equivalent / looser) — not a copy.
|
||||||
type EUNormHint struct {
|
type EUNormHint struct {
|
||||||
Norm string `json:"norm"` // "EN ISO 13857"
|
Norm string `json:"norm"` // "EN ISO 13857"
|
||||||
Section string `json:"section,omitempty"` // "Tab. 4, Schutz gegen Hineingreifen"
|
Section string `json:"section,omitempty"` // "Tab. 4, Schutz gegen Hineingreifen"
|
||||||
DINComparisonNote string `json:"din_comparison_note,omitempty"`
|
DINComparisonNote string `json:"din_comparison_note,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +98,12 @@ func GetOSHAMinimumDistances() []MinimumDistance {
|
|||||||
LicenseRule: 1,
|
LicenseRule: 1,
|
||||||
OriginalUnit: UnitInch,
|
OriginalUnit: UnitInch,
|
||||||
OriginalMin: 0.5, OriginalMax: 1.5, OriginalValue: 0.25,
|
OriginalMin: 0.5, OriginalMax: 1.5, OriginalValue: 0.25,
|
||||||
ExactMinMM: 12.7, ExactMaxMM: 38.1, ExactMM: 6.35,
|
ExactMinMM: 12.7, ExactMaxMM: 38.1, ExactMM: 6.35,
|
||||||
RecommendedMinMM: 15, RecommendedMaxMM: 40, RecommendedMM: 6,
|
RecommendedMinMM: 15, RecommendedMaxMM: 40, RecommendedMM: 6,
|
||||||
RoundingNote: "Distance auf 5-mm-Raster aufgerundet, opening auf 1-mm-Raster abgerundet (konservativ in beide Richtungen).",
|
RoundingNote: "Distance auf 5-mm-Raster aufgerundet, opening auf 1-mm-Raster abgerundet (konservativ in beide Richtungen).",
|
||||||
Context: "Point-of-Operation Guarding bei mechanischen Pressen",
|
Context: "Point-of-Operation Guarding bei mechanischen Pressen",
|
||||||
BodyPart: "finger",
|
BodyPart: "finger",
|
||||||
HazardTags: []string{"crush_point", "cutting_part"},
|
HazardTags: []string{"crush_point", "cutting_part"},
|
||||||
EUNormHints: []EUNormHint{
|
EUNormHints: []EUNormHint{
|
||||||
{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
|
{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
|
||||||
DINComparisonNote: "Andere Methodik (Reichweitenmodell). Unabhaengig pruefen — Werte koennen abweichen."},
|
DINComparisonNote: "Andere Methodik (Reichweitenmodell). Unabhaengig pruefen — Werte koennen abweichen."},
|
||||||
@@ -119,12 +119,12 @@ func GetOSHAMinimumDistances() []MinimumDistance {
|
|||||||
LicenseRule: 1,
|
LicenseRule: 1,
|
||||||
OriginalUnit: UnitInch,
|
OriginalUnit: UnitInch,
|
||||||
OriginalMin: 3.5, OriginalMax: 5.5, OriginalValue: 0.625,
|
OriginalMin: 3.5, OriginalMax: 5.5, OriginalValue: 0.625,
|
||||||
ExactMinMM: 88.9, ExactMaxMM: 139.7, ExactMM: 15.875,
|
ExactMinMM: 88.9, ExactMaxMM: 139.7, ExactMM: 15.875,
|
||||||
RecommendedMinMM: 90, RecommendedMaxMM: 140, RecommendedMM: 15,
|
RecommendedMinMM: 90, RecommendedMaxMM: 140, RecommendedMM: 15,
|
||||||
RoundingNote: "Distance 88.9→90 (+1.1 mm), 139.7→140 (+0.3 mm) aufgerundet; Opening 15.875→15 (-0.875 mm) abgerundet.",
|
RoundingNote: "Distance 88.9→90 (+1.1 mm), 139.7→140 (+0.3 mm) aufgerundet; Opening 15.875→15 (-0.875 mm) abgerundet.",
|
||||||
Context: "Point-of-Operation Guarding bei mechanischen Pressen",
|
Context: "Point-of-Operation Guarding bei mechanischen Pressen",
|
||||||
BodyPart: "finger",
|
BodyPart: "finger",
|
||||||
HazardTags: []string{"crush_point", "cutting_part"},
|
HazardTags: []string{"crush_point", "cutting_part"},
|
||||||
EUNormHints: []EUNormHint{
|
EUNormHints: []EUNormHint{
|
||||||
{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
|
{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
|
||||||
DINComparisonNote: "Andere Methodik (Reichweitenmodell). Compliance-Annotation pflegen."},
|
DINComparisonNote: "Andere Methodik (Reichweitenmodell). Compliance-Annotation pflegen."},
|
||||||
@@ -132,18 +132,18 @@ func GetOSHAMinimumDistances() []MinimumDistance {
|
|||||||
},
|
},
|
||||||
// OSHA §1910.212(a)(5) — fan blade guards. Verbatim 1/2 inch.
|
// OSHA §1910.212(a)(5) — fan blade guards. Verbatim 1/2 inch.
|
||||||
{
|
{
|
||||||
ID: "MD_OSHA_212_FAN",
|
ID: "MD_OSHA_212_FAN",
|
||||||
SourceCFR: "29 CFR §1910.212(a)(5)",
|
SourceCFR: "29 CFR §1910.212(a)(5)",
|
||||||
License: "US Federal Public Domain (17 U.S.C. §105)",
|
License: "US Federal Public Domain (17 U.S.C. §105)",
|
||||||
LicenseRule: 1,
|
LicenseRule: 1,
|
||||||
OriginalUnit: UnitInch,
|
OriginalUnit: UnitInch,
|
||||||
OriginalValue: 0.5,
|
OriginalValue: 0.5,
|
||||||
ExactMM: 12.7,
|
ExactMM: 12.7,
|
||||||
RecommendedMM: 12,
|
RecommendedMM: 12,
|
||||||
RoundingNote: "Luefterblatt-Schutzgitter: max. Spaltoeffnung 1/2 in = 12.7 mm. Konservativ auf 12 mm abgerundet.",
|
RoundingNote: "Luefterblatt-Schutzgitter: max. Spaltoeffnung 1/2 in = 12.7 mm. Konservativ auf 12 mm abgerundet.",
|
||||||
Context: "Lüfterblätter unter 7 ft (2.13 m) Höhe",
|
Context: "Lüfterblätter unter 7 ft (2.13 m) Höhe",
|
||||||
BodyPart: "finger",
|
BodyPart: "finger",
|
||||||
HazardTags: []string{"rotating_part", "cutting_part"},
|
HazardTags: []string{"rotating_part", "cutting_part"},
|
||||||
EUNormHints: []EUNormHint{
|
EUNormHints: []EUNormHint{
|
||||||
{Norm: "EN ISO 13857", Section: "Tab. 4",
|
{Norm: "EN ISO 13857", Section: "Tab. 4",
|
||||||
DINComparisonNote: "DIN-Wert pruefen."},
|
DINComparisonNote: "DIN-Wert pruefen."},
|
||||||
@@ -151,11 +151,11 @@ func GetOSHAMinimumDistances() []MinimumDistance {
|
|||||||
},
|
},
|
||||||
// OSHA §1910.217 Hand-Speed Constant — formula Ds = 63 in/s × Ts
|
// OSHA §1910.217 Hand-Speed Constant — formula Ds = 63 in/s × Ts
|
||||||
{
|
{
|
||||||
ID: "MD_OSHA_217_PSDI",
|
ID: "MD_OSHA_217_PSDI",
|
||||||
SourceCFR: "29 CFR §1910.217 (Ds = 63 in/s × Ts)",
|
SourceCFR: "29 CFR §1910.217 (Ds = 63 in/s × Ts)",
|
||||||
License: "US Federal Public Domain (17 U.S.C. §105)",
|
License: "US Federal Public Domain (17 U.S.C. §105)",
|
||||||
LicenseRule: 1,
|
LicenseRule: 1,
|
||||||
OriginalUnit: UnitInch,
|
OriginalUnit: UnitInch,
|
||||||
FormulaInchPerSecond: 63.0,
|
FormulaInchPerSecond: 63.0,
|
||||||
FormulaMMPerSecond: 1600.2,
|
FormulaMMPerSecond: 1600.2,
|
||||||
FormulaDescription: "Hand-Speed-Konstante 63 in/s ≈ 1600 mm/s. " +
|
FormulaDescription: "Hand-Speed-Konstante 63 in/s ≈ 1600 mm/s. " +
|
||||||
@@ -170,3 +170,96 @@ func GetOSHAMinimumDistances() []MinimumDistance {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MinimumDistanceNote is the ready-to-print licensing posture for the
|
||||||
|
// minimum-distance reference — shown by the API so an auditor sees WHY the
|
||||||
|
// OSHA values may be reproduced while the EU norms are reference-only.
|
||||||
|
const MinimumDistanceNote = "OSHA-Werte (29 CFR 1910) sind US-Public-Domain " +
|
||||||
|
"(17 U.S.C. §105) und werden verbatim wiedergegeben; die mm-Umrechnung ist " +
|
||||||
|
"mathematisch, die sicherheitsseitige Rundung ist BreakPilot-Empfehlung. " +
|
||||||
|
"EU-Normen (EN ISO 13857/13854/13855, EN 349) werden nur per Kennung " +
|
||||||
|
"referenziert — keine Werte reproduziert."
|
||||||
|
|
||||||
|
// GetMinimumDistanceByID returns the OSHA distance entry with the given ID.
|
||||||
|
func GetMinimumDistanceByID(id string) (MinimumDistance, bool) {
|
||||||
|
for _, md := range GetOSHAMinimumDistances() {
|
||||||
|
if md.ID == id {
|
||||||
|
return md, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MinimumDistance{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeasureDistanceLink connects a protective measure to the OSHA distance
|
||||||
|
// entries that anchor it. Relation makes the nature of the link explicit so
|
||||||
|
// the join is honest rather than implying every measure's prose IS the OSHA
|
||||||
|
// value:
|
||||||
|
// - "value_source" — the OSHA value is the source the measure's own
|
||||||
|
// mm figure is derived from (it appears in the measure prose).
|
||||||
|
// - "public_domain_crossref" — the measure is dimensioned by an EU norm; the
|
||||||
|
// OSHA entry is offered as the public-domain pendant for independent check.
|
||||||
|
type MeasureDistanceLink struct {
|
||||||
|
MeasureID string `json:"measure_id"`
|
||||||
|
DistanceIDs []string `json:"distance_ids"`
|
||||||
|
Relation string `json:"relation"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LinkValueSource = "value_source"
|
||||||
|
LinkCrossRef = "public_domain_crossref"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllMeasureDistanceLinks returns the curated measure→OSHA-distance links.
|
||||||
|
// Conservative on purpose: only measures whose CONTEXT genuinely matches an
|
||||||
|
// OSHA entry are linked. Measures whose "OSHA" citation is loose or carries an
|
||||||
|
// ISO value (e.g. M340 robot teach speed, M368 air-receiver wall) are NOT
|
||||||
|
// linked — that would imply a public-domain anchor that does not exist.
|
||||||
|
func AllMeasureDistanceLinks() []MeasureDistanceLink {
|
||||||
|
return []MeasureDistanceLink{
|
||||||
|
{
|
||||||
|
MeasureID: "M600",
|
||||||
|
DistanceIDs: []string{"MD_OSHA_217_PSDI"},
|
||||||
|
Relation: LinkValueSource,
|
||||||
|
Note: "Hand-Speed-Konstante 1.600 mm/s (63 in/s) ist die Obergrenze, aus der die Kriechgeschwindigkeit am Endanschlag abgeleitet ist.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MeasureID: "M254",
|
||||||
|
DistanceIDs: []string{"MD_OSHA_O10_R1", "MD_OSHA_O10_R4"},
|
||||||
|
Relation: LinkCrossRef,
|
||||||
|
Note: "OSHA Table O-10 (Point-of-Operation an mechanischen Pressen) als Public-Domain-Pendant zur ISO-13855-Methode — Werte eigenstaendig pruefen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MeasureID: "M065",
|
||||||
|
DistanceIDs: []string{"MD_OSHA_212_FAN"},
|
||||||
|
Relation: LinkCrossRef,
|
||||||
|
Note: "OSHA §1910.212(a)(5) Luefterschutz (max. 12 mm Spaltoeffnung) als Public-Domain-Pendant zu ISO 13857.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinksForMeasure returns the distance links declared for one measure.
|
||||||
|
func LinksForMeasure(measureID string) []MeasureDistanceLink {
|
||||||
|
var out []MeasureDistanceLink
|
||||||
|
for _, l := range AllMeasureDistanceLinks() {
|
||||||
|
if l.MeasureID == measureID {
|
||||||
|
out = append(out, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinimumDistancesForMeasure resolves the OSHA distance entries linked to a
|
||||||
|
// protective measure. This is the join that finally lets the OSHA mm values
|
||||||
|
// "flow into" the measures (read-side), without mutating the measure object.
|
||||||
|
func MinimumDistancesForMeasure(measureID string) []MinimumDistance {
|
||||||
|
var out []MinimumDistance
|
||||||
|
for _, l := range LinksForMeasure(measureID) {
|
||||||
|
for _, id := range l.DistanceIDs {
|
||||||
|
if md, ok := GetMinimumDistanceByID(id); ok {
|
||||||
|
out = append(out, md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user