diff --git a/packages/mosaic/framework/tools/wake/detector.sh b/packages/mosaic/framework/tools/wake/detector.sh index 77109581..ec29c5ef 100755 --- a/packages/mosaic/framework/tools/wake/detector.sh +++ b/packages/mosaic/framework/tools/wake/detector.sh @@ -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": "", "snapshot_ts": + # } 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,50 @@ _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 + # A ts must be a sane positive epoch BEFORE any arithmetic touches it: a + # negative or absurdly large value would make the shell integer comparison + # below error out and silently KEEP the bad ts — validate first, compare after. + if [ -n "$snap_ts" ] && ! printf '%s' "$snap_ts" | grep -Eq '^[0-9]{1,12}$'; then + echo "detector.sh: source '$kind/$id' snapshot_ts rejected (not a sane positive epoch) — snapshot_ts DROPPED, poll continues (#940)." >&2 + snap_ts="" + fi + # ts is only meaningful anchored to a revision the consumer can re-verify — + # a bare number with no sha behind it is the weakest possible attestation, + # so ts requires a valid sha (#940 review §2). + if [ -n "$snap_ts" ] && [ -z "$snap_sha" ]; then + echo "detector.sh: source '$kind/$id' snapshot_ts without a valid snapshot_sha — snapshot_ts DROPPED (unverifiable dating), poll continues (#940)." >&2 + snap_ts="" + fi + # A FUTURE ts renders a stale snapshot fresher-than-fresh (negative age) — + # wrong in the reassuring direction, the exact failure class #940 fixes. + # Cross-host NTP skew of a few seconds is the NORMAL case, so allow a small + # slack; beyond it, drop the ts (the sha stays: independently verifiable). + if [ -n "$snap_ts" ]; then + local now_s + now_s="$(date +%s)" + if [ "$snap_ts" -gt $((now_s + ${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300})) ]; then + echo "detector.sh: source '$kind/$id' snapshot_ts is beyond the ${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300}s future-skew allowance (ts=$snap_ts now=$now_s) — snapshot_ts DROPPED, poll continues (#940)." >&2 + snap_ts="" + fi + 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 +341,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") diff --git a/packages/mosaic/framework/tools/wake/digest.sh b/packages/mosaic/framework/tools/wake/digest.sh index afd6d453..6db3d12c 100755 --- a/packages/mosaic/framework/tools/wake/digest.sh +++ b/packages/mosaic/framework/tools/wake/digest.sh @@ -235,7 +235,7 @@ _has_hard_locator() { # ACTIONABLE-tier hard-locator FAIL-LOUD gate is untouched. _locator_line() { local loc="$1" repo issue sha file anchor head parts='' reverify='' - local remote path kind id ohash branches + local remote path kind id ohash branches snap_sha snap_ts repo="$(jq -r '.repo // ""' <<<"$loc")" issue="$(jq -r '(.issue // "") | tostring' <<<"$loc")" sha="$(jq -r '.sha // ""' <<<"$loc")" @@ -247,6 +247,8 @@ _locator_line() { kind="$(jq -r '.kind // ""' <<<"$loc")" id="$(jq -r '(.id // "") | tostring' <<<"$loc")" ohash="$(jq -r '.observed_hash // ""' <<<"$loc")" + snap_sha="$(jq -r '.snapshot_sha // ""' <<<"$loc")" + snap_ts="$(jq -r '(.snapshot_ts // "") | tostring' <<<"$loc")" branches="$(jq -r '(.branches // []) | if length > 0 then join(",") else "" end' <<<"$loc" 2>/dev/null || true)" [ -n "$kind" ] && parts="$parts kind=$(_scrub_inline "$kind")" [ "$id" != "" ] && parts="$parts id=$(_scrub_inline "$id")" @@ -273,9 +275,18 @@ _locator_line() { # git commit SHA — kept distinct from `sha` so it never impersonates one or # feeds the `git show ` re-verify hint below. [ -n "$ohash" ] && parts="$parts observed_hash=$(_scrub_inline "$ohash")" + # snapshot_sha/snapshot_ts (#940): the SNAPSHOT'S git commit sha + commit + # epoch, attested by the source adapter at OBSERVE time (out-of-band fd 3, + # detector-validated). Unlike observed_hash this IS a commit sha, so it may + # feed the `git show` re-verify hint; with emit_ts already in the header, + # snapshot age becomes local arithmetic for the consumer. + [ -n "$snap_sha" ] && parts="$parts snapshot_sha=$(_scrub_inline "$snap_sha")" + [ "$snap_ts" != "" ] && parts="$parts snapshot_ts=$(_scrub_inline "$snap_ts")" # One-targeted-call re-verify hint (best available, most-specific first). if [ -n "$sha" ] && [ -n "$file" ]; then reverify="git show $(_scrub_inline "$sha"):$(_scrub_inline "$file")" + elif [ -n "$snap_sha" ] && [ -n "$path" ]; then + reverify="git show $(_scrub_inline "$snap_sha"):$(_scrub_inline "$path")" elif [ -n "$repo" ] && [ "$issue" != "" ]; then reverify="issue $(_scrub_inline "$repo")#$(_scrub_inline "$issue")" elif [ -n "$sha" ]; then diff --git a/packages/mosaic/framework/tools/wake/manifest.txt b/packages/mosaic/framework/tools/wake/manifest.txt index 1c1705be..f1050ea5 100644 --- a/packages/mosaic/framework/tools/wake/manifest.txt +++ b/packages/mosaic/framework/tools/wake/manifest.txt @@ -240,8 +240,50 @@ # guard/witness-marker are REMOVED — T9/T11 now RUN and ASSERT their failure # paths in every environment including non-priv CI. Changed: _wake-common.sh # (seam), test-wake-store-ack.sh (T9/T11 conversion). +# 0.6.12 #940 snapshot-datable digests — the adapter-contract fd-3 snapshot- +# metadata channel (wake-pilot finding fw-wake-digest-snapshot-lag: +# a digest's 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). ADDITIVE + backward-compatible: (a) detector.sh invokes +# the W4 source adapter with fd 3 redirected to a temp file; the +# adapter MAY write one JSON object {"snapshot_sha": "", "snapshot_ts": } there. OUT-OF-BAND is load- +# bearing: everything on stdout is hashed by the delta gate, so an +# in-band tip-commit sha would advance observed_hash on every +# unrelated push (spurious delta wake per watched file). Metadata +# is ADVISORY and validated (sha ^[0-9a-f]{7,64}$, ts number): +# malformed metadata is dropped with a LOUD stderr diagnostic but +# NEVER fails the poll or suppresses the wake — the obligation +# never depends on optional dating. Valid fields join the enqueue +# locators; an adapter that never writes fd 3 is byte-identical +# legacy behavior. (b) digest.sh _locator_line renders +# snapshot_sha=/snapshot_ts= (scrubbed) beside observed_hash=, and +# snapshot_sha+path upgrades the one-call re-verify hint to +# `git show :` (snapshot_sha IS a commit sha, +# unlike observed_hash, so it may feed the git hint). With emit_ts +# already in the header, snapshot age becomes local arithmetic for +# the consumer — zero round trips. Watch-list schema UNTOUCHED +# ([1,1] unchanged — adapter contract + locator vocabulary, not +# watch-list config). store.sh/reconcile.sh/beacon.sh UNCHANGED. +# Review hardening (#941 §2): snapshot_ts additionally requires a +# VALID snapshot_sha (a bare number with no revision to re-verify +# against is the weakest attestation — dropped loudly), must be a +# sane positive epoch (^[0-9]{1,12}$ — validated BEFORE the shell +# integer comparison so an absurd value cannot error past it), and +# must not sit beyond a future-skew allowance +# (WAKE_SNAPSHOT_TS_FUTURE_SLACK, default 300 s): a future ts +# yields a NEGATIVE age — stale-reads-fresher-than-fresh, the +# exact failure class #940 fixes. NOTE for consumers: these fields +# are ADVISORY and their ABSENCE IS DELIBERATELY NOT DIAGNOSTIC — +# a pre-#940 adapter and a dropped-as-malformed attestation render +# identically (no snapshot_* fields); the drop is loud only in the +# detector's own stderr. Do not build load-bearing logic on the +# absence of these fields. +# Changed: detector.sh, digest.sh (+ test-wake-detector.sh +# D10/D11/D12, test-wake-digest-quarantine.sh Q10). component=wake -version=0.6.11 +version=0.6.12 # Watch-list schema this component consumes, and the INCLUSIVE range of # schema_version values it supports. A wake-watch-list.json whose schema_version diff --git a/packages/mosaic/framework/tools/wake/test-wake-detector.sh b/packages/mosaic/framework/tools/wake/test-wake-detector.sh index 4f9952d2..b3adffbd 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-detector.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-detector.sh @@ -445,6 +445,149 @@ EOF echo "$err" | grep -qi 'range' || fail_msg "D9: the out-of-range failure must still state it is out of the supported range [$err]" ) && ok +echo "== D10: snapshot metadata (fd 3, #940) — adapter-attested sha/ts land in the enqueued locators ==" +( + WAKE_STATE_HOME="$(fresh_state d10)" + export WAKE_STATE_HOME + unset WAKE_AGENT + stub="$TMP_ROOT/d10stub" + mkdir -p "$stub" + # Stub adapter that ALSO writes snapshot metadata out-of-band on fd 3. + cat >"$stub/adapter.sh" <&3; } 2>/dev/null +exit 0 +EOF + chmod +x "$stub/adapter.sh" + export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh" + wl="$TMP_ROOT/d10.json" + write_watchlist "$wl" 1 + export WAKE_WATCH_LIST="$wl" + printf 'SHA-AAA\n' >"$stub/repo_r1" + printf '## LANE-X\ndecision: hold\n' >"$stub/board_file_b1" + printf '{"snapshot_sha":"0123abc4567890def0123abc4567890def012345","snapshot_ts":1753850000}\n' >"$stub/repo_r1.meta" + "$DET" poll-once >/dev/null || fail_msg "D10: baseline pass failed" + printf 'SHA-BBB\n' >"$stub/repo_r1" + "$DET" poll-once >/dev/null || fail_msg "D10: change pass failed" + entry="$("$STORE" drain)" + echo "$entry" | jq -e 'select(.locators.snapshot_sha=="0123abc4567890def0123abc4567890def012345" and .locators.snapshot_ts==1753850000)' >/dev/null \ + || fail_msg "D10: enqueued locators must carry the adapter-attested snapshot_sha + snapshot_ts [$entry]" + # And the metadata must NOT have leaked into the hashed content: an unchanged + # source with CHANGED metadata is still a no-op (delta gate intact). + d0="$(det_seq)" + printf '{"snapshot_sha":"ffff111122223333444455556666777788889999","snapshot_ts":1753860000}\n' >"$stub/repo_r1.meta" + "$DET" poll-once >/dev/null || fail_msg "D10: metadata-only pass failed" + [ "$(det_seq)" = "$d0" ] || fail_msg "D10: metadata is OUT-OF-BAND — a metadata-only change must NOT be a delta, got seq $(det_seq)" +) && ok + +echo "== D11: snapshot metadata is ADVISORY — malformed metadata is dropped LOUDLY, the wake still fires, no fields emitted ==" +( + WAKE_STATE_HOME="$(fresh_state d11)" + export WAKE_STATE_HOME + unset WAKE_AGENT + stub="$TMP_ROOT/d11stub" + mkdir -p "$stub" + cat >"$stub/adapter.sh" <&3; } 2>/dev/null +exit 0 +EOF + chmod +x "$stub/adapter.sh" + export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh" + wl="$TMP_ROOT/d11.json" + write_watchlist "$wl" 1 + export WAKE_WATCH_LIST="$wl" + printf 'SHA-AAA\n' >"$stub/repo_r1" + printf '## LANE-X\ndecision: hold\n' >"$stub/board_file_b1" + "$DET" poll-once >/dev/null 2>&1 || fail_msg "D11: baseline pass failed" + # (a) non-JSON garbage on fd 3. + printf 'this is not json\n' >"$stub/repo_r1.meta" + printf 'SHA-BBB\n' >"$stub/repo_r1" + err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D11: malformed metadata must NEVER fail the poll (rc=$rc)" + echo "$err" | grep -qi 'snapshot' || fail_msg "D11: dropping malformed metadata must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \ + && fail_msg "D11: malformed metadata must emit NO snapshot fields [$entry]" + # (b) valid JSON but a non-hex snapshot_sha -> same: loud drop, wake fires, no fields. + printf '{"snapshot_sha":"NOT-A-HEX-SHA","snapshot_ts":"also-not-a-number"}\n' >"$stub/repo_r1.meta" + printf 'SHA-CCC\n' >"$stub/repo_r1" + err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D11: rejected snapshot_sha must NEVER fail the poll (rc=$rc)" + echo "$err" | grep -qi 'snapshot' || fail_msg "D11: rejecting a bad snapshot_sha must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \ + && fail_msg "D11: rejected metadata must emit NO snapshot fields [$entry]" + [ "$(det_seq)" = "2" ] || fail_msg "D11: both real deltas must still have enqueued (advisory metadata never suppresses a wake), got seq $(det_seq)" +) && ok + +echo "== D12: snapshot_ts guards (#940 review §2) — ts requires a sha, a future ts is dropped, an absurd ts cannot reach the comparison ==" +( + WAKE_STATE_HOME="$(fresh_state d12)" + export WAKE_STATE_HOME + unset WAKE_AGENT + stub="$TMP_ROOT/d12stub" + mkdir -p "$stub" + cat >"$stub/adapter.sh" <&3; } 2>/dev/null +exit 0 +EOF + chmod +x "$stub/adapter.sh" + export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh" + wl="$TMP_ROOT/d12.json" + write_watchlist "$wl" 1 + export WAKE_WATCH_LIST="$wl" + printf 'SHA-AAA\n' >"$stub/repo_r1" + printf '## LANE-X\ndecision: hold\n' >"$stub/board_file_b1" + "$DET" poll-once >/dev/null 2>&1 || fail_msg "D12: baseline pass failed" + goodsha="0123abc4567890def0123abc4567890def012345" + # (a) ts WITHOUT sha — the weakest attestation: an unverifiable number with no + # revision to re-verify against. Dropped loudly; the wake still fires. + printf '{"snapshot_ts":1753850000}\n' >"$stub/repo_r1.meta" + printf 'SHA-BBB\n' >"$stub/repo_r1" + err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D12: ts-without-sha must NEVER fail the poll (rc=$rc)" + echo "$err" | grep -qi 'without a valid snapshot_sha' || fail_msg "D12: dropping ts-without-sha must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \ + && fail_msg "D12: ts-without-sha must emit NO snapshot fields [$entry]" + # (b) valid sha + FUTURE ts — a negative age would read fresher-than-fresh, + # wrong in the reassuring direction. ts dropped, sha kept (independently verifiable). + future=$(( $(date +%s) + 9999 )) + printf '{"snapshot_sha":"%s","snapshot_ts":%s}\n' "$goodsha" "$future" >"$stub/repo_r1.meta" + printf 'SHA-CCC\n' >"$stub/repo_r1" + err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D12: future ts must NEVER fail the poll (rc=$rc)" + echo "$err" | grep -qi 'future-skew' || fail_msg "D12: dropping a future ts must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \ + || fail_msg "D12: future ts must drop ts but KEEP the sha [$entry]" + # (c) valid sha + absurdly large ts — must be rejected by the sanity regex BEFORE + # the shell integer comparison (which would error out and silently keep it). + printf '{"snapshot_sha":"%s","snapshot_ts":99999999999999999999}\n' "$goodsha" >"$stub/repo_r1.meta" + printf 'SHA-DDD\n' >"$stub/repo_r1" + err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? + [ "$rc" -eq 0 ] || fail_msg "D12: absurd ts must NEVER fail the poll (rc=$rc)" + echo "$err" | grep -qi 'sane positive epoch' || fail_msg "D12: rejecting an absurd ts must be LOUD on stderr [$err]" + entry="$("$STORE" drain | tail -1)" + echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \ + || fail_msg "D12: absurd ts must drop ts but KEEP the sha [$entry]" + [ "$(det_seq)" = "3" ] || fail_msg "D12: all three real deltas must still have enqueued, got seq $(det_seq)" +) && ok + echo if [ -s "$FAILFILE" ]; then echo "wake detector harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 diff --git a/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh b/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh index 002d5b4f..493a9634 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh @@ -301,6 +301,42 @@ echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (n true ) && ok +echo "== Q10: snapshot metadata (#940) — snapshot_sha/snapshot_ts render on the locator line and feed the re-verify hint ==" +( + home="$(fresh_home q10)" + export WAKE_STATE_HOME="$home" + unset WAKE_AGENT + f="$TMP_ROOT/q10.jsonl" + { + # (a) a digest-class ORIENTATION pointer WITH adapter-attested snapshot + # metadata — the fields must render on the pointer line (the tier where + # the snapshot-lag class actually bit; re-verify hints are actionable- + # tier-only by design and are asserted via (c) below). + printf '%s\n' '{"observed_seq":21,"class":"digest","locators":{"kind":"board_file","id":"SNAP-A","path":"BOARD.md","observed_hash":"aaaa1111","snapshot_sha":"0123abc4567890def0123abc4567890def012345","snapshot_ts":1753850000},"emit_ts":2}' + # (b) a sibling WITHOUT the fields — must render no snapshot vestige. + printf '%s\n' '{"observed_seq":22,"class":"digest","locators":{"kind":"board_file","id":"SNAP-B","path":"OTHER.md","observed_hash":"bbbb2222"},"emit_ts":2}' + # (c) an ACTIONABLE entry (hard locator: repo+issue) carrying path + + # snapshot_sha — its re-verify hint must upgrade to the one-call + # `git show :` (most-specific-first). + printf '%s\n' '{"observed_seq":23,"class":"actionable","locators":{"repo":"example/repo","issue":7,"path":"BOARD.md","observed_hash":"cccc3333","snapshot_sha":"0123abc4567890def0123abc4567890def012345","snapshot_ts":1753850000},"emit_ts":2}' + } >"$f" + out="$("$DIGEST" render --from-file "$f" --agent default 2>/dev/null)" + rc=$? + [ "$rc" -eq 0 ] || fail_msg "Q10: render must succeed (rc=$rc)" + snapa_line="$(printf '%s\n' "$out" | grep 'id=SNAP-A' || true)" + printf '%s' "$snapa_line" | grep -q 'snapshot_sha=0123abc4567890def0123abc4567890def012345' \ + || fail_msg "Q10: snapshot_sha must render on the ORIENTATION pointer line [$snapa_line]" + printf '%s' "$snapa_line" | grep -q 'snapshot_ts=1753850000' \ + || fail_msg "Q10: snapshot_ts must render on the ORIENTATION pointer line (age = emit_ts - snapshot_ts, local arithmetic) [$snapa_line]" + printf '%s' "$out" | grep -q 'git show 0123abc4567890def0123abc4567890def012345:BOARD.md' \ + || fail_msg "Q10: snapshot_sha+path must upgrade the actionable re-verify hint to a one-call git show" + # The sibling without metadata must not grow empty snapshot_ fields. + snapb_line="$(printf '%s\n' "$out" | grep 'id=SNAP-B' || true)" + printf '%s' "$snapb_line" | grep -q 'snapshot_' \ + && fail_msg "Q10: an entry without metadata must render NO snapshot_ fields [$snapb_line]" + true +) && ok + echo if [ -s "$FAILFILE" ]; then echo "wake digest-quarantine harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2