feat(iace): NTRS harvester + licence gate (FMEA P2 stage 1)

Stage 1 of the FailureKnowledge bulk loader: harvest NASA NTRS
lessons-learned with a strict public-reuse gate (NTRSUsable: public
release, not export-controlled/EAR/ITAR, not CUI, PUBLIC_USE_PERMITTED,
no third-party copyright). NTRSPDFURL prefers the PDF download for
downstream text/OCR extraction. GET /iace/failure-knowledge/ntrs runs
the live harvest and returns only the licence-clean records.

Pure parse/gate helpers are fixture-tested (usable vs ITAR / third-party
/ restricted / video-only); accepted licences also pass the FK allowlist.

Next: tuple extraction (abstract -> FailureKnowledge) + Playwright/OCR for
scanned PDFs -> bp_iace_failure_kb.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-13 00:16:41 +02:00
parent 3f90e40807
commit d27c1b9e7d
4 changed files with 231 additions and 0 deletions
@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin"
@@ -27,3 +28,34 @@ func (h *IACEHandler) ListFailureKnowledge(c *gin.Context) {
"total": len(items),
})
}
// HarvestNTRSFailures handles GET /failure-knowledge/ntrs.
// Live-harvests NASA NTRS lessons-learned metadata and returns only the records
// that pass the public-reuse licence gate (Stage 1 of the bulk loader). Tuple
// extraction from the abstracts is a downstream step.
func (h *IACEHandler) HarvestNTRSFailures(c *gin.Context) {
q := c.DefaultQuery("q", "lessons learned failure")
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "25"))
lessons, err := iace.FetchNTRSLessons(c.Request.Context(), q, limit)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
out := []gin.H{}
skipped := 0
for _, l := range lessons {
ok, lic := iace.NTRSUsable(l)
if !ok {
skipped++
continue
}
out = append(out, gin.H{
"id": l.ID, "title": l.Title, "abstract": l.Abstract,
"license": lic, "pdf_url": iace.NTRSPDFURL(l), "is_lessons_learned": l.IsLessonsLearned,
})
}
c.JSON(http.StatusOK, gin.H{
"query": q, "usable": out, "usable_count": len(out),
"skipped_non_open": skipped, "total_fetched": len(lessons),
})
}