@toolstop/export-control
Advanced tools
+51
-21
@@ -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 @@ }; |
+24
-16
@@ -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 @@ }, |
+1
-1
| { | ||
| "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
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
351589
10.31%2128
7.15%155
29.17%3
200%