Extending the Rules
All domain policy lives in declarative tables in src/license-db.js. Adding knowledge is data
entry, not programming — which is the point of ADR-007 and AG-7.
:::warning Changing a tier is a controlled change The tier table affects every verdict ever produced. A change requires re-baselining all historical findings and legal re-review. Adding a new license is routine; changing an existing tier is not. See Decision Records. :::
Add a license
Add an entry to LICENSE_RULES. Matching is longest-prefix, case-insensitive, so
lgpl-2. covers LGPL-2.0-only, LGPL-2.1-or-later and LGPL-2.1+.
{ match: 'lgpl-2.', tier: 'HIGH', cat: 'weak-copyleft',
obl: ['notice', 'state-changes', 'source-modified', 'relinking'],
note: 'Static linking in monolithic firmware is the most common compliance gap.' }
| Field | Meaning |
|---|---|
match | Lowercase prefix to match against the license id |
tier | One of CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN |
cat | Category — drives propagation behaviour (strong / weak / file copyleft, permissive, …) |
obl | Obligation keys from the OBLIGATIONS catalogue |
note | Free-text rationale, surfaced in the UI |
Available obligation keys: notice, state-changes, source-full, source-modified,
relinking, network-source, install-info, patent, no-endorsement, advertising,
non-commercial.
Relax a license via an exception
Add to LINKING_EXCEPTIONS. relax: 1 steps the tier down the ladder
LOW → MEDIUM → HIGH → CRITICAL; relax: 0 records the note but does not downgrade.
'u-boot-exception-2.0': { relax: 1,
note: 'U-Boot exception: static linking of GPL-2.0+ U-Boot into firmware is permitted.' },
'linux-syscall-note': { relax: 0,
note: 'Syscall note: user-space programs calling the kernel are not derivative works.' },
:::caution The ladder excludes UNKNOWN on purpose
If UNKNOWN were on the ladder, CRITICAL would relax onto UNKNOWN and produce a verdict that
looks like missing data rather than copyleft. That was BUG-02.
:::
Add a known incompatibility
Add to COMPAT. Matching is by family prefix, in either direction.
{ a: 'gpl-2.0', b: 'apache-2.0', sev: 'HIGH',
why: 'Apache-2.0 patent/indemnity terms are additional restrictions under GPL-2.0.' }
The why string is surfaced verbatim in the finding, so a reviewer sees why two licenses
conflict rather than just that they do.
Add a finding rule
New rules go in src/risk-engine.js, taking the next free LIC-0xx.
:::danger Never renumber an existing rule ID
LIC-002 is reserved but unimplemented precisely to keep the numbering stable. Findings are
referenced in reports, in diff matching, and — in Phase 2 — in waiver registers. Append; do not
renumber.
:::
Other extension points
| Extension | Where | Effort |
|---|---|---|
| CycloneDX input | A new parser producing the same normalised model as parseSpdx | Days |
| New export format | A new renderer in src/report.js | Hours |
| Enrichment (OSV, ClearlyDefined, deps.dev) | A post-parse step keyed on purl | Days |
| Hosted API | A Worker with a fetch handler wrapping risk-engine.js | Days |
| Persistence and workflow | A backend service; the engine is unchanged | Weeks |
Enrichment worth adding later
All optional, and the tool must stay usable offline:
- SPDX license list validation (also fixes TD-09)
- ClearlyDefined and deps.dev lookups by purl
- OSV / NVD CVE correlation
- A
LicenseRef-text classifier, once you have real legal-reviewed samples
Hosted API caveat
tools/mcp-server.mjs cannot run on Workers — there is no stdin and no filesystem. If you
want a hosted API, wrap src/risk-engine.js in a Worker with a fetch handler. The engine is
pure and has no Node dependencies, so this is straightforward.