Assessment Report Converter (CSV → report.json)
What this skill does
You take a row-oriented CSV of migration issues and produce a canonical report.json conforming to the
assessment report schema. Once written to the versioned reports directory, the
report is picked up by the assessment UI and by the solution-resolution logic — exactly like a native AppCAT report.
This is an LLM-driven conversion, not a fixed mapping script. CSV layouts vary between tools and change over time,
so you read the file, decide what each column means, classify every row, and author report.json. You do not
rely on hard-coded column names.
A small helper script — scripts/report_tools.sh (bash + jq) or its PowerShell twin
scripts/report_tools.ps1 (PowerShell 7+) — provides only the deterministic pieces
you should never guess:
list-solutions — discover the available migration solutions.
rules-for-solution — get the canonical ruleId(s) for a chosen solution.
upgrade-solutions — the canonical JDK / Spring Boot / Spring Framework / Jakarta EE upgrade solutions + ruleIds.
validate — structural + consistency validation of the finished report.json.
flowchart LR
A[Read CSV] --> B[Understand columns] --> C[Find projects] --> D{Audit each row}
D -->|CVE / CWE| E[Security finding]
D -->|Needs upgrade| F[Upgrade incident]
D -->|Other| G[Solution → ruleId → incident]
E --> H[Assemble report]
F --> H
G --> H
H --> I[Validate] --> J[Save report.json] --> K[Summarize]
Input parameters
csv-path (mandatory): Path to the source CSV file.
workspace-path (optional): Output root. Defaults to the current directory. The report is written to
{workspace-path}/.github/modernize/reports/report-{reportId}/report.json.
producer (optional): A label identifying the source tool, stored in report.producer (e.g. "Black Duck").
Defaults to "CSV import".
When to use this skill
Use this when you have a CSV — not an AppCAT report.json — and you want it to behave like a real assessment
report: a Black Duck / third-party export, or a hand-maintained spreadsheet of migration issues. Do not use it to
run analysis from source code (that is the assessment skill).
The helper script
The helper ships as two interchangeable implementations of the same CLI — use whichever fits the machine:
scripts/report_tools.sh (bash + jq) and
scripts/report_tools.ps1 (PowerShell 7+, no extra dependencies). Both behave
identically and never read the CSV. Run them from the scripts/ directory.
# bash + jq
./report_tools.sh list-solutions [--query KW] [--type Formula|Chat] [--ids-only]
./report_tools.sh rules-for-solution <solutionId>
./report_tools.sh upgrade-solutions
./report_tools.sh validate <path/to/report.json>
# PowerShell 7+
pwsh ./report_tools.ps1 list-solutions [--query KW] [--type Formula|Chat] [--ids-only]
pwsh ./report_tools.ps1 rules-for-solution <solutionId>
pwsh ./report_tools.ps1 upgrade-solutions
pwsh ./report_tools.ps1 validate <path/to/report.json>
list-solutions prints the matching solutions from scripts/solution-mapping.json as
whole JSON objects (each always has solutionId, name, type, and tooltip; some also carry effort / prompt);
--query filters by substring on id/name/tooltip.
rules-for-solution prints { solutionId, ruleCount, rules: [{ruleId, sourceCategory}], preferredRuleId }. An empty
list means the solution has no rule (e.g. a security-only solution) — do not invent a ruleId.
upgrade-solutions prints, per component (jdk, spring-boot, spring-framework, jakarta-ee), the solutionId
and its resolved preferredRuleId.
validate runs structural + cross-field consistency checks
(incident→rule references, required domain/category rule fields, enum values, security-finding shape and dedupe, and
metadata.domains↔content consistency). Exit code is 0 valid, 1 invalid, 2 when the report can't be read/parsed.
It accepts a --schema <path> flag for CLI compatibility but ignores it — the checks are self-contained.
Reference material
Detailed reference material is consolidated in the Reference section at the end of this file. The workflow
below links to the relevant part at the step where you need it — you do not have to open any separate files:
- Report structure — the top-level
report.json shape and the authoring conventions the schema can't express (the schema owns the field shape).
- Assessment domains — the
metadata.domains values and when to include each.
- Security severity mapping — normalizing a source CVE/CWE severity to the security
mandatory|potential|optional scale.
- Rule classification — the required
domain / category rule fields that group and render a rule's incidents.
Workflow
Step 1 — Understand the report structure (do this first)
Read the authoritative schema scripts/assessment-report.schema.json — it is
the source of truth for the field shape. The report is a single root object { version, producer, metadata, projects, rules, security? }, and additionalProperties is false almost everywhere (there is no summary object). The
report structure reference adds the authoring conventions the schema can't express.
You must know the required fields before authoring anything.
Step 2 — Read and understand the CSV
Read the CSV file directly (headers + a representative sample of rows). Then interpret the columns by meaning,
since names and order vary by tool. Identify whichever of these the CSV actually carries (any may be absent):
- a project / module / service identifier,
- an application name,
- an issue title and description,
- a severity / criticality / priority,
- a category / domain / type,
- a CVE / CWE identifier,
- an affected component / package / library and its version,
- a file path and line,
- an effort / story-point estimate,
- a reference / URL.
When a concept is missing, leave the corresponding report field empty or at its default — never fabricate data.
Step 3 — Determine the projects
Work out how many projects the CSV describes and group rows accordingly:
- If a project/module/service column exists, group rows by it — one project per distinct value, using that value
as
project.path.
- If the CSV has no project information, create a single project with
path "." and minimal properties
(appName ""; leave optional jdkVersion / frameworks / languages / tools off or empty).
- Populate
project.properties only from what the CSV actually provides; otherwise keep them empty. Only appName
is required.
Every incident you create later belongs to exactly one of these projects.
Step 4 — Audit each row, within its project
Classify every row into exactly one of three branches and attach the result to the row's project. Every rule you add to
rules{} must set its domain and category fields (that is what groups and renders its incidents) — see
rule classification. Incidents do not require any label.
CVE / CWE → security finding (directly).
When a row carries a CVE-… / CWE-… identifier (or unambiguously describes one), add a security finding
to report.security[]. Capture as much of the column as possible: id (the CVE/CWE token), title, category,
severity (the security scale mandatory | potential | optional — normalize the source severity per
security issue severity mapping), description, evidence.files
(affected paths), evidence.explanation, and optional storyPoint. Merge by id — one finding per CVE/CWE;
accumulate evidence files and keep the strongest severity (mandatory > potential > optional). Security
findings are not incidents; they live only in report.security[] (there is no summary to count them in).
Needs a major-component version upgrade → upgrade incident.
If the row implies upgrading a major component — JDK, Spring Boot, Spring Framework, or Java EE /
Jakarta EE (e.g. a CVE against spring-boot, an out-of-support runtime, an explicit "upgrade JDK") — run
the upgrade-solutions command, pick the component, and use its preferredRuleId. Add that rule to rules{}
(severity: "mandatory", domain: "java-upgrade", a category, a reasonable effort) and add an incident to the
project — one upgrade rule per component per project, one incident per triggering row. A CVE that
implies an upgrade produces both a security finding (branch 1) and an upgrade incident — that is what makes the
upgrade resolve as a migration solution.
Any other issue → solution → ruleId → incident.
Find the migration solution that fits the issue with
the list-solutions --query <keyword> command, then get its canonical ruleId with
rules-for-solution <solutionId> (use preferredRuleId). Add that rule to rules{} (with domain,
category, severity, effort) and an incident to the project. If no solution fits, you may still record the issue
with a clear synthetic ruleId (it just won't
carry an automatic Formula solution) — or leave it for the "remaining" list in your summary. Either way, report it.
Process rows in parallel
Classifying a row is independent work, so for large CSVs do it concurrently rather than one row at a time:
- Batch the rows (e.g. 20–50 per batch, or one batch per project) and dispatch the batches in parallel — launch
several
Explore/worker subagents at once, each auditing its batch into a partial result (security findings, upgrade
hits, and ordinary incidents with their resolved ruleIds). Ask each worker to return structured JSON; it does not
write files.
- Share the deterministic lookups. Run the
upgrade-solutions command and the list-solutions /
rules-for-solution queries once up front (results are stable) and pass them to the workers, so parallel batches
don't repeat the same lookups or race on them.
- Keep workers side-effect free, then merge sequentially in one place so shared state stays correct:
- Security findings — merge by
id (one finding per CVE/CWE; union evidence.files, keep the strongest severity).
- Upgrade rules — collapse to one rule per
(project, component); keep every triggering incident.
incidentIds — assign "<ruleId>/<n>" after the merge, never inside a worker (so ids are deterministic
regardless of batch order).
- If the CSV is small, just process the rows sequentially — the parallel split only pays off at scale.
Step 5 — Assemble the report and finalize metadata
There is no summary object to compute — assemble the top-level document and fill metadata:
projects[] = your projects, each with properties (only appName required) and its incidents[].
rules{} = every distinct rule you referenced, keyed by ruleId, each with id, title, severity, effort,
domain, and category.
report.security[] = the deduped findings (omit or leave empty when there are none).
metadata.domains = the assessment domains your report actually has content for, consistent with your rule.domain
values — see assessment domains.
metadata.mode (optional) = "full" when any security finding exists, else "issue-only".
metadata.status = "completed"; metadata.targetIds = the target ids in scope (may be empty).
metadata.id (and the report-directory id) = analysisStartTime formatted yyyyMMddHHmmss (UTC); use the current UTC
time when the CSV has no timestamp.
Step 6 — Write and validate
Write report.json to the versioned location (Output location), then validate and fix until clean:
# bash + jq
./report_tools.sh validate "<workspace>/.github/modernize/reports/report-<id>/report.json"
# …or PowerShell 7+
pwsh ./report_tools.ps1 validate "<workspace>/.github/modernize/reports/report-<id>/report.json"
Resolve every reported consistency error before finishing.
Step 7 — Summarize for the user
Report a concise conversion summary:
- Converted rows — number of security findings (CVE/CWE), upgrade incidents (with components), and ordinary issue
incidents (with the solutions they mapped to); plus the project / rule / incident counts and the report path + id.
- Remaining rows — rows you could not confidently map, and why (no matching solution, ambiguous column, missing id).
- Suggestions — concrete next steps (e.g. pick a specific solution for a remaining row, add a project column to the
CSV, supply severities), so the user can close the gaps.
Output location
{workspace-path}/.github/modernize/reports/report-{reportId}/report.json
reportId = the report's metadata.id from Step 5 (analysisStartTime as yyyyMMddHHmmss, UTC).
- Consider copying the original CSV next to
report.json as source.csv for provenance.
Success criteria
- ✅
report.json is written to the versioned reports directory and the validate command reports VALID.
- ✅ Projects reflect the CSV (one per module/service, or a single project with
appName: "" when none is given).
- ✅ CVE/CWE rows are
security[] findings (deduped by id) with a mandatory|potential|optional severity; mode is full.
- ✅ Rows implying a JDK / Spring Boot / Spring Framework / Jakarta EE upgrade add a mandatory upgrade incident whose
ruleId came from the upgrade-solutions command.
- ✅ Other issues map to a solution's canonical
ruleId (via list-solutions + rules-for-solution) wherever one fits.
- ✅ Every rule carries
domain and category fields; every incident's ruleId resolves to a rule in rules{}; CVE/CWE
findings stay in security[], not incidents[].
- ✅
metadata.domains matches the report's content and the emitted rule.domain values (enforced by validate).
- ✅ The user gets a summary of converted rows, remaining rows, and suggestions.
Troubleshooting
validate reports a consistency error (e.g. an incident ruleId with no matching rule, or a
domains/security mismatch) — the message names the exact field; fix that field.
- Don't add fields the schema doesn't allow (e.g. a
summary object, or issues/storyPoints on a project). The
helper's validate is lenient about extra keys, but the app's importer enforces additionalProperties: false and will
reject the report — this schema has no summary. Keep to the documented shape.
- A row has a CVE/CWE and needs an upgrade — emit both: a
security[] finding and an upgrade incident. They are
not duplicates; the finding documents the vulnerability, the incident drives the upgrade solution.
- No solution fits an issue —
list-solutions --query returns nothing useful. Record the issue with a synthetic,
descriptive ruleId (no Formula will attach) or list it under "remaining" with a suggestion.
rules-for-solution returns an empty list — that solution has no rule (often security-only). Don't fabricate a
ruleId; handle the issue via the security-finding path or pick a different solution.
- Wrong severity enum — rule/incident severity is the 4-value
mandatory|potential|optional|information enum;
security-finding severity uses the same scale minus information (mandatory|potential|optional, since a finding
is always at least optional). Don't use the old critical|high|medium|low|info values.
Reference
Consolidated reference material. The workflow above links here at the step where each part is needed.
Report structure
You author a single report.json conforming to the authoritative schema
scripts/assessment-report.schema.json. The schema is one root object
(draft-07, its sub-types live under definitions) with the top-level shape { version, producer, metadata, projects, rules, security? }. Read the schema for the exact required fields, types, and enums per object — it is the source of
truth and the validate command checks it, so this section does not restate the field list. additionalProperties
is false almost everywhere, so do not invent fields (there is no summary object, and projects have no issues /
storyPoints).
Authoring rules of thumb — the conventions and cross-field rules the schema can't fully express:
version is the string "1.0.0" (the schema accepts any string; this is the value to use).
- Rule/incident
severity is the enum mandatory | potential | optional | information. Map source severities by meaning
(e.g. critical/blocker → mandatory, major/medium → potential, minor/low → optional, info → information).
- Classification is done with rule fields, not labels. Each
rules{} entry sets domain
(cloud-readiness | java-upgrade | security) and category (a free string heading) directly — see
Rule classification. rules{}.labels and incidents[].labels are optional free-form arrays;
you normally leave them out.
- Security-finding
severity uses the report criticality scale mandatory | potential | optional (the rule Severity
values minus information — a security finding is always at least optional) — normalize per
Security severity mapping.
metadata.domains records which assessment domains the report has content for — see
Assessment domains. It must be consistent with the rule.domain values you emit.
incidentId convention: "<ruleId>/<n>" (n is a per-rule counter).
locationKind = "source-file" when a file path is present, else "unknown".
status is normally "completed".
metadata.mode is optional; use "full" when the report has security findings, else "issue-only".
Assessment domains
metadata.domains is a string[] recording which assessment domains produced the report. Allowed values are
cloud-readiness, java-upgrade, and security.
| Domain |
Meaning |
When to include it for a CSV conversion |
cloud-readiness |
Azure cloud-migration issues |
any ordinary issue → solution incident (branch 3) |
java-upgrade |
JDK / Spring Boot / Spring Framework / Jakarta EE upgrades |
any upgrade incident (branch 2) |
security |
CVE / CWE vulnerability findings |
report.security[] is non-empty (branch 1) |
- The native Java default is
["cloud-readiness", "java-upgrade"].
- Set
metadata.domains to exactly the domains your report has content for — don't list security with no
findings, or java-upgrade with no upgrade incidents. validate flags a security/report.security mismatch in
either direction.
metadata.domains must be consistent with the rule.domain field on your rules — every rule.domain value you
emit should appear in metadata.domains.
Security severity mapping
A security finding's severity uses the report criticality scale — mandatory | potential | optional. This is the rule
Severity enum minus information (a security finding is always at least optional). The extension renders these
values directly, so there is no separate security severity scale and no conversion step.
Map the source CVE/CWE severity by meaning to the nearest value (case-insensitive):
| Source severity (CSV, case-insensitive) |
Report security[].severity |
critical / blocker |
mandatory |
high |
mandatory |
medium / moderate |
potential |
low |
optional |
| anything else / unknown / missing |
optional |
- When merging duplicate findings by
id, keep the strongest severity (mandatory > potential > optional).
- Security findings live in
report.security[] only; there is no summary object to key by severity. validate checks
each finding's severity is one of the three values and that findings are unique by id.
- Include the
security domain in metadata.domains whenever report.security[] is non-empty (and only then).
Rule classification
In this schema a rule is classified with fields on the rule object, not with labels. Every rules{} entry is
required to carry a domain and a category (the validate command enforces both, and the schema rejects a
rule that is missing them).
| Field |
Value |
Effect |
domain |
cloud-readiness | java-upgrade | security |
Groups the rule under that domain tab. Use the same domain you list in metadata.domains. Required. |
category |
the issue category heading (e.g. postgresql, java-version-upgrade, deprecated-apis) |
Shown as the group heading. Required. |
Choosing category (and the source label). Each mapped rule also has a sourceCategory in the solution mapping,
which you get from rules-for-solution <solutionId> (rules[].sourceCategory). The UI builds the group heading as
category when it equals sourceCategory (or sourceCategory is empty / null), otherwise as
category (sourceCategory). So:
- When a rule's
sourceCategory is non-null, set its category to exactly that value — e.g.
mi-postgresql → rule azure-database-postgresql-02000, sourceCategory: "postgresql" → category: "postgresql",
which renders as one clean Postgresql heading (mismatching it, e.g. category: "database", would render the
doubled Database (Postgresql)).
- When
sourceCategory is null (e.g. the JDK-upgrade rules), choose a sensible category yourself
(e.g. upgrade) — it renders as-is.
Labels are optional. rules{}.labels and incidents[].labels are optional free-form string[]s in the schema.
Classification no longer depends on them, so you normally leave them out. The engine may still emit context labels
(target=, os=, capability=) on native reports, but when hand-authoring a CSV conversion you do not need any label
to make content render — the required domain/category fields do that.
1---2name: assessment-report-converter3description: Convert an arbitrary CSV report (e.g. a Black Duck export or a custom migration-issue inventory) into a schema-valid assessment `report.json` the modernization pipeline can consume — so it appears in the assessment UI and migration solutions resolve automatically. This skill is LLM-driven: you read and interpret the CSV yourself and author `report.json` by hand against the schema. A small helper script only does deterministic lookups (migration solutions, the ruleId for a solution) and validates the finished report. There is NO "convert everything" script and NO assumed column layout. Triggers: "convert csv to assessment report", "import csv report", "turn this spreadsheet into a report.json", "Black Duck csv to report", "build report.json from csv", "migrate a third-party assessment export". NOT for: AppCAT-style analysis from source (use `assessment`), generating a modernization plan (use `create-modernization-plan`), or editing a report.json the pipeline already produced.4---56# Assessment Report Converter (CSV → report.json)78## What this skill does910You take a **row-oriented CSV** of migration issues and produce a canonical **`report.json`** conforming to the11assessment report schema. Once written to the versioned reports directory, the12report is picked up by the assessment UI and by the solution-resolution logic — exactly like a native AppCAT report.1314**This is an LLM-driven conversion, not a fixed mapping script.** CSV layouts vary between tools and change over time,15so *you* read the file, decide what each column means, classify every row, and author `report.json`. You do **not**16rely on hard-coded column names.1718A small helper script — [scripts/report_tools.sh](scripts/report_tools.sh) (bash + jq) or its PowerShell twin19[scripts/report_tools.ps1](scripts/report_tools.ps1) (PowerShell 7+) — provides only the deterministic pieces20you should never guess:2122- **`list-solutions`** — discover the available migration solutions.23- **`rules-for-solution`** — get the canonical `ruleId`(s) for a chosen solution.24- **`upgrade-solutions`** — the canonical JDK / Spring Boot / Spring Framework / Jakarta EE upgrade solutions + ruleIds.25- **`validate`** — structural + consistency validation of the finished `report.json`.2627```mermaid28flowchart LR29 A[Read CSV] --> B[Understand columns] --> C[Find projects] --> D{Audit each row}30 D -->|CVE / CWE| E[Security finding]31 D -->|Needs upgrade| F[Upgrade incident]32 D -->|Other| G[Solution → ruleId → incident]33 E --> H[Assemble report]34 F --> H35 G --> H36 H --> I[Validate] --> J[Save report.json] --> K[Summarize]37```3839## Input parameters4041- `csv-path` (mandatory): Path to the source CSV file.42- `workspace-path` (optional): Output root. Defaults to the current directory. The report is written to43 `{workspace-path}/.github/modernize/reports/report-{reportId}/report.json`.44- `producer` (optional): A label identifying the source tool, stored in `report.producer` (e.g. `"Black Duck"`).45 Defaults to `"CSV import"`.4647## When to use this skill4849Use this when you have a **CSV** — not an AppCAT `report.json` — and you want it to behave like a real assessment50report: a Black Duck / third-party export, or a hand-maintained spreadsheet of migration issues. Do **not** use it to51run analysis from source code (that is the `assessment` skill).5253## The helper script5455The helper ships as two interchangeable implementations of the same CLI — use whichever fits the machine:56[scripts/report_tools.sh](scripts/report_tools.sh) (**bash + jq**) and57[scripts/report_tools.ps1](scripts/report_tools.ps1) (**PowerShell 7+**, no extra dependencies). Both behave58identically and never read the CSV. Run them from the `scripts/` directory.5960```bash61# bash + jq62./report_tools.sh list-solutions [--query KW] [--type Formula|Chat] [--ids-only]63./report_tools.sh rules-for-solution <solutionId>64./report_tools.sh upgrade-solutions65./report_tools.sh validate <path/to/report.json>66```6768```powershell69# PowerShell 7+70pwsh ./report_tools.ps1 list-solutions [--query KW] [--type Formula|Chat] [--ids-only]71pwsh ./report_tools.ps1 rules-for-solution <solutionId>72pwsh ./report_tools.ps1 upgrade-solutions73pwsh ./report_tools.ps1 validate <path/to/report.json>74```7576- `list-solutions` prints the matching solutions from [scripts/solution-mapping.json](scripts/solution-mapping.json) as77 whole JSON objects (each always has `solutionId`, `name`, `type`, and `tooltip`; some also carry `effort` / `prompt`);78 `--query` filters by substring on id/name/tooltip.79- `rules-for-solution` prints `{ solutionId, ruleCount, rules: [{ruleId, sourceCategory}], preferredRuleId }`. An empty80 list means the solution has **no** rule (e.g. a security-only solution) — do not invent a ruleId.81- `upgrade-solutions` prints, per component (`jdk`, `spring-boot`, `spring-framework`, `jakarta-ee`), the `solutionId`82 and its resolved `preferredRuleId`.83- `validate` runs structural + cross-field consistency checks84 (incident→rule references, required `domain`/`category` rule fields, enum values, security-finding shape and dedupe, and85 `metadata.domains`↔content consistency). Exit code is `0` valid, `1` invalid, `2` when the report can't be read/parsed.86 It accepts a `--schema <path>` flag for CLI compatibility but ignores it — the checks are self-contained.8788## Reference material8990Detailed reference material is consolidated in the [Reference](#reference) section at the end of this file. The workflow91below links to the relevant part at the step where you need it — you do not have to open any separate files:9293- [Report structure](#report-structure) — the top-level `report.json` shape and the authoring conventions the schema can't express (the schema owns the field shape).94- [Assessment domains](#assessment-domains) — the `metadata.domains` values and when to include each.95- [Security severity mapping](#security-severity-mapping) — normalizing a source CVE/CWE severity to the security `mandatory|potential|optional` scale.96- [Rule classification](#rule-classification) — the required `domain` / `category` rule fields that group and render a rule's incidents.9798## Workflow99100### Step 1 — Understand the report structure (do this first)101102Read the authoritative schema [scripts/assessment-report.schema.json](scripts/assessment-report.schema.json) — it is103the source of truth for the field shape. The report is a single root object `{ version, producer, metadata, projects,104rules, security? }`, and `additionalProperties` is `false` almost everywhere (there is **no** `summary` object). The105[report structure reference](#report-structure) adds the authoring conventions the schema can't express.106You must know the required fields before authoring anything.107108### Step 2 — Read and understand the CSV109110Read the CSV file directly (headers + a representative sample of rows). Then **interpret the columns by meaning**,111since names and order vary by tool. Identify whichever of these the CSV actually carries (any may be absent):112113- a **project / module / service** identifier,114- an **application** name,115- an issue **title** and **description**,116- a **severity / criticality / priority**,117- a **category / domain / type**,118- a **CVE / CWE** identifier,119- an **affected component / package / library** and its **version**,120- a **file** path and **line**,121- an **effort / story-point** estimate,122- a **reference / URL**.123124When a concept is missing, leave the corresponding report field empty or at its default — never fabricate data.125126### Step 3 — Determine the projects127128Work out **how many projects** the CSV describes and group rows accordingly:129130- If a project/module/service column exists, group rows by it — one project per distinct value, using that value131 as `project.path`.132- If the CSV has **no project information**, create a **single project** with `path` `"."` and minimal properties133 (`appName` `""`; leave optional `jdkVersion` / `frameworks` / `languages` / `tools` off or empty).134- Populate `project.properties` **only** from what the CSV actually provides; otherwise keep them empty. Only `appName`135 is required.136137Every incident you create later belongs to exactly one of these projects.138139### Step 4 — Audit each row, within its project140141Classify every row into exactly one of three branches and attach the result to the row's project. Every rule you add to142`rules{}` must set its `domain` and `category` **fields** (that is what groups and renders its incidents) — see143[rule classification](#rule-classification). Incidents do **not** require any label.1441451. **CVE / CWE → security finding (directly).**146 When a row carries a `CVE-…` / `CWE-…` identifier (or unambiguously describes one), add a security finding147 to `report.security[]`. Capture **as much of the column as possible**: `id` (the CVE/CWE token), `title`, `category`,148 `severity` (the security scale `mandatory | potential | optional` — normalize the source severity per149 [security issue severity mapping](#security-severity-mapping)), `description`, `evidence.files`150 (affected paths), `evidence.explanation`, and optional `storyPoint`. **Merge by id** — one finding per CVE/CWE;151 accumulate evidence files and keep the strongest severity (`mandatory` > `potential` > `optional`). Security152 findings are **not** incidents; they live only in `report.security[]` (there is no summary to count them in).1531542. **Needs a major-component version upgrade → upgrade incident.**155 If the row implies upgrading a major component — **JDK**, **Spring Boot**, **Spring Framework**, or **Java EE /156 Jakarta EE** (e.g. a CVE against `spring-boot`, an out-of-support runtime, an explicit "upgrade JDK") — run157 the `upgrade-solutions` command, pick the component, and use its `preferredRuleId`. Add that rule to `rules{}`158 (`severity: "mandatory"`, `domain: "java-upgrade"`, a `category`, a reasonable `effort`) and add an **incident** to the159 project — one upgrade rule per component per project, one incident per triggering row. A CVE that160 implies an upgrade produces **both** a security finding (branch 1) **and** an upgrade incident — that is what makes the161 upgrade resolve as a migration solution.1621633. **Any other issue → solution → ruleId → incident.**164 Find the migration solution that fits the issue with165 the `list-solutions --query <keyword>` command, then get its canonical ruleId with166 `rules-for-solution <solutionId>` (use `preferredRuleId`). Add that rule to `rules{}` (with `domain`,167 `category`, `severity`, `effort`) and an incident to the project. If no solution fits, you may still record the issue168 with a clear **synthetic** ruleId (it just won't169 carry an automatic Formula solution) — or leave it for the "remaining" list in your summary. Either way, report it.170171#### Process rows in parallel172173Classifying a row is independent work, so for large CSVs do it concurrently rather than one row at a time:174175- **Batch the rows** (e.g. 20–50 per batch, or one batch per project) and dispatch the batches **in parallel** — launch176 several `Explore`/worker subagents at once, each auditing its batch into a partial result (security findings, upgrade177 hits, and ordinary incidents with their resolved ruleIds). Ask each worker to return structured JSON; it does **not**178 write files.179- **Share the deterministic lookups.** Run the `upgrade-solutions` command and the `list-solutions` /180 `rules-for-solution` queries **once up front** (results are stable) and pass them to the workers, so parallel batches181 don't repeat the same lookups or race on them.182- **Keep workers side-effect free**, then **merge sequentially** in one place so shared state stays correct:183 - **Security findings** — merge by `id` (one finding per CVE/CWE; union `evidence.files`, keep the strongest severity).184 - **Upgrade rules** — collapse to one rule per `(project, component)`; keep every triggering incident.185 - **`incidentId`s** — assign `"<ruleId>/<n>"` **after** the merge, never inside a worker (so ids are deterministic186 regardless of batch order).187- If the CSV is small, just process the rows sequentially — the parallel split only pays off at scale.188189### Step 5 — Assemble the report and finalize metadata190191There is **no** `summary` object to compute — assemble the top-level document and fill `metadata`:192193- `projects[]` = your projects, each with `properties` (only `appName` required) and its `incidents[]`.194- `rules{}` = every distinct rule you referenced, keyed by ruleId, each with `id`, `title`, `severity`, `effort`,195 `domain`, and `category`.196- `report.security[]` = the deduped findings (omit or leave empty when there are none).197- `metadata.domains` = the assessment domains your report actually has content for, consistent with your `rule.domain`198 values — see [assessment domains](#assessment-domains).199- `metadata.mode` (optional) = `"full"` when any security finding exists, else `"issue-only"`.200- `metadata.status` = `"completed"`; `metadata.targetIds` = the target ids in scope (may be empty).201- `metadata.id` (and the report-directory id) = `analysisStartTime` formatted `yyyyMMddHHmmss` (UTC); use the current UTC202 time when the CSV has no timestamp.203204### Step 6 — Write and validate205206Write `report.json` to the versioned location ([Output location](#output-location)), then validate and fix until clean:207208```bash209# bash + jq210./report_tools.sh validate "<workspace>/.github/modernize/reports/report-<id>/report.json"211# …or PowerShell 7+212pwsh ./report_tools.ps1 validate "<workspace>/.github/modernize/reports/report-<id>/report.json"213```214215Resolve every reported consistency error before finishing.216217### Step 7 — Summarize for the user218219Report a concise conversion summary:220221- **Converted rows** — number of security findings (CVE/CWE), upgrade incidents (with components), and ordinary issue222 incidents (with the solutions they mapped to); plus the project / rule / incident counts and the report path + id.223- **Remaining rows** — rows you could not confidently map, and *why* (no matching solution, ambiguous column, missing id).224- **Suggestions** — concrete next steps (e.g. pick a specific solution for a remaining row, add a project column to the225 CSV, supply severities), so the user can close the gaps.226227## Output location228229- `{workspace-path}/.github/modernize/reports/report-{reportId}/report.json`230- `reportId` = the report's `metadata.id` from Step 5 (`analysisStartTime` as `yyyyMMddHHmmss`, UTC).231- Consider copying the original CSV next to `report.json` as `source.csv` for provenance.232233## Success criteria234235- ✅ `report.json` is written to the versioned reports directory and the `validate` command reports **VALID**.236- ✅ Projects reflect the CSV (one per module/service, or a single project with `appName: ""` when none is given).237- ✅ CVE/CWE rows are `security[]` findings (deduped by id) with a `mandatory|potential|optional` severity; `mode` is `full`.238- ✅ Rows implying a JDK / Spring Boot / Spring Framework / Jakarta EE upgrade add a mandatory upgrade incident whose239 `ruleId` came from the `upgrade-solutions` command.240- ✅ Other issues map to a solution's canonical `ruleId` (via `list-solutions` + `rules-for-solution`) wherever one fits.241- ✅ Every rule carries `domain` and `category` fields; every incident's `ruleId` resolves to a rule in `rules{}`; CVE/CWE242 findings stay in `security[]`, not `incidents[]`.243- ✅ `metadata.domains` matches the report's content and the emitted `rule.domain` values (enforced by `validate`).244- ✅ The user gets a summary of converted rows, remaining rows, and suggestions.245246## Troubleshooting247248- **`validate` reports a consistency error** (e.g. an incident `ruleId` with no matching rule, or a249 `domains`/`security` mismatch) — the message names the exact field; fix that field.250- **Don't add fields the schema doesn't allow** (e.g. a `summary` object, or `issues`/`storyPoints` on a project). The251 helper's `validate` is lenient about extra keys, but the app's importer enforces `additionalProperties: false` and will252 reject the report — this schema has no summary. Keep to the documented shape.253- **A row has a CVE/CWE *and* needs an upgrade** — emit both: a `security[]` finding **and** an upgrade incident. They are254 not duplicates; the finding documents the vulnerability, the incident drives the upgrade solution.255- **No solution fits an issue** — `list-solutions --query` returns nothing useful. Record the issue with a synthetic,256 descriptive `ruleId` (no Formula will attach) or list it under "remaining" with a suggestion.257- **`rules-for-solution` returns an empty list** — that solution has no rule (often security-only). Don't fabricate a258 ruleId; handle the issue via the security-finding path or pick a different solution.259- **Wrong severity enum** — rule/incident severity is the 4-value `mandatory|potential|optional|information` enum;260 security-finding severity uses the same scale **minus `information`** (`mandatory|potential|optional`, since a finding261 is always at least optional). Don't use the old `critical|high|medium|low|info` values.262263## Reference264265Consolidated reference material. The workflow above links here at the step where each part is needed.266267### Report structure268269You author a single `report.json` conforming to the authoritative schema270[scripts/assessment-report.schema.json](scripts/assessment-report.schema.json). The schema is one root object271(draft-07, its sub-types live under `definitions`) with the top-level shape `{ version, producer, metadata, projects,272rules, security? }`. Read the schema for the exact required fields, types, and enums per object — it is the source of273truth and the `validate` command checks it, so this section does **not** restate the field list. `additionalProperties`274is `false` almost everywhere, so **do not invent fields** (there is no `summary` object, and projects have no `issues` /275`storyPoints`).276277**Authoring rules of thumb** — the conventions and cross-field rules the schema can't fully express:278279- `version` is the string `"1.0.0"` (the schema accepts any string; this is the value to use).280- Rule/incident `severity` is the enum `mandatory | potential | optional | information`. Map source severities by meaning281 (e.g. critical/blocker → `mandatory`, major/medium → `potential`, minor/low → `optional`, info → `information`).282- **Classification is done with rule fields, not labels.** Each `rules{}` entry sets `domain`283 (`cloud-readiness | java-upgrade | security`) and `category` (a free string heading) directly — see284 [Rule classification](#rule-classification). `rules{}.labels` and `incidents[].labels` are optional free-form arrays;285 you normally leave them out.286- Security-finding `severity` uses the report criticality scale `mandatory | potential | optional` (the rule `Severity`287 values minus `information` — a security finding is always at least optional) — normalize per288 [Security severity mapping](#security-severity-mapping).289- `metadata.domains` records which assessment domains the report has content for — see290 [Assessment domains](#assessment-domains). It must be consistent with the `rule.domain` values you emit.291- `incidentId` convention: `"<ruleId>/<n>"` (n is a per-rule counter).292- `locationKind` = `"source-file"` when a file path is present, else `"unknown"`.293- `status` is normally `"completed"`.294- `metadata.mode` is optional; use `"full"` when the report has security findings, else `"issue-only"`.295296### Assessment domains297298`metadata.domains` is a `string[]` recording which assessment **domains** produced the report. Allowed values are299`cloud-readiness`, `java-upgrade`, and `security`.300301| Domain | Meaning | When to include it for a CSV conversion |302|--------|---------|------------------------------------------|303| `cloud-readiness` | Azure cloud-migration issues | any ordinary issue → solution incident (branch 3) |304| `java-upgrade` | JDK / Spring Boot / Spring Framework / Jakarta EE upgrades | any upgrade incident (branch 2) |305| `security` | CVE / CWE vulnerability findings | `report.security[]` is non-empty (branch 1) |306307- The native Java default is `["cloud-readiness", "java-upgrade"]`.308- Set `metadata.domains` to **exactly the domains your report has content for** — don't list `security` with no309 findings, or `java-upgrade` with no upgrade incidents. `validate` flags a `security`/`report.security` mismatch in310 either direction.311- `metadata.domains` must be consistent with the `rule.domain` **field** on your rules — every `rule.domain` value you312 emit should appear in `metadata.domains`.313314### Security severity mapping315316A security finding's `severity` uses the report criticality scale — `mandatory | potential | optional`. This is the rule317`Severity` enum **minus `information`** (a security finding is always at least optional). The extension renders these318values directly, so there is no separate security severity scale and no conversion step.319320**Map the source CVE/CWE severity by meaning to the nearest value** (case-insensitive):321322| Source severity (CSV, case-insensitive) | Report `security[].severity` |323|-----------------------------------------|------------------------------|324| `critical` / `blocker` | `mandatory` |325| `high` | `mandatory` |326| `medium` / `moderate` | `potential` |327| `low` | `optional` |328| anything else / unknown / missing | `optional` |329330- When merging duplicate findings by `id`, keep the **strongest** severity (`mandatory` > `potential` > `optional`).331- Security findings live in `report.security[]` only; there is no `summary` object to key by severity. `validate` checks332 each finding's `severity` is one of the three values and that findings are unique by `id`.333- Include the `security` domain in `metadata.domains` whenever `report.security[]` is non-empty (and only then).334335### Rule classification336337In this schema a rule is classified with **fields on the rule object**, not with labels. Every `rules{}` entry is338**required** to carry a `domain` and a `category` (the `validate` command enforces both, and the schema rejects a339rule that is missing them).340341| Field | Value | Effect |342|-------|-------|--------|343| `domain` | `cloud-readiness` \| `java-upgrade` \| `security` | Groups the rule under that domain tab. Use the same domain you list in `metadata.domains`. **Required.** |344| `category` | the issue category heading (e.g. `postgresql`, `java-version-upgrade`, `deprecated-apis`) | Shown as the group heading. **Required.** |345346**Choosing `category` (and the source label).** Each mapped rule also has a `sourceCategory` in the solution mapping,347which you get from `rules-for-solution <solutionId>` (`rules[].sourceCategory`). The UI builds the group heading as348`category` when it **equals** `sourceCategory` (or `sourceCategory` is empty / `null`), otherwise as349`category (sourceCategory)`. So:350351- When a rule's `sourceCategory` is **non-null**, set its `category` to **exactly that value** — e.g.352 `mi-postgresql` → rule `azure-database-postgresql-02000`, `sourceCategory: "postgresql"` → `category: "postgresql"`,353 which renders as one clean **Postgresql** heading (mismatching it, e.g. `category: "database"`, would render the354 doubled **Database (Postgresql)**).355- When `sourceCategory` is **`null`** (e.g. the JDK-upgrade rules), choose a sensible `category` yourself356 (e.g. `upgrade`) — it renders as-is.357358**Labels are optional.** `rules{}.labels` and `incidents[].labels` are optional free-form `string[]`s in the schema.359Classification no longer depends on them, so you normally leave them out. The engine may still emit context labels360(`target=`, `os=`, `capability=`) on native reports, but when hand-authoring a CSV conversion you do not need any label361to make content render — the required `domain`/`category` **fields** do that.