From 9b4c2ba5916e7dee39bd98d846d1752372b127aa Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 01:43:39 +0000 Subject: [PATCH] Give a module and a candidate the new-form fingerprint, the manifest digest and the fpset, in version 1 (issue 1357) --- go/delivery/delivery.go | 4 ++ go/delivery/source.go | 111 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index ff6625e..848fbfa 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -30,6 +30,10 @@ // **The source fingerprint** (issue 1203; ADR 0437 §5): one function of named inputs that the controller's builder and // the indexer both call, so they agree byte for byte (fingerprint.go). // +// **The release 4 layouts** (issues 1356 and 1357; ADR 0453 clauses 6 and 16): the new fingerprint form (fingerprint2.go), +// and the indexer's manifests by digest and the new fields of its modules and candidates (manifests.go, source.go). A +// manifests value is the one exception to the size above: a manifest is up to about 21 KB, whole, byte for byte. +// // Standard library only, and this module's own asks package. package delivery diff --git a/go/delivery/source.go b/go/delivery/source.go index 25bbe5e..31c1a9a 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -31,6 +31,10 @@ const ( ModulesBucket = IndexerModule + "_modules" // ModulesHistory is the revisions the modules bucket keeps per key. ModulesHistory = 20 + // ManifestsBucket is the indexer's bucket of manifests by digest (novox/hq ADR 0429 clause 16); see manifests.go. + ManifestsBucket = IndexerModule + "_manifests" + // ManifestsHistory is the revisions the manifests bucket keeps per key: a key is written once. + ManifestsHistory = 1 ) // ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names. @@ -245,19 +249,86 @@ func (s Source) check() error { // Candidate is the value of a candidates key: the group's pull requests and heads, the heads hash, the base (the // source revision it was previewed onto) and the modules each pull request touches. It carries no fingerprint -// until release 4 (ADR 0437 §5). +// until release 4 (ADR 0437 §5); from R4c the indexer writes the new form's fingerprint of each touched module in +// the pull request's Fps and, over all of them, FPSet (novox/hq ADR 0453 clauses 6 and 16). Both are absent in a +// value written before, and a reader of today's shape ignores them. type Candidate struct { V int `json:"v"` Group string `json:"group"` Base uint64 `json:"base"` HeadsHash string `json:"heads"` Pulls []CandidatePull `json:"pulls"` + FPSet string `json:"fpset,omitempty"` } -// CandidatePull is one pull request of a candidate and the modules it touches. +// CandidatePull is one pull request of a candidate and the modules it touches. Fps is, by module name, the new +// form's fingerprint of the module at the merge preview, or why it has none, and the digest of its manifest (the key +// of the indexer's manifests bucket); a module of Modules without an entry has none written yet. type CandidatePull struct { Head - Modules []string `json:"modules"` + Modules []string `json:"modules"` + Fps map[string]CandidateModule `json:"fps,omitempty"` +} + +// CandidateModule is one touched module's new-form fingerprint FP2 (`src2:` and 64 hex digits), or None2, the reason +// it has none; exactly one of them. Manifest is the digest of the module's manifest as read at the preview. +type CandidateModule struct { + FP2 string `json:"fp2,omitempty"` + None2 string `json:"none2,omitempty"` + Manifest string `json:"manifest,omitempty"` +} + +func (m CandidateModule) check() error { + switch { + case m.FP2 != "" && m.None2 != "": + return fmt.Errorf("delivery: a module has both fp2 and none2") + case m.FP2 == "" && m.None2 == "": + return fmt.Errorf("delivery: a module has neither fp2 nor none2") + } + return checkNewForm(m.FP2, m.Manifest) +} + +// checkNewForm refuses an fp2 that is not of the new form and a manifest digest that is not a digest, naming it. +func checkNewForm(fp2, manifest string) error { + if fp2 != "" { + if _, err := FingerprintKeyPart(fp2); err != nil { + return err + } + } + if manifest != "" { + if _, err := ManifestsKey(manifest); err != nil { + return err + } + } + return nil +} + +// FingerprintSet is the fpset of a candidate's pull requests: the first 12 hex digits of the sha256 over the lines +// `=` or `=none:` of every module with an entry in Fps, sorted and joined by a newline, +// so it is the same for every order and changes with any fingerprint or reason. A manifest digest does not enter: the +// fingerprint already covers the manifest. It is the empty string when no pull request carries a fingerprint. The +// checker keys its verdict by it (research 063 section 01). +func FingerprintSet(pulls []CandidatePull) string { + lines := map[string]bool{} + for _, p := range pulls { + for module, m := range p.Fps { + if m.FP2 != "" { + lines[module+"="+m.FP2] = true + } else { + lines[module+"=none:"+m.None2] = true + } + } + } + if len(lines) == 0 { + return "" + } + all := make([]string, 0, len(lines)) + for l := range lines { + all = append(all, l) + } + sort.Strings(all) + sum := sha256.Sum256([]byte(strings.Join(all, "\n"))) + return hex.EncodeToString(sum[:])[:12] } func (c Candidate) check() error { @@ -272,16 +343,46 @@ func (c Candidate) check() error { if c.HeadsHash != want { return fmt.Errorf("delivery: the candidate's heads hash %q is not the hash %q of its pull requests' heads", c.HeadsHash, want) } + seen := map[string]CandidateModule{} + for _, p := range c.Pulls { + touched := map[string]bool{} + for _, m := range p.Modules { + touched[m] = true + } + for module, m := range p.Fps { + if !touched[module] { + return fmt.Errorf("delivery: the candidate gives a fingerprint of the module %q, which pull request %s#%d does not touch", module, p.Repository, p.Number) + } + if err := m.check(); err != nil { + return fmt.Errorf("delivery: the candidate's module %q: %w", module, err) + } + if before, ok := seen[module]; ok && before != m { + return fmt.Errorf("delivery: two pull requests of the candidate give the module %q different fingerprints", module) + } + seen[module] = m + } + } + if wantSet := FingerprintSet(c.Pulls); c.FPSet != wantSet { + return fmt.Errorf("delivery: the candidate's fpset %q is not the hash %q of its fingerprints", c.FPSet, wantSet) + } return nil } // Module is the value of a modules key: the module's source fingerprint, or none and why (a build that resolves // packages when built gets none, said here), and where it was computed: Repository and Commit are the trunk merge, // Src the source revision then (0 before the gate writes source.main). Exactly one of FP and None is set. +// +// From R4c the indexer writes the new form beside it (novox/hq ADR 0453 clause 6): FP2, the fingerprint of the new +// form (`src2:` and 64 hex digits), or None2, why it has none; one of them whenever either is, never both; and +// Manifest, the digest of the module's manifest, the key of the manifests bucket. A value written before has none of +// the three, and a reader of today's shape ignores them. type Module struct { V int `json:"v"` FP string `json:"fp,omitempty"` None string `json:"none,omitempty"` + FP2 string `json:"fp2,omitempty"` + None2 string `json:"none2,omitempty"` + Manifest string `json:"manifest,omitempty"` Src uint64 `json:"src,omitempty"` Repository string `json:"repository"` Commit string `json:"commit"` @@ -295,8 +396,10 @@ func (m Module) check() error { return fmt.Errorf("delivery: a module has neither a fingerprint nor a reason for none") case m.FP != "" && !strings.HasPrefix(m.FP, SourcePrefix): return fmt.Errorf("delivery: the module's fingerprint %q does not start with %s", m.FP, SourcePrefix) + case m.FP2 != "" && m.None2 != "": + return fmt.Errorf("delivery: a module has both fp2 and none2") } - return nil + return checkNewForm(m.FP2, m.Manifest) } // Forge is a message of the forge bridge's stream, trimmed to repository, branch, pull request and head. A pull