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):@SpringBootTesttest that reads the full taxonomy viaTaxonomyNodeRepository, then recursively distributes each parent's score to its children using deterministic hash-based weights ((|code.hashCode()| % 100) + 1), guaranteeingsum(children) == parentat 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 assertingscores.size() > 2500and 1 assertingBR = 0propagates 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)
- Triggering command:
If you need me to access, download, or install something from one of these locations, you can either:
- Configure Actions setup steps to set up my environment, which run before the firewall is enabled
- Add the appropriate URLs or hosts to the custom allowlist in this repository's Copilot coding agent settings (admins only)
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, currently1) -
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.0means 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
SavedAnalysiswith current timestamp and version - Returns the DTO ready for JSON serialization
SavedAnalysis importFromJson(String json)
- Deserializes JSON
- Validates:
-
versionmust be1 -
requirementmust not be blank -
scoresmust not be null/empty - Node codes in
scoresshould be validated against the taxonomy (warn on unknown codes but don't reject)
-
- Returns a
SavedAnalysisDTO
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/exportwith the current state - Downloads the response as a
.jsonfile (using the existingdownloadBlobhelper)
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.