D4 (sdk, checker): a compose request's manifest names its fingerprint beside its digest
composer.Manifest gains Fingerprint (wire {"digest","fingerprint"}) and Fill's get takes the Manifest. The checker fills it from a
pull's fingerprint, else from the trunk revision's entry of the module (repoModules keeps each entry's fingerprint).
Builds: design 47 G A-310
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -266,6 +266,7 @@ type repoModules struct {
|
||||
path map[string]string // by module: its directory in its repository
|
||||
named map[string]map[string][]string // by module: each artifact's named paths, as the indexer keeps them
|
||||
repoOf map[string]string // by module: its repository
|
||||
fp map[string]string // by module: its entry's fingerprint, whose graph entry holds the manifest
|
||||
commit map[string]string // by module: the commit its record was computed at (the commit the mesh runs)
|
||||
typed map[string]string // by module: its manifest digest
|
||||
parsed map[string]catalogue.Manifest // by manifest digest: the manifest as read, once (a digest's manifest never changes)
|
||||
@@ -311,6 +312,7 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
r.mu.Lock()
|
||||
if r.repoOf == nil {
|
||||
r.repoOf, r.typed, r.langs, r.commit = map[string]string{}, map[string]string{}, map[string]map[string]bool{}, map[string]string{}
|
||||
r.fp = map[string]string{}
|
||||
r.tsPkgs, r.path, r.named = map[string][]string{}, map[string]string{}, map[string]map[string][]string{}
|
||||
}
|
||||
if deleted {
|
||||
@@ -319,6 +321,7 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
}
|
||||
delete(r.repoOf, key)
|
||||
delete(r.typed, key)
|
||||
delete(r.fp, key)
|
||||
delete(r.commit, key)
|
||||
delete(r.path, key)
|
||||
delete(r.named, key)
|
||||
@@ -336,6 +339,7 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
|
||||
r.mu.Lock()
|
||||
r.repoOf[key] = m.Repository
|
||||
r.typed[key] = m.Manifest
|
||||
r.fp[key] = m.Fp2
|
||||
r.commit[key] = m.Commit
|
||||
r.path[key] = m.Path
|
||||
r.named[key] = m.Paths
|
||||
@@ -575,6 +579,20 @@ func (r *repoModules) ManifestDigests() map[string]string {
|
||||
return out
|
||||
}
|
||||
|
||||
// ManifestFingerprints is the fingerprint of every module the indexer holds, by module, whose catalog graph entry holds its manifest
|
||||
// (design 47 G A-310).
|
||||
func (r *repoModules) ManifestFingerprints() map[string]string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := map[string]string{}
|
||||
for module, fp := range r.fp {
|
||||
if fp != "" {
|
||||
out[module] = fp
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CommitOf is the commit the indexer's record of the module names: the commit its fingerprint first appeared at, so the source
|
||||
// of the build the mesh runs (a later merge that leaves the module alone writes no record, the operator 2026-10-25).
|
||||
func (r *repoModules) CommitOf(module string) string {
|
||||
|
||||
@@ -174,14 +174,15 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s
|
||||
}
|
||||
manifests := map[string]composer.Manifest{}
|
||||
if e.Manifests != nil {
|
||||
fps := e.Manifests.ManifestFingerprints()
|
||||
for name, digest := range e.Manifests.ManifestDigests() {
|
||||
manifests[name] = composer.Manifest{Digest: digest}
|
||||
manifests[name] = composer.Manifest{Digest: digest, Fingerprint: fps[name]}
|
||||
}
|
||||
}
|
||||
for _, p := range c.Pulls {
|
||||
for name, m := range p.Fps {
|
||||
if m.Manifest != "" {
|
||||
manifests[name] = composer.Manifest{Digest: m.Manifest}
|
||||
manifests[name] = composer.Manifest{Digest: m.Manifest, Fingerprint: m.Fp2}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,15 @@ type digests map[string]string
|
||||
|
||||
func (d digests) ManifestDigests() map[string]string { return d }
|
||||
|
||||
// ManifestFingerprints: the trunk entry of each module is src2:<module> in a test.
|
||||
func (d digests) ManifestFingerprints() map[string]string {
|
||||
out := map[string]string{}
|
||||
for m := range d {
|
||||
out[m] = "src2:" + m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ADR 0593 §7, §9 (D12, G1, G2): the checker asks every placed node's engine in parallel and makes the composition from
|
||||
// their answers: a refusal fails it naming the node and each module; a node that does not answer within the bound,
|
||||
// answers unready or cannot be asked is named "not composed on that node: no answer" and the check passes with that
|
||||
|
||||
@@ -1270,6 +1270,14 @@ type fixedDigests map[string]string
|
||||
|
||||
func (f fixedDigests) ManifestDigests() map[string]string { return f }
|
||||
|
||||
func (f fixedDigests) ManifestFingerprints() map[string]string {
|
||||
out := map[string]string{}
|
||||
for m := range f {
|
||||
out[m] = "src2:" + m
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ADR 0578 §2: a change's manifests are judged by the indexer's judge, built at mesh-delivery's trunk and run as the
|
||||
// composer is (a compose ask of its command); a refusal fails the check naming the manifest and each problem; a group
|
||||
// whose mesh-delivery member changes the judge is judged by that member's head.
|
||||
|
||||
@@ -34,7 +34,7 @@ func (st *state) gatherGraph(ctx context.Context) {
|
||||
if st.j.Manifests == nil || st.j.Read == nil || st.narrowTry() {
|
||||
return
|
||||
}
|
||||
req, touched := judgeRequest(st.j.Manifests.ManifestDigests(), st.c.Pulls)
|
||||
req, touched := judgeRequest(st.j.Manifests.ManifestDigests(), st.j.Manifests.ManifestFingerprints(), st.c.Pulls)
|
||||
if len(touched) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,6 +63,9 @@ func changesJudge(repository string, merged []Fetched) bool {
|
||||
// TrunkManifests says the manifest digest of every module the indexer holds (its modules state), by module.
|
||||
type TrunkManifests interface {
|
||||
ManifestDigests() map[string]string
|
||||
// ManifestFingerprints is the fingerprint of the entry of every such module in the trunk revision, by module, whose
|
||||
// catalog graph entry holds that manifest (design 47 G A-310).
|
||||
ManifestFingerprints() map[string]string
|
||||
}
|
||||
|
||||
// SeatList gives the seat definitions the manifests are judged against, whole, as raw definitions (a request's Seats): the
|
||||
@@ -81,12 +84,12 @@ type PredefinedList interface {
|
||||
// judgeRequest is the judge's request over the catalogue as the case leaves it: every trunk manifest by its digest, each
|
||||
// one a pull request touches replaced by the digest its candidate names; touched are the modules whose manifest the
|
||||
// change adds or changes (a digest the trunk does not hold for it), sorted (novox/hq ADR 0579 §2).
|
||||
func judgeRequest(trunk map[string]string, pulls []Pull) (req composer.Request, touched []string) {
|
||||
func judgeRequest(trunk, trunkFps map[string]string, pulls []Pull) (req composer.Request, touched []string) {
|
||||
manifests, before := map[string]composer.Manifest{}, map[string]composer.Manifest{}
|
||||
for module, digest := range trunk {
|
||||
if digest != "" {
|
||||
manifests[module] = composer.Manifest{Digest: digest}
|
||||
before[module] = composer.Manifest{Digest: digest}
|
||||
manifests[module] = composer.Manifest{Digest: digest, Fingerprint: trunkFps[module]}
|
||||
before[module] = composer.Manifest{Digest: digest, Fingerprint: trunkFps[module]}
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
@@ -95,7 +98,7 @@ func judgeRequest(trunk map[string]string, pulls []Pull) (req composer.Request,
|
||||
if cm.Manifest == "" {
|
||||
continue
|
||||
}
|
||||
manifests[module] = composer.Manifest{Digest: cm.Manifest}
|
||||
manifests[module] = composer.Manifest{Digest: cm.Manifest, Fingerprint: cm.Fp2}
|
||||
if trunk[module] != cm.Manifest {
|
||||
seen[module] = true
|
||||
}
|
||||
@@ -113,7 +116,7 @@ func (st *state) judgeManifests(ctx context.Context) {
|
||||
if st.j.Manifests == nil || st.narrowTry() {
|
||||
return
|
||||
}
|
||||
req, touched := judgeRequest(st.j.Manifests.ManifestDigests(), st.c.Pulls)
|
||||
req, touched := judgeRequest(st.j.Manifests.ManifestDigests(), st.j.Manifests.ManifestFingerprints(), st.c.Pulls)
|
||||
if len(touched) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer"
|
||||
)
|
||||
|
||||
// Design 47 G A-310: a manifest the judge's request names by its digest names the fingerprint whose catalog graph entry holds
|
||||
// it: a pull's own fingerprint for a module the pull changes, the trunk revision's entry for the rest.
|
||||
func TestTheJudgeRequestNamesEachManifestsFingerprint(t *testing.T) {
|
||||
pull := Pull{Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"changed": {Manifest: "d2", Fp2: "src2:pull"}}}
|
||||
req, _ := judgeRequest(map[string]string{"changed": "d1", "same": "d3"}, map[string]string{"changed": "src2:trunk", "same": "src2:same"}, []Pull{pull})
|
||||
got, trunk := req.Cases[0].Manifests, req.Cases[1].Manifests
|
||||
if !reflect.DeepEqual(got["changed"], composer.Manifest{Digest: "d2", Fingerprint: "src2:pull"}) || !reflect.DeepEqual(got["same"], composer.Manifest{Digest: "d3", Fingerprint: "src2:same"}) {
|
||||
t.Errorf("the change's manifests = %+v", got)
|
||||
}
|
||||
if !reflect.DeepEqual(trunk["changed"], composer.Manifest{Digest: "d1", Fingerprint: "src2:trunk"}) {
|
||||
t.Errorf("the trunk's manifest = %+v, want the trunk entry's fingerprint", trunk["changed"])
|
||||
}
|
||||
}
|
||||
@@ -88,31 +88,44 @@ type Case struct {
|
||||
}
|
||||
|
||||
// Manifest is a module's manifest as its repository holds it (Raw, the bytes), or the digest the delivery indexer keeps
|
||||
// it under (Digest), which Fill replaces by the bytes. On the wire it is the manifest itself, or {"digest": "..."}.
|
||||
// it under (Digest) with the fingerprint whose catalog graph entry holds it (Fingerprint), which Fill replaces by the bytes.
|
||||
// On the wire it is the manifest itself, or {"digest": "...", "fingerprint": "..."} (design 47 G A-310).
|
||||
type Manifest struct {
|
||||
Raw json.RawMessage
|
||||
Digest string
|
||||
Raw json.RawMessage
|
||||
Digest string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// MarshalJSON writes the raw manifest, or {"digest": ...} when only the digest is known.
|
||||
// MarshalJSON writes the raw manifest, or {"digest": ..., "fingerprint": ...} when only the digest is known (the fingerprint
|
||||
// left out where none is named).
|
||||
func (m Manifest) MarshalJSON() ([]byte, error) {
|
||||
if len(m.Raw) > 0 {
|
||||
return m.Raw, nil
|
||||
}
|
||||
if m.Fingerprint != "" {
|
||||
return json.Marshal(map[string]string{"digest": m.Digest, "fingerprint": m.Fingerprint})
|
||||
}
|
||||
return json.Marshal(map[string]string{"digest": m.Digest})
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads {"digest": "..."} (an object with that one key) as a digest, and any other value as the raw
|
||||
// manifest.
|
||||
// UnmarshalJSON reads an object with the key "digest" and no key but "fingerprint" besides as a digest, and any other value as
|
||||
// the raw manifest.
|
||||
func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
var probe map[string]json.RawMessage
|
||||
if json.Unmarshal(raw, &probe) == nil && len(probe) == 1 {
|
||||
if d, ok := probe["digest"]; ok {
|
||||
var digest string
|
||||
if json.Unmarshal(raw, &probe) == nil && len(probe) >= 1 && len(probe) <= 2 {
|
||||
d, hasDigest := probe["digest"]
|
||||
f, hasFp := probe["fingerprint"]
|
||||
if hasDigest && len(probe) == 1+btoi(hasFp) {
|
||||
var digest, fp string
|
||||
if err := json.Unmarshal(d, &digest); err != nil {
|
||||
return fmt.Errorf("a manifest's digest is not a string: %w", err)
|
||||
}
|
||||
*m = Manifest{Digest: digest}
|
||||
if hasFp {
|
||||
if err := json.Unmarshal(f, &fp); err != nil {
|
||||
return fmt.Errorf("a manifest's fingerprint is not a string: %w", err)
|
||||
}
|
||||
}
|
||||
*m = Manifest{Digest: digest, Fingerprint: fp}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -120,6 +133,13 @@ func (m *Manifest) UnmarshalJSON(raw []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func btoi(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Verdict is what one run answers: the mesh-sdk the command was built with, and each case's verdict in the order asked.
|
||||
type Verdict struct {
|
||||
V int `json:"v"`
|
||||
@@ -183,15 +203,15 @@ func Decode(data []byte) (Request, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Fill replaces every manifest named by its digest by the bytes get answers for that digest; a digest get cannot answer
|
||||
// stops it, named.
|
||||
func Fill(r *Request, get func(digest string) ([]byte, error)) error {
|
||||
// Fill replaces every manifest named by its digest by the bytes get answers for it (its digest and fingerprint); a manifest get
|
||||
// cannot answer stops it, named.
|
||||
func Fill(r *Request, get func(m Manifest) ([]byte, error)) error {
|
||||
fill := func(m map[string]Manifest) error {
|
||||
for name, man := range m {
|
||||
if len(man.Raw) > 0 {
|
||||
continue
|
||||
}
|
||||
raw, err := get(man.Digest)
|
||||
raw, err := get(man)
|
||||
if err != nil {
|
||||
return fmt.Errorf("composer: the manifest of %s (digest %s): %w", name, man.Digest, err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package composer
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -13,23 +14,24 @@ const demo = `{"module": "demo", "version": "1", "claims": [{"name": "node-files
|
||||
// place; a raw manifest goes as itself.
|
||||
func TestAManifestIsItsBytesOrItsDigest(t *testing.T) {
|
||||
r := Request{V: Version, Cases: []Case{{Name: "change", Manifests: map[string]Manifest{
|
||||
"demo": {Digest: "d1"}, "raw": {Raw: json.RawMessage(`{"module":"raw","version":"1"}`)}},
|
||||
"demo": {Digest: "d1", Fingerprint: "src2:f1"}, "raw": {Raw: json.RawMessage(`{"module":"raw","version":"1"}`)}},
|
||||
Kept: map[string]map[string]Manifest{"laptop": {"demo": {Digest: "d1"}}}}}}
|
||||
wire, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(wire), `"demo":{"digest":"d1"}`) || !strings.Contains(string(wire), `"raw":{"module":"raw","version":"1"}`) {
|
||||
if !strings.Contains(string(wire), `"demo":{"digest":"d1","fingerprint":"src2:f1"}`) || !strings.Contains(string(wire), `"raw":{"module":"raw","version":"1"}`) {
|
||||
t.Fatalf("on the wire: %s", wire)
|
||||
}
|
||||
back, err := Decode(wire)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asked := 0
|
||||
if err := Fill(&back, func(d string) ([]byte, error) {
|
||||
asked, fingerprints := 0, []string{}
|
||||
if err := Fill(&back, func(m Manifest) ([]byte, error) {
|
||||
asked++
|
||||
if d != "d1" {
|
||||
fingerprints = append(fingerprints, m.Fingerprint)
|
||||
if m.Digest != "d1" {
|
||||
return nil, errors.New("no such digest")
|
||||
}
|
||||
return []byte(demo), nil
|
||||
@@ -39,6 +41,10 @@ func TestAManifestIsItsBytesOrItsDigest(t *testing.T) {
|
||||
if asked != 2 || string(back.Cases[0].Manifests["demo"].Raw) != demo || string(back.Cases[0].Kept["laptop"]["demo"].Raw) != demo {
|
||||
t.Fatalf("filled %d times: %+v", asked, back.Cases[0])
|
||||
}
|
||||
// get is given the fingerprint the request names beside the digest, and none where it names none (design 47 G A-310).
|
||||
if slices.Sort(fingerprints); !slices.Equal(fingerprints, []string{"", "src2:f1"}) {
|
||||
t.Errorf("get was given the fingerprints %q, want one named and one not", fingerprints)
|
||||
}
|
||||
// The known packages: none is no list (not on the wire), an empty list is a list, and a list reads back whole.
|
||||
for _, known := range [][]string{nil, {}, {"git.novox.be/novox/mesh-sdk", "@novox/mesh-sdk"}} {
|
||||
wire, err := json.Marshal(Request{V: Version, Packages: known})
|
||||
@@ -52,7 +58,7 @@ func TestAManifestIsItsBytesOrItsDigest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
bad := Request{V: Version, Cases: []Case{{Name: "c", Manifests: map[string]Manifest{"x": {Digest: "nope"}}}}}
|
||||
if err := Fill(&bad, func(string) ([]byte, error) { return nil, errors.New("not there") }); err == nil || !strings.Contains(err.Error(), "nope") {
|
||||
if err := Fill(&bad, func(Manifest) ([]byte, error) { return nil, errors.New("not there") }); err == nil || !strings.Contains(err.Error(), "nope") {
|
||||
t.Fatalf("a digest the indexer does not hold: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user