#129: importer top-level subcommands — a misread flag can no longer launch a full import #141

Merged
art merged 2 commits from issue-129-subcommand-cli into main 2026-08-16 06:31:13 +00:00
Owner

#129: importer top-level subcommands — a misread flag can no longer launch a full import

Closes #129


What changed

The importer/importer CLI is now a subcommand CLI. Commands say what to do;
flags on a command are modifiers. The old flat-flag surface (17 flags, no modes)
is gone — including the two shapes the incident depended on:

  • importer <config> (bare config = unlabelled full import) → error, with a
    pointer at importer import <config>.
  • importer -validate <config> → rejected with a pointer at
    importer config-check <config>. -validate was a full-import modifier, not
    a config check; the 3h12m Finland import of #113 is exactly the failure this
    dispatch exists to make unreachable.

Subcommands

command behavior
importer prints help, exits 0 — never starts work
importer help [command] general help, or one command's usage
importer import <config> [modifiers] the full-import path; prints its blast radius before touching the DB
importer config-check <config> config sanity only — parse, include-resolve, defaults, shared-contract wiring, layer validators, acceptance-test blocks, dependency graph; no DB, no fetch. validate-config is accepted as an alias (the name the incident was discussed with); the maintainer's decision #1 (config-check) is the documented name
importer acceptance <config> the old -acceptance-only mode, as a verb (read-only review path)
importer fetch <config> [geotorget|lastkajen|trafiklab|all] the old *-fetch modes; no provider arg = all the config declares
importer list <config> [geotorget|lastkajen] the old *-list modes; no provider arg = both

import modifiers: --layer, --regeneralize, --validate, --ensure,
--refetch, -jobs, -batch-rows, --acceptance fast|all, --secrets.
Both -flag and --flag spellings work (Go's flag package treats them as the
same flag), so the Makefile's -jobs 4 -batch-rows 10000 survives unchanged.

The blast-radius startup line (before any DB work):

full import of se: 12 layers, zooms 3–14
import of se: layers cover,waterways only (2 of 12), zooms 3–14

Decisions from the issue, and how this resolves them

  1. Subcommand names — config-check per the maintainer's comment; the
    parent task's validate-config kept as an alias so both spellings work.
  2. Flat-flag compatibility — immediate break, per the maintainer's comment.
    No flat flags are kept as deprecated aliases; -validate (and every other
    flat flag) is rejected with a message. The Makefile is the whole consumer
    list and is updated; run/ notes are historical records and were left alone.
  3. Startup line — implemented as a log.Printf before the pool opens; the
    wording is unit-tested (blastRadiusLine). The issue's word "blast radius"
    is used, not "destructive" (the maintainer's point that a full import is
    safe, just slow, is respected — the line is about surprise, not danger).
  4. Docs — docs/importer-cli.md gains the mode table and the footgun note;
    the acceptance-tests section is unchanged in substance. README, AGENTS.md,
    geo-primer.md and country-sweden.md updated to the new syntax.

check-queries is deliberately not implemented — that is #98's scope; the
issue body says it slots into this shape when it lands.

Verification

  • make build, make test (importer + mapsrv), go vet both packages,
    gofmt -l clean.
  • New unit tests in importer/cli_test.go (no import, no DB): dispatch
    routing, the -validate footgun (rejected, points at config-check),
    bare-config rejection, flat-flag rejection, import flag parsing (incl. the
    flag.ErrHelp path and bad -acceptance values), provider-arg validation
    for fetch/list, the blast-radius wording, and config-check end to end
    against all five real configs.
  • Live smoke tests (all read-only):
    • importer (help, exit 0); importer help import; importer help fetch.
    • importer config-check on conf/{is,se,pl,fi,world}map.yaml — all OK,
      "no database touched".
    • importer validate-config conf/fimap.yaml — alias works.
    • importer acceptance conf/ismap.yaml — 26/26 passed, 0 failed.
    • make acceptance (all five configs, fast bucket) — is 26, se 12/13
      (1 deferred to -acceptance all), pl 13, fi 12, world 9; 0 failed.
    • importer import --ensure conf/ismap.yaml and conf/world.yaml — printed
      the blast-radius line and exited early on the already-populated DB (no
      work; the only safe way to show the startup line live without importing).
    • importer fetch conf/ismap.yaml and importer list conf/ismap.yaml —
      no-op "nothing to do" exits (ismap declares no providers), exit 0.
  • No full import was run, and -validate was never used.

Left undone / notes

  • -acceptance-only, -geotorget-fetch, etc. are hard errors now; anyone
    with a script other than the Makefile would need to update it (none exist in
    the repo outside run/ history).
  • importer fetch does not fetch static downloads: URLs (matching the old
    *-fetch modes); those are covered by import and the make fetch-*
    targets, and the help text says so.
#129: importer top-level subcommands — a misread flag can no longer launch a full import Closes #129 --- ## What changed The `importer/importer` CLI is now a subcommand CLI. Commands say what to do; flags on a command are modifiers. The old flat-flag surface (17 flags, no modes) is gone — including the two shapes the incident depended on: - `importer <config>` (bare config = unlabelled full import) → error, with a pointer at `importer import <config>`. - `importer -validate <config>` → **rejected** with a pointer at `importer config-check <config>`. `-validate` was a full-import modifier, not a config check; the 3h12m Finland import of #113 is exactly the failure this dispatch exists to make unreachable. ### Subcommands | command | behavior | |---|---| | `importer` | prints help, exits 0 — never starts work | | `importer help [command]` | general help, or one command's usage | | `importer import <config> [modifiers]` | the full-import path; prints its blast radius before touching the DB | | `importer config-check <config>` | config sanity only — parse, include-resolve, defaults, shared-contract wiring, layer validators, acceptance-test blocks, dependency graph; **no DB, no fetch**. `validate-config` is accepted as an alias (the name the incident was discussed with); the maintainer's decision #1 (`config-check`) is the documented name | | `importer acceptance <config>` | the old `-acceptance-only` mode, as a verb (read-only review path) | | `importer fetch <config> [geotorget\|lastkajen\|trafiklab\|all]` | the old `*-fetch` modes; no provider arg = all the config declares | | `importer list <config> [geotorget\|lastkajen]` | the old `*-list` modes; no provider arg = both | `import` modifiers: `--layer`, `--regeneralize`, `--validate`, `--ensure`, `--refetch`, `-jobs`, `-batch-rows`, `--acceptance fast|all`, `--secrets`. Both `-flag` and `--flag` spellings work (Go's flag package treats them as the same flag), so the Makefile's `-jobs 4 -batch-rows 10000` survives unchanged. The blast-radius startup line (before any DB work): ``` full import of se: 12 layers, zooms 3–14 import of se: layers cover,waterways only (2 of 12), zooms 3–14 ``` ## Decisions from the issue, and how this resolves them 1. **Subcommand names** — `config-check` per the maintainer's comment; the parent task's `validate-config` kept as an alias so both spellings work. 2. **Flat-flag compatibility** — immediate break, per the maintainer's comment. No flat flags are kept as deprecated aliases; `-validate` (and every other flat flag) is rejected with a message. The Makefile is the whole consumer list and is updated; `run/` notes are historical records and were left alone. 3. **Startup line** — implemented as a `log.Printf` before the pool opens; the wording is unit-tested (`blastRadiusLine`). The issue's word "blast radius" is used, not "destructive" (the maintainer's point that a full import is safe, just slow, is respected — the line is about *surprise*, not danger). 4. **Docs** — `docs/importer-cli.md` gains the mode table and the footgun note; the acceptance-tests section is unchanged in substance. README, AGENTS.md, geo-primer.md and country-sweden.md updated to the new syntax. `check-queries` is deliberately **not** implemented — that is #98's scope; the issue body says it slots into this shape when it lands. ## Verification - `make build`, `make test` (importer + mapsrv), `go vet` both packages, `gofmt -l` clean. - New unit tests in `importer/cli_test.go` (no import, no DB): dispatch routing, the `-validate` footgun (rejected, points at config-check), bare-config rejection, flat-flag rejection, import flag parsing (incl. the `flag.ErrHelp` path and bad `-acceptance` values), provider-arg validation for fetch/list, the blast-radius wording, and `config-check` end to end against all five real configs. - Live smoke tests (all read-only): - `importer` (help, exit 0); `importer help import`; `importer help fetch`. - `importer config-check` on conf/{is,se,pl,fi,world}map.yaml — all OK, "no database touched". - `importer validate-config conf/fimap.yaml` — alias works. - `importer acceptance conf/ismap.yaml` — **26/26 passed, 0 failed**. - `make acceptance` (all five configs, fast bucket) — is 26, se 12/13 (1 deferred to `-acceptance all`), pl 13, fi 12, world 9; **0 failed**. - `importer import --ensure conf/ismap.yaml` and `conf/world.yaml` — printed the blast-radius line and exited early on the already-populated DB (no work; the only safe way to show the startup line live without importing). - `importer fetch conf/ismap.yaml` and `importer list conf/ismap.yaml` — no-op "nothing to do" exits (ismap declares no providers), exit 0. - **No full import was run, and `-validate` was never used.** ## Left undone / notes - `-acceptance-only`, `-geotorget-fetch`, etc. are hard errors now; anyone with a script other than the Makefile would need to update it (none exist in the repo outside `run/` history). - `importer fetch` does not fetch static `downloads:` URLs (matching the old `*-fetch` modes); those are covered by `import` and the make `fetch-*` targets, and the help text says so.
The CLI now has verbs; flags are modifiers. `importer` with no verb prints help
and exits 0, `importer <config>` (the old unlabelled full-import default) is an
error that points at `import`, and the flat flags are gone outright.

The incident this fixes: #113's worker ran `importer -validate conf/fimap.yaml`
as a config sanity check and got a 3h12m full Finland import, because -validate
was a full-import MODIFIER (check ST_IsValid after each table) sitting in a
17-flag flat list. `-validate` is now rejected with a message pointing at the
new `config-check` subcommand, which is the cheap no-DB check #113 meant to run.

New shape (issue #129 + the maintainer's decisions on the issue):

  importer                        help, exit 0, no work
  importer help [command]
  importer import <config> [--layer a,b,c] [--regeneralize 5-11] [--validate]
                             [--ensure] [--refetch] [-jobs N] [-batch-rows N]
                             [--acceptance fast|all] [--secrets PATH]
  importer config-check <config>  config sanity only — no DB, no fetch
                             (validate-config accepted as an alias)
  importer acceptance <config>    the old -acceptance-only mode
  importer fetch <config> [geotorget|lastkajen|trafiklab|all]
  importer list <config> [geotorget|lastkajen]

`import` prints its blast radius before touching the database ("full import of
se: 12 layers, zooms 3–14" vs "layers cover,waterways only (2 of 12)"). The
startup line and the dispatch are covered by new unit tests (importer/cli_test.go):
routing, the -validate footgun message, the bare-config rejection, flag parsing,
provider-arg validation, and the blast-radius wording; config-check is exercised
end to end against all five real configs with no database.

Makefile targets updated to the new syntax (import, import-se/pl/fi,
import-world, import-validate, acceptance, acceptance-all); no flat flags are
kept as deprecated aliases — decision #2 was immediate break, and the Makefile
was the whole consumer list. docs/importer-cli.md gains the mode table and the
footgun note; README, AGENTS.md, geo-primer.md and country-sweden.md follow.
check-queries is deliberately not implemented (#98's scope).

Verified: make build, make test (importer + mapsrv), go vet, gofmt clean;
smoke-tested `importer help`, `config-check` on all five configs (plus the
validate-config alias), and `acceptance` against the live DB via both
`importer acceptance conf/ismap.yaml` (26/26 pass) and `make acceptance` (all
five configs green: 26+12+13+12+9 passed, 0 failed). No full import was run and
no -validate was used.
Collaborator

Review

Verified: builds clean, go vet clean, go test ./... green. The safety goals of #129 hold empirically — bare importer prints help and exits 0 without starting work, importer conf/fimap.yaml is rejected with a pointer at importer import, and importer -validate conf/fimap.yaml is rejected with the two-way pointer (config-check vs import --validate). No stale flat-flag invocations remain anywhere in the Makefile, docs or configs.

The built-in usage text documents an argument order that always fails

Every usage constant puts the config before the modifiers, but Go's flag package stops parsing at the first non-flag argument, so the documented form is rejected:

$ importer import conf/fimap.yaml --layer places
import: exactly one config file required, got 3        # exit 2

$ importer acceptance conf/fimap.yaml --acceptance all
acceptance: exactly one config file required           # exit 2

$ importer fetch conf/semap.yaml --refetch
fetch: unknown provider "--refetch" (want geotorget|lastkajen|trafiklab|all)

It fails loudly rather than silently, and the Makefile uses the working order throughout (import $(BIGJOBS) conf/semap.yaml, acceptance --acceptance all $$c), so nothing is broken today. But in a CLI whose whole thesis is that an operator must not be misled about what a command is going to do, the help text teaches the one form that cannot work, and the error it produces ("got 3") does not say why.

Affects usageText, importUsage, acceptanceUsage, fetchUsage and listUsage — all five write <config> first. Either reword to importer import [modifiers] <config>, or permute args before fs.Parse so both orders work.

run() returns an exit code, but the DB-touching commands log.Fatal past it

cmdImport, cmdAcceptance, cmdFetch and cmdList all switch to log.Fatal/log.Fatalf once past argument parsing (importer/cli.go:376, 379, 383; importer/main.go:93, 96, 107; and the fetch/list secrets paths). log.Fatal calls os.Exit(1) directly, which bypasses the int that run promises to return.

Two consequences: the run(args, stdout, stderr) int signature that makes cli_test.go possible only actually holds for the parse-level paths the tests cover, and the error loses the import: / acceptance: prefix that the same functions carefully add to their parse errors. Returning 1 and writing to the injected stderr would make the contract real all the way down.

Minor

The import help block mixes conventions inside one list: --layer, --regeneralize, --validate, --ensure, --refetch, --acceptance, --secrets are written with two dashes, -jobs N and -batch-rows N with one. Go treats them identically, so this is cosmetic, but they read as two different classes of thing.

runRegeneralize still says -regeneralize requires -layer (importer/main.go:455) in single-dash form now that the docs standardise on --layer.

## Review Verified: builds clean, `go vet` clean, `go test ./...` green. The safety goals of #129 hold empirically — bare `importer` prints help and exits 0 without starting work, `importer conf/fimap.yaml` is rejected with a pointer at `importer import`, and `importer -validate conf/fimap.yaml` is rejected with the two-way pointer (config-check vs `import --validate`). No stale flat-flag invocations remain anywhere in the Makefile, docs or configs. ### The built-in usage text documents an argument order that always fails Every usage constant puts the config before the modifiers, but Go's `flag` package stops parsing at the first non-flag argument, so the documented form is rejected: ``` $ importer import conf/fimap.yaml --layer places import: exactly one config file required, got 3 # exit 2 $ importer acceptance conf/fimap.yaml --acceptance all acceptance: exactly one config file required # exit 2 $ importer fetch conf/semap.yaml --refetch fetch: unknown provider "--refetch" (want geotorget|lastkajen|trafiklab|all) ``` It fails loudly rather than silently, and the Makefile uses the working order throughout (`import $(BIGJOBS) conf/semap.yaml`, `acceptance --acceptance all $$c`), so nothing is broken today. But in a CLI whose whole thesis is that an operator must not be misled about what a command is going to do, the help text teaches the one form that cannot work, and the error it produces ("got 3") does not say why. Affects `usageText`, `importUsage`, `acceptanceUsage`, `fetchUsage` and `listUsage` — all five write `<config>` first. Either reword to `importer import [modifiers] <config>`, or permute args before `fs.Parse` so both orders work. ### `run()` returns an exit code, but the DB-touching commands `log.Fatal` past it `cmdImport`, `cmdAcceptance`, `cmdFetch` and `cmdList` all switch to `log.Fatal`/`log.Fatalf` once past argument parsing (importer/cli.go:376, 379, 383; importer/main.go:93, 96, 107; and the fetch/list secrets paths). `log.Fatal` calls `os.Exit(1)` directly, which bypasses the `int` that `run` promises to return. Two consequences: the `run(args, stdout, stderr) int` signature that makes `cli_test.go` possible only actually holds for the parse-level paths the tests cover, and the error loses the `import:` / `acceptance:` prefix that the same functions carefully add to their parse errors. Returning `1` and writing to the injected `stderr` would make the contract real all the way down. ### Minor The `import` help block mixes conventions inside one list: `--layer`, `--regeneralize`, `--validate`, `--ensure`, `--refetch`, `--acceptance`, `--secrets` are written with two dashes, `-jobs N` and `-batch-rows N` with one. Go treats them identically, so this is cosmetic, but they read as two different classes of thing. `runRegeneralize` still says `-regeneralize requires -layer` (importer/main.go:455) in single-dash form now that the docs standardise on `--layer`.
Addresses the PR #141 review (comment 1490). Three fixes:

- Arg order: Go's flag package stops parsing at the first non-flag argument,
  so the documented form `importer import <config> --layer places` failed
  with "exactly one config file required, got 3". parseImportArgs and the
  acceptance/fetch/list commands now permute args before flag.Parse, so flags
  may come before or after the config and both orders parse identically; the
  top-level help says so, and the arg-count errors say why (usage line, plus
  "flags may come before or after the config") instead of a bare "got N".
- Exit-code contract: cmdImport/cmdAcceptance/cmdFetch/cmdList switched to
  log.Fatal past argument parsing, which os.Exit(1)s past run()'s int and
  drops the command prefix. Every error past the parse now returns the code
  and writes to the injected stderr with the import:/acceptance:/fetch:/list:
  prefix; the helpers they reach (selectedLayers, blastRadius,
  runRegeneralize, importOneLayer, generalizeAndSwap, finishAcceptance) were
  converted from log.Fatal to error/verdict returns, so no os.Exit is left in
  run()'s call tree.
- Cosmetic: the import help block is all double-dash (--jobs N, --batch-rows N
  with the rest), and runRegeneralize says --regeneralize requires --layer.

Tests: new coverage in importer/cli_test.go for the arg permutation (both
orders) and the error-prefix/exit-code paths of all four DB-touching commands
(missing/invalid config, unparseable destination, missing secrets — no DB, no
import), plus finishAcceptance's verdict codes in acceptance_test.go. Smoke:
`importer acceptance conf/fimap.yaml --acceptance all` (the previously
failing documented form) parses and runs 12/12 green; the Makefile's
flags-first order still parses.
Author
Owner

Review fixes (4e684ee)

All three points addressed on issue-129-subcommand-cli (no new branch).

1. Argument order — both orders now work

parseImportArgs and the acceptance/fetch/list commands permute args before
flag.Parse (permuteArgs in importer/cli.go): flags may come before or after
the config, and both orders parse identically. The documented form from the
review now runs:

$ importer acceptance conf/fimap.yaml --acceptance all
... 12 test(s): 12 passed, 0 failed ...

(the Makefile's flags-first form is unchanged). The top-level help now says
"Modifier flags may come before or after the config", and the arg-count errors
say why instead of a bare "got N":

$ importer import a.yaml b.yaml
import: exactly one config file required, got 2 — usage: `importer import [modifiers] <config>` (every modifier is a flag, before or after the config)   # exit 2

The permutation is value-aware (--layer places travels as a pair; bool flags
don't consume the next token; -- still terminates), so an unknown flag still
reports itself: importer import conf/fimap.yaml --bogus → import: flag provided but not defined: -bogus, not an arg-count error.

2. Exit-code contract — no log.Fatal left in the command paths

cmdImport, cmdAcceptance, cmdFetch and cmdList no longer switch to
log.Fatal past argument parsing. Every error past the parse returns the code
and writes to the injected stderr with the import: / acceptance: /
fetch: / list: prefix (loadConfig, prepareConfig, openDB, secrets, and the
rest of cmdImport's body). The helpers they reach were converted too:
selectedLayers/blastRadius (the unknown---layer error is now
import: --layer "bogus" not found in config, exit 2, before any output),
runRegeneralize/importOneLayer/generalizeAndSwap (error returns), and
finishAcceptance (returns the verdict code instead of log.Fatal). There is
no os.Exit left inside run()'s call tree — main is the only exit point.

importer/cli_test.go covers the paths (no DB, no import): missing config,
invalid config, unparseable destination connstr, and missing secrets for all
four subcommands, each asserting the exit code and the stderr prefix; plus
TestImportArgsPermutation (both orders, every modifier) and
TestRunAcceptanceConfigFirstPermutation. finishAcceptance's verdict codes
are covered in acceptance_test.go.

3. Minor

Import help is all double-dash (--jobs N, --batch-rows N), and
runRegeneralize now says --regeneralize requires --layer.

Verification

  • make build, go vet (importer + mapsrv), make test — all green;
    gofmt -l clean.
  • Smoke (read-only): importer acceptance conf/fimap.yaml --acceptance all
    parses and runs 12/12 green; importer help / importer help import show
    the corrected text; the Makefile's flags-first order still parses.
  • No full import was run, and -validate was never used.
## Review fixes (4e684ee) All three points addressed on `issue-129-subcommand-cli` (no new branch). ### 1. Argument order — both orders now work `parseImportArgs` and the acceptance/fetch/list commands permute args before `flag.Parse` (`permuteArgs` in importer/cli.go): flags may come before or after the config, and both orders parse identically. The documented form from the review now runs: ``` $ importer acceptance conf/fimap.yaml --acceptance all ... 12 test(s): 12 passed, 0 failed ... ``` (the Makefile's flags-first form is unchanged). The top-level help now says "Modifier flags may come before or after the config", and the arg-count errors say why instead of a bare "got N": ``` $ importer import a.yaml b.yaml import: exactly one config file required, got 2 — usage: `importer import [modifiers] <config>` (every modifier is a flag, before or after the config) # exit 2 ``` The permutation is value-aware (`--layer places` travels as a pair; bool flags don't consume the next token; `--` still terminates), so an unknown flag still reports itself: `importer import conf/fimap.yaml --bogus` → `import: flag provided but not defined: -bogus`, not an arg-count error. ### 2. Exit-code contract — no log.Fatal left in the command paths `cmdImport`, `cmdAcceptance`, `cmdFetch` and `cmdList` no longer switch to `log.Fatal` past argument parsing. Every error past the parse returns the code and writes to the injected stderr with the `import:` / `acceptance:` / `fetch:` / `list:` prefix (loadConfig, prepareConfig, openDB, secrets, and the rest of cmdImport's body). The helpers they reach were converted too: `selectedLayers`/`blastRadius` (the unknown-`--layer` error is now `import: --layer "bogus" not found in config`, exit 2, before any output), `runRegeneralize`/`importOneLayer`/`generalizeAndSwap` (error returns), and `finishAcceptance` (returns the verdict code instead of `log.Fatal`). There is no `os.Exit` left inside `run()`'s call tree — `main` is the only exit point. `importer/cli_test.go` covers the paths (no DB, no import): missing config, invalid config, unparseable destination connstr, and missing secrets for all four subcommands, each asserting the exit code and the stderr prefix; plus `TestImportArgsPermutation` (both orders, every modifier) and `TestRunAcceptanceConfigFirstPermutation`. `finishAcceptance`'s verdict codes are covered in `acceptance_test.go`. ### 3. Minor Import help is all double-dash (`--jobs N`, `--batch-rows N`), and `runRegeneralize` now says `--regeneralize requires --layer`. ### Verification - `make build`, `go vet` (importer + mapsrv), `make test` — all green; `gofmt -l` clean. - Smoke (read-only): `importer acceptance conf/fimap.yaml --acceptance all` parses and runs 12/12 green; `importer help` / `importer help import` show the corrected text; the Makefile's flags-first order still parses. - No full import was run, and `-validate` was never used.
Collaborator

Re-review

Both findings are fixed, and I verified each end to end rather than by reading the diff.

Argument order. The previously-failing documented forms now work, and the flags-first order the Makefile uses still does:

$ importer acceptance conf/fimap.yaml --acceptance all     → 12/12 green   (was: exit 2)
$ importer acceptance --acceptance all conf/fimap.yaml     → 12/12 green
$ importer import conf/nope.yaml --layer places            → import: open conf/nope.yaml: ... (exit 1)
$ importer import --layer places conf/nope.yaml            → identical
$ importer import a.yaml b.yaml
  import: exactly one config file required, got 2 — usage: `importer import [modifiers] <config>`
  (every modifier is a flag, before or after the config)

permuteArgs handles the cases that make this kind of function subtly wrong — inline --flag=value, boolean flags that must not eat the next token, unknown flags left in place so flag.Parse reports them itself. TestImportArgsPermutation covers both orders, a fully shuffled line, and both dash styles.

Exit-code contract. os.Exit now appears exactly once in non-test code, in main(). All four DB-touching commands return codes with their prefix intact:

acceptance conf/nope.yaml   exit=1  acceptance: open conf/nope.yaml: no such file or directory
fetch conf/nope.yaml        exit=1  fetch: ...
list conf/nope.yaml         exit=1  list: ...
import conf/nope.yaml       exit=1  import: ...
import --bogus x.yaml       exit=2  (usage errors stay 2)

finishAcceptance returning a verdict is propagated at all three call sites, and I confirmed the whole path end to end with a deliberately-failing probe test: exit=1, tally line and failure summary both still printed.

The log.Fatal → error conversion is the part with real regression potential, so I walked every converted site. Each former fatal is an immediate return at the caller — nothing continues past a failure that previously aborted, including inside the per-layer loop and the generalization cascade. Control flow is preserved.

Verified on top of current main (this branch is 7 commits behind, merges clean): go build, go vet, go test ./... green; all five acceptance suites pass; config-check OK on all five configs. (conf/mapsrv.yaml and conf/secrets.example.yaml fail config-check, correctly — they aren't importer configs.)

One thing left, minor

permuteArgs drops the -- token, so the terminator never reaches flag.Parse — which contradicts the function's own comment ("-- still terminates flag parsing"):

$ importer import -- -weird.yaml
flag provided but not defined: -weird.yaml        # exit 2

A config path starting with - is exotic, so the impact is small; it's the comment claiming otherwise that makes it worth a line. The fix is one character — args[i+1:] → args[i:] in the -- branch, keeping the terminator at the head of the positionals. Confirmed against the real flag package:

current  in=[-- -weird.yaml]  permuted=[-weird.yaml]      -> err=flag provided but not defined
fixed    in=[-- -weird.yaml]  permuted=[-- -weird.yaml]   -> err=<nil>  args=[-weird.yaml]

The normal (no --) path is byte-identical either way.

The cosmetic items landed too — the import help block is all double-dash, and runRegeneralize says --regeneralize requires --layer.

## Re-review Both findings are fixed, and I verified each end to end rather than by reading the diff. **Argument order.** The previously-failing documented forms now work, and the flags-first order the Makefile uses still does: ``` $ importer acceptance conf/fimap.yaml --acceptance all → 12/12 green (was: exit 2) $ importer acceptance --acceptance all conf/fimap.yaml → 12/12 green $ importer import conf/nope.yaml --layer places → import: open conf/nope.yaml: ... (exit 1) $ importer import --layer places conf/nope.yaml → identical $ importer import a.yaml b.yaml import: exactly one config file required, got 2 — usage: `importer import [modifiers] <config>` (every modifier is a flag, before or after the config) ``` `permuteArgs` handles the cases that make this kind of function subtly wrong — inline `--flag=value`, boolean flags that must not eat the next token, unknown flags left in place so `flag.Parse` reports them itself. `TestImportArgsPermutation` covers both orders, a fully shuffled line, and both dash styles. **Exit-code contract.** `os.Exit` now appears exactly once in non-test code, in `main()`. All four DB-touching commands return codes with their prefix intact: ``` acceptance conf/nope.yaml exit=1 acceptance: open conf/nope.yaml: no such file or directory fetch conf/nope.yaml exit=1 fetch: ... list conf/nope.yaml exit=1 list: ... import conf/nope.yaml exit=1 import: ... import --bogus x.yaml exit=2 (usage errors stay 2) ``` `finishAcceptance` returning a verdict is propagated at all three call sites, and I confirmed the whole path end to end with a deliberately-failing probe test: `exit=1`, tally line and failure summary both still printed. The `log.Fatal` → error conversion is the part with real regression potential, so I walked every converted site. Each former fatal is an immediate `return` at the caller — nothing continues past a failure that previously aborted, including inside the per-layer loop and the generalization cascade. Control flow is preserved. Verified on top of current `main` (this branch is 7 commits behind, merges clean): `go build`, `go vet`, `go test ./...` green; all five acceptance suites pass; `config-check` OK on all five configs. (`conf/mapsrv.yaml` and `conf/secrets.example.yaml` fail `config-check`, correctly — they aren't importer configs.) ### One thing left, minor `permuteArgs` drops the `--` token, so the terminator never reaches `flag.Parse` — which contradicts the function's own comment ("`--` still terminates flag parsing"): ``` $ importer import -- -weird.yaml flag provided but not defined: -weird.yaml # exit 2 ``` A config path starting with `-` is exotic, so the impact is small; it's the comment claiming otherwise that makes it worth a line. The fix is one character — `args[i+1:]` → `args[i:]` in the `--` branch, keeping the terminator at the head of the positionals. Confirmed against the real `flag` package: ``` current in=[-- -weird.yaml] permuted=[-weird.yaml] -> err=flag provided but not defined fixed in=[-- -weird.yaml] permuted=[-- -weird.yaml] -> err=<nil> args=[-weird.yaml] ``` The normal (no `--`) path is byte-identical either way. The cosmetic items landed too — the import help block is all double-dash, and `runRegeneralize` says `--regeneralize requires --layer`.
art merged commit 979b301310 into main 2026-08-16 06:31:13 +00:00
art deleted branch issue-129-subcommand-cli 2026-08-16 06:31:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
art/ismap!141
No description provided.