IX16: the catalog graph revisions as objects (the research 085 half joined), an entry carrying its manifest whole and the digest of its bytes, a full store holding the take-in's merges

Builds: design 47 G §173, §174, §175, §176, §177, §178, §179, §180, §181, §194, §200, §209, §210, §263, §443, §506

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 13:58:09 +01:00
parent 3442b7af37
commit f30c54faa1
8 changed files with 78 additions and 21 deletions
@@ -1099,7 +1099,7 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
Manifest: digest, None2: "the manifest cannot be read: claims node-build-go, which is a seat in the mesh's own namespace that it does not define",
Inputs2: &meshcatalog.ModulesByModuleValueInputs2{Tree: "tree-avahi", Locks: map[string]string{"go.sum": goSum}}}
rev, objects, err := index.CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 1, Kind: "trunk"},
map[string]meshcatalog.CatalogEntry{"avahi": index.CatalogEntryOf("avahi", stale, catalogue.Manifest{}, graph.Node{None: stale.None2})})
map[string]meshcatalog.CatalogEntry{"avahi": index.CatalogEntryOf("avahi", stale, catalogue.Manifest{}, "", graph.Node{None: stale.None2})})
if err != nil {
t.Fatal(err)
}
@@ -28,9 +28,9 @@ const CatalogEntryLayout = 1
// its manifest's digest and verdict, the main package its process runs (MainOf), the package versions it builds against
// and what the indexer computes its next revision from (Inputs: the lead's ruling on milestone 47/G finding 1), so that
// ModuleValueOf gives the module's value back from the entry alone. The acting rules are the revision's to add.
func CatalogEntryOf(module string, cur meshcatalog.ModulesByModuleValue, m catalogue.Manifest, n graph.Node) meshcatalog.CatalogEntry {
func CatalogEntryOf(module string, cur meshcatalog.ModulesByModuleValue, m catalogue.Manifest, raw string, n graph.Node) meshcatalog.CatalogEntry {
e := meshcatalog.CatalogEntry{V: CatalogEntryLayout, Module: module, Repository: cur.Repository, Path: cur.Path,
Commit: cur.Commit, Manifest: cur.Manifest, Verdict: cur.Verdict, Fp: n.FP, None: n.None, Bases: n.Bases,
Commit: cur.Commit, Manifest: manifestWhole(raw), ManifestDigest: cur.Manifest, Verdict: cur.Verdict, Fp: n.FP, None: n.None, Bases: n.Bases,
Packages: maps.Clone(cur.Packages), Provides: n.Provides, Requires: n.Requires, Awaits: n.Awaits, Invokes: n.Invokes,
Serves: n.Serves, LeftOut: n.LeftOut, Main: MainOf(m)}
if n.FP == "" && n.None == "" {
@@ -60,7 +60,7 @@ func CatalogEntryOf(module string, cur meshcatalog.ModulesByModuleValue, m catal
// ruling on milestone 47/G finding 1; ADR 0656 §6: one form of the fact). The old line's fields (the old-form
// fingerprint, the reader needs) are not kept.
func ModuleValueOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue {
v := meshcatalog.ModulesByModuleValue{V: keys.Version, Repository: e.Repository, Path: e.Path, Commit: e.Commit, Manifest: e.Manifest,
v := meshcatalog.ModulesByModuleValue{V: keys.Version, Repository: e.Repository, Path: e.Path, Commit: e.Commit, Manifest: e.ManifestDigest,
Verdict: e.Verdict, Fp2: e.Fp, None2: e.None, Packages: maps.Clone(e.Packages)}
if in := e.Inputs; in != nil {
v.Toolchains, v.Paths, v.Problems, v.Warnings, v.Unknown = maps.Clone(in.Toolchains), maps.Clone(in.Paths), in.Problems, in.Warnings, in.Unknown
@@ -139,3 +139,16 @@ func MainOf(m catalogue.Manifest) string {
}
return ""
}
// manifestWhole is the validated manifest the entry carries whole (design 47 G §443): the bytes the forge answered,
// read as the object they are; an entry of a module whose manifest is not kept carries an empty object.
func manifestWhole(raw string) map[string]any {
whole := map[string]any{}
if raw == "" {
return whole
}
if err := json.Unmarshal([]byte(raw), &whole); err != nil {
return map[string]any{}
}
return whole
}
@@ -0,0 +1,25 @@
package index
import (
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Design 47 G §506, §443: an entry carries the manifest whole and the digest of the bytes the forge answered, set at take-in
// and never computed again from the manifest whole; the module's value read back from the entry holds that digest.
func TestAnEntryCarriesItsManifestWholeAndTheDigestOfItsBytes(t *testing.T) {
raw := `{ "module":"avahi", "version":"1" }` // its encoding is not the one a map gives back
digest := meshcatalogclient.ManifestDigest([]byte(raw))
cur := meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Path: "modules/avahi", Commit: "c", Manifest: digest}
e := CatalogEntryOf("avahi", cur, catalogue.Manifest{}, raw, graph.Node{})
if e.ManifestDigest != digest || e.Manifest["module"] != "avahi" {
t.Fatalf("entry digest %q manifest %v, want the digest %s of the bytes and the manifest whole", e.ManifestDigest, e.Manifest, digest)
}
if back := ModuleValueOf(e); back.Manifest != digest {
t.Errorf("the module's value read back holds %q, want the entry's digest %s", back.Manifest, digest)
}
}
@@ -189,11 +189,11 @@ func (ix *Indexer) keepTrunkRevision(ctx context.Context, edges map[string]graph
// Every module the view holds is an entry, one the recompute computed no edges for (a manifest it cannot read) too.
entries := make(map[string]meshcatalog.CatalogEntry, len(held))
for name, cur := range held {
entries[name] = CatalogEntryOf(name, cur, edges[name].manifest, whole[name])
entries[name] = CatalogEntryOf(name, cur, edges[name].manifest, ix.manifestBytes(ctx, cur.Manifest), whole[name])
}
for name, e := range edges {
if _, ok := held[name]; !ok {
entries[name] = CatalogEntryOf(name, e.cur, e.manifest, whole[name])
entries[name] = CatalogEntryOf(name, e.cur, e.manifest, ix.manifestBytes(ctx, e.cur.Manifest), whole[name])
}
}
return ix.keepEntries(ctx, entries)
@@ -229,12 +229,13 @@ func (ix *Indexer) KeepView(ctx context.Context) error {
entries := make(map[string]meshcatalog.CatalogEntry, len(held))
for name, cur := range held {
var m catalogue.Manifest
var raw string
if cur.Manifest != "" {
if _, read, err := ix.manifestRaw(ctx, cur.Manifest); err == nil {
m = read
if bytes, read, err := ix.manifestRaw(ctx, cur.Manifest); err == nil {
m, raw = read, bytes
}
}
entries[name] = CatalogEntryOf(name, cur, m, nodes[name])
entries[name] = CatalogEntryOf(name, cur, m, raw, nodes[name])
}
return ix.keepEntries(ctx, entries)
}
@@ -481,3 +482,15 @@ func (m markSource) Get(state string) (*views.Entry, bool, error) {
}
return &views.Entry{Value: data, Revision: uint64(at)}, true, nil
}
// manifestBytes is the manifest the module's value names by its digest, as the forge answered it; empty when it is not kept.
func (ix *Indexer) manifestBytes(ctx context.Context, digest string) string {
if digest == "" || ix.Manifests == nil {
return ""
}
raw, _, err := ix.manifestRaw(ctx, digest)
if err != nil {
return ""
}
return raw
}
@@ -1105,6 +1105,12 @@ func (ix *Indexer) takeInWholeBy(ctx context.Context, entries []PendingEntry, ow
return applied, fmt.Errorf("taking %s (%s#%d) in: %w", e.Key, e.Forge.Repository, e.Forge.Number, err)
}
}
// What the judge pass of the take-in carried (the verdicts) is kept with the view too; a store that is full refuses it and
// the take-in holds its merges as it holds them for the revision itself (design 47 G §181).
if err := ix.KeepView(ctx); errors.Is(err, ErrObjectsFull) {
ix.release(ctx, claimed)
return applied, err
}
for _, e := range claimed {
if err := ix.Pending.Delete(ctx, e.Key, e.Revision); err != nil {
ix.logf("take-in: %s.%s is taken in and not removed: %v", PendingState, e.Key, err)
@@ -866,18 +866,18 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
}
list, err := tools[CatalogRevisionsTool].Run(map[string]any{})
l, _ := list.(RevisionsAnswer)
if err != nil || len(l.Revisions) != 2 || l.Revisions[0].Name != "candidate.g2.h2" || l.Revisions[1].Name != "trunk.1" || !l.Revisions[1].Current ||
if err != nil || len(l.Revisions) != 3 || l.Revisions[0].Name != "candidate.g2.h2" || l.Revisions[1].Name != "trunk.1" || l.Revisions[2].Name != "trunk.2" || !l.Revisions[2].Current ||
l.Fill.Cap != 2*size || l.Fill.Bytes != store.used() || len(l.Unlisted) != 0 {
t.Errorf("indexer_catalog_revisions = %+v (%v): want candidate.g2.h2 and the current trunk.1, the fill against its cap", l, err)
t.Errorf("indexer_catalog_revisions = %+v (%v): want candidate.g2.h2, trunk.1 of the view and the current trunk.2 of the merge, the fill against its cap", l, err)
}
if c, err := tools[CandidateGraphsTool].Run(map[string]any{}); err != nil || len(c.(RevisionsAnswer).Revisions) != 1 {
t.Errorf("indexer_candidate_graphs = %+v (%v): want g2's alone", c, err)
}
if one, err := tools[CatalogRevisionTool].Run(map[string]any{}); err != nil || one.(map[string]any)["name"] != "trunk.1" {
if one, err := tools[CatalogRevisionTool].Run(map[string]any{}); err != nil || one.(map[string]any)["name"] != "trunk.2" {
t.Errorf("indexer_catalog_revision = %v (%v): want the newest trunk revision whole", one, err)
}
if got, err := tools[RecomputeTool].Run(map[string]any{"why": "a check"}); err != nil || !strings.Contains(got.(*TakenIn).Answer, "equal to the kept revision 1") {
t.Errorf("indexer_recompute = %+v (%v): want equal to the kept revision 1", got, err)
if got, err := tools[RecomputeTool].Run(map[string]any{"why": "a check"}); err != nil || !strings.Contains(got.(*TakenIn).Answer, "equal to the kept revision 2") {
t.Errorf("indexer_recompute = %+v (%v): want equal to the kept revision 2", got, err)
}
})
t.Run("T7 a toolchain report evaluates pending again: the revision then names the running build and is taken in", func(t *testing.T) {
@@ -296,7 +296,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
entries := map[string]meshcatalog.CatalogEntry{}
for name := range mods.vals {
m, _ := moduleOf(t, mods, name)
entries[name] = CatalogEntryOf(name, m, catalogue.Manifest{}, graphOf(t, ix, name))
entries[name] = CatalogEntryOf(name, m, catalogue.Manifest{}, "", graphOf(t, ix, name))
}
// W3.1's main: the Go main package the module's process runs, read from its manifest (here the indexer's own); a
// manifest that runs no process names none.
@@ -307,7 +307,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
if m, err := catalogue.Decode(own); err != nil || MainOf(m) != "cmd/mesh-delivery-indexer" || MainOf(catalogue.Manifest{}) != "" {
t.Errorf("the indexer's main = %q (%v), want cmd/mesh-delivery-indexer", MainOf(m), err)
}
if e := entries["avahi"]; e.Fp != want["avahi"] || e.Commit == "" || e.Manifest == "" || !reflect.DeepEqual(e.Bases, []string{"mesh-tools"}) ||
if e := entries["avahi"]; e.Fp != want["avahi"] || e.Commit == "" || e.ManifestDigest == "" || !reflect.DeepEqual(e.Bases, []string{"mesh-tools"}) ||
e.Toolchains["go"] != (meshcatalog.BuildEdge{Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}) {
t.Errorf("the catalog entry of avahi = %+v", e)
}
@@ -373,7 +373,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
gk, _ := graph.Key(name)
rawNode, _, found, _ := nodes.Get(context.Background(), gk)
if back, err := graph.Decode(rawNode); !found || err != nil || !reflect.DeepEqual(NodeOf(e), back) || back.FP != e.Fp ||
!reflect.DeepEqual(back.Bases, e.Bases) || !reflect.DeepEqual(CatalogEntryOf(name, ModuleValueOf(e), catalogue.Manifest{}, back).Bases, e.Bases) {
!reflect.DeepEqual(back.Bases, e.Bases) || !reflect.DeepEqual(CatalogEntryOf(name, ModuleValueOf(e), catalogue.Manifest{}, "", back).Bases, e.Bases) {
t.Errorf("the view's node of %s = %+v (%v), want %+v", name, back, err, NodeOf(e))
}
}
@@ -483,7 +483,7 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
}
return json.Marshal(merged)
}
heads := meshdelivery.SourceByMainValue{Heads: map[string]string{catalogueRepo: merged.Merge}}
heads := sourceOf(catalogueRepo, merged.Merge)
var firstLog []string
var logMu sync.Mutex
first.Logf = func(f string, a ...any) {
@@ -491,13 +491,13 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
firstLog = append(firstLog, fmt.Sprintf(f, a...))
logMu.Unlock()
}
err = first.ApplySource(context.Background(), 5, heads)
err = applyS(first, context.Background(), 5, heads)
close(applied)
first.catchUpWG.Wait()
if err != nil || len(firstSaid) != 0 || !strings.Contains(strings.Join(firstLog, "\n"), "the forge bridge does not answer") {
t.Fatalf("a catch-up whose message is not read said %+v (%v), log %q, want no revision and the bridge's silence said", firstSaid, err, firstLog)
}
err = first.ApplySource(context.Background(), 6, heads)
err = applyS(first, context.Background(), 6, heads)
first.catchUpWG.Wait()
if err != nil || len(firstSaid) != 1 || firstSaid[0].Revision != 1 ||
firstSaid[0].Entries != int64(len(builderModules(t))) || firstSaid[0].Source != 6 {
@@ -192,7 +192,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
index.PendingTool, index.TakeInPendingTool, index.DropPendingTool, index.TryCandidatesTool, index.DropTryCandidateTool, index.RecoverTriesTool, index.TakeInMergeTool, index.FingerprintsAgainTool, // issue 1966; 1585/T12, T13 (ADR 0633 §5)
// milestone 47/G W4.12 (design 47, the indexer's tools): the catalog graph revisions read and steered.
index.CatalogRevisionTool, index.CatalogRevisionsTool, index.CandidateGraphsTool, index.RecomputeTool, index.DropCandidateGraphTool)
if !reflect.DeepEqual(m.Tools, want) || len(want) != 45 || slices.Contains(m.Tools, "modules") {
if !reflect.DeepEqual(m.Tools, want) || len(want) != 44 || slices.Contains(m.Tools, "modules") {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
// Every read tool of the indexer a reader of this repository invokes is one it serves (the checker's seventh first