feat(iace): wire crossref into tech-file, library UI, and contract tests

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>
This commit is contained in:
Benjamin Admin
2026-05-22 09:48:07 +02:00
parent cf6005a47c
commit 0a84c747f2
6 changed files with 587 additions and 2 deletions
@@ -153,8 +153,61 @@ func (g *TechFileGenerator) GenerateSection(ctx context.Context, projectID uuid.
})
if err != nil {
// LLM unavailable — return structured fallback with real project data
return buildFallbackContent(sctx, sectionType), nil
return appendCrossRefIfApplicable(buildFallbackContent(sctx, sectionType), sctx, sectionType), nil
}
return resp.Message.Content, nil
return appendCrossRefIfApplicable(resp.Message.Content, sctx, sectionType), nil
}
// appendCrossRefIfApplicable adds the international cross-reference appendix
// (DIN/ANSI/GB/JIS) to the "standards_applied" section. For other section
// types it returns content unchanged. The appendix is built deterministically
// from the in-process registry, so it is never hallucinated by the LLM.
func appendCrossRefIfApplicable(content string, sctx *SectionGenerationContext, sectionType string) string {
if sectionType != SectionStandardsApplied {
return content
}
normIDs := suggestNormIDsForProject(sctx)
appendix := RenderCrossRefAppendix(normIDs)
if appendix == "" {
return content
}
return content + appendix
}
// suggestNormIDsForProject reuses the existing SuggestNorms heuristic to pick
// the norms most likely applicable to this project. We only need the IDs;
// the rest of the SuggestNorms output (scores, reasons) is discarded.
func suggestNormIDsForProject(sctx *SectionGenerationContext) []string {
if sctx == nil || sctx.Project == nil {
return nil
}
hazardCats := make([]string, 0, len(sctx.Hazards))
seenCat := map[string]bool{}
for _, h := range sctx.Hazards {
if h.Category != "" && !seenCat[h.Category] {
seenCat[h.Category] = true
hazardCats = append(hazardCats, h.Category)
}
}
result := SuggestNorms(sctx.Project.MachineType, hazardCats, nil)
if result == nil {
return nil
}
ids := make([]string, 0, result.Total)
seenID := map[string]bool{}
push := func(suggs []NormSuggestion) {
for _, s := range suggs {
if s.Norm.ID == "" || seenID[s.Norm.ID] {
continue
}
seenID[s.Norm.ID] = true
ids = append(ids, s.Norm.ID)
}
}
push(result.ANorms)
push(result.B1Norms)
push(result.B2Norms)
push(result.CNorms)
return ids
}