mailwoman 10.4.0 → 10.6.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 (205) hide show
  1. package/cli/commands/corpus/download.tsx +33 -7
  2. package/cli/commands/corpus/fetch.tsx +3 -0
  3. package/cli/commands/corpus/sub/venue/extract.tsx +4 -4
  4. package/cli/commands/corpus/upload.tsx +6 -5
  5. package/cli/commands/eval/pins.tsx +4 -4
  6. package/cli/commands/gazetteer/build/poi/coverage.tsx +2 -2
  7. package/cli/commands/gazetteer/build/poi/index.tsx +4 -4
  8. package/cli/commands/gazetteer/inspect/sync/index.tsx +42 -12
  9. package/cli/commands/gazetteer/repos-sync.tsx +40 -28
  10. package/cli/commands/release/merge-admin.tsx +3 -3
  11. package/cli/doctor/checks.ts +20 -2
  12. package/cli/doctor/runner.ts +37 -1
  13. package/cli/kit/progress.ts +103 -0
  14. package/cli/kit.ts +1 -0
  15. package/lib/data/bom.ts +3 -0
  16. package/lib/data/bundles.ts +1 -1
  17. package/lib/data/inventory.ts +12 -0
  18. package/lib/data/obligations.ts +35 -0
  19. package/lib/fork-entity.ts +6 -0
  20. package/lib/geocode/layer-paths.ts +43 -0
  21. package/lib/geocode/osm-layers.ts +47 -0
  22. package/lib/geocode/parse-priors.ts +1 -1
  23. package/lib/geocode/result.ts +30 -3
  24. package/lib/geocode/session.ts +3 -9
  25. package/lib/plus-code-override.ts +1 -0
  26. package/man/mailwoman.1 +1 -1
  27. package/out/cli/commands/corpus/download.d.ts.map +1 -1
  28. package/out/cli/commands/corpus/download.js +30 -6
  29. package/out/cli/commands/corpus/download.js.map +1 -1
  30. package/out/cli/commands/corpus/fetch.d.ts.map +1 -1
  31. package/out/cli/commands/corpus/fetch.js +3 -0
  32. package/out/cli/commands/corpus/fetch.js.map +1 -1
  33. package/out/cli/commands/corpus/sub/venue/extract.js +4 -4
  34. package/out/cli/commands/corpus/sub/venue/extract.js.map +1 -1
  35. package/out/cli/commands/corpus/upload.d.ts.map +1 -1
  36. package/out/cli/commands/corpus/upload.js +2 -4
  37. package/out/cli/commands/corpus/upload.js.map +1 -1
  38. package/out/cli/commands/eval/pins.js +4 -4
  39. package/out/cli/commands/eval/pins.js.map +1 -1
  40. package/out/cli/commands/gazetteer/build/poi/coverage.js +2 -2
  41. package/out/cli/commands/gazetteer/build/poi/coverage.js.map +1 -1
  42. package/out/cli/commands/gazetteer/build/poi/index.js +4 -4
  43. package/out/cli/commands/gazetteer/build/poi/index.js.map +1 -1
  44. package/out/cli/commands/gazetteer/inspect/sync/index.d.ts.map +1 -1
  45. package/out/cli/commands/gazetteer/inspect/sync/index.js +30 -7
  46. package/out/cli/commands/gazetteer/inspect/sync/index.js.map +1 -1
  47. package/out/cli/commands/gazetteer/repos-sync.d.ts.map +1 -1
  48. package/out/cli/commands/gazetteer/repos-sync.js +22 -26
  49. package/out/cli/commands/gazetteer/repos-sync.js.map +1 -1
  50. package/out/cli/commands/release/merge-admin.js +3 -3
  51. package/out/cli/commands/release/merge-admin.js.map +1 -1
  52. package/out/cli/doctor/checks.d.ts +1 -1
  53. package/out/cli/doctor/checks.d.ts.map +1 -1
  54. package/out/cli/doctor/checks.js +17 -2
  55. package/out/cli/doctor/checks.js.map +1 -1
  56. package/out/cli/doctor/runner.d.ts +6 -1
  57. package/out/cli/doctor/runner.d.ts.map +1 -1
  58. package/out/cli/doctor/runner.js +24 -2
  59. package/out/cli/doctor/runner.js.map +1 -1
  60. package/out/cli/kit/progress.d.ts +55 -0
  61. package/out/cli/kit/progress.d.ts.map +1 -0
  62. package/out/cli/kit/progress.js +46 -0
  63. package/out/cli/kit/progress.js.map +1 -0
  64. package/out/cli/kit.d.ts +1 -0
  65. package/out/cli/kit.d.ts.map +1 -1
  66. package/out/cli/kit.js +1 -0
  67. package/out/cli/kit.js.map +1 -1
  68. package/out/data/bom.d.ts.map +1 -1
  69. package/out/data/bom.js +3 -0
  70. package/out/data/bom.js.map +1 -1
  71. package/out/data/bundles.js +1 -1
  72. package/out/data/bundles.js.map +1 -1
  73. package/out/data/inventory.d.ts +12 -0
  74. package/out/data/inventory.d.ts.map +1 -1
  75. package/out/data/inventory.js.map +1 -1
  76. package/out/data/obligations.d.ts +12 -0
  77. package/out/data/obligations.d.ts.map +1 -1
  78. package/out/data/obligations.js +25 -0
  79. package/out/data/obligations.js.map +1 -1
  80. package/out/fork-entity.d.ts +4 -0
  81. package/out/fork-entity.d.ts.map +1 -1
  82. package/out/fork-entity.js +2 -0
  83. package/out/fork-entity.js.map +1 -1
  84. package/out/geocode/layer-paths.d.ts +25 -0
  85. package/out/geocode/layer-paths.d.ts.map +1 -1
  86. package/out/geocode/layer-paths.js +27 -0
  87. package/out/geocode/layer-paths.js.map +1 -1
  88. package/out/geocode/osm-layers.d.ts +27 -0
  89. package/out/geocode/osm-layers.d.ts.map +1 -0
  90. package/out/geocode/osm-layers.js +42 -0
  91. package/out/geocode/osm-layers.js.map +1 -0
  92. package/out/geocode/parse-priors.js +1 -1
  93. package/out/geocode/result.d.ts.map +1 -1
  94. package/out/geocode/result.js +15 -0
  95. package/out/geocode/result.js.map +1 -1
  96. package/out/geocode/session.d.ts.map +1 -1
  97. package/out/geocode/session.js +3 -8
  98. package/out/geocode/session.js.map +1 -1
  99. package/out/plus-code-override.d.ts.map +1 -1
  100. package/out/plus-code-override.js +1 -0
  101. package/out/plus-code-override.js.map +1 -1
  102. package/out/tools/coverage/core.js +2 -2
  103. package/out/tools/coverage/core.js.map +1 -1
  104. package/out/tools/coverage/jurisdictions.d.ts.map +1 -1
  105. package/out/tools/coverage/jurisdictions.js +4 -3
  106. package/out/tools/coverage/jurisdictions.js.map +1 -1
  107. package/out/tools/dev-tools/build/oa-coord-golden.run.js +1 -1
  108. package/out/tools/dev-tools/build/oa-coord-golden.run.js.map +1 -1
  109. package/out/tools/dev-tools/corpus/freeze-overlay-manifest.run.d.ts +31 -0
  110. package/out/tools/dev-tools/corpus/freeze-overlay-manifest.run.d.ts.map +1 -0
  111. package/out/tools/dev-tools/corpus/freeze-overlay-manifest.run.js +125 -0
  112. package/out/tools/dev-tools/corpus/freeze-overlay-manifest.run.js.map +1 -0
  113. package/out/tools/dev-tools/harness-neural.run.js +2 -2
  114. package/out/tools/dev-tools/harness-neural.run.js.map +1 -1
  115. package/out/tools/dev-tools/heldout-geography-eval.run.js +9 -9
  116. package/out/tools/dev-tools/heldout-geography-eval.run.js.map +1 -1
  117. package/out/tools/dev-tools/lint-mdx-angles.js +2 -2
  118. package/out/tools/dev-tools/lint-mdx-angles.js.map +1 -1
  119. package/out/tools/dev-tools/locality/context-swap.run.js +2 -1
  120. package/out/tools/dev-tools/locality/context-swap.run.js.map +1 -1
  121. package/out/tools/eval-harness/external-arenas.d.ts +1 -1
  122. package/out/tools/eval-harness/external-arenas.d.ts.map +1 -1
  123. package/out/tools/eval-harness/external-arenas.js +29 -9
  124. package/out/tools/eval-harness/external-arenas.js.map +1 -1
  125. package/out/tools/eval-harness/gauntlet/harness.js +2 -2
  126. package/out/tools/eval-harness/gauntlet/harness.js.map +1 -1
  127. package/out/tools/eval-harness/semantic-utility/run.d.ts +1 -1
  128. package/out/tools/gazetteer-pipeline/postcode/codepoint/database.d.ts +2 -1
  129. package/out/tools/gazetteer-pipeline/postcode/codepoint/database.d.ts.map +1 -1
  130. package/out/tools/gazetteer-pipeline/postcode/codepoint/database.js +2 -1
  131. package/out/tools/gazetteer-pipeline/postcode/codepoint/database.js.map +1 -1
  132. package/out/tools/gazetteer-pipeline/postcode/codepoint/fetch.d.ts +4 -38
  133. package/out/tools/gazetteer-pipeline/postcode/codepoint/fetch.d.ts.map +1 -1
  134. package/out/tools/gazetteer-pipeline/postcode/codepoint/fetch.js +1 -5
  135. package/out/tools/gazetteer-pipeline/postcode/codepoint/fetch.js.map +1 -1
  136. package/out/tools/gazetteer-pipeline/postcode/codepoint.d.ts +2 -3
  137. package/out/tools/gazetteer-pipeline/postcode/codepoint.d.ts.map +1 -1
  138. package/out/tools/gazetteer-pipeline/postcode/codepoint.js +2 -3
  139. package/out/tools/gazetteer-pipeline/postcode/codepoint.js.map +1 -1
  140. package/out/tools/gazetteer-pipeline/postcode/geonames/tail.js +1 -1
  141. package/out/tools/gazetteer-pipeline/postcode/ni/osm/parse.d.ts +1 -1
  142. package/out/tools/gazetteer-pipeline/postcode/ni/osm/parse.js +2 -2
  143. package/out/tools/gazetteer-pipeline/postcode/ni/osm/parse.js.map +1 -1
  144. package/out/tools/gazetteer-pipeline/repos/audit.js +2 -2
  145. package/out/tools/gazetteer-pipeline/repos/audit.js.map +1 -1
  146. package/out/tools/gazetteer-pipeline/repos/sync.d.ts +18 -1
  147. package/out/tools/gazetteer-pipeline/repos/sync.d.ts.map +1 -1
  148. package/out/tools/gazetteer-pipeline/repos/sync.js +58 -32
  149. package/out/tools/gazetteer-pipeline/repos/sync.js.map +1 -1
  150. package/out/tools/gazetteer-pipeline/stamp-manifest.d.ts.map +1 -1
  151. package/out/tools/gazetteer-pipeline/stamp-manifest.js +2 -6
  152. package/out/tools/gazetteer-pipeline/stamp-manifest.js.map +1 -1
  153. package/out/tools/gazetteer-pipeline/uprn-layer.d.ts +2 -14
  154. package/out/tools/gazetteer-pipeline/uprn-layer.d.ts.map +1 -1
  155. package/out/tools/gazetteer-pipeline/uprn-layer.js +2 -1
  156. package/out/tools/gazetteer-pipeline/uprn-layer.js.map +1 -1
  157. package/out/tools/generate-clients.d.ts.map +1 -1
  158. package/out/tools/generate-clients.js +15 -7
  159. package/out/tools/generate-clients.js.map +1 -1
  160. package/out/tools/release-tools/publish/hf.d.ts.map +1 -1
  161. package/out/tools/release-tools/publish/hf.js +21 -18
  162. package/out/tools/release-tools/publish/hf.js.map +1 -1
  163. package/out/tools/tiles/publish.d.ts.map +1 -1
  164. package/out/tools/tiles/publish.js +2 -2
  165. package/out/tools/tiles/publish.js.map +1 -1
  166. package/package.json +42 -39
  167. package/tools/coverage/core.ts +2 -2
  168. package/tools/coverage/jurisdictions.ts +5 -3
  169. package/tools/dev-tools/build/oa-coord-golden.run.ts +1 -1
  170. package/tools/dev-tools/corpus/freeze-overlay-manifest.run.ts +162 -0
  171. package/tools/dev-tools/harness-neural.run.ts +2 -2
  172. package/tools/dev-tools/heldout-geography-eval.run.ts +13 -10
  173. package/tools/dev-tools/lint-mdx-angles.ts +2 -2
  174. package/tools/dev-tools/locality/context-swap.run.ts +2 -1
  175. package/tools/eval-harness/external-arenas.ts +29 -12
  176. package/tools/eval-harness/gauntlet/harness.ts +2 -2
  177. package/tools/eval-harness/premise-linkage/adapter.ts +1 -1
  178. package/tools/eval-harness/semantic-utility/run.ts +1 -1
  179. package/tools/gazetteer-pipeline/postcode/codepoint/database.ts +1 -3
  180. package/tools/gazetteer-pipeline/postcode/codepoint/fetch.ts +5 -48
  181. package/tools/gazetteer-pipeline/postcode/codepoint.ts +2 -3
  182. package/tools/gazetteer-pipeline/postcode/geonames/tail.ts +1 -1
  183. package/tools/gazetteer-pipeline/postcode/ni/osm/parse.ts +2 -2
  184. package/tools/gazetteer-pipeline/repos/audit.ts +2 -2
  185. package/tools/gazetteer-pipeline/repos/sync.ts +79 -32
  186. package/tools/gazetteer-pipeline/stamp-manifest.ts +2 -6
  187. package/tools/gazetteer-pipeline/uprn-layer.ts +4 -17
  188. package/tools/generate-clients.ts +16 -7
  189. package/tools/release-tools/publish/hf.ts +21 -19
  190. package/tools/tiles/publish.ts +2 -2
  191. package/lib/process.ts +0 -36
  192. package/out/process.d.ts +0 -15
  193. package/out/process.d.ts.map +0 -1
  194. package/out/process.js +0 -27
  195. package/out/process.js.map +0 -1
  196. package/out/tools/gazetteer-pipeline/postcode/codepoint/parse.d.ts +0 -126
  197. package/out/tools/gazetteer-pipeline/postcode/codepoint/parse.d.ts.map +0 -1
  198. package/out/tools/gazetteer-pipeline/postcode/codepoint/parse.js +0 -117
  199. package/out/tools/gazetteer-pipeline/postcode/codepoint/parse.js.map +0 -1
  200. package/out/tools/gazetteer-pipeline/postcode/display-form.d.ts +0 -14
  201. package/out/tools/gazetteer-pipeline/postcode/display-form.d.ts.map +0 -1
  202. package/out/tools/gazetteer-pipeline/postcode/display-form.js +0 -16
  203. package/out/tools/gazetteer-pipeline/postcode/display-form.js.map +0 -1
  204. package/tools/gazetteer-pipeline/postcode/codepoint/parse.ts +0 -210
  205. package/tools/gazetteer-pipeline/postcode/display-form.ts +0 -16
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { dataRootPath } from "@mailwoman/core/data-root"
16
+ import { runProcess } from "@mailwoman/core/process"
16
17
  import { Box, Text } from "ink"
17
18
  import { useState } from "react"
18
19
 
@@ -52,17 +53,26 @@ const CorpusDownload: CommandComponent<typeof spec> = ({ options }) => {
52
53
  }
53
54
 
54
55
  useCommandTask(async () => {
55
- const { $ } = await import("zx")
56
-
57
56
  const rcloneBase = `:s3:${options.bucket}`
58
- const dryFlag = options.dryRun ? "--dry-run" : ""
57
+ const dryFlag = options.dryRun ? ["--dry-run"] : []
59
58
  const out = options.outDir
60
59
 
61
60
  // Step 0: Download v0.3.0 corpus
62
61
  updateStep(0, { status: "running" })
63
62
 
64
63
  try {
65
- await $`rclone sync ${rcloneBase}/corpus/v0.3.0/ ${out}/corpus/versioned/v0.3.0/corpus-v0.3.0/ --progress --transfers 8 --checkers 16 ${dryFlag}`.quiet()
64
+ await runProcess("rclone", [
65
+ "sync",
66
+ `${rcloneBase}/corpus/v0.3.0/`,
67
+ `${out}/corpus/versioned/v0.3.0/corpus-v0.3.0/`,
68
+ "--progress",
69
+ "--transfers",
70
+ "8",
71
+ "--checkers",
72
+ "16",
73
+ ...dryFlag,
74
+ ])
75
+
66
76
  updateStep(0, { status: "done" })
67
77
  } catch (error: unknown) {
68
78
  const e = error as Record<string, unknown>
@@ -75,7 +85,16 @@ const CorpusDownload: CommandComponent<typeof spec> = ({ options }) => {
75
85
  updateStep(1, { status: "running" })
76
86
 
77
87
  try {
78
- await $`rclone sync ${rcloneBase}/corpus/v0.4.0/ ${out}/corpus/versioned/v0.4.0/corpus-v0.4.0/ --progress --transfers 4 ${dryFlag}`.quiet()
88
+ await runProcess("rclone", [
89
+ "sync",
90
+ `${rcloneBase}/corpus/v0.4.0/`,
91
+ `${out}/corpus/versioned/v0.4.0/corpus-v0.4.0/`,
92
+ "--progress",
93
+ "--transfers",
94
+ "4",
95
+ ...dryFlag,
96
+ ])
97
+
79
98
  updateStep(1, { status: "done" })
80
99
  } catch (error: unknown) {
81
100
  const e = error as Record<string, unknown>
@@ -88,7 +107,14 @@ const CorpusDownload: CommandComponent<typeof spec> = ({ options }) => {
88
107
  updateStep(2, { status: "running" })
89
108
 
90
109
  try {
91
- await $`rclone sync ${rcloneBase}/models/tokenizer/ ${out}/models/tokenizer/ --progress ${dryFlag}`.quiet()
110
+ await runProcess("rclone", [
111
+ "sync",
112
+ `${rcloneBase}/models/tokenizer/`,
113
+ `${out}/models/tokenizer/`,
114
+ "--progress",
115
+ ...dryFlag,
116
+ ])
117
+
92
118
  updateStep(2, { status: "done" })
93
119
  } catch (error: unknown) {
94
120
  const e = error as Record<string, unknown>
@@ -101,7 +127,7 @@ const CorpusDownload: CommandComponent<typeof spec> = ({ options }) => {
101
127
  updateStep(3, { status: "running" })
102
128
 
103
129
  try {
104
- await $`rclone sync ${rcloneBase}/corpus-python/ ./corpus-python/ --progress ${dryFlag}`.quiet()
130
+ await runProcess("rclone", ["sync", `${rcloneBase}/corpus-python/`, "./corpus-python/", "--progress", ...dryFlag])
105
131
  updateStep(3, { status: "done" })
106
132
  } catch (error: unknown) {
107
133
  const e = error as Record<string, unknown>
@@ -132,6 +132,7 @@ async function runSource(source: FetchSourceID, options: Options): Promise<Fetch
132
132
  const { fetchCORDIS } = await import("@mailwoman/corpus/tools/fetch/cordis")
133
133
  const { fetchAnnuaireEducationOverseas } = await import("@mailwoman/corpus/fr/tools/fetch/annuaire-education")
134
134
  const { fetchFinessOverseas } = await import("@mailwoman/corpus/fr/tools/fetch/finess")
135
+ const { fetchGBOSOpen } = await import("@mailwoman/corpus/gb/tools/fetch/os-open")
135
136
  const { fetchGLEIF } = await import("@mailwoman/corpus/tools/fetch/gleif")
136
137
  const { fetchVaris } = await import("@mailwoman/corpus/lv/tools/fetch/varis")
137
138
  const { fetchVlaanderenAD } = await import("@mailwoman/corpus/tools/fetch/vlaanderen-ad")
@@ -185,6 +186,8 @@ async function runSource(source: FetchSourceID, options: Options): Promise<Fetch
185
186
  return fetchAnnuaireEducationOverseas(base, reportToStderr)
186
187
  case "fr-finess-overseas":
187
188
  return fetchFinessOverseas(base, reportToStderr)
189
+ case "gb-os-open":
190
+ return fetchGBOSOpen(base, reportToStderr)
188
191
  case "gleif":
189
192
  return fetchGLEIF(base, reportToStderr)
190
193
  case "ro-retea-scolara":
@@ -40,12 +40,12 @@ export const spec = {
40
40
 
41
41
  const CorpusSubVenueExtract: CommandComponent<typeof spec> = ({ options }) => {
42
42
  const state = useCommandTask(async () => {
43
- // @mailwoman/osm is a devDependency only. It is unpublished (ODbL counsel sign-off pending,
44
- // see osm/readme.md), so a static import here breaks every clean install of the published CLI (the 2026-08-05 smoke failure). Lazy-load it and fail with provenance when absent.
43
+ // @mailwoman/osm is an optional peer that an operator installs to opt in to OpenStreetMap data, so a
44
+ // static import would break every install without it. Lazy-load it and say how to install it when absent.
45
45
  const { writeSubVenueJSONL } = await import("@mailwoman/osm/sdk/extract/subvenue").catch(() => {
46
46
  throw new Error(
47
- "corpus sub-venue-extract requires @mailwoman/osm, which is not yet published — " +
48
- "run this command from the mailwoman monorepo (osm/README.md has the publish status)."
47
+ "corpus sub-venue-extract requires @mailwoman/osm, an optional package — install it with " +
48
+ "`npm install @mailwoman/osm` (its README states the OpenStreetMap license terms)."
49
49
  )
50
50
  })
51
51
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { dataRootPath } from "@mailwoman/core/data-root"
11
11
  import { pathExists } from "@mailwoman/core/fs/readers/stat"
12
+ import { runProcess } from "@mailwoman/core/process"
12
13
  import { extractDelimited } from "@mailwoman/core/scripting/arguments"
13
14
  import { childEnv } from "@mailwoman/core/scripting/utils"
14
15
  import type { BuildCorpusManifest } from "@mailwoman/corpus/build"
@@ -235,8 +236,6 @@ const CorpusUpload: CommandComponent<typeof spec> = ({ options }) => {
235
236
  const [steps, setSteps] = useState<Step[]>([])
236
237
 
237
238
  const state = useCommandTask(async () => {
238
- const { $ } = await import("zx")
239
-
240
239
  const corpusRoot = PathBuilder.from(options.corpusDir ?? dataRootPath("corpus", "versioned"))
241
240
 
242
241
  const directories = extractDelimited(options.corpusDirectory)
@@ -350,9 +349,11 @@ const CorpusUpload: CommandComponent<typeof spec> = ({ options }) => {
350
349
  update(index, { status: "running" })
351
350
 
352
351
  try {
353
- await $({
354
- env,
355
- })`rclone sync ${job.source.toString()} ${job.dest} ${job.extra} ${dry} ${R2_RETRIES} --stats-one-line`.quiet()
352
+ await runProcess(
353
+ "rclone",
354
+ ["sync", job.source.toString(), job.dest, ...job.extra, ...dry, ...R2_RETRIES, "--stats-one-line"],
355
+ { env }
356
+ )
356
357
 
357
358
  update(index, { status: "done", detail: options.dryRun ? "would sync" : "synced" })
358
359
  } catch (error: unknown) {
@@ -17,8 +17,8 @@
17
17
  import { gitHead } from "@mailwoman/core/git"
18
18
  import { parseJSONStrict, stringifyJSON } from "@mailwoman/core/json"
19
19
  import { repoRootPath } from "@mailwoman/core/paths"
20
+ import { runProcess } from "@mailwoman/core/process"
20
21
  import { Box, Text } from "ink"
21
- import { $ } from "zx"
22
22
 
23
23
  import { type CommandSpec, CommandTaskResult, type CommandComponent, useCommandTask } from "#cli-kit"
24
24
 
@@ -68,17 +68,17 @@ async function reportStalePins(drift: string, testPath: string): Promise<string>
68
68
  `Run \`mailwoman eval pins --update\`, commit ${testPath}, and this issue closes on the next green audit.`
69
69
 
70
70
  const search = `in:title ${stringifyJSON(STALE_PIN_ISSUE_TITLE)}`
71
- const issueList = await $`gh issue list --state open --search ${search} --json number`.quiet()
71
+ const issueList = await runProcess("gh", ["issue", "list", "--state", "open", "--search", search, "--json", "number"])
72
72
  const existing = parseJSONStrict<Array<{ number: number }>>(issueList.stdout)
73
73
  const first = existing[0]
74
74
 
75
75
  if (first) {
76
- await $`gh issue comment ${first.number} --body ${body}`.quiet()
76
+ await runProcess("gh", ["issue", "comment", String(first.number), "--body", body])
77
77
 
78
78
  return `updated issue #${first.number}`
79
79
  }
80
80
 
81
- await $`gh issue create --title ${STALE_PIN_ISSUE_TITLE} --body ${body}`.quiet()
81
+ await runProcess("gh", ["issue", "create", "--title", STALE_PIN_ISSUE_TITLE, "--body", body])
82
82
 
83
83
  return "opened the audit issue"
84
84
  }
@@ -117,8 +117,8 @@ const GazetteerBuildPOICoverage: CommandComponent<typeof spec> = ({ options }) =
117
117
  const out = options.out ?? poiDatabasePath(`poi-coverage-${options.category}-${slugify(region)}.db`)
118
118
  const buildSHA = resolveBuildSHA(repoRootPath())
119
119
 
120
- // Dynamic import is required: @mailwoman/osm is unpublished, so a top-level
121
- // import breaks the published CLI on a clean install.
120
+ // Dynamic import is required: @mailwoman/osm is an optional peer, so a top-level
121
+ // import breaks the published CLI on an install without it.
122
122
  const [{ extractOSMBoundary }, { extractOSMPOIs, tagRuleFromOSMTag }] = await Promise.all([
123
123
  import("@mailwoman/osm/sdk/extract/boundary"),
124
124
  import("@mailwoman/osm/sdk/extract/poi"),
@@ -115,10 +115,10 @@ const GazetteerBuildPOI: CommandComponent<typeof spec> = ({ options }) => {
115
115
  // twice, once here to derive coverage, once as buildPOIDatabase's `rows` injection point.
116
116
  const rows: POISourceRow[] = []
117
117
 
118
- // dynamic import, required: @mailwoman/osm is unpublished
119
- // (ODbL counsel sign-off pending — see osm/readme.md), and this dependency belongs
120
- // only on the selected build path, so a top-level import breaks the whole published
121
- // CLI on a clean install — the smoke test's clean-install leg is what catches it.
118
+ // Dynamic import, required: @mailwoman/osm is an optional peer that an operator installs
119
+ // to opt in to OpenStreetMap data, and this dependency belongs only on the selected build
120
+ // path, so a top-level import breaks the whole published CLI on an install without it.
121
+ // The smoke test's clean-install leg is what catches it.
122
122
  // The osm source branch is build-local by design.
123
123
  // It may only resolve its SDK when a run selects it.
124
124
  const { extractOSMPOIs } = await import("@mailwoman/osm/sdk/extract/poi")
@@ -21,6 +21,9 @@ import { ProgressBar } from "@inkjs/ui"
21
21
  import type { RepositorySource, SynchronizeAction } from "@mailwoman/core"
22
22
  import { wofReposPath } from "@mailwoman/core/data-root"
23
23
  import { ByteFormatter } from "@mailwoman/core/fs/formatters"
24
+ import { GitClient } from "@mailwoman/core/git"
25
+ import { parseJSONStrict } from "@mailwoman/core/json"
26
+ import { runFile } from "@mailwoman/core/process"
24
27
  import { formatQuantity } from "@mailwoman/core/resources/locale"
25
28
  import { CommandError } from "@mailwoman/core/scripting/command"
26
29
  import { Box, Text } from "ink"
@@ -33,7 +36,9 @@ import {
33
36
  type CommandSpec,
34
37
  CommandTaskResult,
35
38
  type CommandComponent,
39
+ ProgressLines,
36
40
  useCommandTask,
41
+ useProgressFeed,
37
42
  } from "#cli-kit"
38
43
  import {
39
44
  assertDestinationNotARepoName,
@@ -92,12 +97,25 @@ interface SyncPlan {
92
97
  * failure surfaces as an opaque spawn error from deep inside the task.
93
98
  */
94
99
  async function discoverRepos(): Promise<DiscoveredRepo[]> {
95
- const { $ } = await import("zx")
96
-
97
100
  try {
98
- return $.sync`gh repo list ${WOF_REPO_OWNER} --no-archived --limit 1000 --json name --json url --json diskUsage`
99
- .json<Array<{ name: string; url: string; diskUsage: number }>>()
100
- .map(({ name, url, diskUsage }) => ({ name, url, diskUsageKB: diskUsage }))
101
+ const { stdout } = await runFile("gh", [
102
+ "repo",
103
+ "list",
104
+ WOF_REPO_OWNER,
105
+ "--no-archived",
106
+ "--limit",
107
+ "1000",
108
+ "--json",
109
+ "name",
110
+ "--json",
111
+ "url",
112
+ "--json",
113
+ "diskUsage",
114
+ ])
115
+
116
+ return parseJSONStrict<Array<{ name: string; url: string; diskUsage: number }>>(stdout).map(
117
+ ({ name, url, diskUsage }) => ({ name, url, diskUsageKB: diskUsage })
118
+ )
101
119
  } catch (error) {
102
120
  throw new CommandError(
103
121
  `Could not list ${WOF_REPO_OWNER} through \`gh\`. Install the GitHub CLI and run \`gh auth login\`.`,
@@ -110,6 +128,7 @@ const WOFSync: CommandComponent<typeof spec, [string?]> = ({ options, args }) =>
110
128
  const [plan, setPlan] = useState<SyncPlan>()
111
129
  const [active, setActive] = useState<readonly string[]>([])
112
130
  const [doneCount, setDoneCount] = useState(0)
131
+ const progress = useProgressFeed()
113
132
 
114
133
  const state = useCommandTask(
115
134
  async () => {
@@ -197,11 +216,18 @@ const WOFSync: CommandComponent<typeof spec, [string?]> = ({ options, args }) =>
197
216
  let action: SynchronizeAction | null = null
198
217
  let failure: string | null = null
199
218
 
219
+ await using client = new GitClient({ cwd: destination })
220
+
221
+ const unwatch = progress.watch(client, source.name)
222
+
200
223
  try {
201
- action = await synchronizeRepo(source, destination)
224
+ action = await synchronizeRepo(source, destination, client)
202
225
  } catch (error) {
203
226
  ok = false
204
227
  failure = error instanceof Error ? error.message : String(error)
228
+ } finally {
229
+ unwatch()
230
+ progress.clear(source.name)
205
231
  }
206
232
 
207
233
  const elapsed = `${((performance.now() - startedAt) / 1000).toFixed(1)}s`
@@ -260,12 +286,16 @@ const WOFSync: CommandComponent<typeof spec, [string?]> = ({ options, args }) =>
260
286
  {ByteFormatter.formatSI(plan.selection.totalDiskUsageKB * 1024)}
261
287
  </Text>
262
288
 
263
- {active.map((name) => (
264
- <Text key={name} color="cyan">
265
- {" "}
266
- ⠋ {name}
267
- </Text>
268
- ))}
289
+ {active
290
+ .filter((name) => !progress.entries.has(name))
291
+ .map((name) => (
292
+ <Text key={name} color="cyan">
293
+ {" "}
294
+ ⠋ {name}
295
+ </Text>
296
+ ))}
297
+
298
+ <ProgressLines entries={progress.entries} />
269
299
 
270
300
  <Box paddingX={1}>
271
301
  <ProgressBar value={percentage} />
@@ -9,12 +9,19 @@
9
9
 
10
10
  import { pathExists } from "@mailwoman/core/fs/readers/stat"
11
11
  import { makeDirectories, writeLocalJSONFile } from "@mailwoman/core/fs/writers"
12
- import { git } from "@mailwoman/core/git"
12
+ import { GitClient } from "@mailwoman/core/git"
13
13
  import { extractDelimited } from "@mailwoman/core/scripting/arguments"
14
14
  import { Box, Text } from "ink"
15
- import { dirname, PathBuilder } from "path-ts"
16
-
17
- import { type CommandSpec, CommandTaskResult, type CommandComponent, useCommandTask } from "#cli-kit"
15
+ import { PathBuilder } from "path-ts"
16
+
17
+ import {
18
+ type CommandSpec,
19
+ CommandTaskResult,
20
+ type CommandComponent,
21
+ ProgressLines,
22
+ useCommandTask,
23
+ useProgressFeed,
24
+ } from "#cli-kit"
18
25
  import type { RepoSyncPlan } from "#gazetteer/repos/sync"
19
26
 
20
27
  /**
@@ -43,11 +50,13 @@ const ACTION_MARK: Record<string, string> = {
43
50
  }
44
51
 
45
52
  const GazetteerReposSync: CommandComponent<typeof spec> = ({ options }) => {
53
+ const progress = useProgressFeed()
54
+
46
55
  const state = useCommandTask(async () => {
47
56
  const { wofReposPath } = await import("@mailwoman/core/data-root")
48
57
  const { wofDatabasePath } = await import("@mailwoman/resolver-wof-sqlite/paths")
49
58
  const { auditReposRoot } = await import("#gazetteer/repos/audit")
50
- const { planReposSync, SyncAction, syncSentence } = await import("#gazetteer/repos/sync")
59
+ const { applyRepoSyncPlan, planReposSync, syncSentence } = await import("#gazetteer/repos/sync")
51
60
  const { githubForkProbe, UPSTREAM_ORG } = await import("#gazetteer/wof/repo-origin")
52
61
 
53
62
  const root = PathBuilder.from(options.root ?? wofReposPath())
@@ -79,39 +88,33 @@ const GazetteerReposSync: CommandComponent<typeof spec> = ({ options }) => {
79
88
  probe: githubForkProbe,
80
89
  directoryFor: (repo) => directories.get(repo)!,
81
90
  fetchFirst: !options.offline,
91
+ watch: (client) => progress.watch(client, PathBuilder.from(client.cwd).basename()),
82
92
  })
83
93
 
94
+ for (const repo of repos) {
95
+ progress.clear(repo)
96
+ }
97
+
84
98
  const performed: string[] = []
85
99
 
86
100
  if (options.apply) {
87
101
  for (const plan of plans) {
102
+ await using client = new GitClient({ cwd: root })
103
+
104
+ const unwatch = progress.watch(client, plan.repo)
105
+
88
106
  try {
89
- if (plan.action === SyncAction.Clone) {
90
- await makeDirectories(dirname(plan.directory))
91
- await git(["clone", "--depth", "1", plan.origin.url, plan.directory])
92
- performed.push(`cloned ${plan.repo} from ${plan.origin.source}`)
93
- } else if (plan.action === SyncAction.FastForward) {
94
- await git(["-C", plan.directory, "merge", "--ff-only", "origin/HEAD"])
95
- performed.push(`fast-forwarded ${plan.repo}`)
96
- } else if (plan.action === SyncAction.RepointRequired && options.repoint) {
97
- // The next upstream sync would need a guess without the fork's address.
98
- await git(["-C", plan.directory, "remote", "rename", "origin", "upstream"])
99
- await git(["-C", plan.directory, "remote", "add", "origin", plan.origin.url])
100
- await git(["-C", plan.directory, "fetch", "--quiet", "origin"])
101
-
102
- // The rename preserved `branch.<name>.remote`, so without re-pointing tracking the
103
- // next `git pull` here pulls upstream over the corrections this command preserves.
104
- const branch = await git(["-C", plan.directory, "rev-parse", "--abbrev-ref", "HEAD"])
105
-
106
- if (branch && branch !== "HEAD") {
107
- await git(["-C", plan.directory, "branch", `--set-upstream-to=origin/${branch}`, branch])
108
- }
109
-
110
- performed.push(`re-pointed ${plan.repo} at ${plan.origin.org} (previous remote kept as upstream)`)
107
+ const sentence = await applyRepoSyncPlan(plan, { client, repoint: options.repoint })
108
+
109
+ if (sentence) {
110
+ performed.push(sentence)
111
111
  }
112
112
  } catch (error) {
113
113
  // oxlint-disable-next-line mailwoman/prefer-spliterator -- one git error message, read for its first line only
114
114
  performed.push(`FAILED ${plan.repo}: ${(error as Error).message.split("\n")[0]}`)
115
+ } finally {
116
+ unwatch()
117
+ progress.clear(plan.repo)
115
118
  }
116
119
  }
117
120
  }
@@ -140,7 +143,16 @@ const GazetteerReposSync: CommandComponent<typeof spec> = ({ options }) => {
140
143
  return { plans, sentence: syncSentence(plans), performed, vintagePath, applied: options.apply }
141
144
  })
142
145
 
143
- if (state.status !== "done") return <CommandTaskResult state={state} running="Resolving WOF repo origins…" />
146
+ if (state.status === "running") {
147
+ return (
148
+ <Box flexDirection="column">
149
+ <Text color="gray">Resolving WOF repo origins…</Text>
150
+ <ProgressLines entries={progress.entries} />
151
+ </Box>
152
+ )
153
+ }
154
+
155
+ if (state.status !== "done") return <CommandTaskResult state={state} />
144
156
 
145
157
  const { plans, sentence, performed, vintagePath, applied } = state.result
146
158
 
@@ -14,8 +14,8 @@
14
14
  import { gitHead } from "@mailwoman/core/git"
15
15
  import { parseJSONStrict, stringifyJSON } from "@mailwoman/core/json"
16
16
  import { repoRootPath } from "@mailwoman/core/paths"
17
+ import { runProcess } from "@mailwoman/core/process"
17
18
  import { Box, Text } from "ink"
18
- import { $ } from "zx"
19
19
 
20
20
  import { type CommandSpec, CommandTaskResult, CLIUsageError, type CommandComponent, useCommandTask } from "#cli-kit"
21
21
 
@@ -64,7 +64,7 @@ async function mergeAdmin(prNumber: string, method: string): Promise<MergeAdminR
64
64
  throw new CLIUsageError(`--method ${stringifyJSON(method)} is not one of ${MERGE_METHODS.join("|")}`)
65
65
  }
66
66
 
67
- const prView = await $`gh pr view ${prNumber} --json state,title,headRefOid,files`.quiet()
67
+ const prView = await runProcess("gh", ["pr", "view", String(prNumber), "--json", "state,title,headRefOid,files"])
68
68
 
69
69
  const pr = parseJSONStrict<{
70
70
  state: string
@@ -109,7 +109,7 @@ async function mergeAdmin(prNumber: string, method: string): Promise<MergeAdminR
109
109
  }
110
110
 
111
111
  const methodFlag = `--${method}`
112
- const merged = await $`gh pr merge ${prNumber} --admin ${methodFlag}`.quiet()
112
+ const merged = await runProcess("gh", ["pr", "merge", String(prNumber), "--admin", methodFlag])
113
113
 
114
114
  return { prNumber, title: pr.title, method, ranChecks, mergeOutput: merged.stdout.trim() }
115
115
  }
@@ -12,9 +12,9 @@ import {
12
12
  docsSiteURL,
13
13
  isSelfServicePayload,
14
14
  chooseLicenseBranch,
15
+ LicenseObligation,
15
16
  summarizeLicense,
16
17
  type LicenseKeyVerification,
17
- type LicenseObligation,
18
18
  } from "@mailwoman/core/license"
19
19
  import type { LicenseKeyPublication } from "@mailwoman/core/license/publication"
20
20
  import type { LicenseStatusAnswer } from "@mailwoman/core/license/status"
@@ -711,10 +711,27 @@ export function layerLicenseCheck(o: LayerLicenseObservation): DoctorCheck {
711
711
  ...base,
712
712
  status: CheckStatus.OK,
713
713
  detail: `${o.manifest.name} · ${o.manifest.license} · obligations: ${describeObligations(summary.obligations, true)}`,
714
+ consequence: summary.obligations.includes(LicenseObligation.ShareAlike) ? shareAlikeNotice(o.manifest) : null,
714
715
  license,
715
716
  }
716
717
  }
717
718
 
719
+ /**
720
+ * The notice the doctor prints under a layer whose license carries share-alike.
721
+ *
722
+ * It states the credit the manifest records and what the license asks when the layer,
723
+ * or a database derived from it, is publicly used.
724
+ * Reading results from the layer for the operator's own use is not public use,
725
+ * so the notice states the condition rather than an obligation already owed.
726
+ */
727
+ function shareAlikeNotice(manifest: LayerIdentity): string {
728
+ return (
729
+ `${manifest.name} is a database under ${manifest.license}. ` +
730
+ `Credit ${stringifyJSON(manifest.attribution ?? "the source the license names")} wherever its data is shown. ` +
731
+ "If you publicly use it, or a database derived from it, the license requires that database to be offered under the same terms."
732
+ )
733
+ }
734
+
718
735
  /**
719
736
  * Formats obligations as a comma-separated list for a check detail.
720
737
  */
@@ -776,7 +793,8 @@ export function obligationPostureCheck(o: ObligationPostureObservation): DoctorC
776
793
  status: CheckStatus.Degraded,
777
794
  detail: `${refusing}: ${findings.length} finding(s) on disk — ${findings.join("; ")}`,
778
795
  consequence:
779
- "A layer on disk carries an obligation this installation declined. Reading from it takes the obligation on, " +
796
+ "A layer on disk carries an obligation this installation declined. Reading from it takes the obligation on. " +
797
+ "The geocode session refuses to start while an OpenStreetMap address-point database carrying it is on disk, " +
780
798
  "and `mailwoman data pull` refuses the bundle that carries it.",
781
799
  fix: "remove the layer, or drop the class from MAILWOMAN_REFUSE_OBLIGATIONS",
782
800
  }
@@ -42,9 +42,11 @@ import {
42
42
  import { type ObligationRefusal, parseObligationRefusals } from "#data/obligations"
43
43
  import { $public } from "#env"
44
44
  import {
45
+ addressPointDatabases,
45
46
  layerDatabaseAlternates,
46
47
  layerDatabasePath,
47
48
  layerDatabases,
49
+ type AddressPointDatabaseRef,
48
50
  type LayerDatabaseRef,
49
51
  type LayerID,
50
52
  } from "#geocode/layer-paths"
@@ -102,6 +104,11 @@ export interface DoctorDeps {
102
104
  * `.db` files in a layer's directory other than the one the session attaches, a build under another name.
103
105
  */
104
106
  layerAlternates(id: LayerID): Promise<string[]>
107
+ /**
108
+ * The per-country address-point databases on disk, including the OpenStreetMap
109
+ * databases an operator built.
110
+ */
111
+ addressPointDatabases(): Promise<AddressPointDatabaseRef[]>
105
112
  runtimeLicense(): Promise<string>
106
113
  /**
107
114
  * The configured license key, verified offline against the trusted keys this
@@ -196,6 +203,7 @@ export async function defaultDoctorDeps(): Promise<DoctorDeps> {
196
203
  readLayerIdentity,
197
204
  layerDatabases: () => layerDatabases(dataRoot),
198
205
  layerAlternates: (id) => layerDatabaseAlternates(dataRoot, id),
206
+ addressPointDatabases: () => addressPointDatabases(dataRoot),
199
207
  runtimeLicense: readRuntimeLicense,
200
208
  licenseKey: () => verifyConfiguredLicenseKey(),
201
209
  confirmLicenseKeyPublished: (kid) => confirmLicenseKeyPublished(kid),
@@ -313,6 +321,28 @@ async function gatherLayerLicense(deps: DoctorDeps, layer: LayerDatabaseRef): Pr
313
321
  }
314
322
  }
315
323
 
324
+ /**
325
+ * The license observation for an address-point database found on disk.
326
+ *
327
+ * The file was listed from its directory, so it exists, and an unreadable manifest
328
+ * is reported as unreadable rather than as an absent layer.
329
+ */
330
+ async function gatherAddressPointLicense(
331
+ deps: DoctorDeps,
332
+ database: AddressPointDatabaseRef
333
+ ): Promise<LayerLicenseObservation> {
334
+ try {
335
+ return { ...database, manifest: await deps.readLayerIdentity(database.path), error: null, alternates: null }
336
+ } catch (error) {
337
+ return {
338
+ ...database,
339
+ manifest: null,
340
+ error: error instanceof Error ? error.message : String(error),
341
+ alternates: null,
342
+ }
343
+ }
344
+ }
345
+
316
346
  async function gatherOverlay(deps: DoctorDeps, locale: string): Promise<DoctorCheck> {
317
347
  const packageName = deps.weightsPackageName(locale)
318
348
 
@@ -373,10 +403,16 @@ export async function runDoctor(overrides?: Partial<DoctorDeps>): Promise<Doctor
373
403
  lidStatus: lidStatus ?? null,
374
404
  })
375
405
 
376
- const layerObservations = (
406
+ const fixedLayerObservations = (
377
407
  await Promise.all(deps.layerDatabases().map((layer) => gatherLayerLicense(deps, layer)))
378
408
  ).filter((o): o is LayerLicenseObservation => o !== null)
379
409
 
410
+ const addressPointObservations = await Promise.all(
411
+ (await deps.addressPointDatabases()).map((database) => gatherAddressPointLicense(deps, database))
412
+ )
413
+
414
+ const layerObservations = [...fixedLayerObservations, ...addressPointObservations]
415
+
380
416
  const layerLicenses = layerObservations.map(layerLicenseCheck)
381
417
 
382
418
  const posture = await gatherObligationPosture(deps, gazetteerObservation, layerObservations)