Files
distributor/docs/roadmap/implementation.md

864 lines
25 KiB
Markdown

# Distributor Implementation Roadmap
This roadmap defines a staged implementation plan for the `distributor` MVP. Each stage is intended to map cleanly to one Codex implementation prompt.
The roadmap assumes the project includes these planning documents before implementation begins:
- `docs/policy/architecture.md`
- `docs/policy/documentation.md`
- `docs/roadmap/packages.md`
- `docs/roadmap/contracts.md`
- `docs/roadmap/config.md`
- `docs/roadmap/storage.md`
The MVP goal is a domain-agnostic bundle distributor that discovers source bundles, validates `manifest.json`, optionally transforms Markdown to HTML, publishes selected outputs to one or more destinations, and records destination state in `.distributor.json`.
## Global Implementation Rules
All stages should preserve these invariants:
- Producer applications own source bundle creation.
- `distributor` owns validation, transformation, publication, destination state, and future notification hooks.
- Source bundle state is defined by `manifest.json`.
- Destination publication state is defined by `.distributor.json`.
- `manifest.json` is not copied to the destination as destination state.
- Pipelines have exactly one source and one or more destinations.
- Transform and publish policy are destination-specific.
- Destructive replacement is allowed only inside a managed destination bundle path. Unsafe force or unmanaged overwrite behavior is deferred.
- Dry-run behavior should be implemented before broad remote write behavior.
- Config, bundle, state, publish planning, storage adapters, transforms, and CLI wiring should remain separate packages.
Unless a stage explicitly says otherwise, each implementation prompt should:
1. read the project policy and roadmap documents;
2. implement only the current stage;
3. add or update tests for the current stage;
4. run the relevant test suite;
5. update documentation only when the implemented behavior now exists;
6. avoid implementing future roadmap stages early.
## Stage 1: Project Skeleton, CLI Shell, and Baseline Tooling
### Goal
Create the initial Go application structure and a minimal executable `distributor` command with no business behavior beyond version/help output and placeholder commands.
### Scope
Implement the accepted package skeleton from `docs/roadmap/packages.md` at the level needed for compilation.
Create:
```text
cmd/distributor/main.go
internal/cli/
internal/app/
internal/config/
internal/logging/
```
Initial CLI commands:
- `distributor --help`
- `distributor version`
- `distributor run`
- `distributor validate`
- `distributor inspect`
At this stage, `run`, `validate`, and `inspect` may return clear “not implemented” errors, but the command structure should be present.
### Notes
Prefer a small CLI dependency only if the project already standardizes on one. Otherwise, the standard library is acceptable for the first pass.
Add a version variable that can later be set at build time.
### Tests
Add tests for:
- command construction if testable;
- version string behavior if exposed through a package;
- basic package compilation.
### Completion Criteria
- `go test ./...` passes.
- `go run ./cmd/distributor --help` works.
- `go run ./cmd/distributor version` works.
- Placeholder operational commands fail clearly and intentionally.
## Stage 2: Config Schema, Loading, Defaults, and Validation
### Goal
Implement the MVP `config.yml` schema described in `docs/roadmap/config.md`.
### Scope
Create config structs for:
- top-level config;
- pipelines;
- source backend config;
- destination backend config;
- validation policy;
- publish policy;
- transform policy;
- transfer/replacement policy;
- backend-specific local, SSH, and S3 fields.
Support loading YAML from a file path.
Implement validation for:
- required top-level `pipelines`;
- unique pipeline ids;
- required pipeline `id`, `source`, and non-empty `destinations`;
- unique destination ids within a pipeline;
- supported backend names: `local`, `ssh`, `s3`;
- required backend fields;
- supported validation action: `fail`;
- supported transfer actions;
- valid `publish` policy;
- valid Markdown-to-HTML transform config.
Default behavior should match `docs/roadmap/config.md`.
### CLI Integration
Add `--config` to `run`.
For this stage, `distributor run --config config.yml --dry-run` may only load and validate config, then print a concise summary of configured pipelines and destinations.
### Tests
Add unit tests for:
- valid minimal local-to-local config;
- valid fan-out config;
- valid local, SSH, and S3 backend configs;
- duplicate pipeline ids;
- duplicate destination ids;
- missing required fields;
- unsupported backend;
- invalid transfer action;
- invalid validation action, including `warn`.
### Completion Criteria
- Config load/default/validate behavior is implemented and tested.
- `distributor run --config <file> --dry-run` validates config and prints a summary.
- No bundle discovery or publication occurs yet.
## Stage 3: Storage Abstraction, Local Backend, and Fake Backend
### Goal
Introduce the storage backend abstraction before bundle validation so source discovery, validation, and publication are backend-agnostic from the start.
### Scope
Create:
```text
internal/storage/backend.go
internal/storage/registry.go
internal/storage/path.go
internal/storage/errors.go
internal/adapters/local/backend.go
internal/storage/fake/
```
Implement the storage contract in `docs/roadmap/storage.md`, including backend-rooted logical paths, hybrid byte/stream IO, metadata, traversal, typed errors, managed deletion, and efficient destination emptiness helper behavior.
The fake backend should exist for unit tests of config, bundle, state, and publish logic without real local, SSH, or S3 IO.
### Safety Requirements
The local backend must:
- clean and join paths safely;
- reject path traversal;
- reject unsafe destructive deletion requests;
- avoid following symlinks for source bundle files unless explicitly supported;
- avoid deleting configured roots;
- classify destination bundle emptiness deterministically.
### Tests
Add tests for:
- path validation;
- backend byte and stream read/write behavior;
- `Stat`, `Walk`, and materialized deterministic traversal helper behavior;
- traversal rejection;
- symlink entry reporting and source-read rejection;
- staged write behavior where testable;
- typed storage errors and helper predicates;
- managed deletion guard behavior;
- early-stop destination emptiness helper behavior;
- fake backend parity for core package tests.
### Completion Criteria
- Local backend implements the storage interface.
- Fake backend can support bundle and publish tests without external services.
- `go test ./...` passes.
- No SSH or S3 implementation exists yet.
## Stage 4: Source Bundle Manifest, Digest, Validation, and Discovery
### Goal
Implement the source bundle contract from `docs/roadmap/contracts.md` through the storage abstraction.
### Scope
Create:
```text
internal/bundle/manifest.go
internal/bundle/digest.go
internal/bundle/validate.go
internal/bundle/discover.go
```
Implement:
- parsing `manifest.json`;
- strict required field validation, including `schema_version: 1`;
- RFC3339 `created` parsing;
- lowercase `sha256:<64 hex>` digest validation;
- source file path safety checks;
- duplicate logical file path rejection;
- per-file SHA256 validation;
- per-file size validation;
- bundle digest validation using the canonical ordered file-record algorithm;
- deterministic storage-backed bundle discovery under a source root;
- nested manifest detection and failure.
Discovery and validation should use `internal/storage` rather than direct `os` APIs. The local CLI path should be adapted to the local backend.
### CLI Integration
Implement:
```text
distributor validate <path>
distributor inspect <path>
```
For local paths:
- `validate` should validate either a single bundle directory or a tree containing bundles.
- `inspect` should print a concise normalized summary of discovered bundle ids, relative paths, created timestamps, digest values, and files.
### Tests
Add fixture bundles under a testdata directory.
Test:
- valid bundle;
- invalid JSON;
- missing required fields;
- invalid schema version;
- invalid timestamp;
- invalid digest format;
- unsafe file paths;
- duplicate normalized file paths;
- missing files;
- size mismatch;
- per-file digest mismatch;
- bundle digest mismatch;
- canonical bundle digest reference fixture;
- multiple discovered bundles in deterministic order;
- nested manifests fail.
### Completion Criteria
- Storage-backed bundle validation is deterministic and well-tested.
- `distributor validate <path>` works for local bundle fixtures.
- `distributor inspect <path>` works for local bundle fixtures.
- No destination publication occurs yet.
## Stage 5: Destination State Contract and Comparison Logic
### Goal
Implement `.distributor.json` parsing, validation, and source-to-destination comparison.
### Scope
Create:
```text
internal/state/distributor.go
internal/state/compare.go
internal/state/validate.go
```
Implement the destination state schema from `docs/roadmap/contracts.md`, including:
- `schema_version`;
- optional `distributor_version`;
- `pipeline_id`;
- `destination_id`;
- `published_at`;
- embedded normalized source manifest;
- outputs array;
- output file metadata.
Implement comparison outcomes:
- destination absent;
- destination unmanaged/non-empty;
- destination state pipeline or destination id mismatch;
- same source manifest;
- same source id, destination older;
- same source id, destination newer;
- same source id and same created but different digest;
- different source id;
- invalid destination state.
### Tests
Add unit tests for every comparison outcome.
Test validation for:
- valid state;
- missing fields;
- invalid schema version;
- invalid embedded source manifest;
- invalid output metadata;
- malformed published timestamp.
Timestamps should parse RFC3339 input and distributor-written timestamps should normalize to RFC3339 UTC.
### Completion Criteria
- Destination state can be parsed and validated independently.
- Source manifest to destination state comparison is deterministic and fully tested.
- No publication execution occurs yet.
## Stage 6: Publish Planning, Dry-Run, and Local-to-Local Publication Without Transform
### Goal
Implement the core publish planner and execute local-to-local publication for source files only.
### Scope
Create:
```text
internal/publish/plan.go
internal/publish/reconcile.go
internal/publish/safety.go
internal/publish/output.go
internal/publish/execute.go
```
Implement planning for one source bundle to one destination based on:
- source manifest;
- destination config;
- publish policy;
- transfer policy;
- existing `.distributor.json`;
- destination path state.
Actions should include:
- publish new;
- replace older destination;
- skip same;
- skip destination newer;
- fail conflict;
- fail unmanaged destination.
Implement local-to-local execution for `publish.source: true` and `publish.html: false`.
Execution should:
- copy listed source files selected by publish policy;
- write `.distributor.json` with copied source output metadata;
- avoid copying source `manifest.json` as destination state;
- preserve relative bundle paths from source root beneath destination root;
- detect destination output collisions before writing;
- use staging or equivalent cleanup behavior for local writes;
- support fan-out to multiple local destinations;
- support dry-run without writes.
### CLI Integration
`distributor run --config <file>` should now execute local-to-local pipelines when configured.
`--dry-run` should print the planned action for each discovered bundle and destination.
### Tests
Add integration-style tests using temp directories for:
- new local publication;
- no-op when destination state matches;
- replacement when destination state is older;
- skip when destination state is newer;
- fail on conflict;
- fail on unmanaged non-empty destination;
- fail on output path collision;
- fan-out from one source to two local destinations;
- failed local write does not leave a destination that appears unmanaged on retry;
- dry-run performs no writes;
- `.distributor.json` is written correctly.
### Completion Criteria
- Local-to-local source-file publication works end to end.
- Dry-run produces meaningful planned actions.
- Destination state is authoritative.
- No Markdown-to-HTML transform exists yet.
## Stage 7: Markdown-to-HTML Transform and Destination-Specific Publish Policy
### Goal
Add MVP Markdown-to-HTML transformation and destination-specific source/html output selection.
### Scope
Create:
```text
internal/transform/transform.go
internal/transform/registry.go
internal/transform/plan.go
internal/transform/markdown/markdown.go
internal/transform/markdown/template.go
```
Implement only:
```yaml
transform:
markdown_to_html:
enabled: true
mode: sidecar
```
MVP sidecar behavior:
- for each listed source artifact ending in `.md`, generate a same-directory `.html` sidecar;
- preserve the original Markdown file unchanged;
- do not generate HTML for non-Markdown files;
- escape or disable raw HTML embedded in Markdown;
- fail before writing when generated output paths collide with copied source outputs or other generated outputs;
- record generated output metadata in `.distributor.json`;
- if `publish.source: false`, do not publish source files;
- if `publish.html: true`, publish generated HTML files;
- if `publish.html: true` but transform is disabled or no Markdown files exist, fail with a clear error unless config later defines another behavior.
Use a well-maintained Markdown renderer. Keep HTML templating minimal and deterministic.
### Tests
Add tests for:
- Markdown sidecar generation;
- source-only destination;
- HTML-only destination;
- source-plus-HTML destination;
- no mutation of source bundle;
- generated output metadata in `.distributor.json`;
- failure when HTML publication is requested without transform support;
- failure when generated HTML collides with a source artifact path;
- raw HTML in Markdown is escaped or disabled consistently;
- deterministic output for a fixture Markdown file.
### Completion Criteria
- Local-to-local publication supports source-only, HTML-only, and source-plus-HTML destinations.
- Generated outputs are recorded in destination state.
- Dry-run reports transform outputs that would be generated.
## Stage 8: No-Op Notification Stage, Pipeline Polish, and Local MVP Checkpoint
### Goal
Add the internal no-op notification stage and polish orchestration around per-destination outcomes.
### Scope
Create:
```text
internal/notify/notify.go
internal/notify/noop.go
```
Integrate a no-op notifier after actual successful publication or replacement. Skipped destinations should not invoke the notifier in the MVP.
Clarify orchestration behavior when one destination fails. For MVP, fan-out should be deterministic and sequential. Continue planning and reporting later destinations where safe, but return non-zero if any destination fails.
Improve run summary output:
- pipeline id;
- source backend;
- discovered bundle count;
- destination ids;
- action per bundle/destination;
- final status.
### Tests
Add tests for:
- notifier is invoked at the expected orchestration point where testable;
- pipeline failure when a destination fails;
- run summary contains meaningful status information;
- dry-run does not invoke write-side effects.
### Completion Criteria
- The pipeline shape includes notification as an internal no-op stage.
- Run output is useful for unattended operation logs.
- Local MVP behavior remains passing and is ready for one real local producer pipeline.
Stages 1 through 8 define the local MVP checkpoint. Later stages extend the local MVP with remote backends, cross-backend hardening, user-facing documentation sync, and release readiness.
## Stage 9: Native SSH/SFTP Backend Roadmap Extension
### Goal
Implement SSH/SFTP storage backend support for sources and destinations.
### Scope
Create:
```text
internal/adapters/ssh/backend.go
internal/adapters/ssh/config.go
```
Implement the storage backend interface over native SSH/SFTP.
Required config:
```yaml
backend: ssh
uri: ssh://user@example.com:22
path: /remote/root
```
Authentication expectations:
- prefer SSH agent by default;
- use known_hosts validation by default where practical;
- do not require passwords in YAML;
- optional key-file support may be implemented if straightforward, but should not distract from agent-based auth.
Support SSH/SFTP backend as both source and destination:
- local -> ssh;
- ssh -> local;
- ssh -> ssh where feasible through staging or streaming.
### Safety Requirements
The SSH backend must enforce the same logical path safety rules as the local backend.
Deletion must remain limited to managed destination bundle paths guarded by valid `.distributor.json`.
### Tests
Unit-test path handling and config validation.
If practical, add integration tests that can be skipped unless an SSH test endpoint is configured through environment variables. Do not require a live SSH server for normal `go test ./...`.
### Completion Criteria
- SSH/SFTP backend compiles and satisfies the storage interface.
- Backend config validation is tested.
- Normal tests do not depend on a live SSH server.
- At least local-to-SSH and SSH-to-local flows are documented or manually testable.
## Stage 10: S3-Compatible Backend Roadmap Extension
### Goal
Implement S3-compatible backend support for sources and destinations.
### Scope
Create:
```text
internal/adapters/s3/backend.go
internal/adapters/s3/config.go
```
Required config should align with `docs/roadmap/config.md`:
```yaml
backend: s3
endpoint: https://s3.example.com
bucket: reports
prefix: some/prefix
region: us-east-1
force_path_style: true
credentials:
access_key_id_env: DISTRIBUTOR_S3_ACCESS_KEY_ID
secret_access_key_env: DISTRIBUTOR_S3_SECRET_ACCESS_KEY
```
Implement storage operations over S3 object keys through the common storage contract:
- `ReadFile` and `OpenReader`;
- `WriteFile` and `WriteFrom`;
- `Stat`;
- `Walk` using object-list pagination where available;
- `HasAny` with early stop for destination emptiness checks;
- `DeleteManagedBundle`;
- read/write `.distributor.json` through normal storage operations.
Set reasonable content types where available:
- `.md`: `text/markdown; charset=utf-8`;
- `.html`: `text/html; charset=utf-8`;
- `.json`: `application/json`;
- `.txt`: `text/plain; charset=utf-8`.
Support S3 backend as both source and destination.
### Safety Requirements
Treat S3 prefixes as object trees. Do not assume real directories exist.
Deletion must be limited to destination bundle prefixes that are confirmed managed by `.distributor.json`.
### Tests
Add unit tests for:
- config validation;
- key/prefix normalization;
- content type selection;
- path traversal rejection;
- publish planning with S3 destination state fixtures.
If practical, add integration tests gated by environment variables or a local S3-compatible test service. Normal `go test ./...` must not require live S3 credentials.
### Completion Criteria
- S3 backend compiles and satisfies the storage interface.
- S3 source and destination flows are supported through the common pipeline path.
- Normal tests do not require live S3.
## Stage 11: Cross-Backend End-to-End Coverage and Hardening Roadmap Extension
### Goal
Harden the MVP across backend combinations, destination policies, and failure cases.
### Scope
Add end-to-end coverage for representative scenarios:
- local source -> local archive destination;
- local source -> local HTML destination;
- local source -> two destinations with different publish policies;
- local source -> SSH destination, where integration credentials exist;
- local source -> S3 destination, where integration credentials exist;
- S3 source -> local destination, where integration credentials exist;
- SSH source -> local destination, where integration credentials exist.
Improve logging and error messages for:
- invalid config;
- invalid source manifest;
- digest mismatch;
- destination conflict;
- unmanaged destination path;
- backend read/write/list failures;
- transform failures.
Ensure all destructive paths have tests or explicit safeguards.
### Tests
Add or expand tests for:
- dry-run across multiple destinations;
- partial failure behavior;
- repeated run idempotency;
- older/newer destination state behavior;
- destination state output metadata accuracy;
- generated HTML output metadata accuracy.
### Completion Criteria
- MVP behavior is reliable across implemented backend types.
- Error messages identify pipeline id, destination id, bundle id, and reason where practical.
- Idempotent repeated runs behave as expected.
## Stage 12: User-Facing Documentation Sync
### Goal
Update documentation to reflect implemented MVP behavior.
### Scope
Following `docs/policy/documentation.md`, create or update user-facing documentation only for implemented features.
Likely docs:
```text
README.md
docs/config.md
docs/cli.md
docs/policy/architecture.md
docs/internal/bundles.md
docs/internal/backends.md
```
Document:
- what `distributor` does;
- bundle contract summary;
- `.distributor.json` role;
- example source bundle;
- example local-to-local config;
- example local-to-S3 config;
- example local-to-SSH config;
- `run`, `validate`, and `inspect` commands;
- dry-run behavior;
- replacement and safety rules;
- Markdown-to-HTML transform behavior;
- environment-variable credential handling.
Move roadmap material to historical/planning status only if your documentation policy allows it. Do not describe unimplemented notification adapters as available features.
### Tests
Run the full test suite.
If docs include command examples, verify that basic examples correspond to actual CLI behavior.
### Completion Criteria
- User-facing docs describe the implemented MVP accurately.
- Roadmap docs no longer masquerade as implemented behavior.
- `go test ./...` passes.
## Stage 13: MVP Release Readiness Pass
### Goal
Perform a final pre-release quality pass.
### Scope
Review:
- package boundaries against `docs/policy/architecture.md`;
- package layout against `docs/roadmap/packages.md`;
- implemented contracts against `docs/roadmap/contracts.md`;
- implemented config behavior against `docs/roadmap/config.md`;
- docs against `docs/policy/documentation.md`;
- destructive operation safety;
- logs and errors for unattended operation;
- command UX;
- test coverage for core invariants.
Add any missing small tests or docs discovered during review.
Do not add new product features in this stage.
### Completion Criteria
- MVP is ready to deploy against one real producer pipeline.
- A dry-run can be performed safely against a real source and destination.
- Repeated runs are idempotent.
- Destructive replacement cannot occur outside managed destination bundle paths.
- Final docs accurately reflect the application.
## Stage 14: Explicit Force Overwrite Roadmap Extension
### Goal
Introduce explicit operator-requested force behavior for controlled overwrite cases that are intentionally outside the local MVP.
### Scope
Add a CLI-only force option such as:
```bash
distributor run --config config.yml --force
```
Define and implement force planning for:
- unmanaged non-empty destination paths;
- destination state with a different source id;
- destination state with matching source id and created timestamp but different digest;
- destination state with mismatched `pipeline_id` or `destination_id`;
- destination newer than source when the transfer policy explicitly allows replacement.
Force behavior must be explicit per run. It should not be a persistent default in config for this stage.
Update transfer policy validation to allow broader values only when force behavior is implemented and documented:
- `on_destination_newer: replace`
- `on_conflict: replace`
### Safety Requirements
- Dry-run must show every file or object that would be written or deleted before a forced run.
- Force must still never delete above the resolved destination bundle path or configured destination prefix.
- Filesystem replacement should remain staged where practical.
- S3 replacement must remain constrained to the destination bundle prefix.
- Logs must clearly mark force decisions and include pipeline id, destination id, bundle id, and reason.
### Tests
Add tests for:
- force rejected when the flag is absent;
- unmanaged non-empty destination overwritten only with force;
- different source id overwritten only with force and allowed policy;
- same id and created but different digest overwritten only with force and allowed policy;
- destination newer replaced only with force and allowed policy;
- pipeline or destination id mismatch overwritten only with force and allowed policy;
- dry-run reports destructive force actions without writing;
- destructive paths remain bounded to the destination bundle path.
### Completion Criteria
- Force overwrite behavior is explicit, logged, dry-runnable, and test-covered.
- Default non-force behavior remains unchanged and conservative.
## Deferred Post-MVP Work
The following items are intentionally outside the MVP unless explicitly pulled into a later roadmap:
- email notifications;
- ntfy/Gotify/Pushover notifications;
- RSS/Atom feed generation;
- static site index pages beyond sidecar HTML output;
- templated HTML themes beyond a minimal deterministic template;
- destination path remapping rules;
- full plugin architecture;
- web UI;
- report editing;
- producer pipeline execution;
- database-backed state;
- complex retry queues;
- concurrent publication workers;
- symlink support;
- warning-only digest mismatch handling;
- password-based SSH authentication in YAML.