← Files NightshiftARCHIVED FILE
runtime/morning-receipt.sh
43.6 KB · Oct 2, 2026 · 00:30 UTC
#!/usr/bin/env bash
# morning-receipt.sh — the compact receipt for one shift, in Markdown.
#
# morning-receipt.sh --project DIR [--view owner|reviewer|release|artifact] [--out PATH]
#
# Without --view, the owner's handoff.view decides; handoff.sections picks and orders the
# documented sections when it is not empty. Presentation only: a section the owner dropped
# changes what the page shows, never what was measured.
#
# Renders from records only: the findings ledger, the shift policy that ran (the live file or the
# archived snapshot), the resolved policy, the punch list, the usage marks, the shift log, the
# parking lot, the snag log, the opportunity map, the work target's history and the shift markers.
# It measures nothing, reruns nothing, and never renders a check the owner disabled as one that
# passed. Eleven sections in a fixed order, each omitted when it is empty; the view chooses which
# of them a reader gets:
#
# owner every section
# reviewer review first, baseline, what changed
# release how it ended, and what changed filtered to regressions
# artifact every section but baseline and what changed, in the vocabulary of a site rather
# than a repository
#
# Without --out the Markdown goes to stdout; with it the file is written by rename and its path
# is printed. NIGHTSHIFT_COMPARE_HELPER overrides the comparison helper's path —
# a session lever for the suite, never policy.
# Exit: 0 ok · 1 usage · 2 contract failure
set -u
_ns_src="${BASH_SOURCE[0]//\\//}"
case "$_ns_src" in
[A-Za-z]:/*)
_ns_drive=$(printf '%s' "${_ns_src%"${_ns_src#?}"}" | tr '[:upper:]' '[:lower:]')
_ns_src="/${_ns_drive}${_ns_src#?:}"
;;
esac
_here="$(cd -P -- "$(dirname -- "$_ns_src")" && pwd)" || exit 2
# shellcheck source=plugins/nightshift/lib/lib.sh
. "$_here/../lib/lib.sh"
EMIT_JQ="$_here/morning-receipt-emit.jq"
# Native Windows jq.exe cannot open a /d/... program path. The display form is
# the Windows path here and the same POSIX path everywhere else.
EMIT_JQ="$(ns_native_display_path "$EMIT_JQ")"
# The text half: it reads the Markdown records, the shift log and the history into rows.
READ_AWK="$_here/morning-receipt-read.awk"
COMPARE="${NIGHTSHIFT_COMPARE_HELPER:-$_here/evidence-compare.sh}"
NL='
'
FS=$(printf '\037')
RS=$(printf '\036')
DASH=$(printf '\xe2\x80\x94')
ARROW=$(printf '\xe2\x86\x92')
MIDDOT=$(printf '\xc2\xb7')
# The ledger cells the receipt draws, in slot order. morning-receipt-emit.jq is handed the same
# list, so neither half hard-codes the other.
RFIELDS="id
domain
sourceClass
source
scope
status
ladder
locator
rawDigest
lastChecked
action
fix
verificationLocator
host
digest"
usage() {
awk 'NR == 1 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0" >&2
exit 1
}
die() {
printf 'morning-receipt: %s\n' "$1" >&2
exit "$2"
}
# ---------------------------------------------------------------- arguments
PROJECT="${CLAUDE_PROJECT_DIR:-${CODEX_PROJECT_DIR:-$PWD}}"
VIEW=""
OUT=""
while [ $# -gt 0 ]; do
case "$1" in
--project)
[ $# -ge 2 ] || usage
PROJECT="$2"
shift 2
;;
--view)
[ $# -ge 2 ] || usage
VIEW="$2"
shift 2
;;
--out)
[ $# -ge 2 ] || usage
OUT="$2"
shift 2
;;
-h | --help) usage ;;
*)
printf 'morning-receipt: unknown argument: %s\n' "$1" >&2
usage
;;
esac
done
# An explicit --view wins; without one the owner's configured reader decides, and that is
# resolved after the workspace is known.
case "${VIEW:-owner}" in
owner | reviewer | release | artifact) ;;
*) die 'view must be owner, reviewer, release, or artifact' 1 ;;
esac
PROJECT="$(ns_msys_path "$PROJECT")"
HOST_DIR="$(cd -P "$PROJECT" 2>/dev/null && pwd)" || die "cannot cd to $PROJECT" 1
WORKSPACE="$HOST_DIR"
if [ -e "$HOST_DIR/.nightshift-link" ] || [ -L "$HOST_DIR/.nightshift-link" ]; then
WORKSPACE="$(ns_workspace_root "$HOST_DIR" 2>/dev/null)" ||
die 'invalid .nightshift-link — Nightshift will not guess a workspace' 2
fi
NS="$WORKSPACE/.nightshift"
[ -d "$NS" ] || die "no .nightshift/ at $WORKSPACE — run setup first" 2
JSONL="$(ns_layout_path "$NS" evidence)/findings.jsonl"
declare PUNCH LOT SNAGS LOG MAP STOP SESSION_REC LIVE_POLICY USAGE ENDED_MARKER
ns_layout_set PUNCH "$NS" punch-list
ns_layout_set LOT "$NS" parking-lot
ns_layout_set SNAGS "$NS" snag-log
ns_layout_set LOG "$NS" shift-log
ns_layout_set MAP "$NS" opportunity-map
ns_layout_set STOP "$NS" stop
ns_layout_set SESSION_REC "$NS" session
ns_layout_set LIVE_POLICY "$NS" shift-policy
ns_layout_set USAGE "$NS" usage
ns_layout_set ENDED_MARKER "$NS" ended
RECEIPTS_DIR="$(ns_receipts_dir "$WORKSPACE")"
JSON_TOOL=""
if command -v jq >/dev/null 2>&1; then
JSON_TOOL=jq
elif command -v python3 >/dev/null 2>&1; then
JSON_TOOL=python3
else
die 'JSON parser unavailable; write the morning receipt in the skill' 2
fi
TMPD="$(mktemp -d "${TMPDIR:-/tmp}/ns-receipt.XXXXXX")" || die 'cannot create a temporary directory' 2
chmod 700 "$TMPD" || {
rm -rf "$TMPD"
die 'cannot create a temporary directory' 2
}
trap 'rm -rf "$TMPD"' EXIT
# ---------------------------------------------------------------- small helpers
# _scrub TEXT -> SCRUBBED: control characters become spaces, the ends are trimmed. Ledger cells
# arrive scrubbed already; owner-authored Markdown comes through here.
_scrub() {
SCRUBBED="$(printf '%s' "$1" | sed 's/[[:cntrl:]]/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')"
}
# _order KEYFILE -> ORDER: the positions in KEYFILE, byte-ordered by its sort key. Every table
# is drawn from one of these, so no table can come out in filesystem order.
_order() {
ORDER=""
[ -s "$1" ] || return 0
ORDER="$(LC_ALL=C sort "$1" | cut -d "$FS" -f2)"
}
# _join_sorted FILE SEP -> JOINED: the file's lines, byte-ordered, de-duplicated, joined.
_join_sorted() {
local line out=""
JOINED=""
[ -s "$1" ] || return 0
while IFS= read -r line; do
[ -n "$line" ] || continue
[ -z "$out" ] || out="$out$2"
out="$out$line"
done <<EOF
$(LC_ALL=C sort -u "$1")
EOF
JOINED="$out"
}
JOINER=" $DASH "
SOURCE_SEP=', '
NONE='none'
GATES_FROM='punch list'
NO_POLICY='no shift policy was written'
POLICY_MALFORMED='the policy file is present but unreadable or fails the schema'
POLICY_KIND=absent
REVIEW_ARTIFACT='Does not apply: an artifact shift is reviewed through its receipts.'
# _short_digest FULL -> SHORT_DIGEST: the first twelve hex chars PowerShell shows.
_short_digest() {
SHORT_DIGEST=""
[ -n "$1" ] || return 0
if [ "${#1}" -le 12 ]; then
SHORT_DIGEST="$1"
else
SHORT_DIGEST="${1:0:12}"
fi
}
# _md_cell TEXT -> MD_CELL: one comparison-table cell; empty reads as an em dash.
_md_cell() {
if [ -z "$1" ]; then
MD_CELL="$DASH"
else
MD_CELL="${1//|/\\|}"
fi
}
# _utc_stamp EPOCH -> UTC_STAMP: that moment in UTC to the second, zone included.
_utc_stamp() {
UTC_STAMP=""
case "$1" in '' | *[!0-9]*) return 1 ;; esac
UTC_STAMP="$(date -u -r "$1" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null ||
date -u -d "@$1" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"
[ -n "$UTC_STAMP" ]
}
# _count N NOUN -> COUNTED: "1 file", "3 files".
_count() {
if [ "$1" = 1 ]; then COUNTED="1 $2"; else COUNTED="$1 $2s"; fi
}
# _item_link LABEL ID -> ITEM_LINK: the label, linked to its item receipt when that file exists.
_item_link() {
local base
ITEM_LINK="$1"
base="$(ns_receipt_base "$WORKSPACE" "$1" "$2")"
[ -n "$base" ] && [ -n "$RECEIPTS_DIR" ] || return 0
[ -f "$RECEIPTS_DIR/$base.md" ] && [ ! -L "$RECEIPTS_DIR/$base.md" ] || return 0
ITEM_LINK="[$1](./$base.md)"
}
# _shift_log_lines interruptions|handover — shift-log lines written since the last `shift started`:
# every interruption the runtime recorded, or the last handover line.
_shift_log_lines() {
[ -f "$LOG" ] && [ ! -L "$LOG" ] || return 0
sed 's/[[:cntrl:]]/ /g' "$LOG" | awk -v op="$1" -f "$READ_AWK"
}
# _gate_commands -> $TMPD/gates: backtick commands under ## Gates, byte-ordered.
_gate_commands() {
local in_gates=0 line rest cmd
: >"$TMPD/gates"
[ -f "$PUNCH" ] && [ ! -L "$PUNCH" ] || return 0
while IFS= read -r line; do
case "$line" in
"## Gates"*) in_gates=1; continue ;;
"## "*) [ "$in_gates" -eq 1 ] && break; continue ;;
esac
[ "$in_gates" -eq 1 ] || continue
rest="$line"
while [ -n "$rest" ]; do
case "$rest" in
*'`'*)
rest="${rest#*\`}"
cmd="${rest%%\`*}"
rest="${rest#*\`}"
[ -n "$cmd" ] && printf '%s\n' "$cmd" >>"$TMPD/gates"
;;
*) break ;;
esac
done
done <"$PUNCH"
}
# _commit_count -> COMMIT_COUNT: commits on the work target since the shift started.
_commit_count() {
local target count
COMMIT_COUNT=""
[ -n "$SHIFT_SINCE" ] || return 0
target="$(ns_work_target "$WORKSPACE" 2>/dev/null)" || target=""
[ -n "$target" ] || return 0
target="$(ns_msys_path "$target")"
count="$(cd -P "$target" 2>/dev/null && git rev-list --count --since "$SHIFT_SINCE" HEAD)" || return 0
case "$count" in
'' | *[!0-9]*) return 0 ;;
esac
COMMIT_COUNT="$count"
}
# _session_host -> SESSION_HOST: the bound session, else the last record host.
_session_host() {
local i host
SESSION_HOST=""
if [ -f "$SESSION_REC" ] && [ ! -L "$SESSION_REC" ]; then
host="$(ns_session_line "$NS" 5 2>/dev/null | tr -d '[:space:]')"
[ -n "$host" ] && SESSION_HOST="$host" && return 0
fi
i=0
while [ "$i" -lt "$NREC" ]; do
[ -n "${R_HOST[$i]}" ] && SESSION_HOST="${R_HOST[$i]}"
i=$((i + 1))
done
}
# _policy_profile -> POLICY_PROFILE: the defaults profile, or fast when unset.
_policy_profile() {
POLICY_PROFILE=fast
if ns_policy_read_defaults "$WORKSPACE" >/dev/null 2>&1; then
POLICY_PROFILE="${NS_POLICY_DEF_PROFILE#\"}"
POLICY_PROFILE="${POLICY_PROFILE%\"}"
[ -n "$POLICY_PROFILE" ] || POLICY_PROFILE=fast
fi
}
# ---------------------------------------------------------------- JSON bridge
PY='
import json, re, sys
OP = sys.argv[1]
def scrub(s):
return re.sub(r"[\x00-\x1f\x7f]", " ", s)
def canon(v):
return json.dumps(v, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def cell(v):
if v is None:
text = ""
elif isinstance(v, str):
text = v
elif v is True:
text = "true"
elif v is False:
text = "false"
else:
text = canon(v)
return scrub(text)
def field(rec, key):
return rec.get(key) if isinstance(rec, dict) else None
def firstof(rec, keys):
for key in keys:
value = field(rec, key)
if value is not None:
return value
return None
def parse(line):
try:
return json.loads(line)
except ValueError:
return None
text = sys.stdin.buffer.read().decode("utf-8", "surrogateescape")
out = sys.stdout
if OP == "recs":
fields = [x for x in sys.argv[2].split("\n") if x]
FS, RS = sys.argv[3], sys.argv[4]
for i, line in enumerate([x for x in text.split("\n") if x]):
rec = parse(line)
if isinstance(rec, dict):
out.write("r" + FS + "".join(cell(field(rec, k)) + FS for k in fields))
else:
out.write("u" + FS + "".join("" + FS for k in fields))
out.write(str(i) + RS)
elif OP == "details":
FS, RS = sys.argv[2], sys.argv[3]
for i, line in enumerate([x for x in text.split("\n") if x]):
d = field(parse(line), "details")
if not isinstance(d, dict):
continue
for key in sorted(d):
out.write("%d%s%s%s%s%s" % (i, FS, scrub(key), FS, cell(d[key]), RS))
elif OP == "policy":
FS, RS = sys.argv[2], sys.argv[3]
doc = json.loads(text)
if not isinstance(doc, dict):
doc = {}
head = ["shiftId", "createdAt", "source", "verificationLevel", "toolingPolicy",
"completionMode"]
debt = field(doc, "selectedDebt")
if not isinstance(debt, list):
debt = []
out.write("h" + FS + "".join(cell(field(doc, k)) + FS for k in head))
out.write(", ".join(cell(x) for x in debt) + RS)
allowances = field(doc, "allowances")
if not isinstance(allowances, list):
allowances = []
for one in allowances:
if not isinstance(one, dict):
continue
out.write("a" + FS + cell(field(one, "category")) + FS + cell(field(one, "scope"))
+ FS + cell(field(one, "provenance")) + RS)
elif OP == "compare":
FS, RS = sys.argv[2], sys.argv[3]
doc = json.loads(text)
rows = None
if isinstance(doc, list):
rows = doc
elif isinstance(doc, dict):
for key in ("rows", "findings", "comparison", "entries"):
if isinstance(doc.get(key), list):
rows = doc[key]
break
for row in rows or []:
if not isinstance(row, dict):
continue
sources = firstof(row, ["sources", "sourceClass", "source"])
if isinstance(sources, list):
sources = ", ".join(cell(x) for x in sources)
else:
sources = cell(sources)
out.write(cell(firstof(row, ["id", "finding", "recordId"])) + FS
+ cell(firstof(row, ["class", "classification", "state"])) + FS
+ cell(firstof(row, ["digest"])) + FS + sources + FS
+ cell(firstof(row, ["locator", "at"])) + RS)
'
# jq resolves every $name at compile time, so one program file means one argument set whichever
# operation is being asked for.
_jq_args() {
JQARGS=(--arg op "$1" --arg fields "$RFIELDS" --arg FS "$FS" --arg RS "$RS")
}
# _emit_recs FILE — the ledger's cells, one group per line, on stdout.
_emit_recs() {
if [ "$JSON_TOOL" = jq ]; then
_jq_args recs
jq -Rsj -f "$EMIT_JQ" "${JQARGS[@]}" <"$1"
else
python3 -c "$PY" recs "$RFIELDS" "$FS" "$RS" <"$1"
fi
}
# _emit_details FILE — every details pair in the ledger, on stdout.
_emit_details() {
if [ "$JSON_TOOL" = jq ]; then
_jq_args details
jq -Rsj -f "$EMIT_JQ" "${JQARGS[@]}" <"$1"
else
python3 -c "$PY" details "$FS" "$RS" <"$1"
fi
}
# _emit_policy FILE — the shift policy's header and allowances, on stdout.
_emit_policy() {
if [ "$JSON_TOOL" = jq ]; then
_jq_args policy
jq -j -f "$EMIT_JQ" "${JQARGS[@]}" <"$1"
else
python3 -c "$PY" policy "$FS" "$RS" <"$1"
fi
}
# _emit_compare FILE — one comparison row per group, on stdout.
_emit_compare() {
if [ "$JSON_TOOL" = jq ]; then
_jq_args compare
jq -j -f "$EMIT_JQ" "${JQARGS[@]}" <"$1"
else
python3 -c "$PY" compare "$FS" "$RS" <"$1"
fi
}
# ---------------------------------------------------------------- the ledger
NREC=0
NDET=0
ULINES=0
R_ID=()
R_DOMAIN=()
R_SCLASS=()
R_SOURCE=()
R_SCOPE=()
R_STATUS=()
R_LADDER=()
R_LOCATOR=()
R_RAWDIGEST=()
R_FIX=()
R_VERIF=()
R_HOST=()
R_DIGEST=()
R_INDEX=()
D_IDX=()
D_KEY=()
D_VAL=()
_load_ledger() {
local kind c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 idx key val
[ -f "$JSONL" ] && [ ! -L "$JSONL" ] || return 0
_emit_recs "$JSONL" >"$TMPD/recs" 2>/dev/null || die "cannot read $JSONL" 2
while IFS="$FS" read -r -d "$RS" \
kind c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 idx; do
if [ "$kind" != r ]; then
ULINES=$((ULINES + 1))
continue
fi
R_ID[NREC]="$c0"
R_DOMAIN[NREC]="$c1"
R_SCLASS[NREC]="$c2"
R_SOURCE[NREC]="$c3"
R_SCOPE[NREC]="$c4"
R_STATUS[NREC]="$c5"
R_LADDER[NREC]="$c6"
R_LOCATOR[NREC]="$c7"
R_RAWDIGEST[NREC]="$c8"
: "$c9" "$c10"
R_FIX[NREC]="$c11"
R_VERIF[NREC]="$c12"
R_HOST[NREC]="$c13"
R_DIGEST[NREC]="$c14"
R_INDEX[NREC]="$idx"
NREC=$((NREC + 1))
done <"$TMPD/recs"
_emit_details "$JSONL" >"$TMPD/details" 2>/dev/null || return 0
while IFS="$FS" read -r -d "$RS" idx key val; do
D_IDX[NDET]="$idx"
D_KEY[NDET]="$key"
D_VAL[NDET]="$val"
NDET=$((NDET + 1))
done <"$TMPD/details"
}
# _detail INDEX KEY -> DVAL: one details value, empty when the record does not carry the key.
_detail() {
local i=0
DVAL=""
while [ "$i" -lt "$NDET" ]; do
if [ "${D_IDX[$i]}" = "$1" ] && [ "${D_KEY[$i]}" = "$2" ]; then
DVAL="${D_VAL[$i]}"
return 0
fi
i=$((i + 1))
done
}
# ---------------------------------------------------------------- the policy that ran
POLICY_FILE=""
POLICY_WANT_ID=""
P_SHIFTID=""
P_CREATEDAT=""
NALLOW=0
A_CATEGORY=()
A_SCOPE=()
A_PROVENANCE=()
# The snapshot the shift ran under is the live file until the clock-out gate files it, and the
# copy filed in the shift's own folder afterwards. A receipt rendered either side of that move says
# the same thing. An archived snapshot counts only when it is the one the ending marker names: any
# other is a different night's, and this one then has no policy record.
_find_policy() {
local id cand
if [ -f "$LIVE_POLICY" ] && [ ! -L "$LIVE_POLICY" ]; then
POLICY_FILE="$LIVE_POLICY"
return 0
fi
id="$(ns_ended_field "$WORKSPACE" shiftId)"
# A 16-hex token or a UUID, as the schema allows; nothing that could reach a pattern.
case "$id" in '' | *[!0-9a-f-]*) return 0 ;; esac
cand="$(ns_archived_policy "$WORKSPACE" "$id")"
[ -n "$cand" ] || return 0
POLICY_FILE="$cand"
POLICY_WANT_ID="$id"
}
_classify_policy() {
POLICY_KIND=absent
[ -n "$POLICY_FILE" ] || return 0
_ns_policy_load_shift_file "$POLICY_FILE" >/dev/null 2>&1
case "$NS_POLICY_SHIFT_STATE" in
ok) POLICY_KIND=accepted ;;
absent) POLICY_KIND=absent ;;
*) POLICY_KIND=malformed ;;
esac
}
# _match_policy — an archived snapshot filed under this shift's id that names another shift inside
# is not this night's record either.
_match_policy() {
[ -n "$POLICY_WANT_ID" ] && [ "$POLICY_KIND" = accepted ] || return 0
[ "$P_SHIFTID" != "$POLICY_WANT_ID" ] || return 0
POLICY_FILE=""
POLICY_KIND=absent
P_SHIFTID=""
P_CREATEDAT=""
NALLOW=0
A_CATEGORY=()
A_SCOPE=()
A_PROVENANCE=()
}
_load_policy() {
local kind h1 h2 h3 h4 h5 h6 h7
[ -n "$POLICY_FILE" ] || return 0
[ "$POLICY_KIND" = accepted ] || return 0
_emit_policy "$POLICY_FILE" >"$TMPD/policy" 2>/dev/null || return 0
while IFS="$FS" read -r -d "$RS" kind h1 h2 h3 h4 h5 h6 h7; do
case "$kind" in
h)
P_SHIFTID="$h1"
P_CREATEDAT="$h2"
: "$h3" "$h4" "$h5" "$h6" "$h7"
;;
a)
A_CATEGORY[NALLOW]="$h1"
A_SCOPE[NALLOW]="$h2"
A_PROVENANCE[NALLOW]="$h3"
NALLOW=$((NALLOW + 1))
;;
esac
done <"$TMPD/policy"
}
# _resolved NAME -> RVALUE, RSOURCE: one row of the resolved view. Every helper that needs a
# policy answer reads the same resolver, so the receipt cannot report a different one. RSOURCE
# is `one-shift` only when tonight's policy set the value, which is how the receipt tells an
# owner's choice apart from the built-in floor a shift that wrote no policy runs on.
_resolved() {
local line rest meta
RVALUE=""
RSOURCE=""
while IFS= read -r line; do
case "$line" in
"$1="*) ;;
*) continue ;;
esac
rest="${line#"$1="}"
case "$rest" in
*" ("*)
meta="${rest##*" ("}"
RVALUE="${rest%" ($meta"}"
RSOURCE="${meta%%,*}"
RSOURCE="${RSOURCE%)}"
;;
*) RVALUE="$rest" ;;
esac
return 0
done <"$TMPD/resolved"
}
# ---------------------------------------------------------------- owner-authored files
# _parked -> P_COUNT and P_TITLE[], P_DEFAULT[], P_ROLLBACK[]: every decision below the parking
# lot's rule that still waits for the owner, whole. An entry is a `### ` heading and everything
# under it, a top-level bullet and its wrapped and nested lines, or a paragraph; its Default and
# Rollback lines are kept apart. Filed pointers, runtime notices and answered entries are skipped.
P_COUNT=0
P_TITLE=()
P_DEFAULT=()
P_ROLLBACK=()
_parked() {
local title def rb kind
P_COUNT=0
[ -f "$LOT" ] && [ ! -L "$LOT" ] || return 0
sed 's/[[:cntrl:]]/ /g' "$LOT" \
| awk -v op=parked -v fs="$FS" -v dot="$MIDDOT" -v dispositions="$NS_REVIEW_DISPOSITIONS" \
-f "$READ_AWK" >"$TMPD/parked-parse"
while IFS="$FS" read -r kind title def rb; do
[ "$kind" = E ] || continue
[ -n "$title" ] || continue
P_TITLE[P_COUNT]="$title"
P_DEFAULT[P_COUNT]="$def"
P_ROLLBACK[P_COUNT]="$rb"
P_COUNT=$((P_COUNT + 1))
done <"$TMPD/parked-parse"
}
# _snags -> $TMPD/snags: `<finding> FS <disposition>` for each snag-log entry of this shift whose
# disposition is not fixed. An entry is `finding · evidence · disposition · date`; one without a
# disposition is open. This shift's entries are the ones dated on or after the day it started.
_snags() {
: >"$TMPD/snags"
[ -f "$SNAGS" ] && [ ! -L "$SNAGS" ] || return 0
sed 's/[[:cntrl:]]/ /g' "$SNAGS" |
awk -v op=snags -v fs="$FS" -v dot="$MIDDOT" -v day="$SHIFT_DAY" -f "$READ_AWK" >"$TMPD/snags"
}
# _building -> BUILD_TITLE, BUILD_PHASE, BUILD_NEXT: the one opportunity the map
# marks `Status: building`. The template's own illustration sits in an HTML comment and is not
# a live entry, so comment blocks are skipped.
_building() {
local kind value
BUILD_TITLE=""
BUILD_PHASE=""
BUILD_NEXT=""
[ -f "$MAP" ] && [ ! -L "$MAP" ] || return 0
sed 's/[[:cntrl:]]/ /g' "$MAP" | awk -v tab="$(printf '\t')" '
/<!--/ { comment = 1 }
/-->/ { comment = 0; next }
comment { next }
/^###[[:space:]]+/ {
title = $0
sub(/^###[[:space:]]+/, "", title)
sub(/[[:space:]]*$/, "", title)
tline = NR
building = 0
next
}
/^Status:[[:space:]]*building[[:space:]]*$/ {
building = 1
print "T" tab tline tab title
next
}
building && /^Current phase:[[:space:]]*/ {
value = $0
sub(/^Current phase:[[:space:]]*/, "", value)
print "P" tab NR tab value
next
}
building && /^Next:[[:space:]]*/ {
value = $0
sub(/^Next:[[:space:]]*/, "", value)
print "N" tab NR tab value
next
}
' >"$TMPD/building"
while IFS="$(printf '\t')" read -r kind _ value; do
case "$kind" in
T)
[ -z "$BUILD_TITLE" ] || continue
BUILD_TITLE="$value"
;;
P) [ -n "$BUILD_PHASE" ] || BUILD_PHASE="$value" ;;
N) [ -n "$BUILD_NEXT" ] || BUILD_NEXT="$value" ;;
esac
done <"$TMPD/building"
}
# ---------------------------------------------------------------- section 1 facts
# _load_marks -> NMARK, M_EPOCH[], M_LABEL[]: the usage marks in the order they were written.
NMARK=0
M_EPOCH=()
M_LABEL=()
_load_marks() {
local file="$USAGE/marks.tsv" at label
[ -f "$file" ] && [ ! -L "$file" ] || return 0
while IFS="$FS" read -r at label; do
case "$at" in '' | *[!0-9]*) continue ;; esac
M_EPOCH[NMARK]="$at"
M_LABEL[NMARK]="$label"
NMARK=$((NMARK + 1))
done <<EOF
$(awk -F'\t' -v fs="$FS" '{ print $1 fs $2 }' "$file")
EOF
}
# _shift_times -> STARTED, SHIFT_SINCE, ENDED, ENDED_EPOCH, SHIFT_DAY. The start is the arming
# mark, or the policy's createdAt for a shift that kept no usage marks, and commits are counted
# from that same moment; the end is when the clock-out gate wrote .ended. Both are UTC with the
# zone written out.
STARTED=""
SHIFT_SINCE=""
ENDED=""
ENDED_EPOCH=""
SHIFT_DAY=""
_shift_times() {
local at
STARTED="$P_CREATEDAT"
SHIFT_SINCE="$P_CREATEDAT"
if [ "$NMARK" -gt 0 ] && _utc_stamp "${M_EPOCH[0]}"; then
STARTED="$UTC_STAMP"
SHIFT_SINCE="@${M_EPOCH[0]}"
fi
case "$STARTED" in
[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]*) SHIFT_DAY="${STARTED:0:10}" ;;
esac
if [ -f "$ENDED_MARKER" ] && [ ! -L "$ENDED_MARKER" ]; then
at="$(ns_mtime "$ENDED_MARKER")" || at=""
case "$at" in
'' | *[!0-9]*) ;;
*) _utc_stamp "$at" && ENDED_EPOCH="$at" && ENDED="$UTC_STAMP" ;;
esac
fi
}
ENDING=""
_ending() {
local reason="" open=0
if [ -f "$STOP" ] && [ ! -L "$STOP" ]; then
reason="$(sed -n 1p "$STOP" 2>/dev/null)"
_scrub "$reason"
reason="$SCRUBBED"
case "$reason" in
*" $DASH "*) reason="${reason%% "$DASH" *}" ;;
esac
case "$reason" in
deadline) ENDING=deadline ;;
stalled) ENDING=stall ;;
*) ENDING=stop ;;
esac
return 0
fi
if [ -f "$PUNCH" ] && [ ! -L "$PUNCH" ]; then
if ! open="$(ns_open_boxes "$PUNCH")"; then
ENDING=unknown
return 0
fi
fi
if [ "${open:-0}" -eq 0 ]; then
ENDING="done"
else
ENDING=unknown
fi
}
# ---------------------------------------------------------------- rendering
MD=""
add() { MD="$MD$1$NL"; }
SEC=""
sec_add() { SEC="$SEC$1$NL"; }
# sec_flush TITLE — append a non-empty section body under its heading.
sec_flush() {
[ -n "$SEC" ] || return 1
add ''
add "$1"
add ''
MD="$MD$SEC"
SEC=""
return 0
}
# sec_field LABEL VALUE — one bullet when value is present.
sec_field() {
[ -n "$2" ] || return 0
sec_add "- $1: $2"
}
# ---------------------------------------------------------------- sections
_key_by_domain() { # <destination> <domain>
local dest="$1" i=0
: >"$dest"
while [ "$i" -lt "$NREC" ]; do
if [ "${R_DOMAIN[$i]}" = "$2" ]; then
printf '%s%s%s\n' "${R_ID[$i]}" "$FS" "$i" >>"$dest"
fi
i=$((i + 1))
done
}
_lines_shift() {
local i mode ticked open level chosen tooling target gates
SEC=""
sec_field Shift "$P_SHIFTID"
_session_host
sec_field Host "$SESSION_HOST"
target="$(ns_work_target "$WORKSPACE" 2>/dev/null)" || target=""
target="$(ns_native_display_path "$target")"
sec_field 'Work target' "$target"
sec_field Started "$STARTED"
sec_field Ended "$ENDED"
sec_field Ending "$ENDING"
ticked=0
open=0
if [ -f "$PUNCH" ] && [ ! -L "$PUNCH" ]; then
if ticked="$(ns_ticked_boxes "$PUNCH")" && open="$(ns_open_boxes "$PUNCH")"; then
:
else
sec_add "- Items: unknown"
ticked=""
fi
fi
[ -z "$ticked" ] || sec_add "- Items: $ticked ticked, $open open"
mode="$(ns_work_mode "$WORKSPACE" 2>/dev/null)" || mode=repository
if [ "$VIEW" = artifact ] || [ "$mode" = artifact ]; then
sec_add "- Receipts: $(ns_receipts_count "$WORKSPACE")"
else
_commit_count
sec_field Commits "$COMMIT_COUNT"
fi
_policy_profile
_resolved verificationLevel
level="$RVALUE"
chosen="$RSOURCE"
_resolved toolingPolicy
tooling="$RVALUE"
sec_add "- Policy: profile $POLICY_PROFILE, verification $level, tooling $tooling"
: >"$TMPD/allow"
i=0
while [ "$i" -lt "$NALLOW" ]; do
printf '%s (%s, %s)%s%s\n' "${A_CATEGORY[$i]}" "${A_SCOPE[$i]}" "${A_PROVENANCE[$i]}" \
"$FS" "$i" >>"$TMPD/allow"
i=$((i + 1))
done
if [ -s "$TMPD/allow" ]; then
while IFS= read -r line; do
[ -n "$line" ] || continue
sec_add "- Allowance: ${line%%"$FS"*}"
done <<EOF
$(LC_ALL=C sort "$TMPD/allow")
EOF
fi
: >"$TMPD/verified"
i=0
while [ "$i" -lt "$NREC" ]; do
case "${R_DOMAIN[$i]}" in
baseline | checkpoint) ;;
*)
if [ "${R_LADDER[$i]}" = verified-after-change ] && [ -n "${R_SOURCE[$i]}" ]; then
printf '%s\n' "${R_SOURCE[$i]}" >>"$TMPD/verified"
fi
;;
esac
i=$((i + 1))
done
_gate_commands
_join_sorted "$TMPD/gates" "$SOURCE_SEP"
gates="$JOINED"
# A shift that wrote no policy runs on the built-in floor, and the punch list's own Gates
# section is what it was told to run. Naming those commands as the shift's gate keeps the
# receipt from crediting the owner with a decision they never made.
if [ "$chosen" != one-shift ] && [ -n "$gates" ]; then
sec_add "- Gates: $gates ($GATES_FROM)"
fi
_join_sorted "$TMPD/verified" "$SOURCE_SEP"
if [ -n "$JOINED" ]; then
sec_add "- Verified: $JOINED"
elif [ "$chosen" = one-shift ]; then
sec_add "- Verified: none $DASH verification level $level (owner)"
elif [ "$POLICY_KIND" = malformed ]; then
sec_add "- Verified: none $DASH $POLICY_MALFORMED"
else
sec_add "- Verified: none $DASH $NO_POLICY"
fi
if [ "$level" = none ] && [ "$chosen" = one-shift ] && [ -n "$gates" ]; then
sec_add "- Disabled by owner: $gates"
else
sec_add "- Disabled by owner: $NONE"
fi
: >"$TMPD/unavail"
i=0
while [ "$i" -lt "$NREC" ]; do
case "${R_DOMAIN[$i]}" in
baseline | checkpoint) ;;
*)
case "${R_STATUS[$i]}" in
unavailable | unsupported | unmeasured)
[ -n "${R_SOURCE[$i]}" ] && printf '%s\n' "${R_SOURCE[$i]}" >>"$TMPD/unavail"
;;
esac
;;
esac
i=$((i + 1))
done
_join_sorted "$TMPD/unavail" "$SOURCE_SEP"
if [ -n "$JOINED" ]; then
sec_add "- Unavailable: $JOINED"
else
sec_add "- Unavailable: $NONE"
fi
[ -n "$SEC" ]
}
# The whole shift, from the arming mark to the end: working time with every recorded pause listed
# by its reason, and the tokens the host reported, in its own counting. Nothing is priced, and a
# measurement the owner turned off says off.
_lines_usage() {
local start end wall paused=0 work reason secs fields host segs dim v word
SEC=""
[ "$NMARK" -gt 0 ] || return 1
start="${M_EPOCH[0]}"
end="${ENDED_EPOCH:-${M_EPOCH[$((NMARK - 1))]}}"
[ "$end" -ge "$start" ] || end="$start"
if [ "$(ns_report "$WORKSPACE" duration)" = off ]; then
sec_add '- Time: off'
else
wall=$((end - start))
ns_usage_pauses_by_reason "$NS" "$start" "$end" >"$TMPD/pauses" 2>/dev/null || : >"$TMPD/pauses"
while IFS=$'\t' read -r reason secs; do
case "$secs" in '' | *[!0-9]*) continue ;; esac
paused=$((paused + secs))
done <"$TMPD/pauses"
work=$((wall - paused))
[ "$work" -ge 0 ] || work=0
_utc_stamp "$start"
v="$UTC_STAMP"
_utc_stamp "$end"
sec_add "- Span: $v $ARROW $UTC_STAMP"
sec_add "- Working: $(ns_usage_duration "$work")"
if [ "$paused" -gt 0 ]; then
sec_add "- Paused: $(ns_usage_duration "$paused")"
while IFS=$'\t' read -r reason secs; do
case "$secs" in '' | *[!0-9]*) continue ;; esac
sec_add " - $reason: $(ns_usage_duration "$secs")"
done <"$TMPD/pauses"
else
sec_add "- Paused: $NONE"
fi
sec_add "- Wall: $(ns_usage_duration "$wall")"
fi
if [ "$(ns_report "$WORKSPACE" usage)" = off ]; then
sec_add '- Tokens: off'
else
fields="$(ns_usage_total "$NS")" || fields=""
host="$(ns_usage_hosts "$NS" 2>/dev/null)" || host=""
[ -n "$host" ] || host=unknown
segs="$(ns_usage_segments "$NS")"
sec_add ''
sec_add '| Tokens | Amount |'
sec_add '| --- | ---: |'
for dim in $NS_USAGE_DIMENSIONS; do
v="$(ns_usage_field "$fields" "$dim")" || v=""
if [ -n "$v" ]; then v="$(ns_usage_scale "$v")"; else v=unavailable; fi
sec_add "| $(ns_usage_dim_label "$dim") | $v |"
done
sec_add ''
word=segments
[ "$segs" != 1 ] || word=segment
sec_add "$host $MIDDOT $segs $word. $(ns_usage_overlap "${host%% *}")"
fi
[ -n "$SEC" ]
}
# One line per item, in list order, linked to its item receipt where that file exists. The receipt
# carries the item's own cost, sessions, checks and story; this page does not copy them.
_lines_items() {
local state label id
SEC=""
[ -f "$PUNCH" ] && [ ! -L "$PUNCH" ] || return 1
while IFS=$'\t' read -r state label id || [ -n "$state" ]; do
[ -n "$label" ] || continue
_item_link "$label" "$id"
sec_add "- $ITEM_LINK $DASH $state"
done <<EOF
$(ns_item_states "$PUNCH")
EOF
[ -n "$SEC" ]
}
# Where review should start: the three largest changes of the shift, by lines and then files, each
# charged to the item whose span its commit landed in, and the one command that shows the range.
# A commit outside every item's span stands on its own line.
_lines_review() {
local mode target first="" last="" range lines files add del commits kind k1 k2 display n=0
local fcount lcount ccount
SEC=""
mode="$(ns_work_mode "$WORKSPACE" 2>/dev/null)" || mode=repository
if [ "$VIEW" = artifact ] || [ "$mode" = artifact ]; then
sec_add "- $REVIEW_ARTIFACT"
return 0
fi
[ -n "$SHIFT_SINCE" ] || return 1
target="$(ns_work_target "$WORKSPACE" 2>/dev/null)" || target=""
[ -n "$target" ] || return 1
target="$(ns_msys_path "$target")"
# git runs inside the target rather than being handed its path: Git for Windows cannot resolve
# the shell's /c/... form when path conversion is off.
(cd -P "$target" 2>/dev/null && git log --no-merges --reverse --since "$SHIFT_SINCE" \
--format='@@%x09%h%x09%ct%x09%s' --numstat HEAD) >"$TMPD/review-log" 2>/dev/null || return 1
[ -s "$TMPD/review-log" ] || return 1
: >"$TMPD/review-marks"
n=0
while [ "$n" -lt "$NMARK" ]; do
printf '%s%s%s\n' "${M_EPOCH[$n]}" "$FS" "${M_LABEL[$n]}" >>"$TMPD/review-marks"
n=$((n + 1))
done
awk -v op=review -v fs="$FS" -f "$READ_AWK" "$TMPD/review-marks" "$TMPD/review-log" \
>"$TMPD/review-rows" || return 1
IFS="$FS" read -r first last <"$TMPD/review-rows" || return 1
[ -n "$first" ] || return 1
n=0
while IFS="$FS" read -r lines files add del commits kind k1 k2; do
[ "$n" -lt 3 ] || break
n=$((n + 1))
if [ "$kind" = i ]; then
_item_link "$k1" "$(ns_item_id_for "$PUNCH" "$k1")"
display="$ITEM_LINK"
else
display="\`$k1\` $k2"
fi
_count "$files" file
fcount="$COUNTED"
_count "$lines" line
lcount="$COUNTED"
_count "$commits" commit
ccount="$COUNTED"
sec_add "- $display $DASH $fcount, $lcount (+$add/-$del), $ccount"
done <<EOF
$(tail -n +2 "$TMPD/review-rows" | LC_ALL=C sort -t "$FS" -k1,1nr -k2,2nr -k6)
EOF
if (cd -P "$target" 2>/dev/null && git rev-parse --verify --quiet "$first^") >/dev/null 2>&1; then
range="$first^..$last"
else
range="$last"
fi
sec_add "- Whole range: \`git log --stat $range\`"
[ -n "$SEC" ]
}
# What interrupted the night, as the runtime wrote it into the shift log: revivals, API failures,
# stalls, usage limits and how it was stopped.
_lines_interruptions() {
local line
SEC=""
while IFS= read -r line; do
[ -n "$line" ] || continue
sec_add "- $line"
done <<EOF
$(_shift_log_lines interruptions)
EOF
[ -n "$SEC" ]
}
_lines_baseline() {
local pos idx sclass cmd env raw scope
SEC=""
_key_by_domain "$TMPD/k-baseline" baseline
_order "$TMPD/k-baseline"
while IFS= read -r pos; do
[ -n "$pos" ] || continue
idx="${R_INDEX[$pos]}"
_detail "$idx" sourceClass
sclass="$DVAL"
[ -n "$sclass" ] || sclass="${R_SCLASS[$pos]}"
_detail "$idx" command
cmd="$DVAL"
[ -n "$cmd" ] || cmd="${R_SOURCE[$pos]}"
_detail "$idx" environmentDigest
env="$DVAL"
_short_digest "$env"
[ -n "$SHORT_DIGEST" ] || env="$NONE"
env="${SHORT_DIGEST:-$NONE}"
_detail "$idx" rawDigest
raw="$DVAL"
[ -n "$raw" ] || raw="${R_RAWDIGEST[$pos]}"
_short_digest "$raw"
[ -n "$SHORT_DIGEST" ] || raw="$NONE"
raw="${SHORT_DIGEST:-$NONE}"
_detail "$idx" scope
scope="$DVAL"
[ -n "$scope" ] || scope="${R_SCOPE[$pos]}"
[ -n "$scope" ] || scope="$NONE"
sec_add "- ${R_ID[$pos]}: $sclass \`$cmd\` $DASH env $env raw $raw ($scope)"
done <<EOF
$ORDER
EOF
[ -n "$SEC" ]
}
CMP_ROWS=0
CMP_ID=()
CMP_CLASS=()
CMP_DIGEST=()
CMP_SOURCES=()
CMP_LOCATOR=()
_load_comparison_merged() {
local pos id rc cid cclass cdig csrc cloc
CMP_ROWS=0
: >"$TMPD/cmp-seen-ids"
: >"$TMPD/cmp-debt"
_order "$TMPD/k-baseline"
while IFS= read -r pos; do
[ -n "$pos" ] || continue
id="${R_ID[$pos]}"
[ -f "$COMPARE" ] || continue
rc=0
bash "$COMPARE" --project "$WORKSPACE" --baseline "$id" --json \
>"$TMPD/cmp.json" 2>/dev/null || rc=$?
if [ "$rc" -ne 0 ] && [ "$rc" -ne 3 ]; then
continue
fi
if [ "$JSON_TOOL" = jq ]; then
jq -r '.summary.selectedDebtOutstanding[]? // empty' <"$TMPD/cmp.json" >>"$TMPD/cmp-debt" 2>/dev/null ||
:
else
python3 -c 'import json,sys
doc=json.load(open(sys.argv[1]))
for x in (doc.get("summary") or {}).get("selectedDebtOutstanding") or []:
print(x)' "$TMPD/cmp.json" >>"$TMPD/cmp-debt" 2>/dev/null || :
fi
_emit_compare "$TMPD/cmp.json" >"$TMPD/cmp.rows" 2>/dev/null || continue
while IFS="$FS" read -r -d "$RS" cid cclass cdig csrc cloc; do
if grep -Fqx "$cid" "$TMPD/cmp-seen-ids" 2>/dev/null; then
continue
fi
printf '%s\n' "$cid" >>"$TMPD/cmp-seen-ids"
CMP_ID[CMP_ROWS]="$cid"
CMP_CLASS[CMP_ROWS]="$cclass"
CMP_DIGEST[CMP_ROWS]="$cdig"
CMP_SOURCES[CMP_ROWS]="$csrc"
CMP_LOCATOR[CMP_ROWS]="$cloc"
CMP_ROWS=$((CMP_ROWS + 1))
done <"$TMPD/cmp.rows"
done <<EOF
$ORDER
EOF
}
_lines_changed() {
local only="$1" i=0 row line cls cells nfix pos fix loc dig
SEC=""
_key_by_domain "$TMPD/k-baseline" baseline
if [ ! -s "$TMPD/k-baseline" ]; then
return 1
fi
_load_comparison_merged
: >"$TMPD/cmp-order"
i=0
while [ "$i" -lt "$CMP_ROWS" ]; do
if [ "$only" != 1 ] || [ "${CMP_CLASS[$i]}" = regressed ]; then
printf '%s\n' "${CMP_ID[$i]}" >>"$TMPD/cmp-order"
fi
i=$((i + 1))
done
sec_add '| ID | Class | Digest | Sources | Locator |'
sec_add '| --- | --- | --- | --- | --- |'
if [ -s "$TMPD/cmp-order" ]; then
while IFS= read -r row; do
[ -n "$row" ] || continue
i=0
while [ "$i" -lt "$CMP_ROWS" ]; do
[ "${CMP_ID[$i]}" = "$row" ] || { i=$((i + 1)); continue; }
_short_digest "${CMP_DIGEST[$i]}"
_md_cell "$row"
line="| $MD_CELL"
_md_cell "${CMP_CLASS[$i]}"
line="$line | $MD_CELL"
_md_cell "${SHORT_DIGEST:-}"
line="$line | $MD_CELL"
_md_cell "${CMP_SOURCES[$i]}"
line="$line | $MD_CELL"
_md_cell "${CMP_LOCATOR[$i]}"
line="$line | $MD_CELL |"
sec_add "$line"
break
done
done <<EOF
$(LC_ALL=C sort -u "$TMPD/cmp-order")
EOF
else
sec_add "| $DASH | $DASH | $DASH | $DASH | empty |"
fi
sec_add ''
cells=""
for cls in new cleared unchanged regressed unavailable rejected-duplicate parked human-only; do
nfix=0
i=0
while [ "$i" -lt "$CMP_ROWS" ]; do
if [ "$only" = 1 ] && [ "${CMP_CLASS[$i]}" != regressed ]; then
i=$((i + 1))
continue
fi
[ "${CMP_CLASS[$i]}" = "$cls" ] && nfix=$((nfix + 1))
i=$((i + 1))
done
[ -n "$cells" ] && cells="$cells, "
cells="$cells$cls $nfix"
done
sec_add "Summary: $cells"
if [ -s "$TMPD/cmp-debt" ]; then
_join_sorted "$TMPD/cmp-debt" "$SOURCE_SEP"
[ -n "$JOINED" ] && sec_add "Selected debt outstanding: $JOINED"
fi
: >"$TMPD/fixed-ids"
i=0
while [ "$i" -lt "$NREC" ]; do
case "${R_DOMAIN[$i]}" in
baseline | checkpoint) ;;
*)
[ "${R_STATUS[$i]}" = fixed ] && printf '%s\n' "${R_ID[$i]}" >>"$TMPD/fixed-ids"
;;
esac
i=$((i + 1))
done
if [ -s "$TMPD/fixed-ids" ]; then
sec_add ''
while IFS= read -r row; do
[ -n "$row" ] || continue
pos=-1
i=0
while [ "$i" -lt "$NREC" ]; do
case "${R_DOMAIN[$i]}" in
baseline | checkpoint) ;;
*) [ "${R_ID[$i]}" = "$row" ] && pos="$i" ;;
esac
i=$((i + 1))
done
[ "$pos" -ge 0 ] || continue
fix="${R_FIX[$pos]}"
[ -n "$fix" ] || fix="$NONE"
loc="${R_VERIF[$pos]}"
[ -n "$loc" ] || loc="${R_LOCATOR[$pos]}"
[ -n "$loc" ] || loc="$NONE"
_short_digest "${R_DIGEST[$pos]}"
dig="${SHORT_DIGEST:-$NONE}"
sec_add "- $row: $fix$JOINER$loc$JOINER$dig"
done <<EOF
$(LC_ALL=C sort -u "$TMPD/fixed-ids")
EOF
fi
[ -n "$SEC" ]
}
_lines_parked() {
local i=0
SEC=""
while [ "$i" -lt "$P_COUNT" ]; do
sec_add "- ${P_TITLE[$i]}"
[ -n "${P_DEFAULT[$i]}" ] && sec_add " - Default: ${P_DEFAULT[$i]}"
[ -n "${P_ROLLBACK[$i]}" ] && sec_add " - Rollback: ${P_ROLLBACK[$i]}"
i=$((i + 1))
done
[ -n "$SEC" ]
}
_lines_snags() {
local finding disposition
SEC=""
while IFS="$FS" read -r finding disposition; do
[ -n "$finding" ] || continue
sec_add "- $finding $DASH $disposition"
done <"$TMPD/snags"
[ -n "$SEC" ]
}
_lines_unsupported() {
local i=0 id status loc
SEC=""
: >"$TMPD/unsup-ids"
i=0
while [ "$i" -lt "$NREC" ]; do
case "${R_DOMAIN[$i]}" in
baseline | checkpoint) ;;
*)
case "${R_STATUS[$i]}" in
human-only | unsupported | unmeasured)
printf '%s\n' "${R_ID[$i]}" >>"$TMPD/unsup-ids"
;;
esac
;;
esac
i=$((i + 1))
done
while IFS= read -r id; do
[ -n "$id" ] || continue
pos=-1
i=0
while [ "$i" -lt "$NREC" ]; do
[ "${R_ID[$i]}" = "$id" ] && pos="$i"
i=$((i + 1))
done
[ "$pos" -ge 0 ] || continue
status="${R_STATUS[$pos]}"
loc="${R_LOCATOR[$pos]}"
[ -n "$loc" ] || loc="$NONE"
sec_add "- $id: $status$JOINER$loc"
done <<EOF
$(LC_ALL=C sort -u "$TMPD/unsup-ids")
EOF
[ -n "$SEC" ]
}
_lines_next() {
local label handover
SEC=""
if [ -f "$PUNCH" ] && [ ! -L "$PUNCH" ]; then
while IFS=$'\t' read -r label _ || [ -n "$label" ]; do
[ -n "$label" ] || continue
sec_add "- $label"
done <<EOF
$(ns_item_rows "$PUNCH" open)
EOF
fi
if [ -n "$BUILD_TITLE" ] && [ -n "$BUILD_NEXT" ]; then
sec_add "- Building: $BUILD_TITLE $DASH next: $BUILD_NEXT"
fi
handover="$(_shift_log_lines handover)"
[ -z "$handover" ] || sec_add "- Handover: $handover"
[ -n "$SEC" ]
}
# ---------------------------------------------------------------- assembly
_load_ledger
_find_policy
_classify_policy
_load_policy
_match_policy
ns_policy_resolve_table "$WORKSPACE" >"$TMPD/resolved" 2>/dev/null ||
: >"$TMPD/resolved"
_load_marks
_shift_times
_parked
_snags
_building
_ending
add '# Morning receipt'
add 'Receipts:'
add '- [index](./README.md)'
case "$POLICY_KIND" in
accepted) add "- Policy record: accepted" ;;
malformed) add "- Policy record: malformed $DASH $POLICY_MALFORMED" ;;
*) add "- Policy record: absent $DASH the shift wrote no policy" ;;
esac
# Without an explicit --view, the owner's configured reader decides. The sections a view renders
# are the documented factual ones; an owner list picks from those and orders them, and an empty
# list keeps the built-in order for that view.
[ -n "$VIEW" ] || VIEW="$(ns_handoff_view "$WORKSPACE")"
HANDOFF_SECTIONS="$(ns_handoff "$WORKSPACE" sections 2>/dev/null)" || HANDOFF_SECTIONS=""
case "$HANDOFF_SECTIONS" in
'' | '[]') HANDOFF_SECTIONS="" ;;
esac
# _sections_in_order — the owner's names in the order they wrote them, else nothing.
_sections_in_order() {
[ -n "$HANDOFF_SECTIONS" ] || return 1
printf '%s' "$HANDOFF_SECTIONS" | tr ',' '\n' | tr -d '[]" '
}
# The release reader sees regressions only, whichever list chose the section.
REGRESSIONS_ONLY=0
[ "$VIEW" != release ] || REGRESSIONS_ONLY=1
_emit_section() { # <name>
case "$1" in
shift) _lines_shift && sec_flush '## How it ended' ;;
usage) _lines_usage && sec_flush '## Time and tokens' ;;
items) _lines_items && sec_flush '## Items' ;;
review) _lines_review && sec_flush '## Review first' ;;
interruptions) _lines_interruptions && sec_flush '## Interruptions' ;;
parked) _lines_parked && sec_flush '## Decisions for you' ;;
snags) _lines_snags && sec_flush '## Found but not fixed' ;;
baseline) _lines_baseline && sec_flush '## Baseline' ;;
changed) _lines_changed "$REGRESSIONS_ONLY" && sec_flush '## What changed' ;;
unsupported) _lines_unsupported && sec_flush '## Unsupported / unmeasured' ;;
next) _lines_next && sec_flush '## Next step' ;;
esac
}
if [ -n "$HANDOFF_SECTIONS" ]; then
VIEW_SECTIONS="$(_sections_in_order)"
else
case "$VIEW" in
owner) VIEW_SECTIONS='shift usage items review interruptions parked snags baseline changed unsupported next' ;;
reviewer) VIEW_SECTIONS='review baseline changed' ;;
release) VIEW_SECTIONS='shift changed' ;;
artifact) VIEW_SECTIONS='shift usage items review interruptions parked snags unsupported next' ;;
esac
fi
for _sec in $VIEW_SECTIONS; do
_emit_section "$_sec"
done
# One trailing newline, whichever section came last.
while :; do
case "$MD" in
*"$NL$NL") MD="${MD%"$NL"}" ;;
*) break ;;
esac
done
if [ -n "$OUT" ]; then
case "$OUT" in
*/*) mkdir -p "${OUT%/*}" || die "cannot write $OUT" 2 ;;
esac
printf '%s' "$MD" >"$OUT.tmp.$$" || {
rm -f "$OUT.tmp.$$"
die "cannot write $OUT" 2
}
mv "$OUT.tmp.$$" "$OUT" || {
rm -f "$OUT.tmp.$$"
die "cannot write $OUT" 2
}
printf '%s\n' "$OUT"
exit 0
fi
printf '%s' "$MD"
exit 0
SHA-256: a984292ee9f629ed62a52ab8a9d59bdaea30de6854d1d318742571734ad9a67b