Added initial MVP roadmap documentation and implementation plan
This commit is contained in:
629
docs/roadmap/packages.md
Normal file
629
docs/roadmap/packages.md
Normal file
@@ -0,0 +1,629 @@
|
||||
# Package Layout Roadmap
|
||||
|
||||
This roadmap defines the proposed package layout, boundaries, and implementation responsibilities for the `distributor` MVP.
|
||||
|
||||
`distributor` is expected to be a domain-agnostic bundle publisher. Producer applications emit source bundles containing `manifest.json`; `distributor` validates those bundles and publishes selected source and generated artifacts to one or more configured destinations.
|
||||
|
||||
This document is roadmap material. It describes the intended package design before implementation and should move into `docs/internal/` only after corresponding behavior exists.
|
||||
|
||||
## Accepted Package Layout
|
||||
|
||||
```text
|
||||
cmd/distributor/
|
||||
main.go
|
||||
|
||||
internal/app/
|
||||
app.go
|
||||
run.go
|
||||
pipeline.go
|
||||
|
||||
internal/cli/
|
||||
root.go
|
||||
run.go
|
||||
validate.go
|
||||
inspect.go
|
||||
|
||||
internal/config/
|
||||
config.go
|
||||
defaults.go
|
||||
load.go
|
||||
validate.go
|
||||
|
||||
internal/bundle/
|
||||
manifest.go
|
||||
digest.go
|
||||
validate.go
|
||||
discover.go
|
||||
|
||||
internal/state/
|
||||
distributor.go
|
||||
compare.go
|
||||
validate.go
|
||||
|
||||
internal/storage/
|
||||
backend.go
|
||||
registry.go
|
||||
path.go
|
||||
errors.go
|
||||
|
||||
internal/storage/fake/
|
||||
backend.go
|
||||
|
||||
internal/adapters/local/
|
||||
backend.go
|
||||
|
||||
internal/adapters/ssh/
|
||||
backend.go
|
||||
config.go
|
||||
|
||||
internal/adapters/s3/
|
||||
backend.go
|
||||
config.go
|
||||
|
||||
internal/transform/
|
||||
transform.go
|
||||
registry.go
|
||||
plan.go
|
||||
|
||||
internal/transform/markdown/
|
||||
markdown.go
|
||||
template.go
|
||||
|
||||
internal/publish/
|
||||
plan.go
|
||||
execute.go
|
||||
output.go
|
||||
reconcile.go
|
||||
safety.go
|
||||
|
||||
internal/notify/
|
||||
notify.go
|
||||
noop.go
|
||||
|
||||
internal/logging/
|
||||
logging.go
|
||||
```
|
||||
|
||||
## Package Responsibilities
|
||||
|
||||
### `cmd/distributor`
|
||||
|
||||
Application entrypoint only.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- call CLI execution;
|
||||
- translate process exit status;
|
||||
- avoid business logic.
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- config loading;
|
||||
- backend construction;
|
||||
- bundle validation;
|
||||
- publish decisions.
|
||||
|
||||
### `internal/cli`
|
||||
|
||||
CLI command definitions, flags, argument parsing, and command wiring.
|
||||
|
||||
Expected MVP commands:
|
||||
|
||||
- `distributor run` — run configured pipelines.
|
||||
- `distributor run --dry-run` — plan without modifying destinations.
|
||||
- `distributor run --pipeline <id>` — run one configured pipeline.
|
||||
- `distributor validate <path>` — validate a source bundle or source tree where feasible.
|
||||
- `distributor inspect <path>` — inspect a bundle or destination state where feasible.
|
||||
|
||||
Boundaries:
|
||||
|
||||
- CLI should call `internal/app` use cases.
|
||||
- CLI should not parse manifests directly except through application APIs.
|
||||
- CLI should not import backend adapter implementation details unless only for registration side effects.
|
||||
|
||||
### `internal/app`
|
||||
|
||||
Application orchestration and top-level use cases.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- load and validate configuration;
|
||||
- construct configured pipelines;
|
||||
- build source and destination backends through registries;
|
||||
- orchestrate discovery, validation, planning, publishing, and notification;
|
||||
- coordinate dry-run output;
|
||||
- run destination fan-out deterministically and sequentially;
|
||||
- aggregate destination outcomes into run-level failure behavior.
|
||||
|
||||
Core orchestration shape:
|
||||
|
||||
```text
|
||||
for each selected pipeline:
|
||||
open source backend
|
||||
discover source bundles
|
||||
for each source bundle:
|
||||
validate source manifest and digest
|
||||
for each destination:
|
||||
inspect .distributor.json
|
||||
build publish plan
|
||||
transform as required by that destination
|
||||
execute publish plan unless dry-run
|
||||
run noop notifier
|
||||
```
|
||||
|
||||
Boundaries:
|
||||
|
||||
- `internal/app` composes packages but should not contain backend-specific logic.
|
||||
- Publish decisions should live in `internal/publish`, not inline in orchestration.
|
||||
- Destination state comparison should live in `internal/state` or `internal/publish`, not CLI code.
|
||||
|
||||
### `internal/config`
|
||||
|
||||
Configuration structs, defaults, loading, precedence, and validation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- load `/usr/local/etc/distributor/config.yml` by default;
|
||||
- support `--config` override;
|
||||
- apply defaults;
|
||||
- validate required fields;
|
||||
- validate pipeline ids and destination ids;
|
||||
- validate backend-specific config shapes;
|
||||
- validate transform and publish policy combinations.
|
||||
|
||||
MVP config model:
|
||||
|
||||
```yaml
|
||||
pipelines:
|
||||
- id: weather-daily
|
||||
source:
|
||||
backend: local
|
||||
path: /var/spool/distributor/weather
|
||||
validation:
|
||||
on_digest_mismatch: fail
|
||||
destinations:
|
||||
- id: markdown-archive
|
||||
backend: s3
|
||||
endpoint: https://s3.example.com
|
||||
bucket: reports
|
||||
prefix: weather/archive
|
||||
region: us-east-1
|
||||
force_path_style: true
|
||||
publish:
|
||||
source: true
|
||||
html: false
|
||||
transfer:
|
||||
on_destination_same: skip
|
||||
on_destination_older: replace
|
||||
on_destination_newer: skip
|
||||
on_conflict: fail
|
||||
- id: static-site
|
||||
backend: ssh
|
||||
uri: ssh://deploy@example.com:22
|
||||
path: /srv/www/weather
|
||||
publish:
|
||||
source: false
|
||||
html: true
|
||||
transform:
|
||||
markdown_to_html:
|
||||
enabled: true
|
||||
mode: sidecar
|
||||
```
|
||||
|
||||
Configuration principles:
|
||||
|
||||
- one source per pipeline;
|
||||
- one or more destinations per pipeline;
|
||||
- transforms are destination-specific;
|
||||
- publish policy is destination-specific;
|
||||
- secrets should use environment variables, secret files, SSH agent, or standard credential mechanisms rather than raw YAML values.
|
||||
|
||||
### `internal/bundle`
|
||||
|
||||
Source bundle contract and validation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- parse source `manifest.json`;
|
||||
- represent source manifests and files;
|
||||
- discover bundle roots beneath a configured source root;
|
||||
- validate required manifest fields;
|
||||
- validate RFC3339 `created` values;
|
||||
- validate relative paths;
|
||||
- validate file existence, size, per-file SHA-256, and bundle digest;
|
||||
- expose normalized source bundle models to other packages.
|
||||
|
||||
Core types:
|
||||
|
||||
```go
|
||||
type Manifest struct {
|
||||
SchemaVersion int
|
||||
ID string
|
||||
Digest string
|
||||
Created time.Time
|
||||
Files []ManifestFile
|
||||
}
|
||||
|
||||
type ManifestFile struct {
|
||||
Path string
|
||||
SHA256 string
|
||||
Size int64
|
||||
}
|
||||
|
||||
type Bundle struct {
|
||||
RootRelativePath string
|
||||
Manifest Manifest
|
||||
}
|
||||
```
|
||||
|
||||
Boundaries:
|
||||
|
||||
- `internal/bundle` does not know about `.distributor.json`.
|
||||
- `internal/bundle` does not know about destinations, transforms, or notification.
|
||||
- `internal/bundle` may use the storage abstraction to read source files, but it should not import backend adapter packages.
|
||||
|
||||
### `internal/state`
|
||||
|
||||
Destination state contract for `.distributor.json`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- parse `.distributor.json`;
|
||||
- validate destination state;
|
||||
- represent copied source outputs and generated outputs;
|
||||
- embed the source manifest used for publication;
|
||||
- compare destination state against a current source manifest;
|
||||
- classify destination state as same, older, newer, conflict, absent, invalid, or unmanaged.
|
||||
|
||||
Core types:
|
||||
|
||||
```go
|
||||
type DistributorState struct {
|
||||
SchemaVersion int
|
||||
DistributorVersion string
|
||||
PipelineID string
|
||||
DestinationID string
|
||||
PublishedAt time.Time
|
||||
Source SourceState
|
||||
Outputs []OutputFile
|
||||
}
|
||||
|
||||
type SourceState struct {
|
||||
Manifest bundle.Manifest
|
||||
}
|
||||
|
||||
type OutputFile struct {
|
||||
Path string
|
||||
Kind string // source | generated
|
||||
SourcePath string
|
||||
Transform string
|
||||
SHA256 string
|
||||
Size int64
|
||||
}
|
||||
```
|
||||
|
||||
Comparison rules:
|
||||
|
||||
- same source manifest: skip;
|
||||
- same source id, older destination source `created`: replace;
|
||||
- same source id, newer destination source `created`: skip;
|
||||
- same source id, same `created`, different digest: conflict;
|
||||
- different source id: conflict;
|
||||
- absent state: publish only if safe;
|
||||
- unmanaged non-empty path: fail.
|
||||
|
||||
Boundaries:
|
||||
|
||||
- `internal/state` owns destination state semantics, not publish execution.
|
||||
- `internal/state` should not know about S3, SSH/SFTP, local filesystem details, or Markdown rendering.
|
||||
|
||||
### `internal/storage`
|
||||
|
||||
Backend abstraction and shared storage types.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- define storage backend interfaces;
|
||||
- define object/file metadata types;
|
||||
- define path/prefix helpers;
|
||||
- define common storage errors;
|
||||
- provide backend registry mechanisms.
|
||||
- provide a fake backend for core package tests.
|
||||
|
||||
The core application should use storage interfaces such as:
|
||||
|
||||
```go
|
||||
type Backend interface {
|
||||
ReadFile(ctx context.Context, path string) ([]byte, error)
|
||||
WriteFile(ctx context.Context, path string, data []byte, opts WriteOptions) error
|
||||
Exists(ctx context.Context, path string) (bool, error)
|
||||
List(ctx context.Context, prefix string) ([]Entry, error)
|
||||
DeleteFiles(ctx context.Context, paths []string) error
|
||||
}
|
||||
```
|
||||
|
||||
Destructive APIs should remain narrow. Prefer deleting explicit files recorded in `.distributor.json` instead of broad recursive deletion.
|
||||
|
||||
Boundaries:
|
||||
|
||||
- `internal/storage` should not contain backend implementation details.
|
||||
- Adapter dependencies must not leak through storage interfaces.
|
||||
- The fake backend exists for tests and should not become an application runtime backend.
|
||||
|
||||
### `internal/adapters/local`
|
||||
|
||||
Local filesystem backend.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- implement `storage.Backend` for local paths;
|
||||
- clean and constrain paths;
|
||||
- perform safe reads/writes/listing/deletion;
|
||||
- use atomic writes where practical;
|
||||
- reject unsafe path traversal;
|
||||
- handle symlink policy explicitly.
|
||||
|
||||
Testing expectations:
|
||||
|
||||
- use temporary directories;
|
||||
- verify path traversal rejection;
|
||||
- verify write and delete safety.
|
||||
|
||||
### `internal/adapters/ssh`
|
||||
|
||||
SSH/SFTP backend.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- implement `storage.Backend` over SSH/SFTP;
|
||||
- support `uri` and `path` config;
|
||||
- prefer native SFTP implementation;
|
||||
- use SSH agent, key files, known hosts, or documented auth mechanisms;
|
||||
- avoid raw passwords in config unless explicitly designed and documented later;
|
||||
- translate SSH/SFTP errors into storage-level errors.
|
||||
|
||||
Testing expectations:
|
||||
|
||||
- core app tests should use fake backends;
|
||||
- adapter tests may use local test servers or targeted integration tests if practical;
|
||||
- do not require a real production SSH host for normal unit tests.
|
||||
|
||||
### `internal/adapters/s3`
|
||||
|
||||
S3-compatible object storage backend.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- implement `storage.Backend` over S3-compatible object storage;
|
||||
- support endpoint, bucket, prefix, region, and force-path-style configuration;
|
||||
- support standard credential mechanisms or explicit environment-variable references;
|
||||
- treat S3 as an object tree, not a filesystem;
|
||||
- set reasonable content types where practical;
|
||||
- guard against prefix/root deletion mistakes.
|
||||
|
||||
Testing expectations:
|
||||
|
||||
- core app tests should use fake backends;
|
||||
- adapter behavior may be tested through mocks, local S3-compatible services, or narrow integration tests;
|
||||
- config examples should avoid real secrets.
|
||||
|
||||
### `internal/transform`
|
||||
|
||||
Transform interfaces, registry, and transform planning.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- define transform interfaces;
|
||||
- register available transforms;
|
||||
- represent transform requests and outputs;
|
||||
- keep transform execution independent of destination backend details.
|
||||
|
||||
Boundaries:
|
||||
|
||||
- transforms operate on source bundle content and destination transform config;
|
||||
- transforms do not publish outputs;
|
||||
- transforms do not mutate source bundles;
|
||||
- transforms should return generated output metadata for `.distributor.json`.
|
||||
|
||||
### `internal/transform/markdown`
|
||||
|
||||
Markdown-to-HTML implementation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- render listed Markdown files to HTML;
|
||||
- support MVP sidecar behavior, such as `report.md` -> `report.html`;
|
||||
- record generated output path, source path, transform name, SHA-256, and size;
|
||||
- optionally use embedded templates if needed.
|
||||
|
||||
MVP scope:
|
||||
|
||||
- Markdown to HTML only;
|
||||
- no PDF generation;
|
||||
- no email-specific HTML;
|
||||
- no complex theming unless required for basic output correctness.
|
||||
|
||||
### `internal/publish`
|
||||
|
||||
Destination planning, reconciliation, safety checks, and publish execution.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- inspect destination state;
|
||||
- plan destination action;
|
||||
- enforce destination conflict rules;
|
||||
- enforce destructive-operation safety rules;
|
||||
- detect output path collisions before writing;
|
||||
- combine source files and transform outputs according to destination publish policy;
|
||||
- write destination outputs;
|
||||
- write `.distributor.json`;
|
||||
- use staging or equivalent cleanup behavior where practical;
|
||||
- support dry-run planning;
|
||||
- report skipped, replaced, failed, and published actions.
|
||||
|
||||
Action model:
|
||||
|
||||
```text
|
||||
publish
|
||||
replace
|
||||
skip_same
|
||||
skip_destination_newer
|
||||
fail_conflict
|
||||
fail_unmanaged
|
||||
```
|
||||
|
||||
Boundaries:
|
||||
|
||||
- publish logic should not parse CLI flags;
|
||||
- publish logic should not know adapter implementation details;
|
||||
- publish logic should use `internal/state` for destination state semantics;
|
||||
- publish logic should use `internal/storage` interfaces for IO.
|
||||
|
||||
### `internal/notify`
|
||||
|
||||
Notification stage abstraction.
|
||||
|
||||
MVP responsibilities:
|
||||
|
||||
- define notifier interface;
|
||||
- implement no-op notifier;
|
||||
- preserve future extension point for email, ntfy, Gotify, RSS update hooks, or other notification channels.
|
||||
|
||||
Future notification rules:
|
||||
|
||||
- notify only after successful publication to the relevant destination or destinations;
|
||||
- notification must be idempotent with respect to source id, digest, pipeline id, and destination id where applicable;
|
||||
- notification should not run for skipped or failed publications unless explicitly configured.
|
||||
|
||||
### `internal/logging`
|
||||
|
||||
Logging setup and helpers.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- centralize structured logging setup;
|
||||
- ensure logs omit secrets;
|
||||
- provide consistent fields for pipeline id, bundle id, destination id, backend, path, action, and reason.
|
||||
|
||||
## Implementation Slices
|
||||
|
||||
### Slice 1: Skeleton, config, storage, and source bundle validation
|
||||
|
||||
Deliver:
|
||||
|
||||
- basic CLI skeleton;
|
||||
- config loading and validation;
|
||||
- storage interface, local backend, and fake backend;
|
||||
- source manifest model;
|
||||
- source bundle discovery;
|
||||
- file size and SHA-256 validation;
|
||||
- bundle digest validation;
|
||||
- local backend sufficient for validation;
|
||||
- fixtures for valid and invalid bundles.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
distributor validate ./examples/weather-bundle
|
||||
```
|
||||
|
||||
### Slice 2: Destination state and dry-run planning
|
||||
|
||||
Deliver:
|
||||
|
||||
- `.distributor.json` model;
|
||||
- destination state comparison;
|
||||
- publish plan model;
|
||||
- dry-run output;
|
||||
- local source to local destination planning;
|
||||
- tests for same/older/newer/conflict/unmanaged cases.
|
||||
|
||||
Useful command:
|
||||
|
||||
```bash
|
||||
distributor run --config ./examples/local.yml --dry-run
|
||||
```
|
||||
|
||||
### Slice 3: Local publish execution
|
||||
|
||||
Deliver:
|
||||
|
||||
- local destination writes;
|
||||
- source-file publication;
|
||||
- `.distributor.json` writes;
|
||||
- replacement safety checks;
|
||||
- skip behavior;
|
||||
- narrow deletion behavior based on destination state outputs.
|
||||
|
||||
Useful command:
|
||||
|
||||
```bash
|
||||
distributor run --config ./examples/local.yml
|
||||
```
|
||||
|
||||
### Slice 4: Markdown-to-HTML transform
|
||||
|
||||
Deliver:
|
||||
|
||||
- Markdown transform registry;
|
||||
- Markdown-to-HTML implementation;
|
||||
- per-destination `publish.source` and `publish.html` behavior;
|
||||
- generated output metadata in `.distributor.json`;
|
||||
- tests for source-only, HTML-only, and source-plus-HTML destinations.
|
||||
|
||||
### Slice 5: S3 backend
|
||||
|
||||
Deliver:
|
||||
|
||||
- S3-compatible backend;
|
||||
- endpoint/bucket/prefix/region/force-path-style config;
|
||||
- credential handling via environment or standard mechanisms;
|
||||
- object listing, read, write, and narrow delete operations;
|
||||
- dry-run and publish coverage using fake or local-compatible test strategy.
|
||||
|
||||
### Slice 6: SSH/SFTP backend
|
||||
|
||||
Deliver:
|
||||
|
||||
- native SFTP backend;
|
||||
- `uri` and `path` config;
|
||||
- documented authentication behavior;
|
||||
- read/write/list/delete operations;
|
||||
- adapter tests or documented integration test strategy.
|
||||
|
||||
### Slice 7: No-op notification stage and future extension seam
|
||||
|
||||
Deliver:
|
||||
|
||||
- no-op notifier wired into orchestration;
|
||||
- clear internal contract for future email/ntfy adapters;
|
||||
- no user-facing notification behavior beyond no-op unless implemented.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
The following are intentionally out of MVP unless separately accepted in a later roadmap:
|
||||
|
||||
- email, ntfy, Gotify, or other real notification adapters;
|
||||
- RSS/Atom feed generation;
|
||||
- PDF generation;
|
||||
- web UI;
|
||||
- full-text search;
|
||||
- dynamic plugin loading;
|
||||
- arbitrary transform chains;
|
||||
- workflow DAGs;
|
||||
- producer execution;
|
||||
- complex templating/theming;
|
||||
- bidirectional sync;
|
||||
- backup semantics.
|
||||
|
||||
## Key Invariants
|
||||
|
||||
- Producer apps own source bundle creation.
|
||||
- `distributor` owns destination publication state.
|
||||
- Source `manifest.json` is not copied as destination state.
|
||||
- Destination `.distributor.json` is the managed sentinel.
|
||||
- One pipeline has one source and one or more destinations.
|
||||
- Transform and publish policy are destination-specific.
|
||||
- Source files are canonical; HTML is derived.
|
||||
- Destructive replacement is allowed only inside managed destination bundle paths.
|
||||
- Core logic must be testable without real S3, SSH, or remote services.
|
||||
Reference in New Issue
Block a user