Files
mesh-sdk/go/delivery/fingerprint2.go
T
jschoubben a20bdf7952 Carry mesh package versions in build asks and fingerprints
ADR 0573 §2 has every module build against the latest published version of
each mesh package it uses, chosen by the planner and written by the build,
never pinned by hand (issue 1585, S4). The ask, the fetch request and the
merge preview now carry the packages and their versions, and the new-form
fingerprint takes each version, so a package's trunk merge changes every
dependent's fingerprint. A module using no package keeps its fingerprint.

Folded onto feat/judge-once by builder-1585a: Module keeps both Packages and the verdict fields.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 02:40:12 +01:00

127 lines
5.1 KiB
Go

package delivery
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
)
// SourcePrefix2 opens every source fingerprint of the new form.
const SourcePrefix2 = "src2:"
// NoPackageVersion opens the reason a module has no fingerprint while a mesh package it uses has no published version
// the indexer holds (novox/hq ADR 0573 §2); the packages follow, by module path.
const NoPackageVersion = "no published version of the package "
// Toolchain2 is what an artifact's toolchain enters the new fingerprint as (novox/hq ADR 0405 clause 7): the
// artifact's language, the installed toolchain's version and the build module's recipe version, in place of the
// toolchain image's digest. A toolchain update so changes the fingerprints of that language's modules only.
type Toolchain2 struct {
Language string
Version string
Recipe string
}
// SourceInputs2 are what a module's source fingerprint of the new form is of (novox/hq ADR 0429 clause 5 as ADR 0437
// clause 5 changed it; ADR 0453 clause 6). It differs from SourceInputs in exactly four things: a base enters by its
// module's fingerprint (src2:...), not its image digest; the builder enters by its own module's fingerprint, not its
// executable's digest; an artifact that resolves packages when it is built enters by the hash of its lock file; and
// an artifact's toolchain enters as Toolchain2. Module, Tree, Contexts and Unpinned are as in SourceInputs.
//
// Locks names the artifacts that resolve packages when built, each with the hash of its lock file; an entry with an
// empty hash is such an artifact with no lock, which has no fingerprint. An artifact that resolves nothing is not in
// Locks.
type SourceInputs2 struct {
Module string
Tree string
Bases []string
Builder string
Contexts map[string]string
Locks map[string]string
Toolchains map[string]Toolchain2
// Packages are the version of each mesh package the module uses, by package (novox/hq ADR 0573 §2): a package with
// no version gives no fingerprint. None leaves the fingerprint as it was before packages entered it.
Packages map[string]string
Unpinned string
}
// SourceFingerprint2 is the module's source fingerprint of the new form, `src2:` and the hex sha256 of its inputs'
// lines, or none and the reason when the source does not pin the build: Unpinned when set, "no tree" when Tree is
// empty, "no lock" when an artifact resolves packages and has no lock. It is a pure function of its inputs: the order
// of maps and a base given twice change nothing. The lines are "module <m>", "tree <t>" and "builder <fp>", then
// "base <fp>" (once each), "context <artifact> <tree>", "lock <artifact> <hash>", "toolchain <artifact> <language>
// <version> <recipe>" and "package <package> <version>", sorted, all joined by a newline. A module that uses no mesh
// package has no package line, so its fingerprint is the one it had before packages entered (novox/hq ADR 0573 §2).
func SourceFingerprint2(in SourceInputs2) (fp, none string) {
if in.Unpinned != "" {
return "", in.Unpinned
}
if in.Tree == "" {
return "", "no tree"
}
for _, h := range in.Locks {
if h == "" {
return "", "no lock"
}
}
var unpublished []string
for p, v := range in.Packages {
if v == "" {
unpublished = append(unpublished, p)
}
}
if len(unpublished) > 0 {
sort.Strings(unpublished)
return "", NoPackageVersion + strings.Join(unpublished, ", ")
}
lines := []string{"module " + in.Module, "tree " + in.Tree, "builder " + in.Builder}
bases := map[string]bool{}
for _, b := range in.Bases {
bases[b] = true
}
for b := range bases {
lines = append(lines, "base "+b)
}
for a, t := range in.Contexts {
lines = append(lines, "context "+a+" "+t)
}
for a, h := range in.Locks {
lines = append(lines, "lock "+a+" "+h)
}
for a, t := range in.Toolchains {
lines = append(lines, "toolchain "+a+" "+t.Language+" "+t.Version+" "+t.Recipe)
}
for p, v := range in.Packages {
lines = append(lines, "package "+p+" "+v)
}
sort.Strings(lines[3:])
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
return SourcePrefix2 + hex.EncodeToString(sum[:]), ""
}
// FingerprintKeyPart is a new-form fingerprint as a key part: its 64 hex digits without the prefix, since a bus key
// may not hold ':'. It refuses a value that is not `src2:` and 64 lowercase hex digits, a src1 fingerprint included,
// naming it.
func FingerprintKeyPart(fp string) (string, error) {
part, ok := strings.CutPrefix(fp, SourcePrefix2)
if !ok || !isHex64(part) {
return "", fmt.Errorf("delivery: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", fp, SourcePrefix2)
}
return part, nil
}
// ParseFingerprintKeyPart is the inverse of FingerprintKeyPart. It refuses a part that is not 64 lowercase hex
// digits, a src1 fingerprint or a prefixed value included, naming it.
func ParseFingerprintKeyPart(part string) (string, error) {
if !isHex64(part) {
return "", fmt.Errorf("delivery: %q is not the key part of a fingerprint, 64 lowercase hex digits", part)
}
return SourcePrefix2 + part, nil
}
func isHex64(s string) bool {
return len(s) == 64 && strings.Trim(s, "0123456789abcdef") == ""
}