feat(wake): #940 snapshot-datable digests — adapter fd-3 snapshot-metadata channel
ci/woodpecker/pr/ci Pipeline was successful

A digest locator carried observed_hash + emit_ts but nothing DATING the
snapshot, so a consumer could not tell a fresh snapshot from one already
superseded at delivery without a tool call (wake-pilot finding
fw-wake-digest-snapshot-lag: seq 56 rendered "(none)" from a board revision
superseded ~5s before delivery by one adding a FLEET STOP block).

Additive + backward-compatible:
- detector.sh: invoke the W4 source adapter with fd 3 -> temp file; the
  adapter MAY write {"snapshot_sha": "<git commit sha>", "snapshot_ts":
  <epoch>}. Out-of-band is load-bearing: stdout is hashed by the delta
  gate, so an in-band tip-commit sha would advance observed_hash on every
  unrelated push. Metadata is ADVISORY and validated (sha ^[0-9a-f]{7,64}$,
  ts number); malformed metadata drops LOUDLY but never fails the poll or
  suppresses the wake. Valid fields join the enqueue locators. An adapter
  that never writes fd 3 is byte-identical legacy behavior.
- digest.sh: _locator_line renders snapshot_sha=/snapshot_ts= (scrubbed)
  beside observed_hash=; snapshot_sha+path upgrades the actionable-tier
  re-verify hint to one-call `git show <snapshot_sha>:<path>`. With
  emit_ts already in the header, snapshot age is local arithmetic.
- tests: detector D10 (fields land in locators; metadata-only change is
  NOT a delta) + D11 (malformed metadata: loud drop, wake still fires, no
  fields); digest Q10 (orientation dating render, no-vestige sibling,
  actionable re-verify upgrade). manifest 0.6.12; watch-list schema
  untouched ([1,1]); store/reconcile/beacon unchanged.

Closes #940

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB
This commit is contained in:
Jason Woltje
2026-07-30 05:13:47 -05:00
co-authored by Claude Fable 5
parent b981b4ec10
commit bb54e09aec
5 changed files with 202 additions and 4 deletions
@@ -225,15 +225,31 @@ _poll_source() {
# The source definition is handed to the adapter on STDIN via a temp FILE
# (not a pipe): an adapter that ignores stdin must not take SIGPIPE and read
# back as a spurious "source error" under `pipefail`.
local raw rc deftmp
local raw rc deftmp metatmp
mkdir -p "$DET_DIR"
# _wake_tmp_prefix is provided by _wake-common.sh (sourced above).
# shellcheck disable=SC2154
deftmp="$(mktemp "$DET_DIR/${_wake_tmp_prefix}defXXXXXX")" || return 1
printf '%s' "$def" >"$deftmp"
raw="$("$WAKE_DETECTOR_SOURCE_CMD" "$kind" "$id" <"$deftmp" 2>/dev/null)"
# fd 3 is the OUT-OF-BAND snapshot-metadata channel (#940): the adapter MAY
# write one JSON object {"snapshot_sha": "<git commit sha>", "snapshot_ts":
# <epoch>} there. It must stay out of stdout because EVERYTHING on stdout is
# hashed by the delta gate — an in-band tip-commit SHA would advance
# observed_hash on every unrelated push. Adapters that never write fd 3 leave
# the file empty: byte-identical legacy behavior.
metatmp="$(mktemp "$DET_DIR/${_wake_tmp_prefix}metaXXXXXX")" || {
rm -f "$deftmp"
return 1
}
raw="$("$WAKE_DETECTOR_SOURCE_CMD" "$kind" "$id" <"$deftmp" 3>"$metatmp" 2>/dev/null)"
rc=$?
rm -f "$deftmp"
# Slurp + remove the metadata file NOW so every return path below is clean;
# it is parsed only after the fail-loud gates (metadata from a FAILED
# observation is meaningless and must not be trusted or diagnosed).
local rawmeta=""
[ -s "$metatmp" ] && rawmeta="$(cat "$metatmp")"
rm -f "$metatmp"
if [ "$rc" -ne 0 ]; then
echo "detector.sh: FAIL LOUD (G2a) — source '$kind/$id' errored (adapter exit $rc: network/401/403/privacy-404/partial). observed_seq NOT advanced; NOT treated as 'no change'." >&2
return 1
@@ -243,6 +259,24 @@ _poll_source() {
return 1
fi
# --- snapshot metadata (fd 3, #940) — ADVISORY, validated, never gating ----
# Malformed metadata is dropped with a LOUD diagnostic but cannot fail the
# poll or suppress the wake: the obligation never depends on optional dating.
local snap_sha="" snap_ts="" snap_json=""
if [ -n "$rawmeta" ]; then
if snap_json="$(jq -ce '.' <<<"$rawmeta" 2>/dev/null)"; then
snap_sha="$(jq -r 'if (.snapshot_sha|type) == "string" then .snapshot_sha else "" end' <<<"$snap_json")"
snap_ts="$(jq -r 'if (.snapshot_ts|type) == "number" then (.snapshot_ts|floor|tostring) else "" end' <<<"$snap_json")"
if [ -n "$snap_sha" ] && ! printf '%s' "$snap_sha" | grep -Eq '^[0-9a-f]{7,64}$'; then
echo "detector.sh: source '$kind/$id' snapshot_sha rejected (not a 7-64 char lowercase-hex git sha) — snapshot metadata DROPPED, poll continues (#940)." >&2
snap_sha=""
snap_ts=""
fi
else
echo "detector.sh: source '$kind/$id' wrote UNPARSEABLE snapshot metadata on fd 3 — DROPPED, poll continues (#940)." >&2
fi
fi
# --- anchor-scope (feature (a)) then hash ---------------------------------
local scoped
if [ -n "$anchor" ]; then
@@ -281,12 +315,17 @@ _poll_source() {
local locators emit_ts
emit_ts="$(date +%s)"
# NB: `def` is a reserved word in jq — the source-definition arg is `$sdef`.
# snapshot_sha/snapshot_ts (#940) join only when the adapter attested them.
locators="$(jq -cn \
--arg kind "$kind" \
--arg id "$id" \
--argjson sdef "$def" \
--arg hash "$h" \
--arg ssha "$snap_sha" \
--arg sts "$snap_ts" \
'{kind:$kind, id:$id, observed_hash:$hash}
+ (if $ssha != "" then {snapshot_sha: $ssha} else {} end)
+ (if $sts != "" then {snapshot_ts: ($sts|tonumber)} else {} end)
+ ( $sdef | {repo, path, anchor, remote, branches} | with_entries(select(.value != null)) )')"
local args=(enqueue --locators "$locators" --emit-ts "$emit_ts")