Commit Graph

15 Commits

Author SHA1 Message Date
agra
6dc6433c2d P3.1: dist CLI wiring (BLOCKED by sx compiler bug — std.cli+std.json parse collision)
Intended `dist` entrypoint: std.cli os_args/parse dispatch to stub handlers
for `ci publish`, `release promote`, `release rollback`, with help/usage,
EX_USAGE(64) exit contract, and `--json` purity (machine JSON on stdout via
std.json, human text on stderr).

NOT wired into `make build`/`tests/` and NOT buildable: `std.cli` and
`std.json` both export a top-level `parse`; with both in the compilation
closure the sx IR lowering crashes on `cli.parse`
(lower.zig lazyLowerFunction assert `func.params.len == fd.params.len + ctx_slots`).
json_out.sx isolates the std.json import, but `sx build` lowers the whole
transitive closure so the bare-name collision persists. See the P3.1 worker
report for the minimal repro. Step is BLOCKED pending an sx fix.
2026-06-06 01:51:59 +03:00
agra
882dce4f6d Merge branch 'flow/distribution/P2.3' into distribution-plan 2026-06-06 01:30:57 +03:00
agra
c541fac7ce P2.3: publish enforces channel-name target + release-id uniqueness
Close the remaining publish aggregate-consistency edges (review round 2, F1
continued):

- chan.name == release.channel — the promoted channel must be the one the
  release declares as its target; promoting a "beta" channel for a release
  whose channel is "stable" committed an edge contradicting the release's own
  target. Now rejected with Integrity + rollback.
- release.id must be new — a colliding id would shadow the existing release
  (get_release resolves to the OLD one), so the channel edge would silently
  point at a different release than the one published. Now rejected with
  Integrity + rollback.

tests/repo_transaction.sx: add a channel-name-mismatch case and a
release-id-collision case (both assert Integrity + model unchanged); existing
fully-consistent publish still commits. Both new cases fail on the pre-fix
repo.sx and pass after.
2026-06-06 01:27:54 +03:00
agra
d8380ed451 P2.3: publish enforces cross-entity identity (no cross-app dangling edge)
Repo.publish validated entities individually and checked
artifact.release_id == release.id, but never verified the published
aggregate forms one consistent identity graph. It could commit a channel
whose app_id differs from the release's app (a channel of app B pointing
at app A's release) or artifacts whose app_id differs from the release's
app — exactly the dangling/cross-app edge the acceptance forbids.

Add Integrity preconditions to the publish transaction (reusing the
existing len-reset/channel-restore rollback so the model is unchanged on
failure): the release's app must exist, the promoted channel must belong
to that app (chan.app_id == release.app_id), and every artifact must
belong to that app AND name this release (a.app_id == release.app_id and
a.release_id == release.id).

Extend tests/repo_transaction.sx with cross-app channel and cross-app
artifact cases asserting publish raises Integrity and leaves the model
unchanged; the existing rollback and no-dangling assertions stay green.
2026-06-06 01:19:22 +03:00
agra
aa3b690381 P2.3: in-memory repository + db.json persistence (SQLite stand-in)
Adds the in-memory repository over the P2.1 domain and whole-model
persistence to <root>/db.json via std.json (subplan-02 Slice 1, the part
P2.1's mapping deferred).

src/repo/repo.sx
  - Repo over App/Release/Artifact/Channel/AuditEvent, each a growable
    List scanned LINEARLY (no index — Slice 1).
  - create/get/list/update per entity; find_app_by_slug;
    find_artifact_by_digest (the P2.2 content-address key).
  - publish(): atomic-ish transaction (release + artifacts + channel
    pointer). A failure midway rolls the model back by snapshot/restore —
    no half-inserted entities and no channel left pointing at a release
    that isn't in the repo.
  - Long-lived-container rule: init captures own_allocator :=
    context.allocator and every List growth forwards it explicitly, so the
    backing stores outlive any single call's transient context allocator.

src/repo/db.sx
  - save()/load() the whole model to/from <root>/db.json via std.json.
  - Stable (insertion-order) field order: entities emit in declaration
    order; top-level order is apps, releases, artifacts, channels,
    audit_events. Re-saving an unchanged model is byte-identical.
  - Enums serialize as their variant name. Read-back is strict: a missing
    field, wrong JSON type, or unknown enum name -> typed LoadErr.BadShape.
    Loaded strings are copied into the new repo's own allocator.

tests/
  - repo_roundtrip.sx: save -> reparse (valid JSON) -> reload into a fresh
    repo, asserting every field round-trips and a re-save is byte-identical.
  - repo_transaction.sx: a publish that fails midway leaves the model
    unchanged (no dangling release/channel), in memory and after reload.
  - repo_owns_allocator.sx: deterministic proof that every owned list grows
    through the captured allocator, not the call-site context allocator.

Gate: make build + make test both green (6/6).
2026-06-06 01:08:01 +03:00
agra
a2f7ad2a79 Merge branch 'flow/distribution/P2.2' into distribution-plan 2026-06-06 00:51:04 +03:00
agra
3bc019c736 P2.2: fix put_file content-addressing — hash the published bytes (single source read)
put_file hashed the source path, then copied the source again — two reads.
A source mutated in between would publish bytes whose digest != returned key,
breaking the content-addressed invariant. Now copy the source once into a
provisional staging file, derive the key from the SHA-256 of that staged file
(the exact bytes published), then dedup/atomic-rename. Guarantees
key == digest(published object) with a single source read.

Extends the acceptance test: re-hashes the stored object and asserts it equals
the returned key (and std.hash / shasum of the fixture), asserts cross-path
dedup (put_file and put_bytes of identical content share one object), and
asserts the staging temp is cleaned up on both the success and dedup paths.
2026-06-06 00:47:45 +03:00
agra
68c002ab06 P2.2: content-addressed artifact store (staging -> atomic move, dedup)
Local blob store under src/store/, the first real consumer of std.hash.
Objects are addressed by lowercase-hex SHA-256: the digest is the storage
key and bytes live at <root>/objects/<sha256>.

- put_bytes / put_file compute the digest via std.hash, write to a
  staging file, then atomically rename into objects/<sha256>. The rename
  is the only step that publishes, so an interrupted/failed write never
  leaves a torn object at the final path.
- Dedup: an already-published object short-circuits without re-staging.
- stage_write/stage_copy + publish expose the two phases for the test.

tests/store_content_addressed.sx asserts the storage key equals std.hash,
an independent `shasum -a 256`, and the pinned SHA-256("abc") vector;
that dedup stores one object and never rewrites it; that a staged write
is invisible until publish and a failed publish leaves no object; and
that put_file round-trips bytes. Gate: make build + make test both green.
2026-06-06 00:34:21 +03:00
agra
b552958378 Merge branch 'flow/distribution/P2.1' into distribution-plan 2026-06-06 00:18:26 +03:00
agra
fd0ebd64f4 P2.1: record flow-step <-> subplan-slice scope mapping
Document the already-decided decomposition of subplan-02 Slice 1 so the
P2.1 boundary is verifiable from the repo:
- P2.1 = Core Structs + boundary validation (validation portion of Slice 1)
- P2.3 = in-memory repository + db.json (rest of Slice 1)
- Token = Slice 5 (Token Security), out of both

Planning-doc record only; no domain or test code changed.
2026-06-06 00:16:17 +03:00
agra
85f9c7c487 P2.1: address review — harden artifact validation, release/artifact fields
- validate_artifact: add empty content_type -> MissingField; size_bytes <= 0
  -> new BadSize; sha256 not exactly 64 lowercase-hex -> new BadDigest (via
  validate_sha256). The valid fixture (64-hex sha, positive size, non-empty
  content_type) stays accepted.
- Release: rename updated_at -> published_at (published_at = 0 means draft).
  Final order: id, app_id, version, build, channel, notes, created_by,
  created_at, published_at.
- Artifact: add opaque metadata: string before validation_status.
- tests: add empty-content_type/MissingField, bad-size/BadSize,
  malformed-sha256/BadDigest cases and contract pins reading back
  Release.published_at and Artifact.metadata.

PO-ruled out of scope and NOT added: Token model, in-memory repository (P2.3).
2026-06-06 00:09:21 +03:00
agra
c3897e3508 P2.1: domain structs + boundary validation
Add the core distribution domain model under src/domain/ (App, Release,
Artifact, Channel, AuditEvent + Platform/Visibility/ValidationStatus/
RolloutPolicy enums) and a boundary validator that returns one distinct
typed ValidationErr per failure class (BadSlug, EmptyVersion, BadVersion,
BadChannelName, UnknownPlatform, MissingField, BadRollout).

Pure sx, depends only on modules/std.sx; lookups left as linear scans over
List (no HashMap). tests/domain_validate.sx asserts valid App/Release/
Artifact/Channel are accepted and each invalid case is rejected with the
exact expected error tag.
2026-06-05 23:02:41 +03:00
agra
331a3e06dc Merge branch 'flow/distribution/P1.1' into distribution-plan 2026-06-03 17:24:09 +03:00
agra
e0f8b96d33 P1.1: repo skeleton + sx build/test gate
Stand up the foundation every later step depends on:

- Source layout: src/, src/infra/, tests/, examples/ (.gitkeep markers).
- Makefile: `build` compiles the smoke program via $SX, `test` runs the
  runner over tests/**/*.sx, `publish-example` placeholder (real in P3.4).
  Compiler located via `SX ?= /Users/agra/projects/sx/zig-out/bin/sx`.
- tests/run.sh: POSIX-sh runner; discovers tests/**/*.sx, runs each via
  `$SX run`, prints ok/FAIL, exits 0 only when all pass (errors on zero
  tests so the gate is never silently empty).
- tests/smoke.sx: passing smoke test importing modules/std.sx — proves
  toolchain wiring end-to-end (std resolves via the binary's own location).
- .gitignore: ignore build/ artifacts.
2026-06-03 17:18:19 +03:00
agra
055c8ced15 Baseline: distribution workspace before observability redesign 2026-06-02 16:12:15 +03:00