Give a module and a candidate the new-form fingerprint, the manifest digest and the fpset, in version 1 (issue 1357)

This commit is contained in:
2026-10-20 01:43:39 +00:00
parent 1bbf75ae19
commit 9b4c2ba591
2 changed files with 111 additions and 4 deletions
+4
View File
@@ -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
+107 -4
View File
@@ -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
// `<module>=<fp2>` or `<module>=none:<reason>` 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