527 lines
14 KiB
Markdown
527 lines
14 KiB
Markdown
# 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 after actual publication or replacement
|
|
```
|
|
|
|
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;
|
|
- pipeline id or destination id mismatch: 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 detailed storage contract is defined in `docs/roadmap/storage.md`. Core application code should use that storage interface for backend-rooted logical paths, byte and stream IO, metadata, traversal, typed errors, emptiness checks, and managed deletion.
|
|
|
|
Destructive APIs should remain narrow. Prefer managed deletion of 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.
|
|
|
|
## 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.
|