# The `.pumapack` file format (PumaSkills, schema 1)

This document describes PumaSkills' `.pumapack` files in enough detail to
**edit an export** or **generate one from scratch** that imports cleanly. The
app opens the result with no warnings, no re-stamped dates and no silently
dropped records. It is written for a reader, human or AI, who has no access to
the app's source.

A `.pumapack` is a UTF-8 JSON file. PumaSkills imports it in either of two
ways:

- from the topbar **Import** button;
- by dropping the file anywhere on the window.

**Importing a pack is a restore.** It replaces every assessment held in the
browser with the ones in the file. Nothing is merged. See §2.

---

## 1. The short version

If you only read one section, read this one.

1. Wrap your assessments in the envelope from §2. Import reads only
   `data.assessments`.
2. Hand back **every** assessment the user had, not just the one you changed.
   Whatever is not in the file is gone after import.
3. Write **every field** of every record, using the shapes in §4. Use `""`,
   `[]`, `{}`, `false` or `0` for "nothing". The only fields that take `null`
   are `cost`, `hours` and `headcount`.
4. Write every date as `YYYY-MM-DD`. Write `createdAt`, `setupAt` and
   `exportedAt` as full ISO datetimes. **Write `assessedOn` on every rating,
   `opened` on every requisition and `meta.date`**, or the app stamps them
   with the day of import.
5. Use competency ids exactly as listed in Appendix A (`MON.1`, `IR.3`), or
   ids you define yourself in `customComps` (`CUS.1`). An id that exists
   nowhere is kept, but has no column and quietly lowers people's scores.
6. Every level is a whole number from `0` to the number of rungs on the
   assessment's scale (4 on the default scale). See §6.
7. Every id reference must point at a record that exists in the same
   assessment. The reference fields are listed in §5. A rating, development
   item or certification whose `personId` matches nobody is **dropped without
   a message**.
8. Tags must be an **array**. A person with no `core` tag is not on the core
   team. See §6.5.
9. Never change `settings.scale` on an existing assessment. See §7.
10. Check the result against the checklist in §9.

§10 is a complete, valid example you can copy and adapt. Appendix A lists
every bundled competency id.

---

## 2. The envelope

```json
{
  "app": "pumaskills",
  "appVersion": "generated",
  "schema": 1,
  "exportedAt": "2026-10-05T09:00:00.000Z",
  "data": {
    "assessments": [ { "...one assessment object, see §3..." } ],
    "activeSlug": "harbor-point-soc-q4"
  }
}
```

| Key | Value | Notes |
|---|---|---|
| `app` | `"pumaskills"` | Exactly this, in lower case. See the warning below. |
| `appVersion` | any string | Informational. The app writes its build id, or `"dev"`. |
| `schema` | `1` | The current data schema. Not checked on import, but write it. |
| `exportedAt` | ISO 8601 datetime | Informational. |
| `data.assessments` | array of assessment objects | One or more. **Must be non-empty.** Array order is tab order. |
| `data.activeSlug` | string | The `slug` of the assessment that opens first. If it matches none, the first one opens. |
| `data.roster`, `data.tag_groups` | arrays | Written by the app's export so other apps can read the people. **Ignored when PumaSkills imports its own pack**, and rebuilt on every export. Omit them when generating. |

A file with no `format` key is normal; the app does not write one, and ignores
it if present.

What the importer actually requires:

- The file text starts with `{` or `[` (after whitespace), and the file name
  does not end in `.csv` or `.tsv`. Otherwise it is read as a spreadsheet of
  self-assessment responses (§11), and a JSON file fails with *"No competency
  columns found. Headers need to start with the competency id, as the
  generated form writes them..."*. **A pack saved with a Markdown code
  fence around it (a first line of three backticks and `json`) fails this way.**
- The file is valid JSON. If not: *"That file is not valid JSON"*.
- It contains a non-empty array of objects at `data.assessments`, or at a
  top-level `assessments` when there is no `data` key. If not: *"No
  assessments found in that file"*.
  - A bare assessment object with no `assessments` wrapper **is rejected**
    this way.

**Warning about `app`.** If `app` names anything other than `"pumaskills"`
(including `"PumaSkills"`) and `data.roster` is a non-empty array, the file is
treated as **another app's roster**: the app offers to merge those people into
the open assessment and says every rating, role, plan and certification in the
file will not be imported. Write `app` in lower case, or leave `data.roster`
out.

Every other envelope key is ignored.

On import:

- If the browser already holds an assessment with at least one person, role or
  rating, the user sees **"Replace everything in this browser?"** with
  **Cancel**, **Export current first** and **Replace**. On a fresh or empty
  browser there is no question.
- On success the app says *"Imported 1 assessment"* (or *"Imported N
  assessments"*).
- Each assessment's `slug` is kept, unless two assessments in the file share
  one; the later one is then renamed from its `name`.
- Rating undo history is cleared.

### Two ways the app exports a pack

Both produce the **same shape** and import the same way:

- **Full backup**: the topbar **Export** button, or `Cmd`/`Ctrl`+`S`. File
  name `pumaskills-backup.pumapack`. Holds every assessment. This is the one
  to hand an AI along with this document.
- **One assessment**: right-click (or long-press) a workspace tab and choose
  **Save this assessment (.pumapack)**. Holds that assessment only.

Because import replaces everything, importing a one-assessment pack leaves the
user with **only** that assessment. To add an assessment alongside existing
ones, append it to the `assessments` array of a full backup.

---

## 3. The assessment object

An assessment is one team snapshot, shown as a workspace tab. Assessments
share nothing with each other.

```json
{
  "slug": "harbor-point-soc-q4",
  "name": "Harbor Point SOC, Q4",
  "accent_color": "#5b8af0",
  "createdAt": "2026-10-05T09:00:00.000Z",
  "setupAt": "2026-10-05T09:00:00.000Z",
  "meta":         { },
  "settings":     { },
  "catalogs":     ["secops"],
  "roleCatalogs": ["secops"],
  "tagGroups":    [ ],
  "people":       [ ],
  "roles":        [ ],
  "ratings":      [ ],
  "plans":        [ ],
  "certs":        [ ],
  "shifts":       [ ],
  "reqs":         [ ],
  "extraComps":   [ ],
  "customComps":  [ ],
  "dispositions": { }
}
```

| Field | Type | Notes |
|---|---|---|
| `slug` | string | The assessment's identity. Lower-case `a-z0-9` and `-`. Must be unique within the file. |
| `name` | string | Shown on the tab. Missing: `"Assessment 1"`, `"Assessment 2"` and so on. |
| `accent_color` | `#rrggbb` | Tab color. The eight offered are `#5b8af0`, `#d4626d`, `#e0855a`, `#d9a94a`, `#5ecc94`, `#4ab8c2`, `#a87fe0`, `#d048a0`. |
| `createdAt` | ISO 8601 datetime | Missing: the moment of import. |
| `setupAt` | ISO 8601 datetime, or `""` | When the scale was chosen. See below. |
| `meta` | object | The deliverable's front matter. See §4.1. |
| `settings` | object | Scale and scoring. See §4.2. |
| `catalogs` | array of ids | Competency catalogs in play: `"secops"`, `"nice"`, `"itops"`. Unknown ids are dropped; an empty list becomes `["secops"]`. |
| `roleCatalogs` | array of ids | Role lists offered when adding a role. See the table below. Unknown ids are dropped; `[]` is allowed. If the key is missing, it is derived from `catalogs`. |
| `tagGroups` | array | The tag vocabulary. See §4.3. |
| `people` … `reqs` | arrays | See §4. Use `[]` when empty. |
| `extraComps` | array of competency ids | Tracked as matrix columns although no role requires them. See §4.11. |
| `customComps` | array | Competencies defined by this assessment. See §4.11. |
| `dispositions` | object | How each gap will be closed. See §4.12. |

**`setupAt`.** The scale is chosen once, when an assessment is started, and
`setupAt` records that it has been. If it is `""` and the assessment holds any
person, role or rating, the importer sets it to `createdAt`. If it is `""` and
the assessment is empty, the app shows *"This assessment has not been set up"*
and asks for a scale. For a generated pack, write a datetime.

**`catalogs` and `roleCatalogs` only control what the app offers** in its
pickers. They do not restrict which competency ids a rating or role may use.
Still, include the catalog of every competency you use.

| `roleCatalogs` id | Role list | Scores against catalog |
|---|---|---|
| `secops` | Security operations roles | `secops` |
| `nice` | NICE work roles (SP 800-181r1) | `nice` |
| `nice2` | NICE work roles (v2) | `nice` |
| `dcwf` | DoD Cyber Workforce Framework roles | `nice` |
| `ecsf` | ENISA ECSF role profiles | `nice` |
| `gdd` | Digital & data profession roles | `itops` |
| `ecfp` | European ICT professional roles | `itops` |

A role list is offered only when its catalog is also in `catalogs`.

**Any other key at the assessment level is silently dropped.** Unknown keys on
people, roles, ratings, plans, certifications, rotas, requisitions, `meta` and
`settings` survive import and are written back on export, but have no effect.

---

## 4. Record shapes

### Conventions for every record

- **`id`** is any non-empty string, unique within its own array in the
  assessment. The app generates UUIDs; short readable ids (`p-ana`, `r-t1`)
  work just as well. Ratings have no `id`.
- **Dates** are `"YYYY-MM-DD"` strings, or `""` for none. See §6.4.
- **Defaults are filled in only for keys that are missing.** An explicit
  `null` in a text field is kept as `null`. Omit nothing; write `""` instead.
- **Enum values are checked for ratings, development items, requisitions and
  dispositions.** An unknown value falls back to the default named below,
  silently. Enum matching is case-sensitive: `"Open"` is not `"open"`.

### 4.1 `meta`

```json
{
  "client": "", "scope": "", "assessor": "", "date": "2026-10-05",
  "method": "", "notes": "", "purpose": "", "controller": "", "retainUntil": ""
}
```

| Field | Type | Meaning |
|---|---|---|
| `client` | string | Whose team this is. |
| `scope` | string | Which team, and what was in scope. |
| `assessor` | string | Who did the assessment. |
| `date` | date | The assessment date. **Missing: the day of import.** Not otherwise checked, so write a real date. |
| `method` | string | How evidence was gathered. Opens the report. |
| `notes` | string | Free text. |
| `purpose` | string | Why this personnel record is held. |
| `controller` | string | Who is accountable for it. |
| `retainUntil` | date or `""` | Delete-by date. Reported on the Overview and in the report as it nears; never enforced. Anything not a date becomes `""`. |

### 4.2 `settings`

```json
{
  "scale": "puma4", "workingBar": 2,
  "healthGood": 0.85, "healthWarn": 0.7,
  "certHorizonDays": 120, "retainWarnDays": 30,
  "matrixRamp": "accent", "matrixSort": "name", "showLevels": true,
  "filter": [],
  "levelLabels": ["None", "Awareness", "Working", "Practitioner", "Authority"]
}
```

| Field | Values |
|---|---|
| `scale` | A scale id from the table below. Unknown ids become `"puma4"`. **Fixed for the life of the assessment.** |
| `workingBar` | Integer from 1 to the scale's rung count: the level that means "can be left to do this unaided". Missing or `0`: the scale's default bar. Out-of-range values are clamped. |
| `healthGood`, `healthWarn` | Fractions greater than 0 and at most 1. A readout at or above `healthGood` reads healthy, at or above `healthWarn` marginal, below that bad. Defaults `0.85` and `0.7`. If `healthWarn` is larger, the two are swapped. |
| `certHorizonDays` | Whole number ≥ 0. How far ahead a certification expiry is flagged. Default `120`. |
| `retainWarnDays` | Whole number ≥ 0. How early `meta.retainUntil` starts being flagged. Default `30`. |
| `matrixRamp` | `"accent"` or `"neutral"`. Matrix cell coloring. |
| `matrixSort` | `"name"` or `"role"`. Matrix row order. |
| `showLevels` | boolean. Show level numbers in matrix cells. |
| `filter` | Array of tag names. When non-empty, the readouts count only people who carry **all** of them. `[]` means everyone. |
| `levelLabels` | Array of exactly rungs + 1 strings: the label for level 0, then each rung. A missing or blank entry takes the scale's own label; extra entries are dropped. |

The five scales. Level 0 ("None") is implicit on every scale and is not a
rung.

| `scale` | Rungs | Default bar | Labels for levels 1 upward |
|---|---|---|---|
| `puma4` (default) | 4 | 2 | Awareness, Working, Practitioner, Authority |
| `gds4` | 4 | 2 | Awareness, Working practitioner, Practitioner, Expert |
| `ecf5` | 5 | 3 | e-1, e-2, e-3, e-4, e-5 |
| `ciisec6` | 6 | 3 | Follow, Assist, Apply, Enable, Advise, Influence |
| `sfia7` | 7 | 3 | Follow, Assist, Apply, Enable, Ensure, Initiate, Set strategy |

### 4.3 `tagGroups[]`

```json
{ "id": "location", "name": "Location", "color": "#d048a0", "tags": ["Leeds", "Remote"] }
```

- A group gives its tags a color and a sort position. It does not define them:
  a person may carry a tag that is in no group, and it still filters.
- `color` must be one of the eight colors listed for `accent_color` in §3.
  Anything else becomes `#5b8af0`.
- A group with no `id`, or with an `id` already used, is dropped.
- The protected names `core`, `vacancy` and `contractor` are removed from every
  group's `tags`.
- The group with `id: "seniority"` is also the grade ladder that
  `roles[].seniority` is checked against (§4.5).
- If `tagGroups` is missing, the five default groups are used:

| `id` | `name` | `color` | Default `tags` |
|---|---|---|---|
| `affiliation` | Affiliation | `#5b8af0` | `extended` |
| `employment` | Employment | `#a87fe0` | `staff`, `apprentice` |
| `seniority` | Seniority | `#4ab8c2` | `Junior`, `Mid`, `Senior`, `Lead`, `Principal`, `Manager` |
| `location` | Location | `#d048a0` | (none) |
| `duty` | Duty | `#e0855a` | (none) |

### 4.4 `people[]`

```json
{
  "id": "p-ana", "name": "Ana Costa",
  "roleId": "r-t2", "alsoRoleIds": [],
  "startDate": "", "cost": null, "notes": "",
  "tags": ["core", "staff", "Senior", "Leeds"],
  "active": true, "color": "",
  "consentOn": "2026-09-28", "consentNote": "Team briefing",
  "email": "ana.costa@example.com"
}
```

| Field | Type | Meaning |
|---|---|---|
| `name` | string | Empty becomes `"Unnamed"`. |
| `roleId` | role id or `""` | The primary role, which their gaps are scored against. An id that matches no role becomes `""`. |
| `alsoRoleIds` | array of role ids | Secondary roles they cover (surge, on-call). They can only **raise** a requirement, never lower it. Unknown ids are removed. |
| `startDate` | date or `""` | Stored; not currently shown. |
| `cost` | number or `null` | Optional loaded cost. |
| `notes` | string | Free text on the person's record. |
| `tags` | array of strings | See §6.5. **Must be an array.** If the key is missing the person gets `["core", "staff"]`; if it is present but not an array, they get no tags at all. Duplicates (ignoring case and surrounding spaces) are removed. |
| `active` | boolean | `false` takes the person out of every count, like a vacancy. Anything other than `false` reads as `true`. |
| `color` | `#rrggbb` or `""` | Avatar color. `""` picks one automatically. |
| `consentOn` | date or `""` | When they were told they are being assessed. `""` means not recorded. |
| `consentNote` | string | How they were told. |
| `email` | string | Work address, used only to match self-assessment responses back to them. |

**Array order does not matter for display.** The People view sorts by name,
and the matrix by name or role (`settings.matrixSort`).

### 4.5 `roles[]`

A role is a requirement profile: the level the work needs in each competency.

```json
{
  "id": "r-t1", "code": "", "name": "SOC analyst, Tier 1",
  "family": "Monitoring", "seniority": "Junior",
  "desc": "Triages alerts and escalates what needs a second pair of eyes.",
  "req": { "MON.1": 2, "MON.6": 2, "MON.8": 2, "MON.9": 1 },
  "headcount": 3, "source": "custom"
}
```

| Field | Type | Meaning |
|---|---|---|
| `code` | string | A framework code such as `PR-CDA-001`, or `""`. |
| `name` | string | Empty becomes `"Untitled role"`. |
| `family` | string | Free-text grouping. |
| `seniority` | string | Must be one of the `tags` of the `seniority` tag group, **spelled and capitalized identically**. Anything else becomes that group's first value (`"Junior"` by default). |
| `desc` | string | What the role does. Used in job specs. |
| `req` | object | `{ competencyId: requiredLevel }`. Levels are whole numbers from 1 to the rung count; they are clamped into range. `0`, or leaving the competency out, means not required. |
| `headcount` | number or `null` | Target number of people in the role. `null` means not tracked. |
| `source` | `"custom"` or a role-catalog id | Where the role came from. Informational. Use `"custom"` for a role you wrote. |

### 4.6 `ratings[]`

A rating is one person's level in one competency, with its provenance.

```json
{
  "personId": "p-ana", "compId": "MON.1", "level": 3,
  "method": "observed", "lastUsed": "2026-10-01",
  "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist",
  "evidence": "Ran triage for the September phishing wave.", "notes": ""
}
```

| Field | Values |
|---|---|
| `personId` | A person id. **A rating whose person does not exist is dropped.** |
| `compId` | A competency id (Appendix A, or a `customComps` id). Not checked. |
| `level` | Whole number, 0 to the rung count. Clamped into range; a non-number becomes 0. |
| `method` | How it was established, weakest first: `"inferred"`, `"self"`, `"peer"`, `"manager"`, `"tested"`, `"observed"`. Unknown values become `"self"`. |
| `lastUsed` | Date or `""`. When the person last used the skill. |
| `assessedOn` | Date. When the rating was made. **Missing or not a date: the day of import.** |
| `assessedBy` | string. Who made the judgement. |
| `evidence`, `notes` | strings. |

- **One rating per person per competency.** If the file holds two, the
  first is kept and the second is dropped.
- **A level-0 rating and no rating are different.** `0` means "assessed, and
  has none". No rating means "not assessed yet", which the app counts
  separately (the Overview's *Matrix assessed* figure).
- `method` matters when self-assessment responses are merged in later: a
  response never overwrites a rating made by a stronger method.

### 4.7 `plans[]` (development items)

```json
{
  "id": "pl-1", "personId": "p-ben", "compId": "MON.6", "targetLevel": 2,
  "method": "course", "name": "Phishing analysis course", "provider": "Internal academy",
  "hours": 16, "cost": 1200, "due": "2026-12-15", "status": "approved", "notes": ""
}
```

| Field | Values |
|---|---|
| `personId` | A person id. **An item whose person does not exist is dropped.** |
| `compId` | The competency it develops. |
| `targetLevel` | Whole number, 0 to the rung count. Missing: `2`. |
| `method` | `"course"`, `"cert"`, `"lab"`, `"shadow"`, `"mentor"`, `"rotation"`, `"project"`, `"reading"` or `"exercise"`. Unknown values become `"course"`. |
| `name`, `provider`, `notes` | strings. |
| `hours`, `cost` | number or `null`. |
| `due` | date or `""`. |
| `status` | `"planned"`, `"approved"`, `"active"` (shown as *In progress*), `"done"` or `"dropped"`. Unknown values become `"planned"`. |

### 4.8 `certs[]`

```json
{ "id": "c-1", "personId": "p-ana", "name": "GCIH", "issuer": "GIAC",
  "issued": "2023-01-20", "expires": "2027-01-31", "ref": "" }
```

- `personId` must match a person, and `name` must be non-empty, or the
  certification is dropped.
- `issued` and `expires` are dates or `""`. An expiry within
  `settings.certHorizonDays` of today is flagged on the Overview.
- `ref` is a certificate number or link.

### 4.9 `shifts[]` (rotas)

```json
{
  "id": "s-days", "name": "Days", "window": "07:00–15:00", "days": "Mon–Fri", "timezone": "UTC",
  "memberIds": ["p-ana", "p-ben"],
  "need": [ { "compId": "MON.1", "minLevel": 2, "minCount": 1 } ],
  "notes": ""
}
```

| Field | Values |
|---|---|
| `name` | string. Empty becomes `"Shift"`. |
| `window`, `days`, `timezone` | Free text. The app's own forms are `"07:00–15:00"`, `"Mon–Fri"`, `"Every day"`, `"Weekends"`, `"Mon, Wed, Fri"`, and a zone such as `"UTC"`. Other apps read these when the pack is shared, so keep to those forms. |
| `memberIds` | Person ids on this rota. Unknown ids are removed. **This is the only place rota membership is stored**; there is no field on the person. |
| `need` | What every instance of this shift must have: at least `minCount` people at `minLevel` or above in `compId`. Entries with no `compId` are dropped. `minLevel` missing: the working bar. `minCount` is at least 1. |
| `notes` | string. |

A shift's name and a person's roles also appear as automatic tags on the
person. They are computed, never stored in `tags`.

### 4.10 `reqs[]` (hiring requisitions)

```json
{
  "id": "q-1", "roleId": "r-t1", "title": "SOC analyst, Tier 1 (backfill)", "count": 1,
  "status": "open", "priority": "med",
  "justification": "Tier 1 is one person short of the three the rota needs.", "notes": "",
  "mustHave": ["MON.6"], "niceHave": ["MON.2"], "opened": "2026-10-01", "target": "2027-01-04"
}
```

| Field | Values |
|---|---|
| `roleId` | A role id, or `""`. An id matching no role becomes `""`. |
| `title` | string. |
| `count` | Whole number ≥ 1: how many hires. |
| `status` | `"draft"`, `"open"`, `"interview"`, `"offer"`, `"filled"` or `"closed"`. Unknown values become `"draft"`. |
| `priority` | Stored but not shown. Write `"med"`. |
| `justification`, `notes` | strings. |
| `mustHave`, `niceHave` | Arrays of competency ids. |
| `opened` | Date. **Missing or not a date: the day of import.** |
| `target` | Target start date, or `""`. |

### 4.11 `customComps[]` and `extraComps[]`

A custom competency is one the assessment defines for itself, beside the
bundled catalogs.

```json
{ "id": "CUS.1", "catalog": "custom", "domain": "CUS:tooling", "domainName": "Tooling",
  "name": "Ticketing platform administration", "desc": "Maintains queues, routing rules and SLA timers." }
```

| Field | Values |
|---|---|
| `id` | Use `CUS.1`, `CUS.2` and so on. The app numbers new ones after the highest existing number. Must not reuse a bundled id. |
| `catalog` | Always `"custom"`. Rewritten on import. |
| `domainName` | The heading it is grouped under. |
| `domain` | Derived: `"CUS:"` followed by `domainName` in lower case, trimmed. Recomputed on import. The one exception: if `domain` is a bundled domain code such as `"MON"`, the competency is filed under that bundled domain and `domainName` is replaced with the catalog's name for it. |
| `name` | Required. A custom competency with no `id` or no `name` is dropped. |
| `desc` | What it covers. |

Unknown keys on a custom competency are dropped.

**A competency is a matrix column only when something tracks it**: a role
requires it above 0, a shift needs it, somebody holds a rating above 0 in it,
or its id is in `extraComps`. When the app creates a custom competency it adds
its id to `extraComps`; do the same.

### 4.12 `dispositions`

How the team intends to close each gap, keyed by competency id. It is a
decision, so it is stored; every other readout is computed.

```json
{
  "DF.5":  { "id": "develop", "note": "Memory forensics course for Ana in Q1." },
  "MON.6": { "id": "hire", "note": "Covered by the Tier 1 backfill." }
}
```

- `id` is `"develop"`, `"hire"`, `"contract"`, `"accept"` or `"none"`
  (shown as *Undecided*). Unknown values become `"none"`.
- An entry that ends up `"none"` with an empty `note` is dropped.
- Unknown keys inside an entry are dropped.

---

## 5. Cross-references

All references stay within one assessment:

| From | Field | To |
|---|---|---|
| person | `roleId`, `alsoRoleIds[]` | `roles[].id` |
| rating | `personId` | `people[].id` |
| rating | `compId` | a competency id |
| plan | `personId` | `people[].id` |
| plan | `compId` | a competency id |
| certification | `personId` | `people[].id` |
| shift | `memberIds[]` | `people[].id` |
| shift | `need[].compId` | a competency id |
| requisition | `roleId` | `roles[].id` |
| requisition | `mustHave[]`, `niceHave[]` | competency ids |
| role | keys of `req` | competency ids |
| assessment | `extraComps[]`, keys of `dispositions` | competency ids |
| envelope | `data.activeSlug` | `assessments[].slug` |

"A competency id" means an id from Appendix A or a `customComps[].id` in the
same assessment.

What the importer does with a broken reference:

- a dangling `roleId` or requisition `roleId` becomes `""`;
- dangling `alsoRoleIds` and `memberIds` entries are removed;
- a rating, plan or certification with a dangling `personId` is **dropped**;
- a competency id that exists nowhere is **kept**. It gets no matrix column
  and no name, but a role requiring it still scores every holder short on it,
  so their personal score drops for a reason the matrix cannot show.

None of this produces a message; the import still reports success.

---

## 6. How the app reads the data

### 6.1 Competency ids

A bundled competency's id is its domain code, a dot, and its position in that
domain: `MON.1`, `IR.12`. Appendix A lists all 246. **Ids are fixed**: never
invent a bundled-looking id (`MON.10` does not exist), and never renumber one.

### 6.2 Levels and the scale

- Every level (`ratings[].level`, `roles[].req` values, `plans[].targetLevel`,
  `shifts[].need[].minLevel`) is a whole number from 0 to the scale's rung
  count. Larger values are clamped to the top rung; negatives become 0.
- The same number means different things on different scales, which is why
  the scale can never change once set.
- **The working bar** (`settings.workingBar`) is the level at which a person
  counts as *capable* in a competency.

### 6.3 What is computed, never stored

Every readout is recomputed from people, roles and ratings each time. Do not
try to write any of these:

- **A person's score** is the sum, over every competency their roles require,
  of the lower of *their level* and *the requirement*, divided by the sum of
  the requirements. Credit is capped at the requirement, so surplus in one
  competency cannot hide a gap in another. An unassessed competency counts as
  0.
- **A person's requirement** in a competency is the highest level any of their
  roles (primary or secondary) asks for.
- **A gap** is a required competency where the level is below the
  requirement: *near* when one level short, *short* when more.
- **Depth** in a competency is the number of active, non-vacancy people at or
  above the working bar. *Nobody can do it* means depth 0 on something a role
  requires; *single point of failure* means depth 1.
- Health colors compare these fractions against `healthGood` and
  `healthWarn`.

### 6.4 Dates

- **Format.** `YYYY-MM-DD`, e.g. `"2026-10-05"`.
  - A full timestamp such as `"2026-10-01T12:00:00Z"` is accepted, and only
    the date part is kept.
  - Anything else (`"05/10/2026"`, `"Oct 5"`) is treated as no date: `""`,
    or the day of import for `assessedOn` and `opened`.
- `createdAt`, `setupAt` and `exportedAt` are full ISO datetimes.
- "Today", for expiry and retention warnings, is the local date of the
  computer running the app.

### 6.5 Tags

- `tags` is a free list per person. Matching ignores case and surrounding
  spaces; the stored spelling is what is displayed.
- **Three tags change the numbers**:

| Tag | Effect |
|---|---|
| `core` | Counted as core capacity. A person **without** `core` is on the extended bench: their skills are recorded, but a readout filtered to `core` leaves them out. |
| `vacancy` | An unfilled seat. Excluded from headcount, depth and shift coverage. Give it a `roleId` to show which post is open. |
| `contractor` | Bought-in capacity. Drives the "held only by contractors" finding. |

- Any other tag is descriptive. A tag listed in a tag group takes that
  group's color; others render neutral. Chip order comes from the groups, not
  from the order in the array.

---

## 7. Editing an existing export

**Preserve:**

- Every `id` and `slug`. Person ids are referenced by ratings, plans,
  certifications and rotas; role ids by people and requisitions. Changing one
  without the others drops records (§5).
- `settings.scale`. **Never change it.** The importer accepts a new scale
  silently, but every stored level keeps its number and changes its meaning,
  and levels above the new top rung are cut down. A different scale means a
  new assessment.
- `createdAt` and `setupAt`.
- Competency ids. Never renumber a bundled one.
- Keys you do not recognize on a person, role, rating, plan, certification,
  rota or requisition. They survive import and export.

**When you change a rating's `level`,** also set `assessedOn` to the date of
the new judgement and `method` to how it was made. The app does the first
itself when a level is edited in the matrix.

**When you remove a person,** also remove their ratings, plans,
certifications and rota memberships. The importer drops the orphans anyway,
but silently.

**Regenerated, so do not edit:**

- `data.roster` and `data.tag_groups` in the envelope. They are rebuilt from
  the assessments on every export and ignored on import. Change people in
  `assessments[].people` and tag groups in `assessments[].tagGroups`.
- `appVersion` and `exportedAt`. Informational only.
- The automatic Shift and Role tags. They are computed from `shifts[]` and
  roles; writing them into `tags` makes a stale copy.

**Hand back the whole file.** Import replaces every assessment in the
browser, so a pack holding one assessment removes all the others.

---

## 8. Things that go wrong

| Mistake | What happens |
|---|---|
| One assessment with no `assessments` wrapper | Rejected: *"No assessments found in that file"*. |
| The JSON wrapped in a Markdown code fence (three backticks and `json` on the first line) | Read as a spreadsheet: *"No competency columns found..."*. Save the bare JSON. |
| File saved with a `.csv` or `.tsv` name | Same: read as self-assessment responses. |
| `app` misspelled (e.g. `"PumaSkills"`) with a non-empty `data.roster` | Offered as another app's roster: *"Import a roster?"*. Accepting merges the people into the open assessment; the file's ratings, roles, plans and certifications are not imported. |
| A pack holding one assessment, imported over several | The others are gone after **Replace**. |
| A rating, plan or certification with a `personId` matching nobody | Dropped. The import still says it succeeded. |
| `people` is `null` or not an array | Read as no people, so **every** rating, plan, certification and rota membership is dropped too. |
| Two ratings for the same person and competency | The first is kept; the second is dropped. |
| `tags` is a string, e.g. `"core"` | The person gets no tags at all and leaves the core team. |
| No `tags` key on a person | The person gets `["core", "staff"]`. |
| No `assessedOn` on a rating, no `opened` on a requisition, or no `meta.date` | Stamped with the day of import. |
| A level above the top rung, e.g. `7` on a four-rung scale | Clamped to `4`. |
| Enum in the wrong case or spelling, e.g. `"Manager"`, `"Open"`, `"in-progress"` | Replaced by the default: `"self"`, `"draft"`, `"planned"`. |
| `roles[].seniority` not an exact value of the Seniority tag group, e.g. `"junior"` | Replaced by the group's first value (`"Junior"`). |
| Tag group `color` outside the eight | Becomes `#5b8af0`. |
| A competency id that exists nowhere, e.g. `MON.99` in a role's `req` | Kept. No matrix column, but everyone in that role scores lower. |
| A dangling `roleId` | Becomes `""`: the person has no role and nothing is required of them. |
| A certification with an empty `name` | Dropped. |
| Dates as `"05/10/2026"` | Treated as no date. |
| A key at the assessment level the app does not know | Dropped. |
| `settings.scale` changed on an existing assessment | Accepted silently; every rating is reinterpreted. |
| `setupAt: ""` on an empty assessment | The app asks for a scale before anything can be entered. |
| `null` in a text field such as `notes` | Kept as `null`. Tolerated, but write `""`. |

---

## 9. Checklist before handing a pack over

A pack that passes all of these imports with no warnings and nothing
re-stamped or dropped.

**Structure**
- [ ] The envelope matches §2: `app` is `"pumaskills"` and
      `data.assessments` is a non-empty array.
- [ ] The file is bare JSON, with no Markdown fence, saved as `.pumapack` or
      `.json`.
- [ ] It holds every assessment the user had, not only the one changed.
- [ ] Every record has every field from §4, with no `null` outside `cost`,
      `hours` and `headcount`.
- [ ] Ids are unique within each array, and slugs are unique in the file.

**References**
- [ ] Every `personId`, `memberIds` entry, `roleId`, `alsoRoleIds` entry and
      requisition `roleId` resolves (§5).
- [ ] Every competency id is in Appendix A or in `customComps`.
- [ ] Every custom competency's id is in `extraComps`.
- [ ] At most one rating per person per competency.

**Values**
- [ ] `settings.scale` is set and, for an existing assessment, unchanged.
- [ ] Every level is a whole number from 0 to the scale's rung count.
- [ ] `settings.levelLabels` has rungs + 1 entries.
- [ ] Every enum is one of the exact lower-case ids in §4.
- [ ] Every `roles[].seniority` appears in the Seniority tag group.
- [ ] Every tag group color is one of the eight.
- [ ] `tags` is an array on every person, and everyone on the core team has
      `core`.

**Dates**
- [ ] Every date is `YYYY-MM-DD`; `createdAt`, `setupAt` and `exportedAt`
      are full ISO datetimes.
- [ ] Every rating has `assessedOn`, every requisition has `opened`, and
      `meta.date` is set.

---

## 10. A complete example

A small security operations team on the default scale: two roles, three
people and one open seat, fourteen ratings (one of them a deliberate level
0), one development item, one certification, two rotas with coverage needs,
one requisition, one custom competency and two gap decisions. It imports with
no warnings.

```json
{
  "app": "pumaskills",
  "appVersion": "generated",
  "schema": 1,
  "exportedAt": "2026-10-05T09:00:00.000Z",
  "data": {
    "assessments": [
      {
        "slug": "harbor-point-soc-q4",
        "name": "Harbor Point SOC, Q4",
        "accent_color": "#5b8af0",
        "createdAt": "2026-10-05T09:00:00.000Z",
        "setupAt": "2026-10-05T09:00:00.000Z",
        "meta": {
          "client": "Harbor Point Logistics",
          "scope": "The 24x5 monitoring team: two tiers, one contractor, one open seat.",
          "assessor": "Maya Lindqvist",
          "date": "2026-10-05",
          "method": "Manager assessment, cross-checked against two tabletop exercises.",
          "notes": "",
          "purpose": "Plan Q1 training spend and the Tier 1 backfill.",
          "controller": "Harbor Point Logistics, Security Operations",
          "retainUntil": "2027-10-05"
        },
        "settings": {
          "scale": "puma4",
          "workingBar": 2,
          "healthGood": 0.85,
          "healthWarn": 0.7,
          "certHorizonDays": 120,
          "retainWarnDays": 30,
          "matrixRamp": "accent",
          "matrixSort": "name",
          "showLevels": true,
          "filter": [],
          "levelLabels": ["None", "Awareness", "Working", "Practitioner", "Authority"]
        },
        "catalogs": ["secops"],
        "roleCatalogs": ["secops"],
        "tagGroups": [
          { "id": "affiliation", "name": "Affiliation", "color": "#5b8af0", "tags": ["extended"] },
          { "id": "employment", "name": "Employment", "color": "#a87fe0", "tags": ["staff", "apprentice"] },
          { "id": "seniority", "name": "Seniority", "color": "#4ab8c2", "tags": ["Junior", "Mid", "Senior", "Lead", "Principal", "Manager"] },
          { "id": "location", "name": "Location", "color": "#d048a0", "tags": ["Leeds", "Remote"] },
          { "id": "duty", "name": "Duty", "color": "#e0855a", "tags": [] }
        ],
        "people": [
          { "id": "p-ana", "name": "Ana Costa", "roleId": "r-t2", "alsoRoleIds": [], "startDate": "",
            "cost": null, "notes": "", "tags": ["core", "staff", "Senior", "Leeds"], "active": true,
            "color": "", "consentOn": "2026-09-28", "consentNote": "Team briefing", "email": "ana.costa@example.com" },
          { "id": "p-ben", "name": "Ben Okafor", "roleId": "r-t1", "alsoRoleIds": [], "startDate": "",
            "cost": null, "notes": "", "tags": ["core", "staff", "Junior", "Leeds"], "active": true,
            "color": "", "consentOn": "2026-09-28", "consentNote": "Team briefing", "email": "ben.okafor@example.com" },
          { "id": "p-dev", "name": "Dev Rao", "roleId": "r-t2", "alsoRoleIds": ["r-t1"], "startDate": "",
            "cost": null, "notes": "Covers nights on a six-month contract.", "tags": ["contractor", "Senior", "Remote"], "active": true,
            "color": "", "consentOn": "", "consentNote": "", "email": "" },
          { "id": "p-vac", "name": "Vacancy: Tier 1", "roleId": "r-t1", "alsoRoleIds": [], "startDate": "",
            "cost": null, "notes": "", "tags": ["vacancy"], "active": true,
            "color": "", "consentOn": "", "consentNote": "", "email": "" }
        ],
        "roles": [
          { "id": "r-t1", "code": "", "name": "SOC analyst, Tier 1", "family": "Monitoring", "seniority": "Junior",
            "desc": "Triages alerts and escalates what needs a second pair of eyes.",
            "req": { "MON.1": 2, "MON.6": 2, "MON.8": 2, "MON.9": 1 }, "headcount": 3, "source": "custom" },
          { "id": "r-t2", "code": "", "name": "SOC analyst, Tier 2", "family": "Monitoring", "seniority": "Senior",
            "desc": "Investigates escalations and makes the first containment call.",
            "req": { "MON.1": 3, "MON.2": 2, "MON.9": 3, "IR.1": 2, "IR.3": 2, "DF.5": 2 }, "headcount": 2, "source": "custom" }
        ],
        "ratings": [
          { "personId": "p-ana", "compId": "MON.1", "level": 3, "method": "observed", "lastUsed": "2026-10-01", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "Ran triage for the September phishing wave.", "notes": "" },
          { "personId": "p-ana", "compId": "MON.2", "level": 3, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "" },
          { "personId": "p-ana", "compId": "MON.9", "level": 3, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "" },
          { "personId": "p-ana", "compId": "IR.1", "level": 2, "method": "tested", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "Tabletop, 2026-09-12.", "notes": "" },
          { "personId": "p-ana", "compId": "IR.3", "level": 2, "method": "tested", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "Tabletop, 2026-09-12.", "notes": "" },
          { "personId": "p-ana", "compId": "DF.5", "level": 1, "method": "self", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "", "evidence": "", "notes": "" },
          { "personId": "p-ben", "compId": "MON.1", "level": 2, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "" },
          { "personId": "p-ben", "compId": "MON.6", "level": 1, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "" },
          { "personId": "p-ben", "compId": "MON.8", "level": 2, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "" },
          { "personId": "p-ben", "compId": "MON.9", "level": 1, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "" },
          { "personId": "p-ben", "compId": "CUS.1", "level": 2, "method": "self", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "", "evidence": "", "notes": "" },
          { "personId": "p-dev", "compId": "MON.1", "level": 3, "method": "peer", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Ana Costa", "evidence": "", "notes": "" },
          { "personId": "p-dev", "compId": "IR.3", "level": 3, "method": "observed", "lastUsed": "2026-08-20", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "Contained the August credential-stuffing incident.", "notes": "" },
          { "personId": "p-dev", "compId": "DF.5", "level": 0, "method": "manager", "lastUsed": "", "assessedOn": "2026-10-05", "assessedBy": "Maya Lindqvist", "evidence": "", "notes": "Has never done memory work." }
        ],
        "plans": [
          { "id": "pl-1", "personId": "p-ben", "compId": "MON.6", "targetLevel": 2, "method": "course",
            "name": "Phishing analysis course", "provider": "Internal academy", "hours": 16, "cost": 1200,
            "due": "2026-12-15", "status": "approved", "notes": "" }
        ],
        "certs": [
          { "id": "c-1", "personId": "p-ana", "name": "GCIH", "issuer": "GIAC",
            "issued": "2023-01-20", "expires": "2027-01-31", "ref": "" }
        ],
        "shifts": [
          { "id": "s-days", "name": "Days", "window": "07:00–15:00", "days": "Mon–Fri", "timezone": "UTC",
            "memberIds": ["p-ana", "p-ben"],
            "need": [ { "compId": "MON.1", "minLevel": 2, "minCount": 1 }, { "compId": "IR.3", "minLevel": 2, "minCount": 1 } ],
            "notes": "" },
          { "id": "s-nights", "name": "Nights", "window": "23:00–07:00", "days": "Mon–Fri", "timezone": "UTC",
            "memberIds": ["p-dev"],
            "need": [ { "compId": "MON.1", "minLevel": 2, "minCount": 1 } ],
            "notes": "" }
        ],
        "reqs": [
          { "id": "q-1", "roleId": "r-t1", "title": "SOC analyst, Tier 1 (backfill)", "count": 1, "status": "open",
            "priority": "med", "justification": "Tier 1 is one person short of the three the rota needs.", "notes": "",
            "mustHave": ["MON.6"], "niceHave": ["MON.2"], "opened": "2026-10-01", "target": "2027-01-04" }
        ],
        "extraComps": ["CUS.1"],
        "customComps": [
          { "id": "CUS.1", "catalog": "custom", "domain": "CUS:tooling", "domainName": "Tooling",
            "name": "Ticketing platform administration", "desc": "Maintains queues, routing rules and SLA timers." }
        ],
        "dispositions": {
          "DF.5": { "id": "develop", "note": "Memory forensics course for Ana in Q1." },
          "MON.6": { "id": "hire", "note": "Covered by the Tier 1 backfill." }
        }
      }
    ],
    "activeSlug": "harbor-point-soc-q4"
  }
}
```

What the app shows for this, as a check on your own reasoning. These figures
were read from the app's Overview after importing this exact file:

- **Headcount 3**, with 1 open vacancy. The vacancy is not counted.
- **Requirement coverage 62%** of everything the roles demand.
- **Matrix assessed 72%**: 13 of 18 required cells have a rating.
- **2 competencies nobody can do**: Email and phishing analysis (`MON.6`),
  which Ben holds at 1 of the 2 required, and Memory acquisition and analysis
  (`DF.5`), where Ana is at 1 and Dev at 0.
- **4 single points of failure**.
- **No shift coverage failures**: Days has Ana for `IR.3` and both for
  `MON.1`; Nights has Dev.
- **6 gaps** in the Gaps view, and **1 live development item** of 16 hours.
- Dev is not on the core team (no `core` tag), so a readout filtered to
  `core` leaves him out.

---

## 11. Other files the Import button accepts

The same Import button and drop target also take two other kinds of file.
Neither is a restore, and neither is described in detail here.

- **Self-assessment responses** (`.csv` or `.tsv`): the answers to the
  questionnaire the app exports for each assessment. They are **merged** into
  the open assessment, matched to people by `email` and then by name, and never
  overwrite a rating made by a stronger method.
- **Another app's pack that carries a roster** (for example PumaShift): the
  app offers to merge those people and their working hours into the open
  assessment as people and rotas. Ratings, roles and anything else in that
  file are not imported.

---

## Appendix A. Competency ids

Every bundled competency, by catalog and domain. The names are shown as the
app spells them. A competency from any catalog can be used in any
assessment, but include its catalog in `catalogs`.

### `secops`: Security operations (IR / SOC / hunting) (109 competencies)

- **MON** Monitoring & triage: `MON.1` Alert triage and prioritisation; `MON.2` SIEM query authoring; `MON.3` Endpoint telemetry interpretation; `MON.4` Network telemetry interpretation; `MON.5` Identity and authentication log analysis; `MON.6` Email and phishing analysis; `MON.7` False-positive analysis and feedback; `MON.8` Case documentation and handover; `MON.9` Escalation judgement.
- **IR** Incident response: `IR.1` Incident lifecycle execution; `IR.2` Scoping and impact assessment; `IR.3` Containment decision-making; `IR.4` Eradication and recovery planning; `IR.5` Evidence preservation under pressure; `IR.6` Incident command and coordination; `IR.7` Stakeholder and executive communication; `IR.8` Regulatory notification awareness; `IR.9` Ransomware response; `IR.10` Business email compromise response; `IR.11` Third-party and supply-chain incidents; `IR.12` Post-incident review facilitation.
- **DF** Digital forensics: `DF.1` Forensic acquisition and verification; `DF.2` Filesystem and metadata analysis; `DF.3` Windows artefact analysis; `DF.4` Linux and macOS artefact analysis; `DF.5` Memory acquisition and analysis; `DF.6` Timeline construction; `DF.7` Cloud and SaaS forensics; `DF.8` Mobile device forensics; `DF.9` Anti-forensics recognition; `DF.10` Chain of custody and evidence handling; `DF.11` Forensic reporting and testimony.
- **MAL** Malware analysis: `MAL.1` Static triage; `MAL.2` Dynamic and sandbox analysis; `MAL.3` Deobfuscation and unpacking; `MAL.4` Script and document macro analysis; `MAL.5` Reverse engineering; `MAL.6` Configuration and capability extraction; `MAL.7` YARA and signature authoring; `MAL.8` Safe handling and lab hygiene.
- **TH** Threat hunting: `TH.1` Hypothesis formulation; `TH.2` Data-source assessment for hunting; `TH.3` Baselining and anomaly reasoning; `TH.4` Stack counting and frequency analysis; `TH.5` Hunt execution and iteration; `TH.6` Hunt documentation and handoff; `TH.7` Hunt programme design.
- **CTI** Threat intelligence: `CTI.1` Intelligence requirements definition; `CTI.2` Collection and source management; `CTI.3` Structured analytic technique; `CTI.4` Adversary tracking and attribution reasoning; `CTI.5` MITRE ATT&CK fluency; `CTI.6` Indicator lifecycle and enrichment; `CTI.7` Intelligence writing; `CTI.8` Sharing frameworks and handling caveats.
- **DE** Detection engineering: `DE.1` Detection logic authoring; `DE.2` Telemetry and data-source engineering; `DE.3` Detection testing and validation; `DE.4` Detection-as-code practice; `DE.5` Coverage mapping and gap analysis; `DE.6` Tuning and false-positive management; `DE.7` Alert enrichment and context design.
- **PT** Adversary emulation & purple: `PT.1` Attack technique execution; `PT.2` Adversary emulation planning; `PT.3` Control efficacy assessment; `PT.4` Purple-team facilitation; `PT.5` Safe execution and blast-radius control; `PT.6` Vulnerability assessment and exploitation basics.
- **PLT** Platform & telemetry engineering: `PLT.1` SIEM administration and content management; `PLT.2` Log pipeline and normalisation; `PLT.3` Endpoint agent deployment and health; `PLT.4` SOAR platform administration; `PLT.5` Data retention and cost management; `PLT.6` Telemetry coverage monitoring.
- **AUT** Automation & development: `AUT.1` Scripting; `AUT.2` API integration; `AUT.3` Playbook and workflow automation; `AUT.4` Version control and code review; `AUT.5` Data wrangling and large-scale analysis; `AUT.6` Tool building for the team.
- **CLD** Cloud & container security: `CLD.1` AWS investigation; `CLD.2` Azure and Entra ID investigation; `CLD.3` Google Cloud investigation; `CLD.4` Cloud control-plane attack reasoning; `CLD.5` Container and Kubernetes response; `CLD.6` Cloud posture and IaC reasoning.
- **IAM** Identity & access: `IAM.1` Active Directory attack paths; `IAM.2` Entra ID and federation abuse; `IAM.3` Privileged access reasoning; `IAM.4` Man-in-the-middle and session-token theft; `IAM.5` Authentication and access log analysis.
- **NET** Network & OT: `NET.1` Packet capture and analysis; `NET.2` Protocol fluency; `NET.3` Segmentation and architecture reasoning; `NET.4` Perimeter, proxy and DNS analysis; `NET.5` OT and ICS awareness; `NET.6` Encrypted traffic reasoning.
- **GOV** Leadership, advisory & communication: `GOV.1` Written reporting and deliverable quality; `GOV.2` Client and stakeholder management; `GOV.3` Briefing and presentation delivery; `GOV.4` Mentoring and coaching; `GOV.5` Shift leadership and workload management; `GOV.6` Metrics and performance reporting; `GOV.7` Risk framing for executives; `GOV.8` Legal, privacy and regulatory awareness; `GOV.9` Tabletop and exercise facilitation; `GOV.10` Vendor and tool evaluation; `GOV.11` Recruitment and interviewing; `GOV.12` Process and runbook authoring.

### `nice`: NICE-aligned (broad cyber workforce) (56 competencies)

- **NSP** Securely provision: `NSP.1` Risk management and authorisation; `NSP.2` Systems and security architecture; `NSP.3` Requirements definition; `NSP.4` Secure software development; `NSP.5` Software security assessment; `NSP.6` Systems development lifecycle; `NSP.7` Test and evaluation; `NSP.8` Technology research and evaluation.
- **NOM** Operate and maintain: `NOM.1` Systems administration; `NOM.2` Network administration; `NOM.3` Database administration; `NOM.4` Data management and analysis; `NOM.5` Knowledge management; `NOM.6` Customer and technical support; `NOM.7` Systems security analysis; `NOM.8` Configuration and change management.
- **NOV** Oversee and govern: `NOV.1` Cybersecurity policy and strategy; `NOV.2` Programme and project management; `NOV.3` Acquisition and supply-chain management; `NOV.4` Workforce planning and development; `NOV.5` Training delivery and curriculum design; `NOV.6` Legal advice and advocacy; `NOV.7` Privacy compliance; `NOV.8` Executive leadership and governance.
- **NPR** Protect and defend: `NPR.1` Cyber defence analysis; `NPR.2` Incident response; `NPR.3` Defensive infrastructure support; `NPR.4` Vulnerability assessment and management; `NPR.5` Threat detection engineering; `NPR.6` Security monitoring operations; `NPR.7` Malware and intrusion analysis; `NPR.8` Continuity and resilience operations.
- **NAN** Analyze: `NAN.1` All-source intelligence analysis; `NAN.2` Threat and warning analysis; `NAN.3` Exploitation analysis; `NAN.4` Target development; `NAN.5` Target network analysis; `NAN.6` Language analysis and translation; `NAN.7` Mission assessment; `NAN.8` Analytic reporting and dissemination.
- **NCO** Collect and operate: `NCO.1` Collection management; `NCO.2` Collection requirements definition; `NCO.3` Cyber operations planning; `NCO.4` Cyber operations execution; `NCO.5` Partner and stakeholder integration; `NCO.6` Operational security; `NCO.7` Mission tasking and prioritisation; `NCO.8` Operational reporting.
- **NIN** Investigate: `NIN.1` Digital forensic examination; `NIN.2` Cybercrime investigation; `NIN.3` Evidence handling and custody; `NIN.4` Interview awareness; `NIN.5` Legal process and authorities; `NIN.6` Media and device exploitation; `NIN.7` Investigative case management; `NIN.8` Expert reporting and testimony.

### `itops`: IT & digital delivery (broad) (81 competencies)

- **SWE** Software engineering: `SWE.1` Programming in a primary language; `SWE.2` Automated testing; `SWE.3` Code review; `SWE.4` Debugging and fault diagnosis; `SWE.5` Version control and branching; `SWE.6` API design; `SWE.7` Refactoring and technical debt; `SWE.8` Performance and profiling.
- **ARC** Architecture & technical design: `ARC.1` Systems design; `ARC.2` Data modelling; `ARC.3` Integration and messaging patterns; `ARC.4` Non-functional requirements; `ARC.5` Technology evaluation; `ARC.6` Architecture documentation; `ARC.7` Legacy and migration strategy.
- **DAT** Data engineering & analytics: `DAT.1` Data pipeline engineering; `DAT.2` SQL and query optimisation; `DAT.3` Data warehouse and lakehouse modelling; `DAT.4` Data quality and validation; `DAT.5` Analytics and visualisation; `DAT.6` Statistical reasoning; `DAT.7` Machine learning delivery; `DAT.8` Data governance and lineage.
- **CLP** Cloud & platform engineering: `CLP.1` Infrastructure as code; `CLP.2` Container and orchestration platforms; `CLP.3` CI/CD pipeline engineering; `CLP.4` Cloud service design and cost; `CLP.5` Networking fundamentals; `CLP.6` Platform as a product; `CLP.7` Configuration and secret management.
- **REL** Reliability & operations: `REL.1` Monitoring and instrumentation; `REL.2` Alerting and on-call practice; `REL.3` Incident response and command; `REL.4` Post-incident review; `REL.5` Service level objectives; `REL.6` Capacity and scaling; `REL.7` Backup, restore and continuity.
- **QUA** Quality engineering & testing: `QUA.1` Test strategy; `QUA.2` Test automation frameworks; `QUA.3` Exploratory testing; `QUA.4` Accessibility testing; `QUA.5` Performance and load testing; `QUA.6` Release and change management.
- **SDL** Security in delivery: `SDL.1` Secure coding practice; `SDL.2` Threat modelling; `SDL.3` Dependency and supply-chain hygiene; `SDL.4` Identity and access in applications; `SDL.5` Privacy and data protection by design; `SDL.6` Vulnerability triage and remediation.
- **PRD** Product & delivery management: `PRD.1` Product discovery; `PRD.2` Prioritisation and roadmapping; `PRD.3` Requirements and acceptance criteria; `PRD.4` Agile delivery practice; `PRD.5` Estimation and forecasting; `PRD.6` Stakeholder management; `PRD.7` Vendor and contract management.
- **UCD** User-centred design: `UCD.1` User research; `UCD.2` Interaction and interface design; `UCD.3` Service design; `UCD.4` Content design; `UCD.5` Accessibility and inclusive design; `UCD.6` Prototyping and usability testing.
- **ITS** IT service management: `ITS.1` Incident and request handling; `ITS.2` Problem management; `ITS.3` Change enablement; `ITS.4` Service catalogue and SLAs; `ITS.5` Configuration and asset management; `ITS.6` Knowledge management; `ITS.7` Service reporting.
- **EUC** End-user & workplace technology: `EUC.1` Endpoint build and management; `EUC.2` Directory and collaboration platforms; `EUC.3` End-user support and troubleshooting; `EUC.4` Device and application security baseline; `EUC.5` Onboarding and offboarding.
- **LDR** Technical leadership & communication: `LDR.1` Technical writing; `LDR.2` Briefing and presentation; `LDR.3` Mentoring and coaching; `LDR.4` Facilitation and decision-making; `LDR.5` Risk framing for executives; `LDR.6` Recruitment and interviewing; `LDR.7` Team and workload management.
