Goal
Implement the three documented CLI entrypoints — fishwrap-build, fishwrap-version, fishwrap-validate-config — as stable, scriptable subcommands of the image. These are the consumer contract.
Context
The ADR (issue A1) commits to a CLI surface that downstream consumers depend on, not a Python ABI. The reason: CLI contracts are easy to keep stable across years; ABI contracts require care every refactor.
Today there is no fishwrap-version CLI. DC's publish_about.py:10 does import fishwrap to read __version__. Today there is no fishwrap-validate-config; a typo in config.py causes a runtime crash four minutes into a fetch (after feeds are pulled). Today there is no fishwrap-build; the four python -m fishwrap.X invocations are stitched together by Makefiles.
This issue creates the stable CLI surface that lets DC stop importing fishwrap as a Python module and lets a stranger validate their config in five seconds instead of five minutes.
Deliverable
In the fishwrap repo:
fishwrap-build — wraps the existing fetcher → editor → enhancer → printer pipeline into a single command. Accepts --config <path>. Sets FISHWRAP_CONFIG and runs the four phases sequentially. Exit code: 0 on success, non-zero on any phase failure with a clear error
fishwrap-version — prints __version__ from fishwrap/__init__.py to stdout, then exits 0. Adds nothing else (no banner, no whitespace surprises). Stable contract: stdout always parseable as a semver string
fishwrap-validate-config — accepts a config file path. Loads the config (the same way fishwrap/_config.py loads it). Validates against a schema covering the keys the engine actually uses: FEEDS, SECTIONS, KEYWORDS, EDITORIAL_POLICIES, SCORING_PROFILES, SOURCE_SECTIONS, EDITION_SIZE, MIN_SECTION_SCORES, BOOST_UNIT_VALUE, FUZZY_BOOST_MULTIPLIER, EXPIRATION_HOURS, TIMEZONE, etc. Reports missing required fields, wrong types, and unknown top-level keys. Exit code: 0 on valid, non-zero on invalid with human-readable errors
- The schema documentation produced as part of issue D2 (
docs/CONFIG_SCHEMA.md) is the user-facing reference; this issue is the implementation
These can be entry-point scripts via pyproject.toml/setup.cfg console_scripts, or shell wrappers in bin/ — implementer's choice as long as the image's entrypoint dispatch (issue A2) routes correctly.
Acceptance Criteria
Out of Scope
- Writing
docs/CONFIG_SCHEMA.md — that is part of D2 (docs rewrite). This issue produces the validator; D2 produces the human-readable reference
- Migrating consumers to use
fishwrap-version — that's C5 (DC) and any other consumers later
Dependencies
- Blocked by: A2 (entrypoint dispatch must exist)
- Blocks: C5 (DC's
publish_about.py decoupling depends on fishwrap-version existing)
References
- ADR:
docs/ADR/0001-release-artifact.md (issue A1)
- Current config loader:
fishwrap/_config.py:53-77
- Current pipeline modules:
fishwrap/{fetcher,editor,enhancer,printer}.py
- Current version source:
fishwrap/__init__.py
- Manifesto: rule 1 (complexity is the enemy — small, stable CLI surface), rule 5 (root cause only — validate before fetching, don't crash mid-pipeline)
Goal
Implement the three documented CLI entrypoints —
fishwrap-build,fishwrap-version,fishwrap-validate-config— as stable, scriptable subcommands of the image. These are the consumer contract.Context
The ADR (issue A1) commits to a CLI surface that downstream consumers depend on, not a Python ABI. The reason: CLI contracts are easy to keep stable across years; ABI contracts require care every refactor.
Today there is no
fishwrap-versionCLI. DC'spublish_about.py:10doesimport fishwrapto read__version__. Today there is nofishwrap-validate-config; a typo inconfig.pycauses a runtime crash four minutes into a fetch (after feeds are pulled). Today there is nofishwrap-build; the fourpython -m fishwrap.Xinvocations are stitched together by Makefiles.This issue creates the stable CLI surface that lets DC stop importing fishwrap as a Python module and lets a stranger validate their config in five seconds instead of five minutes.
Deliverable
In the fishwrap repo:
fishwrap-build— wraps the existing fetcher → editor → enhancer → printer pipeline into a single command. Accepts--config <path>. SetsFISHWRAP_CONFIGand runs the four phases sequentially. Exit code: 0 on success, non-zero on any phase failure with a clear errorfishwrap-version— prints__version__fromfishwrap/__init__.pyto stdout, then exits 0. Adds nothing else (no banner, no whitespace surprises). Stable contract: stdout always parseable as a semver stringfishwrap-validate-config— accepts a config file path. Loads the config (the same wayfishwrap/_config.pyloads it). Validates against a schema covering the keys the engine actually uses:FEEDS,SECTIONS,KEYWORDS,EDITORIAL_POLICIES,SCORING_PROFILES,SOURCE_SECTIONS,EDITION_SIZE,MIN_SECTION_SCORES,BOOST_UNIT_VALUE,FUZZY_BOOST_MULTIPLIER,EXPIRATION_HOURS,TIMEZONE, etc. Reports missing required fields, wrong types, and unknown top-level keys. Exit code: 0 on valid, non-zero on invalid with human-readable errorsdocs/CONFIG_SCHEMA.md) is the user-facing reference; this issue is the implementationThese can be entry-point scripts via
pyproject.toml/setup.cfgconsole_scripts, or shell wrappers inbin/— implementer's choice as long as the image's entrypoint dispatch (issue A2) routes correctly.Acceptance Criteria
fishwrap-versionprints a single semver line and exits 0; tested against fixed expected outputfishwrap-build --config <path>runs the full pipeline; equivalent output to the oldpython -m fishwrap.{fetcher,editor,enhancer,printer}chainfishwrap-validate-config <path>accepts a known-good config and exits 0fishwrap-validate-config <path>rejects: missing required key, wrong type for known key, malformedEDITORIAL_POLICIESentry — each with a clear error pointing at the offending keyOut of Scope
docs/CONFIG_SCHEMA.md— that is part of D2 (docs rewrite). This issue produces the validator; D2 produces the human-readable referencefishwrap-version— that's C5 (DC) and any other consumers laterDependencies
publish_about.pydecoupling depends onfishwrap-versionexisting)References
docs/ADR/0001-release-artifact.md(issue A1)fishwrap/_config.py:53-77fishwrap/{fetcher,editor,enhancer,printer}.pyfishwrap/__init__.py