P3.4a-001: ci publish loads existing db.json (cross-invocation persistence)
`dist ci publish` now seeds the Repo from a pre-existing <store>/db.json before find-or-create, so separate CLI invocations share state: a new version accumulates under the single found app, and re-publishing the same release id is rejected by the P2.3 integrity transaction (db.json left unchanged). An absent db.json still starts empty. The loaded model grows through its owning allocator (context.allocator), per the long-lived rule. Wiring db.load into the dist program (which already links manifest.sx) exposed two latent issues, both fixed: - db.sx's load-path helpers (dup_str/obj_find/req_obj/req_arr/ artifact_from_json) collided by name with manifest.sx's same-named helpers; sx resolves bare top-level names across the whole program, so load_into bound to manifest's versions and failed the LoadErr error-set check. Renamed db.sx's five helpers with a db_ prefix (load-path only; save path and public API untouched). - publish's `existing!.id` (only reachable once an app is found, i.e. never before this change) read garbage: sx miscompiles postfix-`!` chained with `.field`. Bound the unwrap to a local first, matching the codebase idiom. tests/publish_persist.sx drives build/dist twice into one store: publish A, then a different version B accumulates (two releases, one app, both objects), then re-publishing A's id fails and leaves db.json unchanged. Fails on the pre-fix write-only persistence, passes after.
This commit is contained in:
@@ -5,13 +5,15 @@
|
||||
// manifest (P3.2) -> store (P2.2) -> common validation (P3.3) ->
|
||||
// repository transaction + audit (P2.3) -> db.json persistence (P2.3)
|
||||
//
|
||||
// `run_publish(manifest_path, store_dir)` validates the manifest, finds or
|
||||
// creates the app, drafts a release, content-addresses every artifact into
|
||||
// `run_publish(manifest_path, store_dir)` validates the manifest, LOADS any
|
||||
// prior `<store>/db.json` so separate invocations share state (a new version
|
||||
// accumulates; a duplicate release id is rejected), finds or creates the app,
|
||||
// drafts a release, content-addresses every artifact into
|
||||
// `<store>/objects/<sha256>`, validates each stored file, commits the whole
|
||||
// aggregate through the integrity-checked repo transaction (channel
|
||||
// promotion included), records an audit event per upload / publish /
|
||||
// promotion, persists `<store>/db.json`, and returns a `PublishOutcome` the
|
||||
// CLI renders as stable JSON or a human summary.
|
||||
// promotion, persists the merged `<store>/db.json`, and returns a
|
||||
// `PublishOutcome` the CLI renders as stable JSON or a human summary.
|
||||
//
|
||||
// DECLARED-vs-DERIVED EXPECTATIONS (PO ruling): a manifest artifact may
|
||||
// DECLARE `size` / `sha256`; when it does, that value is the expectation the
|
||||
@@ -59,7 +61,7 @@ c_getcwd :: (buf: [*]u8, size: usize) -> *u8 #foreign cstd "getcwd";
|
||||
// Store — an artifact's bytes could not be content-addressed.
|
||||
// Validation — a stored artifact failed the common validation pass.
|
||||
// Transaction — the repo's integrity-checked publish rejected the aggregate.
|
||||
// Persist — db.json could not be written.
|
||||
// Persist — db.json could not be loaded at startup or written at the end.
|
||||
PublishError :: error {
|
||||
Manifest,
|
||||
Store,
|
||||
@@ -135,7 +137,20 @@ run_publish :: (manifest_path: string, store_dir: string) -> (PublishOutcome, !P
|
||||
abs := abs_store(store_dir);
|
||||
now := now_secs();
|
||||
|
||||
// Seed the Repo from any prior state so separate CLI invocations SHARE
|
||||
// state through the store: a pre-existing `<store>/db.json` is loaded so
|
||||
// find-or-create sees earlier apps and the integrity transaction sees
|
||||
// earlier releases. A new version then ACCUMULATES (the app is found, not
|
||||
// duplicated); re-publishing the SAME release id is rejected as a
|
||||
// duplicate by the transaction. An absent db.json starts empty. The loaded
|
||||
// model grows through its own owning allocator (`context.allocator`, the
|
||||
// process-lifetime default), per the long-lived-container rule.
|
||||
repo := Repo.init();
|
||||
if exists(path_join(store_dir, "db.json")) {
|
||||
loaded, le := db.load(store_dir);
|
||||
if le { raise error.Persist; }
|
||||
repo = loaded;
|
||||
}
|
||||
st := Store.init(store_dir);
|
||||
|
||||
// 2. Find or create the app (keyed by slug).
|
||||
@@ -150,7 +165,8 @@ run_publish :: (manifest_path: string, store_dir: string) -> (PublishOutcome, !P
|
||||
created_at = now, updated_at = now,
|
||||
});
|
||||
} else {
|
||||
app_id = existing!.id;
|
||||
found := existing!;
|
||||
app_id = found.id;
|
||||
}
|
||||
|
||||
// 3. Draft the release for this version/channel.
|
||||
|
||||
@@ -226,17 +226,24 @@ save :: (self: *Repo, root_dir: string) -> !LoadErr {
|
||||
}
|
||||
|
||||
// ── read-back helpers (strict; copy strings into `alloc`) ────────────
|
||||
// These carry a `db_` prefix because the `dist` program links this module
|
||||
// alongside `manifest.sx`, which declares its own same-purpose `dup_str` /
|
||||
// `obj_find` / `req_obj` / `req_arr` / `artifact_from_json`. sx resolves a
|
||||
// bare top-level name across the WHOLE program (see the `jsonp` note above),
|
||||
// so an unprefixed name here would bind to manifest's version and mismatch
|
||||
// this module's `LoadErr` error set. The prefix keeps the load path bound to
|
||||
// its own helpers.
|
||||
|
||||
// Copy `s` into `alloc`-owned, null-terminated storage so it survives the
|
||||
// parse scratch / source buffer being freed.
|
||||
dup_str :: (s: string, alloc: Allocator) -> string {
|
||||
db_dup_str :: (s: string, alloc: Allocator) -> string {
|
||||
raw : [*]u8 = xx alloc.alloc(s.len + 1);
|
||||
if s.len > 0 { memcpy(raw, s.ptr, s.len); }
|
||||
raw[s.len] = 0;
|
||||
return string.{ ptr = raw, len = s.len };
|
||||
}
|
||||
|
||||
obj_find :: (o: Object, key: string) -> ?Value {
|
||||
db_obj_find :: (o: Object, key: string) -> ?Value {
|
||||
i := 0;
|
||||
while i < o.len {
|
||||
if o.items[i].key == key { return o.items[i].val; }
|
||||
@@ -247,17 +254,17 @@ obj_find :: (o: Object, key: string) -> ?Value {
|
||||
|
||||
// Required string field, copied into `alloc`.
|
||||
req_str :: (o: Object, key: string, alloc: Allocator) -> (string, !LoadErr) {
|
||||
v := obj_find(o, key);
|
||||
v := db_obj_find(o, key);
|
||||
if v == null { raise error.BadShape; }
|
||||
val := v!;
|
||||
if val != .str { raise error.BadShape; }
|
||||
return dup_str(val.str, alloc);
|
||||
return db_dup_str(val.str, alloc);
|
||||
}
|
||||
|
||||
// Required string field as a borrowed VIEW (for enum-name parsing only;
|
||||
// not stored, so no copy needed).
|
||||
req_str_view :: (o: Object, key: string) -> (string, !LoadErr) {
|
||||
v := obj_find(o, key);
|
||||
v := db_obj_find(o, key);
|
||||
if v == null { raise error.BadShape; }
|
||||
val := v!;
|
||||
if val != .str { raise error.BadShape; }
|
||||
@@ -265,22 +272,22 @@ req_str_view :: (o: Object, key: string) -> (string, !LoadErr) {
|
||||
}
|
||||
|
||||
req_int :: (o: Object, key: string) -> (s64, !LoadErr) {
|
||||
v := obj_find(o, key);
|
||||
v := db_obj_find(o, key);
|
||||
if v == null { raise error.BadShape; }
|
||||
val := v!;
|
||||
if val != .int_ { raise error.BadShape; }
|
||||
return val.int_;
|
||||
}
|
||||
|
||||
req_arr :: (o: Object, key: string) -> (Array, !LoadErr) {
|
||||
v := obj_find(o, key);
|
||||
db_req_arr :: (o: Object, key: string) -> (Array, !LoadErr) {
|
||||
v := db_obj_find(o, key);
|
||||
if v == null { raise error.BadShape; }
|
||||
val := v!;
|
||||
if val != .array { raise error.BadShape; }
|
||||
return val.array;
|
||||
}
|
||||
|
||||
req_obj :: (v: Value) -> (Object, !LoadErr) {
|
||||
db_req_obj :: (v: Value) -> (Object, !LoadErr) {
|
||||
if v != .object { raise error.BadShape; }
|
||||
return v.object;
|
||||
}
|
||||
@@ -291,10 +298,10 @@ app_from_json :: (o: Object, alloc: Allocator) -> (App, !LoadErr) {
|
||||
a.id = try req_str(o, "id", alloc);
|
||||
a.slug = try req_str(o, "slug", alloc);
|
||||
a.display_name = try req_str(o, "display_name", alloc);
|
||||
bids := try req_arr(o, "bundle_ids");
|
||||
bids := try db_req_arr(o, "bundle_ids");
|
||||
i := 0;
|
||||
while i < bids.len {
|
||||
bo := try req_obj(bids.items[i]);
|
||||
bo := try db_req_obj(bids.items[i]);
|
||||
p := try platform_from(try req_str_view(bo, "platform"));
|
||||
bid : BundleId = .{ platform = p, value = try req_str(bo, "value", alloc) };
|
||||
a.bundle_ids.append(bid, alloc);
|
||||
@@ -321,7 +328,7 @@ release_from_json :: (o: Object, alloc: Allocator) -> (Release, !LoadErr) {
|
||||
return r;
|
||||
}
|
||||
|
||||
artifact_from_json :: (o: Object, alloc: Allocator) -> (Artifact, !LoadErr) {
|
||||
db_artifact_from_json :: (o: Object, alloc: Allocator) -> (Artifact, !LoadErr) {
|
||||
a : Artifact = .{};
|
||||
a.id = try req_str(o, "id", alloc);
|
||||
a.app_id = try req_str(o, "app_id", alloc);
|
||||
@@ -366,44 +373,44 @@ load_into :: (repo: *Repo, bytes: string, scratch: Allocator) -> !LoadErr {
|
||||
|
||||
root_val, pe := jsonp.parse(bytes, scratch);
|
||||
if pe { raise error.Parse; }
|
||||
ro := try req_obj(root_val);
|
||||
ro := try db_req_obj(root_val);
|
||||
|
||||
apps_arr := try req_arr(ro, "apps");
|
||||
apps_arr := try db_req_arr(ro, "apps");
|
||||
i := 0;
|
||||
while i < apps_arr.len {
|
||||
ao := try req_obj(apps_arr.items[i]);
|
||||
ao := try db_req_obj(apps_arr.items[i]);
|
||||
repo.create_app(try app_from_json(ao, oa));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
rel_arr := try req_arr(ro, "releases");
|
||||
rel_arr := try db_req_arr(ro, "releases");
|
||||
i = 0;
|
||||
while i < rel_arr.len {
|
||||
o := try req_obj(rel_arr.items[i]);
|
||||
o := try db_req_obj(rel_arr.items[i]);
|
||||
repo.create_release(try release_from_json(o, oa));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
art_arr := try req_arr(ro, "artifacts");
|
||||
art_arr := try db_req_arr(ro, "artifacts");
|
||||
i = 0;
|
||||
while i < art_arr.len {
|
||||
o := try req_obj(art_arr.items[i]);
|
||||
repo.create_artifact(try artifact_from_json(o, oa));
|
||||
o := try db_req_obj(art_arr.items[i]);
|
||||
repo.create_artifact(try db_artifact_from_json(o, oa));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
chan_arr := try req_arr(ro, "channels");
|
||||
chan_arr := try db_req_arr(ro, "channels");
|
||||
i = 0;
|
||||
while i < chan_arr.len {
|
||||
o := try req_obj(chan_arr.items[i]);
|
||||
o := try db_req_obj(chan_arr.items[i]);
|
||||
repo.create_channel(try channel_from_json(o, oa));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
ev_arr := try req_arr(ro, "audit_events");
|
||||
ev_arr := try db_req_arr(ro, "audit_events");
|
||||
i = 0;
|
||||
while i < ev_arr.len {
|
||||
o := try req_obj(ev_arr.items[i]);
|
||||
o := try db_req_obj(ev_arr.items[i]);
|
||||
repo.create_audit_event(try audit_from_json(o, oa));
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user