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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user