diff --git a/docs/decisions/ADR-008-profile-c-domains.json b/docs/decisions/ADR-008-profile-c-domains.json new file mode 100644 index 000000000..7265c6733 --- /dev/null +++ b/docs/decisions/ADR-008-profile-c-domains.json @@ -0,0 +1 @@ +{"domains":[{"domain":"echidnabot-proof-obligation","name":"JCS (RFC 8785) of the JSON object {claim, context, prover, repo} of a proof obligation","purpose":"ProofObligationRecord.id in echidnabot: resubmitting the same obligation gives the same id","repos":["hyperpolymath/echidnabot"]},{"domain":"gv-clade-index","name":"github.com// of a repository","purpose":"clade identifiers (ADR-008 amendment D320)","repos":["hyperpolymath/gv-clade-index"]},{"domain":"proof-goal","name":"JCS (RFC 8785) of the goal text as a JSON string, exact text, not trimmed or normalised","purpose":"the cross-tool content address of a proof goal: one goal has one id in every tool that names it","repos":["hyperpolymath/echidnabot","hyperpolymath/proof-burrower"]},{"domain":"prove-result","name":"JCS (RFC 8785) of an ECHIDNA prove result as serialised by the consumer","purpose":"ProofReceipt.result_id in proof-burrower: identical prove results share an id","repos":["hyperpolymath/proof-burrower"]}],"schema":"estate-uuid-v8-domains/1"} diff --git a/docs/decisions/ADR-008-uuid-v8-estate-standard.adoc b/docs/decisions/ADR-008-uuid-v8-estate-standard.adoc index 18560f47b..59e0132b3 100644 --- a/docs/decisions/ADR-008-uuid-v8-estate-standard.adoc +++ b/docs/decisions/ADR-008-uuid-v8-estate-standard.adoc @@ -298,6 +298,46 @@ shared profile T and C generators are the next standards increment. The gv-clade-index derivation change follows it. Each repository then migrates under P3 as before; strict only means a repository is red until it does. +== Amendment: the profile C domain register (owner answer, 2026-10-14) + +Profile C requires a _registered_ domain, but there was no register. Asked +where domains are registered, the owner chose one register in this +repository over per-repository declarations in each chora deed. Per-repository +declarations can only check uniqueness within one repository. + +*The register* is `docs/decisions/ADR-008-profile-c-domains.json`. It is +I-JSON in JCS form (JSON-POLICY, D306), and each entry is +`{domain, name, purpose, repos}`: + +* `name` says in words what is hashed after `domain:`. +* `repos` lists every repository that mints ids in that domain. + +A domain shared by several repositories is a single registration. For +example, `proof-goal` is the cross-tool content address of a proof goal, +which echidnabot and proof-burrower compute the same way, so one goal has one +id in both. Within that one registration the domain is still used for one +purpose only, as ADR-008 requires. + +*The check* is `scripts/check-uuid-v8-domains.sh [--registry FILE] +[DOMAIN...]`. It validates the register's bytes (one line, printable ASCII, +JCS, one final LF), the entry shape, the domain rule and strict ordering, so +no domain is registered twice. Given DOMAIN arguments, it also fails on any +that is not registered. A repository that mints profile C ids runs it in CI +against a pinned standards checkout with the domains it uses. +`scripts/tests/check-uuid-v8-domains-test.sh` plants a defect for each rule and +expects each one to fail, for its own reason. + +*First entries.* The domains of the P3 pilot repositories are registered +from their current code: + +* `echidnabot-proof-obligation`; +* `proof-goal`, shared by echidnabot and proof-burrower; +* `prove-result`. + +`gv-clade-index` is registered as ruled in the D320 amendment. +echidna has no content-id call site yet, so it registers none. +`berrywiki-import` is left for berrywiki to register with its own name form. + == What this record does not do It does not migrate any repository (P3), change the deed grammar, or update diff --git a/scripts/check-uuid-v8-domains.sh b/scripts/check-uuid-v8-domains.sh new file mode 100755 index 000000000..eb4234464 --- /dev/null +++ b/scripts/check-uuid-v8-domains.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# check-uuid-v8-domains.sh — the ADR-008 profile C domain register. +# +# Validates docs/decisions/ADR-008-profile-c-domains.json and, given DOMAIN +# arguments, checks that each one is registered. A repository that mints +# profile C ids runs it in CI with the domains it uses, against a pinned +# standards checkout, so an unregistered domain fails there. +# +# Usage: check-uuid-v8-domains.sh [--registry FILE] [DOMAIN...] +# +# Exit contract: +# 0 = the register is valid, and every DOMAIN given is registered +# 1 = the register is invalid, or a DOMAIN is not registered (each is named) +# 2 = the check could not run (jq missing, register missing or unparseable) +# +# What "valid" means: +# * One line of printable ASCII and exactly one final LF. Within that shape +# (objects, arrays and strings only, ASCII keys) `jq -cS .` is the JCS +# (RFC 8785) form, so the bytes must equal it (JSON-POLICY, D306). +# * Top level is exactly {domains, schema}, schema "estate-uuid-v8-domains/1". +# * Every entry is exactly {domain, name, purpose, repos}: +# domain non-empty printable ASCII (0x21..0x7e) without ":" (ADR-008); +# name what is hashed after "domain:", in words; +# purpose what the ids identify; +# repos the owner/repo list that mints ids in this domain, sorted, +# unique. A domain shared by several repositories is one +# registration (for example a cross-tool content address). +# * Domains are strictly increasing, so no domain is registered twice. +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +REGISTRY="$ROOT/docs/decisions/ADR-008-profile-c-domains.json" +if [ "${1:-}" = "--registry" ]; then + [ $# -ge 2 ] || { echo "check-uuid-v8-domains: --registry needs a file" >&2; exit 2; } + REGISTRY="$2" + shift 2 +fi + +command -v jq >/dev/null 2>&1 || { echo "check-uuid-v8-domains: jq is not on PATH" >&2; exit 2; } +[ -f "$REGISTRY" ] || { echo "check-uuid-v8-domains: no register at $REGISTRY" >&2; exit 2; } +jq -e . "$REGISTRY" >/dev/null 2>&1 || { + echo "check-uuid-v8-domains: $REGISTRY does not parse as JSON" >&2; exit 2; +} + +errors=() + +# Shape of the bytes: one line, printable ASCII, one final LF, JCS order. +if [ "$(tail -c 1 "$REGISTRY" | od -An -c | tr -d ' ')" != '\n' ] || [ "$(wc -l < "$REGISTRY")" -ne 1 ]; then + errors+=("the register must be exactly one line ending in one LF") +fi +if LC_ALL=C grep -q '[^ -~]' "$REGISTRY"; then + errors+=("the register must be printable ASCII") +fi +if ! cmp -s <(jq -cS . "$REGISTRY") "$REGISTRY"; then + errors+=("the register is not in JCS form (jq -cS . | cmp)") +fi + +# Structure and content. +while IFS= read -r e; do + [ -n "$e" ] && errors+=("$e") +done < <(jq -r ' + def domain_ok: type == "string" and test("^[!-9;-~]+$"); + def repo_ok: type == "string" and test("^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$"); + def text_ok: type == "string" and length > 0; + if type != "object" then "the register is not an object" + elif keys != ["domains", "schema"] then "top-level keys must be exactly domains, schema" + elif .schema != "estate-uuid-v8-domains/1" then "schema must be \"estate-uuid-v8-domains/1\"" + elif (.domains | type) != "array" or (.domains | length) == 0 then "domains must be a non-empty array" + else + (.domains | to_entries[] | .key as $i | .value as $e | + if ($e | type) != "object" then "domains[\($i)] is not an object" + elif ($e | keys) != ["domain", "name", "purpose", "repos"] then + "domains[\($i)]: keys must be exactly domain, name, purpose, repos" + else + (if ($e.domain | domain_ok) then empty else + "domains[\($i)]: invalid domain \($e.domain | tojson) (non-empty printable ASCII, no \":\")" end), + (if ($e.name | text_ok) then empty else "domains[\($i)]: name must be a non-empty string" end), + (if ($e.purpose | text_ok) then empty else "domains[\($i)]: purpose must be a non-empty string" end), + (if ($e.repos | type) != "array" or ($e.repos | length) == 0 or ([$e.repos[] | repo_ok] | all | not) then + "domains[\($i)]: repos must be a non-empty array of owner/repo" + elif $e.repos != ($e.repos | unique) then "domains[\($i)]: repos must be sorted and unique" + else empty end) + end), + ([.domains[] | objects | .domain] as $d | + if $d != ($d | unique) then "domains must be strictly increasing by domain (sorted, none repeated)" else empty end) + end' "$REGISTRY") + +# Each DOMAIN given must be registered. +for d in "$@"; do + jq -e --arg d "$d" 'any(.domains[]?; .domain == $d)' "$REGISTRY" >/dev/null 2>&1 \ + || errors+=("domain \"$d\" is not registered in $(basename "$REGISTRY")") +done + +if [ ${#errors[@]} -gt 0 ]; then + for e in "${errors[@]}"; do + echo "::error file=$REGISTRY::$e" + done + exit 1 +fi +echo "profile C domain register: $(jq '.domains | length' "$REGISTRY") domain(s) valid${*:+; required domain(s) registered: $*}" diff --git a/scripts/tests/check-uuid-v8-domains-test.sh b/scripts/tests/check-uuid-v8-domains-test.sh new file mode 100755 index 000000000..6fb322535 --- /dev/null +++ b/scripts/tests/check-uuid-v8-domains-test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# Proves scripts/check-uuid-v8-domains.sh passes the real ADR-008 profile C +# domain register and fails each planted defect for its own reason, so the +# gate can fail and says why. +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +CHECK="$ROOT/scripts/check-uuid-v8-domains.sh" +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT +pass=0; fail=0 + +ok() { pass=$((pass+1)); echo " ok $1"; } +bad() { fail=$((fail+1)); echo " FAIL $1"; } + +# expect