@mailwoman/codex 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +48 -5
  2. package/ca/index.ts +1 -1
  3. package/ca/postal-code.ts +11 -11
  4. package/country/display-names.ts +150 -0
  5. package/country/index.ts +2 -0
  6. package/country/population.ts +265 -0
  7. package/country/subdivision.ts +59 -0
  8. package/de/index.ts +1 -1
  9. package/de/postleitzahl.ts +3 -3
  10. package/es/codigo-postal.ts +2 -2
  11. package/fr/code-postal.ts +6 -6
  12. package/fr/index.ts +1 -1
  13. package/index.ts +10 -1
  14. package/it/cap.ts +2 -2
  15. package/jp/index.ts +1 -1
  16. package/jp/postal-code.ts +11 -11
  17. package/out/ca/index.d.ts +1 -1
  18. package/out/ca/index.js +1 -1
  19. package/out/ca/postal-code.d.ts +10 -10
  20. package/out/ca/postal-code.js +9 -9
  21. package/out/country/display-names.d.ts +67 -0
  22. package/out/country/display-names.d.ts.map +1 -0
  23. package/out/country/display-names.js +118 -0
  24. package/out/country/display-names.js.map +1 -0
  25. package/out/country/index.d.ts +2 -0
  26. package/out/country/index.d.ts.map +1 -1
  27. package/out/country/index.js +2 -0
  28. package/out/country/index.js.map +1 -1
  29. package/out/country/population.d.ts +16 -0
  30. package/out/country/population.d.ts.map +1 -0
  31. package/out/country/population.js +265 -0
  32. package/out/country/population.js.map +1 -0
  33. package/out/country/subdivision.d.ts +7 -0
  34. package/out/country/subdivision.d.ts.map +1 -1
  35. package/out/country/subdivision.js +49 -0
  36. package/out/country/subdivision.js.map +1 -1
  37. package/out/de/index.d.ts +1 -1
  38. package/out/de/index.js +1 -1
  39. package/out/de/postleitzahl.d.ts +3 -3
  40. package/out/de/postleitzahl.js +2 -2
  41. package/out/es/codigo-postal.d.ts +2 -2
  42. package/out/es/codigo-postal.js +1 -1
  43. package/out/fr/code-postal.d.ts +5 -5
  44. package/out/fr/code-postal.js +6 -6
  45. package/out/fr/index.d.ts +1 -1
  46. package/out/fr/index.js +1 -1
  47. package/out/index.d.ts +1 -1
  48. package/out/index.d.ts.map +1 -1
  49. package/out/index.js +1 -1
  50. package/out/index.js.map +1 -1
  51. package/out/it/cap.d.ts +2 -2
  52. package/out/it/cap.js +1 -1
  53. package/out/jp/index.d.ts +1 -1
  54. package/out/jp/index.js +1 -1
  55. package/out/jp/postal-code.d.ts +11 -11
  56. package/out/jp/postal-code.js +8 -8
  57. package/out/postcode-systems.d.ts +84 -0
  58. package/out/postcode-systems.d.ts.map +1 -1
  59. package/out/postcode-systems.js +106 -0
  60. package/out/postcode-systems.js.map +1 -1
  61. package/out/tools/build-country-surface-lexicon.js +1 -1
  62. package/out/tools/build-country-surface-lexicon.js.map +1 -1
  63. package/out/tools/export-country-surfaces.js +1 -1
  64. package/out/tools/export-country-surfaces.js.map +1 -1
  65. package/out/tools/generate-country-population.d.ts +42 -0
  66. package/out/tools/generate-country-population.d.ts.map +1 -0
  67. package/out/tools/generate-country-population.js +106 -0
  68. package/out/tools/generate-country-population.js.map +1 -0
  69. package/out/tools/generate-country-reference.d.ts.map +1 -1
  70. package/out/tools/generate-country-reference.js +4 -4
  71. package/out/tools/generate-country-reference.js.map +1 -1
  72. package/out/tools/generate-official-languages.d.ts.map +1 -1
  73. package/out/tools/generate-official-languages.js +7 -4
  74. package/out/tools/generate-official-languages.js.map +1 -1
  75. package/out/tools/index.d.ts +2 -2
  76. package/out/tools/index.d.ts.map +1 -1
  77. package/out/tools/index.js +2 -2
  78. package/out/tools/index.js.map +1 -1
  79. package/out/us/zipcode.d.ts +1 -1
  80. package/out/us/zipcode.js +1 -1
  81. package/package.json +326 -4
  82. package/postcode-systems.ts +112 -0
  83. package/tools/build-country-surface-lexicon.ts +1 -1
  84. package/tools/export-country-surfaces.ts +1 -1
  85. package/tools/generate-country-population.ts +143 -0
  86. package/tools/generate-country-reference.ts +5 -3
  87. package/tools/generate-official-languages.ts +8 -4
  88. package/tools/index.ts +2 -2
  89. package/us/zipcode.ts +1 -1
@@ -0,0 +1,143 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Regenerate `codex/country/population.ts` — the per-country population table — from GeoNames
7
+ * `countryInfo.txt` (https://download.geonames.org/export/dump/countryInfo.txt, CC-BY-4.0). The
8
+ * output is committed; this tool makes it reproducible (provenance), not a hand-typed dictionary.
9
+ *
10
+ * Why it exists (#1650): WOF carries no readable population for 147 of 237 country records
11
+ * (measured against the 2026-08-18 candidate build), so those countries entered every prominence
12
+ * race at an asserted ZERO — ranked below any namesake hamlet. The magnitude is what a fame race
13
+ * reads, so a census-vintage figure is entirely sufficient; currency of the estimate is not the
14
+ * point.
15
+ *
16
+ * Rows whose GeoNames population is 0 are DROPPED rather than emitted: an entry in this table is a
17
+ * positive claim, and the consumer's absence branch (`?? undefined`) must stay reachable for
18
+ * territories GeoNames itself declines to estimate (the meaning-of-zero rule).
19
+ *
20
+ * Usage: mailwoman dev generate country-population
21
+ */
22
+
23
+ import { writeFileSync } from "node:fs"
24
+ import { fileURLToPath } from "node:url"
25
+
26
+ import { APIClient, pluckResponseData } from "@mailwoman/core/api"
27
+
28
+ const SOURCE = "https://download.geonames.org/export/dump/countryInfo.txt"
29
+
30
+ /**
31
+ * The committed output path, resolved relative to this module (codex/tools/ → codex/country/). See
32
+ * `generate-country-reference.ts` for why this is `import.meta.url` rather than a `core` path helper (codex is
33
+ * zero-runtime-dep and `core` already references `codex` — importing core here would cycle the project graph).
34
+ */
35
+ const DEFAULT_OUT = fileURLToPath(new URL("../country/population.ts", import.meta.url))
36
+
37
+ /**
38
+ * Tab positions this tool reads from `countryInfo.txt`'s 19-column rows. Named so the parse states which columns it
39
+ * believes in; a GeoNames format change fails the count guard below rather than silently reading the wrong column.
40
+ */
41
+ const COLUMN_ISO2 = 0
42
+ const COLUMN_POPULATION = 7
43
+ const MINIMUM_COLUMNS = 8
44
+
45
+ /**
46
+ * GeoNames publishes ~250 countries/territories; a parse recovering fewer than this read the wrong column or a
47
+ * truncated body, and the guard fails loudly instead of committing a hollow table.
48
+ */
49
+ const MINIMUM_PLAUSIBLE_COUNTRIES = 200
50
+
51
+ /**
52
+ * Numbers below five digits are emitted bare — the house numeric-separator style groups by three and only from five
53
+ * digits up (`8450`, not `8_450`).
54
+ */
55
+ const SEPARATOR_MINIMUM = 10_000
56
+
57
+ /**
58
+ * Options for {@linkcode generateCountryPopulation}.
59
+ */
60
+ export interface GenerateCountryPopulationOptions {
61
+ /**
62
+ * Output path override. Default: `codex/country/population.ts` (the committed table).
63
+ */
64
+ out?: string
65
+ }
66
+
67
+ /**
68
+ * Summary returned by {@linkcode generateCountryPopulation}.
69
+ */
70
+ export interface GenerateCountryPopulationSummary {
71
+ countries: number
72
+ outPath: string
73
+ }
74
+
75
+ /**
76
+ * Fetch GeoNames `countryInfo.txt` and regenerate the committed `COUNTRY_POPULATION` table.
77
+ */
78
+ export async function generateCountryPopulation(
79
+ options: GenerateCountryPopulationOptions = {},
80
+ report?: (line: string) => void
81
+ ): Promise<GenerateCountryPopulationSummary> {
82
+ const outPath = options.out ?? DEFAULT_OUT
83
+
84
+ // `responseType: "text"` because the source is a tab-separated dump, not JSON.
85
+ const text = await new APIClient({ displayName: "geonames-country-info", retry: true })
86
+ .fetch<string>({ url: SOURCE, responseType: "text" })
87
+ .then(pluckResponseData)
88
+
89
+ const rows: Record<string, number> = {}
90
+
91
+ // oxlint-disable-next-line mailwoman/prefer-spliterator -- countryInfo.txt is ~35 kB and bounded (~300 rows)
92
+ for (const line of text.split("\n")) {
93
+ if (!line || line.startsWith("#")) continue
94
+ // oxlint-disable-next-line mailwoman/prefer-spliterator -- one 19-column row
95
+ const columns = line.split("\t")
96
+
97
+ if (columns.length < MINIMUM_COLUMNS) continue
98
+ const alpha2 = columns[COLUMN_ISO2]!.trim()
99
+ const population = Number(columns[COLUMN_POPULATION])
100
+
101
+ if (!/^[A-Z]{2}$/.test(alpha2)) continue
102
+
103
+ if (!Number.isFinite(population) || population <= 0) continue
104
+
105
+ rows[alpha2] = population
106
+ }
107
+
108
+ if (Object.keys(rows).length < MINIMUM_PLAUSIBLE_COUNTRIES) {
109
+ throw new Error(`generateCountryPopulation: only ${Object.keys(rows).length} rows parsed — format drift?`)
110
+ }
111
+
112
+ const body = Object.keys(rows)
113
+ .toSorted()
114
+ .map((k) => {
115
+ const n = rows[k]!
116
+ const literal = n >= SEPARATOR_MINIMUM ? n.toLocaleString("en-US").replaceAll(",", "_") : String(n)
117
+
118
+ return `\t${k}: ${literal},`
119
+ })
120
+ .join("\n")
121
+
122
+ const header = `/**
123
+ * @copyright Sister Software
124
+ * @license AGPL-3.0
125
+ * @author Teffen Ellis, et al.
126
+ *
127
+ * GENERATED — do not edit by hand. Per-country population, derived from GeoNames countryInfo.txt
128
+ * (https://download.geonames.org/export/dump/countryInfo.txt, CC-BY-4.0). Estimates are
129
+ * census-vintage; a prominence race reads the magnitude, not the currency. Countries GeoNames
130
+ * declines to estimate are ABSENT, never zero.
131
+ * Regenerate with: mailwoman dev generate country-population
132
+ */
133
+
134
+ /**
135
+ * ISO 3166-1 alpha-2 → population estimate.
136
+ */
137
+ export const COUNTRY_POPULATION: Readonly<Record<string, number>> = {`
138
+
139
+ writeFileSync(outPath, `${header}\n${body}\n}\n`)
140
+ report?.(`wrote ${outPath} (${Object.keys(rows).length} countries)`)
141
+
142
+ return { countries: Object.keys(rows).length, outPath }
143
+ }
@@ -17,6 +17,8 @@
17
17
  import { writeFileSync } from "node:fs"
18
18
  import { fileURLToPath } from "node:url"
19
19
 
20
+ import { APIClient, pluckResponseData } from "@mailwoman/core/api"
21
+
20
22
  const SOURCE = "https://raw.githubusercontent.com/mledoze/countries/master/countries.json"
21
23
 
22
24
  /**
@@ -97,10 +99,10 @@ export async function generateCountryReference(
97
99
  report?: (line: string) => void
98
100
  ): Promise<GenerateCountryReferenceSummary> {
99
101
  const outPath = options.out ?? DEFAULT_OUT
100
- const response = await fetch(SOURCE)
101
102
 
102
- if (!response.ok) throw new Error(`fetch ${SOURCE} failed: ${response.status}`)
103
- const countries = (await response.json()) as MledozeCountry[]
103
+ const countries = await new APIClient({ displayName: "mledoze-countries", retry: true })
104
+ .fetch<MledozeCountry[]>({ url: SOURCE })
105
+ .then(pluckResponseData)
104
106
 
105
107
  const rows: Record<string, CountryReferenceEntry> = {}
106
108
 
@@ -23,6 +23,7 @@ import { readFileSync, writeFileSync } from "node:fs"
23
23
  import { join } from "node:path"
24
24
  import { fileURLToPath } from "node:url"
25
25
 
26
+ import { APIClient, pluckResponseData } from "@mailwoman/core/api"
26
27
  import { parseJSONStrict } from "@mailwoman/core/objects"
27
28
 
28
29
  /**
@@ -59,6 +60,12 @@ export interface GenerateOfficialLanguagesSummary {
59
60
  outPath: string
60
61
  }
61
62
 
63
+ /**
64
+ * One call per CLDR supplemental file, against a CDN that throttles. Retry only — the caller makes a handful of
65
+ * requests, so a rate budget would be ceremony over a burst that never happens.
66
+ */
67
+ const cldrClient = new APIClient({ displayName: "cldr", retry: true })
68
+
62
69
  interface LanguagePopulation {
63
70
  _officialStatus?: string
64
71
  }
@@ -69,11 +76,8 @@ async function loadCLDR(file: string, cldrDir: string | undefined, cldrVersion:
69
76
  }
70
77
 
71
78
  const url = `https://cdn.jsdelivr.net/npm/cldr-core@${cldrVersion}/supplemental/${file}.json`
72
- const res = await fetch(url)
73
-
74
- if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`)
75
79
 
76
- return res.json()
80
+ return cldrClient.fetch<unknown>({ url }).then(pluckResponseData)
77
81
  }
78
82
 
79
83
  /**
package/tools/index.ts CHANGED
@@ -4,9 +4,9 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
6
  * Codex operator tools — the `run()`-style modules behind `mailwoman dev generate …` commands. No
7
- * argv, no `process.exit`: commands own parsing, rendering, and exit codes (see the 2026-07-09
8
- * scripts→Pastel spec).
7
+ * argv, no `process.exit`: commands own parsing, rendering, and exit codes.
9
8
  */
10
9
 
10
+ export * from "./generate-country-population.ts"
11
11
  export * from "./generate-country-reference.ts"
12
12
  export * from "./generate-official-languages.ts"
package/us/zipcode.ts CHANGED
@@ -191,7 +191,7 @@ export interface PluckedStateZIPCodeResult {
191
191
  }
192
192
 
193
193
  /**
194
- * Given an address string like `"NY"`, `"CA 94016"`, attempts to match the state abbreviation and postal code, if
194
+ * Given an address string like `"NY"`, `"CA 94016"`, attempts to match the state abbreviation and postcode, if
195
195
  * applicable.
196
196
  *
197
197
  * @see {@linkcode isUSStateAbbreviation} to validate the state abbreviation.