fix: full-depth hierarchical scoring in mock JSON files — all 2567 taxonomy nodes, not just 8 roots

The three mock-score JSON files contained scores only for the 8 root taxonomy codes (BP, BR, CI, CO, CP, CR, IP, UA), misrepresenting how the scoring model works. Each root must be scored independently (0–100), and its score must be subdivided down the full hierarchy so that children always sum to their parent.

Scoring model (correct)

CO = 90     ← "Communications Services covers 90% of this requirement"
└── CO-1000 = 90   (only direct child → inherits full score)
    ├── CO-1011 = 9
    ├── CO-1056 = 30
    └── CO-1063 = 51    ← sum = 90 ✓
         ├── CO-1001 = 14
         └── ...

Roots whose score is 0 (e.g. BR = 0 in the voice-comms scenario) propagate zero to every descendant.

Changes

  • MockScoreGeneratorIT.java (new, opt-in): @SpringBootTest test that reads the full taxonomy via TaxonomyNodeRepository, then recursively distributes each parent's score to its children using deterministic hash-based weights ((|code.hashCode()| % 100) + 1), guaranteeing sum(children) == parent at every level with no remainder leakage. Activate with -DgenerateMockScores.

  • Three mock JSON files regenerated: secure-voice-comms.json, logistics-supply-chain.json, cyber-defence-monitoring.json — each now contains 2567 node entries (was 8).

  • SavedAnalysisServiceTest: fixed "root sum" and "dominant root" assertions to operate on the 8 root codes only (not the whole-tree aggregate); added 4 new tests — 3 asserting scores.size() > 2500 and 1 asserting BR = 0 propagates to all 307 BR descendants.

Key implementation note

Taxonomy node codes use opaque numeric IDs (CO-1000, CO-1001) — depth is NOT encoded in the code string. CO-1001 is a grandchild of CO, not a direct child. Parent-child relationships must be resolved via TaxonomyNode.parentCode, not string-prefix matching.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • mlrepo.djl.ai
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java -javaagent:/home/REDACTED/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/REDACTED/work/Taxonomy/Taxonomy/target/jacoco.exec -jar /home/REDACTED/work/Taxonomy/Taxonomy/target/surefire/surefirebooter-20260308144105967_3.jar /home/REDACTED/work/Taxonomy/Taxonomy/target/surefire 2026-03-08T14-41-05_626-jvmRun1 surefire-20260308144105967_1tmp surefire_0-20260308144105967_2tmp (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Feature: Save and Load Analysis Scores

Add the ability to export and import taxonomy analysis results (scores + reasons) as JSON files — both via REST API and the web UI. This enables:

  • Reproducibility: Save analysis results for documentation
  • Sharing: Share scored requirements with colleagues
  • Testing: Use saved analysis files as mock data for tests and screenshots
  • Offline review: Load previously analyzed requirements without re-running the LLM

Important semantic distinction

  • A node code present with value 0 → the node was evaluated and scored 0% (not relevant)
  • A node code absent / null → the node was not yet evaluated (analysis didn't reach it)

This distinction MUST be preserved in the JSON format and the import logic.


1. JSON Format (SavedAnalysis)

{
  "version": 1,
  "requirement": "Provide secure voice communications between HQ and deployed forces",
  "timestamp": "2026-03-08T14:30:00Z",
  "provider": "GEMINI",
  "scores": {
    "CO": 35,
    "CR": 25,
    "CP": 15,
    "IP": 12,
    "BP": 8,
    "CI": 3,
    "UA": 2,
    "BR": 0,
    "CO-1": 20,
    "CO-2": 5
  },
  "reasons": {
    "CO": "Voice communications are directly within CIS scope",
    "CR": "Communication resources are essential for voice infrastructure",
    "CO-1": "Primary voice communication systems"
  }
}
  • version: format version (integer, currently 1)
  • requirement: the business requirement text
  • timestamp: ISO 8601 when the analysis was performed (or when exported)
  • provider: which LLM provider generated these scores (informational)
  • scores: Map<String, Integer> — node code → score. 0 means explicitly scored zero. Absent means not evaluated.
  • reasons: Map<String, String> — node code → reason text (optional, may be sparse)

2. DTO Class

Create src/main/java/com/nato/taxonomy/dto/SavedAnalysis.java:

public class SavedAnalysis {
    private int version = 1;
    private String requirement;
    private String timestamp;  // ISO 8601
    private String provider;
    private Map<String, Integer> scores;    // code → score (0 = scored zero, absent = not evaluated)
    private Map<String, String> reasons;    // code → reason (optional)
    // getters, setters
}

3. Service: SavedAnalysisService

Create src/main/java/com/nato/taxonomy/service/SavedAnalysisService.java:

SavedAnalysis buildExport(String requirement, Map<String, Integer> scores, Map<String, String> reasons, String provider)

  • Populates a SavedAnalysis with current timestamp and version
  • Returns the DTO ready for JSON serialization

SavedAnalysis importFromJson(String json)

  • Deserializes JSON
  • Validates:
    • version must be 1
    • requirement must not be blank
    • scores must not be null/empty
    • Node codes in scores should be validated against the taxonomy (warn on unknown codes but don't reject)
  • Returns a SavedAnalysis DTO

SavedAnalysis loadFromClasspath(String resourcePath)

  • Loads a JSON file from the classpath (for tests and mock data)
  • Delegates to importFromJson()

4. REST API Endpoints

Add to ApiController.java:

POST /api/scores/export

Request body:

{
  "requirement": "...",
  "scores": { "CO": 35, ... },
  "reasons": { "CO": "...", ... },
  "provider": "GEMINI"
}

Response: Returns a SavedAnalysis JSON (with timestamp and version added). The frontend can then trigger a download of this JSON.

POST /api/scores/import

Request body: The full SavedAnalysis JSON (as uploaded by the user).

Response:

{
  "requirement": "...",
  "scores": { "CO": 35, ... },
  "reasons": { "CO": "...", ... },
  "provider": "GEMINI",
  "warnings": ["Unknown node code: XYZ"]
}

Returns the validated scores/reasons that the frontend can apply to the tree, plus any warnings about unknown node codes.

5. Frontend (UI)

Export button

In index.html, add a new button in the exportGroup:

<button id="exportJson" class="btn btn-sm btn-outline-info" title="Download scores as JSON file">📥 JSON</button>

In taxonomy-export.js, add exportJson(scores, reasons, businessText, provider):

  • Calls POST /api/scores/export with the current state
  • Downloads the response as a .json file (using the existing downloadBlob helper)

Import button

In index.html, add a new button outside the exportGroup (always visible, near the analyze button or in the toolbar):

<button id="importJson" class="btn btn-sm btn-outline-secondary" title="Load saved analysis from JSON file">📤 Load Scores</button>
<input type="file" id="importJsonFile" accept=".json" style="display:none;">

In taxonomy.js, add import handler:

  • Click on importJson → triggers hidden file input
  • On file selected: read file, POST to /api/scores/import
  • On success: set currentScores, currentReasons, update the business text field, render the tree with scores, show ...

This pull request was created from Copilot chat.


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Merge request reports

Loading