From 5b342d32dc56a8bded506206258aa44d1ba8d12b Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 28 Sep 2026 18:56:29 +0200 Subject: [PATCH] Version constants: the specification and the module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - datekeys.SpecVersion ("0.8.2") names the specification the module implements. A test ties it to the spec file, its title and spec/README.md, and TestCatalogueMatchesSpec and the vector files use it (testkit.SpecVersion now aliases it), so the vectors regenerate unchanged. - datekeys.Version() is the version of the module as the go command recorded it. That is a tag, or for a binary built in a checkout the pseudo-version of its commit (for example v0.0.0-20260928105528-9ac9cd952f04), or (devel) when it is unknown, as in tests or under a replace directive to a directory. It works as the main module and as a dependency, whatever the module path, which it reads from the root package. - `datekeys version` (also -version and --version) prints both and the Go toolchain. - README.md and README.es.md explain the three versions (format, specification, module) and what the code on main covers. traceability §70 and CHANGELOG follow. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +++ README.es.md | 21 +++++++++++++ README.md | 21 +++++++++++++ cmd/datekeys/main.go | 12 ++++++++ cmd/datekeys/main_test.go | 21 ++++++++++++- errors_test.go | 2 +- internal/testkit/vectors.go | 5 ++-- version.go | 50 +++++++++++++++++++++++++++++++ version_test.go | 60 +++++++++++++++++++++++++++++++++++++ 9 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 version.go create mode 100644 version_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 457ef85..8cc0e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -329,6 +329,10 @@ during the request); `datekeys.TestCode`. ### Added +- `datekeys.SpecVersion` (`0.8.2`), the specification the module implements, + and `datekeys.Version()`, the version of the module as the go command + recorded it: a tag, the pseudo-version of the commit of a checkout build, + or `(devel)`. `datekeys version` prints both and the Go toolchain. - `capsule.OpenOptions.AccessKeyFile`, a `.dkk` still encoded, which `Open` decodes at step 9.a and only for a `time_and_key` capsule (§63, §69.1). - `ErrExtensionDataInvalid` (`ERR_EXTENSION_DATA_INVALID`, §69). diff --git a/README.es.md b/README.es.md index 7ab5a92..b98215d 100644 --- a/README.es.md +++ b/README.es.md @@ -40,6 +40,26 @@ No implementa, a propósito: el servidor y la cola de la Release API, el almacenamiento y la entrega, extensiones concretas ni el cliente TypeScript (plan §2). +## Versiones + +Hay tres números de versión, cada uno con su significado: + +| Versión | Dónde | Cambia cuando | +|---|---|---| +| Formato | Dentro de los objetos: la versión de framing de DKC1 y DKK1 y la versión de schema de la clave 1, hoy todas 1 | Cambia el formato. Un lector rechaza una versión que no conoce (spec §70) | +| Especificación | `datekeys.SpecVersion`, hoy `0.8.2`, y el tag `spec-v0.8.2` | Cambia el texto normativo. §76 del spec recoge cada cambio con su caso | +| Módulo | Los tags de este módulo Go, `vX.Y.Z`, y `datekeys.Version()` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de v1.0.0 | + +`datekeys version` imprime la versión del módulo, la del spec y la del toolchain de Go. Un binario compilado en un checkout muestra la pseudo-versión de su commit, por ejemplo `v0.0.0-20260928105528-9ac9cd952f04`. + +Cada release dice qué cubre, aquí y en el [CHANGELOG](CHANGELOG.md). El código de `main`, aún sin publicar, cubre: +- la especificación 0.8.2, con versiones de formato 1; +- el perfil Quicknet pinneado, y cualquier perfil de los tres schemes de drand que soporta tlock; +- cifrado, inspección y apertura, y la CLI; +- todos los vectores y fixtures compartidos de [`testdata/`](testdata). + +El primer tag, v0.1.0, llegará cuando `go get` funcione desde una máquina limpia. + ## Una cápsula, en un dibujo ```text @@ -69,6 +89,7 @@ datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk carta.dkk -i datekeys inspect -in carta.dkc datekeys decrypt -in carta.dkc -out carta.txt -dkk carta.dkk datekeys profile hash +datekeys version ``` `encrypt` nunca usa la red. `inspect` ejecuta solo las comprobaciones previas diff --git a/README.md b/README.md index 7b7b339..569ed20 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,26 @@ accepted just because age could unwrap a key. Not implemented on purpose: the Release API server and queue, storage and delivery, concrete extensions, and the TypeScript client (plan §2). +## Versions + +Three version numbers, each with its own meaning: + +| Version | Where | Changes when | +|---|---|---| +| Format | Inside the objects: the framing version of DKC1 and DKK1 and the schema version at key 1, all 1 today | The format changes. A reader rejects a version it does not know (spec §70) | +| Specification | `datekeys.SpecVersion`, today `0.8.2`, and the tag `spec-v0.8.2` | The normative text changes. Spec §76 records each change with its case | +| Module | The tags of this Go module, `vX.Y.Z`, and `datekeys.Version()` | The API or the behaviour changes. Semantic versioning, with no stability promise before v1.0.0 | + +`datekeys version` prints the module version, the specification and the Go toolchain. A binary built in a checkout shows the pseudo-version of its commit, for example `v0.0.0-20260928105528-9ac9cd952f04`. + +Each release states what it covers, here and in the [CHANGELOG](CHANGELOG.md). The code on `main`, not yet released, covers: +- specification 0.8.2, with format versions 1; +- the pinned Quicknet profile, and any profile on the three drand schemes that tlock supports; +- encryption, inspection and opening, and the CLI; +- every shared vector and fixture of [`testdata/`](testdata). + +The first tag, v0.1.0, comes once `go get` works from a clean machine. + ## A capsule, in one picture ```text @@ -68,6 +88,7 @@ datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk letter.dkk - datekeys inspect -in letter.dkc datekeys decrypt -in letter.dkc -out letter.txt -dkk letter.dkk datekeys profile hash +datekeys version ``` `encrypt` never touches the network. `inspect` runs only the pre-unlock checks diff --git a/cmd/datekeys/main.go b/cmd/datekeys/main.go index 3651c83..b324e3c 100644 --- a/cmd/datekeys/main.go +++ b/cmd/datekeys/main.go @@ -6,6 +6,7 @@ // datekeys decrypt -in secret.dkc -out secret.txt [-dkk key.dkk] [-identity key.txt] // datekeys datekey resolve -at 2030-01-01T00:00:00Z // datekeys profile hash +// datekeys version // // Encryption never touches the network. Decryption fetches the release from // public drand relays and verifies it locally. Outputs are written to a @@ -22,6 +23,7 @@ import ( "fmt" "io" "os" + "runtime" "strings" "time" @@ -42,6 +44,7 @@ const usage = `usage: datekeys inspect -in FILE.dkc [-json] datekeys datekey resolve -at TIME datekeys profile hash [-in PROFILE.cbor] + datekeys version TIME is RFC 3339 with a time zone, for example 2030-01-01T00:00:00Z.` @@ -94,6 +97,15 @@ func run(args []string, stdout, stderr io.Writer, now func() time.Time) error { return errUsage } return profileHash(args[2:], stdout) + case "version", "-version", "--version": + if len(args) != 1 { + return errUsage + } + // The module version (a tag, or the pseudo-version of the commit a + // checkout was built from), the specification it implements and the + // toolchain. + fmt.Fprintf(stdout, "datekeys %s\nspecification %s\n%s %s/%s\n", datekeys.Version(), datekeys.SpecVersion, runtime.Version(), runtime.GOOS, runtime.GOARCH) + return nil case "-h", "-help", "--help", "help": fmt.Fprintln(stdout, usage) return nil diff --git a/cmd/datekeys/main_test.go b/cmd/datekeys/main_test.go index 672fe82..e769908 100644 --- a/cmd/datekeys/main_test.go +++ b/cmd/datekeys/main_test.go @@ -11,6 +11,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -281,13 +282,31 @@ func TestResolveAndProfile(t *testing.T) { } func TestUsage(t *testing.T) { - for _, args := range [][]string{nil, {"nope"}, {"datekey"}, {"profile", "x"}, {"encrypt", "-bogus"}, {"inspect", "extra"}} { + for _, args := range [][]string{nil, {"nope"}, {"datekey"}, {"profile", "x"}, {"encrypt", "-bogus"}, {"inspect", "extra"}, {"version", "extra"}} { if _, _, err := cli(t, later, args...); err == nil { t.Errorf("%v accepted", args) } } } +// The version command names the module version, the specification and the +// toolchain; in a test the module version is unknown. +func TestVersion(t *testing.T) { + for _, arg := range []string{"version", "-version", "--version"} { + out, _, err := cli(t, later, arg) + if err != nil { + t.Fatal(err) + } + want := "datekeys " + datekeys.Version() + "\nspecification " + datekeys.SpecVersion + "\n" + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n" + if out != want { + t.Errorf("%s: %q, want %q", arg, out, want) + } + } + if !strings.Contains(usage, "datekeys version") { + t.Error("the usage does not list the version command") + } +} + // Spec §53: long horizons get the harvest-now, decrypt-later warning. func TestLongHorizonWarning(t *testing.T) { dir := t.TempDir() diff --git a/errors_test.go b/errors_test.go index 1a2cef9..a682129 100644 --- a/errors_test.go +++ b/errors_test.go @@ -12,7 +12,7 @@ import ( // The catalogue matches spec §69 exactly, in order. func TestCatalogueMatchesSpec(t *testing.T) { - spec, err := os.ReadFile("spec/DateKeys_Protocol_Specification_v0.8.2.md") + spec, err := os.ReadFile("spec/DateKeys_Protocol_Specification_v" + datekeys.SpecVersion + ".md") if err != nil { t.Fatal(err) } diff --git a/internal/testkit/vectors.go b/internal/testkit/vectors.go index b6b7820..61a9124 100644 --- a/internal/testkit/vectors.go +++ b/internal/testkit/vectors.go @@ -12,8 +12,9 @@ import ( "g.activething.com/go/DateKeys/profile" ) -// SpecVersion is the specification the vectors and fixtures implement. -const SpecVersion = "0.8.2" +// SpecVersion is the specification the vectors and fixtures implement: the +// one the module implements. +const SpecVersion = datekeys.SpecVersion // RoundVector is one Quicknet resolution vector (spec §65). type RoundVector struct { diff --git a/version.go b/version.go new file mode 100644 index 0000000..56765dc --- /dev/null +++ b/version.go @@ -0,0 +1,50 @@ +package datekeys + +import ( + "reflect" + "runtime/debug" +) + +// SpecVersion is the version of the DateKeys Protocol Specification that this +// module implements: spec/DateKeys_Protocol_Specification_v0.8.2.md, tagged +// spec-v0.8.2 in its repository. It is neither the version of the module +// (see Version) nor the framing and schema versions inside the objects, +// which are 1 (spec §70). +const SpecVersion = "0.8.2" + +// modulePath is the path of this module: the import path of its root +// package, whatever the module is called. +var modulePath = reflect.TypeFor[Error]().PkgPath() + +// Version returns the version of this module in the running program, as the +// go command recorded it: a release tag such as v0.1.0; a pseudo-version, +// which names the commit, for a program built in a checkout of the module; +// or "(devel)" when it is unknown, as in tests or under a replace directive +// that points to a directory. +func Version() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "(devel)" + } + return versionIn(info) +} + +func versionIn(info *debug.BuildInfo) string { + m := &info.Main + if m.Path != modulePath { + m = nil + for _, d := range info.Deps { + if d.Path == modulePath { + m = d + break + } + } + } + if m != nil && m.Replace != nil { + m = m.Replace + } + if m == nil || m.Version == "" { + return "(devel)" + } + return m.Version +} diff --git a/version_test.go b/version_test.go new file mode 100644 index 0000000..d37d51f --- /dev/null +++ b/version_test.go @@ -0,0 +1,60 @@ +package datekeys + +import ( + "os" + "runtime/debug" + "strings" + "testing" +) + +// SpecVersion names the specification of this repository: its file, its +// title and its record in spec/README.md. +func TestSpecVersionNamesTheSpecification(t *testing.T) { + spec, err := os.ReadFile("spec/DateKeys_Protocol_Specification_v" + SpecVersion + ".md") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(spec), "### Borrador normativo v"+SpecVersion+"\n") { + t.Error("the specification is not titled v" + SpecVersion) + } + readme, err := os.ReadFile("spec/README.md") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(readme), "draft v"+SpecVersion+" (") { + t.Error("spec/README.md does not record v" + SpecVersion) + } +} + +func TestModulePath(t *testing.T) { + if modulePath != "g.activething.com/go/DateKeys" { + t.Fatalf("module path %q", modulePath) + } +} + +func TestVersion(t *testing.T) { + if Version() == "" { + t.Fatal("empty version") + } + other := debug.Module{Path: "example.com/app", Version: "v1.2.3"} + for _, tc := range []struct { + name string + info debug.BuildInfo + want string + }{ + {"the main module, tagged", debug.BuildInfo{Main: debug.Module{Path: modulePath, Version: "v0.1.0"}}, "v0.1.0"}, + {"the main module, from a checkout", debug.BuildInfo{Main: debug.Module{Path: modulePath, Version: "v0.0.0-20260928125528-9ac9cd952f04"}}, + "v0.0.0-20260928125528-9ac9cd952f04"}, + {"the main module in a test", debug.BuildInfo{Main: debug.Module{Path: modulePath}}, "(devel)"}, + {"a dependency", debug.BuildInfo{Main: other, Deps: []*debug.Module{{Path: modulePath, Version: "v0.2.0"}}}, "v0.2.0"}, + {"replaced by another version", debug.BuildInfo{Main: other, Deps: []*debug.Module{ + {Path: modulePath, Version: "v0.2.0", Replace: &debug.Module{Path: modulePath, Version: "v0.2.1"}}}}, "v0.2.1"}, + {"replaced by a directory", debug.BuildInfo{Main: other, Deps: []*debug.Module{ + {Path: modulePath, Version: "v0.2.0", Replace: &debug.Module{Path: "../datekeys-go"}}}}, "(devel)"}, + {"not in the program", debug.BuildInfo{Main: other}, "(devel)"}, + } { + if got := versionIn(&tc.info); got != tc.want { + t.Errorf("%s: %q, want %q", tc.name, got, tc.want) + } + } +}