0a84c747f2
Three follow-ups to the 671-norm cross-reference matrix: 1. Tech-file renderer (Go): standards_applied section now gets a deterministic Markdown appendix with the DIN/ANSI/GB/JIS mappings for the project's suggested norms. Built from registry, never hallucinated by LLM. Applied both to LLM and fallback content paths. 2. Frontend NormCrossRefPanel (Next.js): expandable row in the IACE library norms tab now has a "Internationale Aequivalenzen anzeigen" button that lazy-loads /iace/norms-library/:id/crossref and renders a colour-coded table (relation + confidence). Region labels humanised (US — ANSI, China (GB), Japan (JIS), etc.). 3. Contract tests (Go): 4 new handler tests pinning the response shape of GetNormCrossRef and ListNormCrossRefs. Equivalent to an OpenAPI snapshot for these specific endpoints — ai-compliance-sdk has no full OpenAPI baseline yet (separate ticket). Tests: 6 renderer tests + 4 handler contract tests, all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.7 KiB
Go
160 lines
4.7 KiB
Go
package iace
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// RenderCrossRefAppendix builds a Markdown appendix for a tech-file section
|
|
// that lists the international equivalents of the given norm IDs. It is
|
|
// intended to be appended to the "Applied Harmonised Standards" section so
|
|
// the same tech file is usable for CE + US/CN/JP market submissions.
|
|
//
|
|
// Output format:
|
|
//
|
|
// ## Anhang: Internationale Aequivalenzen / International Cross-Reference
|
|
//
|
|
// Diese Tabelle ordnet die in dieser technischen Dokumentation angewandten
|
|
// EU-Normen den Pendants in anderen Maerkten zu. Die Spalte "Relation" gibt
|
|
// an, ob es sich um eine identische Uebernahme, eine teilweise Ueberdeckung
|
|
// oder ein abgeloestes (superseded_by) Dokument handelt. Vor Nutzung im
|
|
// jeweiligen Marktraum durch eine sachkundige Person verifizieren.
|
|
//
|
|
// | EU Norm | Region | International Identifier | Relation | Confidence |
|
|
// |---------|--------|--------------------------|----------|------------|
|
|
// ...
|
|
//
|
|
// If no norms have crossref entries, returns an empty string so the caller
|
|
// can skip the appendix entirely.
|
|
func RenderCrossRefAppendix(normIDs []string) string {
|
|
rows := collectCrossRefRows(normIDs)
|
|
if len(rows) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("\n\n## Anhang: Internationale Aequivalenzen / International Cross-Reference\n\n")
|
|
b.WriteString("Diese Tabelle ordnet die in dieser technischen Dokumentation angewandten EU-Normen den Pendants in anderen Maerkten zu (DIN, ANSI/NFPA/UL/OSHA, GB, JIS u.a.). Die Spalte ")
|
|
b.WriteString("**Relation** kennzeichnet `identical` (wortgleiche Uebernahme), `equivalent` (Kompatibilitaet auf Verfahrensebene), ")
|
|
b.WriteString("`partial` (Teilueberdeckung — vor Nutzung pruefen), `supersedes`/`superseded_by` (Ablaufverhaeltnis). ")
|
|
b.WriteString("Die Spalte **Confidence** drueckt die intern hinterlegte Verlaesslichkeit der Zuordnung aus. ")
|
|
b.WriteString("Vor Verwendung in einem Drittmarkt durch eine sachkundige Person verifizieren.\n\n")
|
|
b.WriteString("| EU Norm (verwendet) | Region | International Identifier | Relation | Confidence | Hinweis |\n")
|
|
b.WriteString("|---------------------|--------|--------------------------|----------|------------|---------|\n")
|
|
|
|
for _, row := range rows {
|
|
note := row.Notes
|
|
if note == "" {
|
|
note = "—"
|
|
}
|
|
// Escape pipes in identifier and note for markdown table safety.
|
|
fmt.Fprintf(&b,
|
|
"| %s | %s | %s | %s | %s | %s |\n",
|
|
escapeCell(row.SourceNorm),
|
|
escapeCell(row.Region),
|
|
escapeCell(row.Identifier),
|
|
escapeCell(row.Relation),
|
|
escapeCell(row.Confidence),
|
|
escapeCell(note),
|
|
)
|
|
}
|
|
|
|
b.WriteString("\n*Quelle: BreakPilot Cross-Reference Matrix. Keine Originalnormtexte reproduziert — nur Identifikatoren. Stand: Bezugsperiode der jeweiligen Norm-Bibliothek.*\n")
|
|
return b.String()
|
|
}
|
|
|
|
// crossRefRow is a flattened row of the matrix used by the renderer.
|
|
type crossRefRow struct {
|
|
SourceNorm string
|
|
Region string
|
|
Identifier string
|
|
Relation string
|
|
Confidence string
|
|
Notes string
|
|
}
|
|
|
|
// collectCrossRefRows expands the per-norm mapping list into a sorted slice
|
|
// of rows. Sort order: source norm ID first, then region in a canonical
|
|
// regional order so EU markets appear before non-EU.
|
|
func collectCrossRefRows(normIDs []string) []crossRefRow {
|
|
regionRank := map[string]int{
|
|
"EU-DIN": 0,
|
|
"INTL-ISO": 1,
|
|
"US-ANSI": 2,
|
|
"US-NFPA": 3,
|
|
"US-UL": 4,
|
|
"US-OSHA": 5,
|
|
"US-ASME": 6,
|
|
"US-ASTM": 7,
|
|
"US-SAE": 8,
|
|
"US-NIOSH": 9,
|
|
"US-FDA": 10,
|
|
"US-EPA": 11,
|
|
"US-NEMA": 12,
|
|
"US-NSF": 13,
|
|
"US-API": 14,
|
|
"US-CPSC": 15,
|
|
"US-AHRI": 16,
|
|
"US-ASHRAE": 17,
|
|
"US-FCC": 18,
|
|
"US-DOT": 19,
|
|
"US-MSHA": 20,
|
|
"US-FM": 21,
|
|
"US-AAR": 22,
|
|
"US-ACI": 23,
|
|
"US-ADA": 24,
|
|
"US-AAMA": 25,
|
|
"US-APA": 26,
|
|
"US-APSP": 27,
|
|
"US-EJMA": 28,
|
|
"US-ICC": 29,
|
|
"US-SMACNA": 30,
|
|
"CN-GB": 40,
|
|
"JP-JIS": 50,
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
var rows []crossRefRow
|
|
for _, id := range normIDs {
|
|
if seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
cr := GetNormCrossRef(id)
|
|
for _, m := range cr.Mappings {
|
|
rows = append(rows, crossRefRow{
|
|
SourceNorm: id,
|
|
Region: m.Region,
|
|
Identifier: m.Identifier,
|
|
Relation: m.Relation,
|
|
Confidence: m.Confidence,
|
|
Notes: m.Notes,
|
|
})
|
|
}
|
|
}
|
|
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
if rows[i].SourceNorm != rows[j].SourceNorm {
|
|
return rows[i].SourceNorm < rows[j].SourceNorm
|
|
}
|
|
ri, ok := regionRank[rows[i].Region]
|
|
if !ok {
|
|
ri = 99
|
|
}
|
|
rj, ok := regionRank[rows[j].Region]
|
|
if !ok {
|
|
rj = 99
|
|
}
|
|
return ri < rj
|
|
})
|
|
return rows
|
|
}
|
|
|
|
// escapeCell escapes pipes and newlines so a Markdown table cell does not break.
|
|
func escapeCell(s string) string {
|
|
s = strings.ReplaceAll(s, "|", "\\|")
|
|
s = strings.ReplaceAll(s, "\n", " ")
|
|
return s
|
|
}
|