diff --git a/cmd/mesh-control/build.go b/cmd/mesh-control/build.go index dc72584..008adee 100644 --- a/cmd/mesh-control/build.go +++ b/cmd/mesh-control/build.go @@ -72,6 +72,12 @@ func buildFrom(result link.BuildResult) inventory.Build { kept := inventory.Build{ ID: result.ID, Repository: result.Repository, Ref: result.Ref, Commit: result.Commit, On: result.On, Failed: result.Failed, + // **What the announcement carries, kept rather than discarded** (novox/hq 04-ISSUES/050). + // The catalogue turns the manifest into requires/provides edges and `against` into build + // edges, and it is not always listening when a build happens — on a fresh mesh it cannot + // be, for exactly the modules it needs most. Keeping them is what makes a replay able to + // rebuild the graph rather than a list of names. + Path: result.Path, Manifest: result.Manifest, Against: result.Against, } for _, made := range result.Made { kept.Made = append(kept.Made, inventory.Artifact{ diff --git a/internal/inventory/builds.go b/internal/inventory/builds.go index 9990993..ef07fdb 100644 --- a/internal/inventory/builds.go +++ b/internal/inventory/builds.go @@ -23,6 +23,18 @@ type Build struct { Commit string // On is the machine that did it. On string + // Path is where inside the repository the module lives (novox/hq ADR 0069). + Path string + // Manifest is the declaration the builder resolved, as it announced it. + // + // **Kept because the catalogue may not have been listening.** The announcement carries this + // and the catalogue turns it into the module's requires/provides edges. On a fresh mesh the + // modules built before the catalogue exists are exactly the ones it most needs, so the mesh + // has to be able to say afterwards what they declared (novox/hq 04-ISSUES/050). + Manifest []byte + // Against is every artifact this build stood on, as references rather than module names — + // what makes a build edge derived rather than declared (ADR 0009). + Against []string // Failed is the builder's own words, empty when it worked. Failed string Made []Artifact @@ -49,15 +61,21 @@ func (i *Inventory) RecordBuild(ctx context.Context, b Build) error { if err != nil { return err } + against, err := json.Marshal(b.Against) + if err != nil { + return err + } var module *string if b.Module != "" { module = &b.Module } _, err = i.store.Pool().Exec(ctx, - `insert into build (id, repository, ref, module, commit_hash, built_on, failed, made) - values ($1, $2, $3, $4, $5, $6, $7, $8) + `insert into build (id, repository, ref, module, commit_hash, built_on, failed, made, + source_path, manifest, built_against) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) on conflict (id) do nothing`, - b.ID, b.Repository, b.Ref, module, b.Commit, b.On, b.Failed, made) + b.ID, b.Repository, b.Ref, module, b.Commit, b.On, b.Failed, made, + b.Path, manifestOrNil(b.Manifest), against) return err } @@ -144,3 +162,15 @@ func (i *Inventory) Held(ctx context.Context) (map[string]string, error) { } return held, rows.Err() } + +// manifestOrNil keeps the difference between "declared nothing" and "predates this being kept". +// +// A build recorded before the mesh kept manifests has no manifest, and that is not the same as one +// whose manifest was empty. A replay can then say which it is holding instead of inventing an +// empty declaration for a module that certainly had one. +func manifestOrNil(raw []byte) any { + if len(raw) == 0 { + return nil + } + return raw +} diff --git a/internal/inventory/migrations/0022-a-build-keeps-what-it-was-told.sql b/internal/inventory/migrations/0022-a-build-keeps-what-it-was-told.sql new file mode 100644 index 0000000..e86f3ef --- /dev/null +++ b/internal/inventory/migrations/0022-a-build-keeps-what-it-was-told.sql @@ -0,0 +1,26 @@ +-- What a build result carried and the mesh threw away. +-- +-- novox/hq 04-ISSUES/050. The builder announces a build with the resolved manifest, the path +-- inside the repository, and every artifact it was built against. The control plane receives all +-- of it and kept none of it: `build` held the repository, the ref, the commit and what was made. +-- +-- That was survivable while the catalogue heard the same announcement directly. It stops being +-- survivable the moment the catalogue was not there to hear it — which on a fresh mesh is always, +-- and always for the same modules. The shared base, the store the catalogue itself runs on, and +-- the catalogue: each is necessarily built BEFORE the catalogue exists to hear about it, so the +-- graph's foundation is the part the graph never sees. +-- +-- Replaying those builds needs what they said, not a summary of it. Without the manifest there +-- are no requires/provides edges; without `against` there are no build edges, which are the ones +-- that answer "a base moved, what must be rebuilt". A replay carrying neither would restore the +-- module list and leave the question the catalogue exists for still wrong, while looking fixed. +-- +-- Empty and null-free, so every build recorded before this keeps exactly the meaning it had: a +-- row with no manifest is one that predates this, and a replay says so rather than inventing an +-- empty declaration. +-- +-- `built_against` rather than `built_on`: that column already exists and means the MACHINE that +-- did the build, which is a different fact about a different subject. +alter table build add column source_path text not null default ''; +alter table build add column manifest jsonb; +alter table build add column built_against jsonb;