Skip to content

The v0.2 Reset: Plain and Managed Instead of a Universal Control Plane

How SOW replaced the v0.1 Git/CAS/Edge platform with two small execution modes, explicit ownership, SQLite state, and a repository-focused release.
How SOW replaced the v0.1 Git/CAS/Edge platform with two small execution modes, explicit ownership, SQLite state, and a repository-focused release.
Development-line versus release version

The archived “v0.2” design line initially used C2 view-local RPM hardlinks. That was a real development implementation, but it was replaced by the single-payload design before the final v0.2.0 tag. The released v0.2.0 layout already used one canonical Pool and metadata-only views.

Several archived v0.2/v0.3 files nevertheless say “v0.2.0” used C2 hardlinks. They were written before the tag existed and used “v0.2.0” for the planned build. The delivered boundary is defined by the final 0.2.0 Changelog, release note, and source tag, all of which record one canonical Pool and metadata-only views.

On 2026-07-31, immediately after sealing the v0.1 source baseline, SOW began a product reset. The objective was not to salvage every V1 subsystem. It was to identify the smallest repository product that could replace Pigsty’s daily package-build workflow while keeping the safety lessons already learned.

The result was a boundary that still defines SOW: Plain for rebuildable directories, and Managed for long-lived repository ownership.

Why reset instead of incrementally simplifying V1

The v0.1 model had coupled five different jobs:

  1. build valid APT/YUM/asset repositories;
  2. maintain package history and snapshots;
  3. migrate a large legacy Pigsty topology;
  4. publish to multiple cloud/CDN targets;
  5. enforce commercial Edge access.

Removing one feature at a time would leave the same Git/CAS/Route ownership model in place. The reset instead asked which facts the core repository engine truly needed to own.

The initial phase-one boundary was as important as the feature list:

  • no V1 Git database, Route/Manifest/Quarantine model, or global content-addressed product layer;
  • no upstream sync, pull/push/copy/move/promote surface, Edge entitlement, CDN bootstrap, or commercial topology;
  • no remote publication/state, object storage, GC, snapshot/freeze, Managed export, or serving in the first phase;
  • no modulemd, source-package building, Web UI, cross-Repository deduplication, or multi-writer operation.

Upstream synchronization and channel promotion were deliberately removed even though V1 had real streaming, fuzz, and deterministic-proof evidence for them. Package acquisition is an input boundary, not a second owner inside the repository engine. Operators can prepare package sets with dedicated mirror or download tooling, then let SOW own admission, membership, build, and publication. Some phase-one exclusions—publication, retention, GC, and external export—returned before the final v0.2.0 tag under the smaller Repository ownership model.

Two isolated execution modes

Plain: rebuild what the caller owns

Plain mode operates on a caller-owned directory of RPM and DEB files:

sow create /srv/repo

It scans packages, renders protocol metadata, validates the result, and installs pointers last. Plain owns its generated metadata but not a Workspace, Repository database, Generation history, or long-lived membership state.

Because the desired state is simply “the packages currently in this directory,” the correct recovery strategy is to rerun the build. Later releases made that principle even more explicit by removing Plain journals and repeated package reads.

Managed: own lifecycle and history

Managed mode introduces a Workspace with independent Repositories:

Workspace
├── sow.yml
├── .sow/                    private config, locks, SQLite, journals
└── <repository>/
    ├── pool/                canonical package bytes
    └── dists/               client protocol views

Each Repository owns its Pool, Package Objects, Membership, Desired State, Built Generation, Changeset, lock, database, and recovery state. Different Repositories do not silently deduplicate or share counters.

A Dist is a named RPM or DEB membership set. Architecture Views are projections of that membership, not new owners. Neutral packages (noarch / all) project into every applicable architecture without duplicating logical membership.

Decisions that replaced V1 concepts

V1 concept v0.2 replacement
Git commit/ref as canonical state Per-Repository SQLite plus explicit filesystem/journal evidence
Global CAS and Route materialization Repository-owned Pool and protocol views
View/Snapshot/Stable ref graph Dist membership, Desired revision, Built Generation, retained state
One universal command surface Closed Plain and Managed CLI contracts
Implicit legacy topology Strict sow.yml plus explicit names and selectors
Cross-product publication saga Repository build kept separate from target publication
Reuse V1 packages directly Narrow parser/renderer/version/signing ports only

The critical simplification was that private control state and public repository bytes could be understood without Git or an Edge router.

Desired versus Built

Managed mode made interrupted state explicit:

  • Desired State is the current package and membership intent in SQLite.
  • Built Generation is the last complete public tree that rendered and validated successfully.
  • Changeset is the exact delta between two built states.

Desired may advance while Built remains valid. A failed build therefore does not require pretending that the public tree changed or that the request never happened. Operation journals and status expose the difference.

This distinction survived every later release.

P0 through P3

The work was intentionally sequenced:

Phase Contract
P0 Plain create, real RPM/DEB metadata, signing, Pigsty-compatible filtering, pointer-last replacement
P1 Workspace/Repository/Dist lifecycle, strict config, package identity, membership, and query
P2 Add/remove/policy/build operations, journals, crash recovery, and safe mutation
P3 Check, Changeset, handoff, scale, compatibility, and clean-delivery evidence

Each phase had an API contract, implementation specification, adversarial review, and traceability/acceptance record. Those documents were useful while building the release; their lasting decisions are summarized here rather than published as separate authorities.

The first Managed RPM layout kept canonical files in a root Pool and added hardlinks below each architecture View. Metadata used a safe local pool/... href. This C2 layout made a pinned AlmaLinux 9 reposync flow work and avoided duplicate local bytes because aliases shared an inode.

The design had a hidden deployment cost:

  • object storage expands each path into a separate full object;
  • archive and cross-filesystem copies can lose hardlink identity;
  • every Dist/architecture alias looks like another canonical payload;
  • retention and GC risk depending on inode/link-count behavior;
  • the whole Repository no longer maps one-to-one to static object keys.

The pre-release design snapshot on 2026-08-05 accurately records C2 implementation and local acceptance. It is not the final v0.2.0 release contract.

The single-payload contract was written on 2026-08-05, committed as the “next” design on August 7, and implemented before the August 8 release tag. The final release therefore shipped:

<repository>/
  pool/       one canonical payload per Package Object
  dists/      metadata-only APT/RPM views

Default EL reposync became an accepted compatibility limitation; a standalone RPM leaf moved to an explicit external export. The complete reasoning is preserved in Single-Payload Repository.

What the reset reused

The team did not start from blank code. It retained only narrow components that had no V1 ownership assumptions:

  • RPM/DEB parsing and native version comparison;
  • metadata rendering, compression, signing, and validation;
  • safe filesystem primitives;
  • selected client fixtures and fault patterns;
  • exact package identity and provenance checks.

Git state, Route receipts, Edge components, legacy migration machinery, and cloud control plane code were reference material or removal candidates, not dependencies of the new core.

Release evidence boundary

The development line recorded P0/P1 acceptance on August 1, P1–P3 acceptance on August 2, and a local release-gate run on August 5. The final v0.2.0 source was tagged on August 8 after the single-payload implementation, packaging, and CI corrections landed.

The final release also shipped filesystem and R2 publication, retained generations, external RPM leaf export, and local GC. R2 remote deletion was disabled from the start and target GC retained/reporting candidates instead.

That distinction matters:

  • an August 5 C2 PASS proves the development snapshot that produced it;
  • the August 8 v0.2.0 tag and release note define what actually shipped;
  • later v0.3/v0.4 tests do not rewrite either historical result.

What still defines SOW

  • Plain and Managed are isolated modes with different recovery costs.
  • Workspace is configuration/discovery scope; Repository is package/history scope.
  • Package Object identity is immutable; Membership is logical and many-to-many.
  • Desired and Built states are distinct.
  • writes use stable locks, bounded journals, pointer-last installation, and fail-closed recovery;
  • CLI syntax, JSON envelopes, and exit classes form a closed automation contract;
  • protocol, client, mirror-tool, and provider compatibility are reported separately.

The current forms are documented in Get Started, System Model, and Design Principles.

Primary source snapshots

Continue with Design Evolution for the v0.3 and v0.4 hardening that followed.