Sign In

@toolstop/export-control

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@toolstop/export-control - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+51
-21
dist/lib.mjs

@@ -8,3 +8,3 @@ // Licence-requirement lookup against the Commerce Country Chart.

import { COUNTRIES, ECCNS, SOURCE_EDITION } from "./data.mjs";
import { COUNTRIES, ECCNS, FOOTNOTES, SOURCE_EDITION } from "./data.mjs";

@@ -54,6 +54,21 @@ export { SOURCE_EDITION };

"swaziland": "Eswatini",
"turkiye": "Turkey",
// The chart says Türkiye, which normalises to "turkiye" and needs no alias.
// The entry here used to point the other way, at a spelling the chart does
// not carry, so it never resolved anything.
"turkey": "Türkiye",
"macao": "Macau",
};
/**
* Footnote numbers plus the text they stand for.
*
* The numbers alone were what the chart row carried, and a bare `[2, 3]` is
* unreadable: footnote 6 is the Russia and Belarus sanctions and footnote 8 is
* Crimea, so these are conditions that change the answer, not citations.
*/
function footnotesOf(row) {
return (row.f ?? []).map((n) => ({ number: n, text: FOOTNOTES[n] ?? null }));
}
/**
* The United States is not a row on the chart, because the chart lists export

@@ -115,14 +130,17 @@ * *destinations*. Answering "unknown country" would read as a data gap and

/**
* An entry with no controls in the data is a question this server cannot
* answer, and it must never come back as `licenseRequired: false`.
* An entry the country chart does not decide, which must never come back as
* `licenseRequired: false`.
*
* Two different things produce one: an entry whose licence requirements are
* written as prose rather than as a country-chart table (0A983 reads "a license
* is required for ALL destinations", which is the opposite of no licence), and
* an entry that is a pointer to the ITAR and carries no EAR requirement at all
* (0A002). Both are real, controlled items. Neither can be answered from an
* empty control list, and `false` computed from an empty array is not a finding
* about the regulation, it is the absence of one.
* Two kinds reach here. An entry whose controls exist but name no chart column:
* 0A983 reads "a license is required for ALL destinations, regardless of
* end-use", and reporting that the chart requires nothing would be the exact
* inversion of it. And an entry with no licence requirements of its own, which
* is a pointer to another authority, usually the ITAR. A State Department
* licence is not the absence of a requirement either.
*
* `licenseRequired` is deliberately absent rather than false. The field means
* "the chart decided and the answer was no", and neither of these is that.
*/
function indeterminateResult(eccn, entry) {
const controls = entry.c.map(([scope, requirement]) => ({ scope, requirement }));
return {

@@ -133,8 +151,10 @@ status: "indeterminate",

reasons: (entry.r ?? []).map((r) => ({ code: r, name: REASON_CODES[r] ?? null })),
message:
`${eccn} carries no country-chart control in the ${SOURCE_EDITION} data, so the chart cannot ` +
"answer for it. Do not read this as no licence required: entries in this state include items " +
"requiring a licence to all destinations, and items that are subject to the ITAR rather than " +
"the EAR. Read the entry directly in 15 CFR part 774, Supplement No. 1, and check the title " +
"returned here, which often states the requirement.",
controlsNotOnChart: controls,
message: controls.length
? `${eccn} is controlled, but by requirements the Commerce Country Chart does not decide, so the ` +
"chart cannot answer for it. The requirement text is returned verbatim in `controlsNotOnChart` " +
"and often requires a licence to every destination. Read it before treating anything as cleared."
: `${eccn} carries no licence requirements of its own in the ${SOURCE_EDITION} Control List. Entries ` +
"in this state point at another authority, usually the ITAR, and the heading returned here names " +
"it. This is not a finding that no licence is required; see 22 CFR parts 120 through 130.",
notCovered: NOT_COVERED,

@@ -203,3 +223,6 @@ };

if (!entry.c.length) return indeterminateResult(eccn, entry);
// Nothing on this entry is decided by the chart, so the chart has no verdict
// to give. An entry with *some* chart control is answered normally, with the
// off-chart ones surfaced alongside.
if (!entry.c.some((c) => c[2])) return indeterminateResult(eccn, entry);

@@ -226,3 +249,3 @@ const marks = new Set(row.c);

countryColumns: row.c,
footnotes: row.f ?? [],
footnotes: footnotesOf(row),
sourceEdition: SOURCE_EDITION,

@@ -238,3 +261,10 @@ notCovered: NOT_COVERED,

if (!e) return { status: "unknown_eccn", eccn, message: `${eccn} is not in the ${SOURCE_EDITION} Commerce Control List.` };
if (!e.c.length) return { ...indeterminateResult(eccn, e), chartDetermined: false, sourceEdition: SOURCE_EDITION };
if (!e.c.some((c) => c[2])) {
return {
...indeterminateResult(eccn, e),
controls: e.c.map(([scope, requirement, column]) => ({ scope, requirement, column })),
chartDetermined: false,
sourceEdition: SOURCE_EDITION,
};
}
return {

@@ -268,3 +298,3 @@ status: "ok",

columns: row.c.map((c) => ({ column: c, reason: c.slice(0, 2), reasonName: REASON_CODES[c.slice(0, 2)] ?? null })),
footnotes: row.f ?? [],
footnotes: footnotesOf(row),
sourceEdition: SOURCE_EDITION,

@@ -271,0 +301,0 @@ };

@@ -36,3 +36,3 @@ // The single source of truth for this server. Both transports import this and

name: "export-control",
version: "0.2.0",
version: "0.3.0",
instructions:

@@ -63,9 +63,10 @@ "Answers one question: does the Commerce Country Chart require an export " +

"you should put that choice to a human rather than picking.\n\n" +
"Some entries write their licence requirement as prose rather than as a " +
"chart column, and some are pointers to the ITAR carrying no EAR " +
"requirement at all. Those return `indeterminate` rather than an answer, " +
"because a chart verdict computed from an entry with no chart controls " +
"would read as `licenseRequired: false` for items that in fact require a " +
"licence to every destination. Treat `indeterminate` as unanswered and go " +
"to the regulation.\n\n" +
"Not every entry is decided by the chart. Some are controlled by a " +
"requirement written into the entry itself, often a licence to every " +
"destination regardless of end-use, and some are pointers to the ITAR " +
"carrying no EAR requirement at all. Both return `indeterminate` with the " +
"requirement text verbatim, rather than a verdict, because the chart has no " +
"answer to give and `licenseRequired: false` would invert the real one. " +
"Treat `indeterminate` as unanswered, read the text it returns, and go to " +
"the regulation.\n\n" +
"A result of `licenseRequired: false` means only that the country chart " +

@@ -95,6 +96,6 @@ "does not require one for that ECCN. It is not permission to export. The " +

"about the Entity List, embargoes, end-use controls, or whether the " +
"ECCN is the right one. Entries whose requirement is not written as a " +
"chart column return `indeterminate` instead of a verdict, and that is " +
"an unanswered question rather than a negative answer. This tool cannot " +
"classify an item into an ECCN and will not try.",
"ECCN is the right one. An entry the chart does not decide returns " +
"`indeterminate` with its requirement text instead of a verdict, which " +
"is an unanswered question rather than a negative answer. This tool " +
"cannot classify an item into an ECCN and will not try.",
annotations: { readOnlyHint: true, openWorldHint: false },

@@ -161,4 +162,4 @@ inputSchema: {

{
// The requirement is "a license is required for ALL destinations",
// written as prose. An empty control list must not become `false`.
// Controlled by "a license is required for ALL destinations", which
// the chart does not decide. This must never read as `false`.
args: { eccn: "0A983", country: "France" },

@@ -168,2 +169,6 @@ expect: { status: "indeterminate", eccn: "0A983" },

{
args: { eccn: "3D006", country: "China" }, // added to the CCL after the first parse dropped it
expect: { status: "ok", eccn: "3D006", licenseRequired: true },
},
{
args: { eccn: "3A001", country: "Canada" },

@@ -296,4 +301,7 @@ expect: { status: "ok", licenseRequired: false },

type: "array",
description: "Chart footnote numbers on this row, which carry conditions the grid does not show.",
items: { type: "number" },
description:
"Chart footnotes on this row, each with its number and text. They carry conditions the " +
"grid cannot show, such as the Russia and Belarus sanctions and the Crimea controls, and " +
"they can change the answer.",
items: { type: "object" },
},

@@ -300,0 +308,0 @@ },

{
"name": "@toolstop/export-control",
"version": "0.2.0",
"version": "0.3.0",
"mcpName": "dev.toolstop/export-control",

@@ -5,0 +5,0 @@ "description": "MCP server that answers whether the US Commerce Country Chart requires an export licence for a given ECCN to a given destination, from the published CFR tables.",

+47
-12

@@ -10,4 +10,4 @@ # @toolstop/export-control

- **Commerce Country Chart**, 15 CFR part 738, Supplement No. 1. 200 destinations
by 16 control columns.
- **Commerce Control List**, 15 CFR part 774, Supplement No. 1. 636 ECCNs with
by 16 control columns, with the ten chart footnotes.
- **Commerce Control List**, 15 CFR part 774, Supplement No. 1. 637 ECCNs with
their reasons for control and the chart column each one maps to.

@@ -46,2 +46,6 @@

chart does not decide
check_export_license { eccn: "0A983", country: "France" }
-> indeterminate, with "a license is required for ALL destinations,
regardless of end-use" returned verbatim
```

@@ -72,2 +76,11 @@

**Entries the chart does not decide return `indeterminate`, not `false`.** Some
ECCNs carry their requirement in the entry itself rather than in a chart column,
and it is frequently the strongest one in the part: 0A983, implements of torture,
reads *a license is required for ALL destinations, regardless of end-use*. Others
are pointers to the ITAR with no EAR requirement at all. Both return the
requirement text verbatim and no verdict, because `licenseRequired: false` on
either would be the inverse of the answer. 47 of the 637 entries are in the
second group; the first is answered from the text.
**Ambiguous country names are not guessed.** "Congo" returns both Congos and asks

@@ -105,16 +118,38 @@ you to choose. So does anything else matching more than one row.

```bash
curl "https://www.ecfr.gov/api/versioner/v1/full/<date>/title-15.xml?part=738" -o chart.xml
curl "https://www.ecfr.gov/api/versioner/v1/full/<date>/title-15.xml?part=774" -o ccl.xml
node scripts/gen-export-control.mjs # today's edition
node scripts/gen-export-control.mjs --date=2026-01-01
```
The country chart is one table keyed by a 16-column header (`CB 1` through
`AT 2`); the Control List is one entry per `<FP-2><B>` heading whose text starts
with an ECCN, each carrying a `Reason for Control:` line and a two-column table
mapping a control to its chart column.
Read that script before changing it. The first version of this data came from a
generator that was never committed, and it shipped seven defects that were
invisible in the output and green in the test suite: entries whose requirement
is written as prose lost it and then cleared every destination, table rows with
a trailing empty cell were dropped, the reason line was read past its end so one
entry carried 341 reason codes, XML entities were never decoded so Türkiye could
not be looked up by any spelling, and every string was cut at 220 characters
mid-word.
Two shapes in that source will silently lose data if a rewrite misses them.
Some control tables merge both cells into one `colspan="2"` cell, and the four
embargoed destinations carry a referral sentence instead of X marks rather than
appearing as an empty row.
So the invariants at the foot of that script matter more than its parsing does.
Each one is a defect that shipped, and they fail the build rather than writing a
table that looks complete. The load-bearing one: **an entry with a licence
section and no control parsed out of it is a parse failure, not an entry without
controls.** That single assertion catches most of the above.
The shapes in the source that will silently lose data if a rewrite misses them:
| Shape | Where |
|---|---|
| Control table with both cells merged into one `colspan="2"` cell | 1C350 |
| Control table row with a trailing empty third cell | 6D201 |
| Empty chart cell, with the requirement stated in the scope cell | 1E355 |
| `Control(s):` as prose in an `FP-1`, with or without the `<I>` label | 0A981, 0A983 |
| `Controls:`, spelled without the parenthetical | 5D980 |
| `Control(s)` as a bare heading followed by `<P>` paragraphs | 1C355 |
| A control paragraph sitting beside a captured table | 1C350's CW rule |
| Entry heading split across several `<B>` tags to italicise a term | 3D006 |
| `Reason for Control` written in the plural, or wrapped in an `FP-2` | 0A919, 2B910 |
| An entry with no `License Requirements` header, going straight to the reason line | 3A001 |
| Referral sentence in place of X marks, rather than an empty row | Cuba, Iran, North Korea, Syria |
| Footnote text as a row of the destinations table | the ten chart footnotes |
## License

@@ -121,0 +156,0 @@

Sorry, the diff of this file is too big to display