mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-15 17:35:29 +02:00
feat(review): content-addressed staleness via working-tree fingerprint
Review records now bind to the content they were made on. bin/gstack-review-log
stamps every appended record with commit_full, tree, dirty (informational) and
wtree — a working-tree fingerprint from the new bin/gstack-wtree (temp index
seeded from HEAD + git add -A + write-tree). The binding fields are computed
authoritatively; caller-supplied values for those keys are ignored, so a stale
rendered template or a forged field can't bind a record to content it wasn't
made on.
Why a working-tree fingerprint instead of HEAD^{tree}: committing identical
content doesn't change it (a record made on a dirty tree stays valid after the
same content is committed), untracked new source files DO change it (new code
can't hide from freshness), and gitignored scratch stays out. Rebase, amend
and squash with identical content grade CURRENT instead of stale.
Grading: the dashboard (scripts/resolvers/review.ts) and /land-and-deploy Step
3.5a apply a content-first rule to diff-scoped review rows — wtree match with
both sides clean is CURRENT, full stop. Plan-tier reviews grade a plan file,
not the repo tree, so they keep the 7-day logic (optional plan_sha256 caller
field noted). The rev-list fallback no longer errors when the stored commit
was rebased away: it grades UNKNOWN and treats it as stale.
bin/gstack-review-read emits ---WTREE---/---TREE---/---DIRTY--- so graders
consume one tool output. Old records without wtree fall back to the existing
heuristics; no migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
455c805125
commit
8e4a4f7c3d
+39
-4
@@ -1,21 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-review-log — atomically log a review result
|
||||
# Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}'
|
||||
#
|
||||
# Binding fields (content-addressed staleness): every appended record is
|
||||
# stamped with commit_full, tree, dirty (informational) and wtree (the GATING
|
||||
# working-tree fingerprint from bin/gstack-wtree). These are computed
|
||||
# AUTHORITATIVELY here — caller-supplied values for the four keys are ignored,
|
||||
# so a stale rendered template (or a forged field) cannot bind a record to
|
||||
# content it wasn't made on. All other caller fields pass through untouched.
|
||||
# Outside a git repo the fields are simply omitted (legacy consumers fall back
|
||||
# to their heuristics).
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||
mkdir -p "$GSTACK_HOME/projects/$SLUG"
|
||||
|
||||
# Validate: input must be parseable JSON (reject malformed or injection attempts)
|
||||
INPUT="$1"
|
||||
if ! printf '%s' "$INPUT" | bun -e "JSON.parse(await Bun.stdin.text())" 2>/dev/null; then
|
||||
|
||||
# Compute binding fields (best-effort; empty outside a git repo).
|
||||
COMMIT_FULL=$(git rev-parse HEAD 2>/dev/null || true)
|
||||
TREE=""
|
||||
WTREE=""
|
||||
DIRTY=""
|
||||
if [ -n "$COMMIT_FULL" ]; then
|
||||
TREE=$(git rev-parse 'HEAD^{tree}' 2>/dev/null || true)
|
||||
WTREE=$("$SCRIPT_DIR/gstack-wtree" 2>/dev/null || true)
|
||||
if [ -n "$(git status --porcelain -uno 2>/dev/null | head -1)" ]; then
|
||||
DIRTY="true"
|
||||
else
|
||||
DIRTY="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate (reject malformed or injection attempts) AND stamp in one pass.
|
||||
# Caller values for the binding keys are dropped before stamping.
|
||||
STAMPED=$(printf '%s' "$INPUT" | GSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" GSTACK_STAMP_TREE="$TREE" GSTACK_STAMP_WTREE="$WTREE" GSTACK_STAMP_DIRTY="$DIRTY" bun -e "
|
||||
const rec = JSON.parse(await Bun.stdin.text());
|
||||
for (const k of ['commit_full', 'tree', 'wtree', 'dirty']) delete rec[k];
|
||||
const env = process.env;
|
||||
if (env.GSTACK_STAMP_COMMIT_FULL) rec.commit_full = env.GSTACK_STAMP_COMMIT_FULL;
|
||||
if (env.GSTACK_STAMP_TREE) rec.tree = env.GSTACK_STAMP_TREE;
|
||||
if (env.GSTACK_STAMP_WTREE) rec.wtree = env.GSTACK_STAMP_WTREE;
|
||||
if (env.GSTACK_STAMP_DIRTY) rec.dirty = env.GSTACK_STAMP_DIRTY === 'true';
|
||||
console.log(JSON.stringify(rec));
|
||||
" 2>/dev/null) || {
|
||||
# Not valid JSON — refuse to append
|
||||
echo "gstack-review-log: invalid JSON, skipping" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
|
||||
echo "$STAMPED" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
|
||||
|
||||
# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off).
|
||||
"$SCRIPT_DIR/gstack-brain-enqueue" "projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null &
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-review-read — read review log and config for dashboard
|
||||
# Usage: gstack-review-read
|
||||
#
|
||||
# Emits, in order: the raw reviews JSONL, ---CONFIG--- (skip_eng_review),
|
||||
# ---HEAD--- (short sha), ---WTREE--- (current working-tree fingerprint from
|
||||
# bin/gstack-wtree, or "unknown"), ---TREE--- (HEAD tree, informational) and
|
||||
# ---DIRTY--- (tracked-file dirty flag). Consumers grade diff-scoped review
|
||||
# rows CURRENT when a record's `wtree` equals ---WTREE---; everything needed
|
||||
# for that rule ships in this one output so graders run no extra commands.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
@@ -10,3 +17,9 @@ echo "---CONFIG---"
|
||||
"$SCRIPT_DIR/gstack-config" get skip_eng_review 2>/dev/null || echo "false"
|
||||
echo "---HEAD---"
|
||||
git rev-parse --short HEAD 2>/dev/null || echo "unknown"
|
||||
echo "---WTREE---"
|
||||
"$SCRIPT_DIR/gstack-wtree" 2>/dev/null || echo "unknown"
|
||||
echo "---TREE---"
|
||||
git rev-parse 'HEAD^{tree}' 2>/dev/null || echo "unknown"
|
||||
echo "---DIRTY---"
|
||||
if [ -n "$(git status --porcelain -uno 2>/dev/null | head -1)" ]; then echo "true"; else echo "false"; fi
|
||||
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-wtree — print a working-tree CONTENT fingerprint (a git tree hash).
|
||||
#
|
||||
# Builds a temp index seeded from HEAD, stages the full working tree into it
|
||||
# (`git add -A`, so .gitignore'd scratch stays out and UNTRACKED source is
|
||||
# included), and prints `git write-tree` of that index. Properties that make
|
||||
# this the right staleness fingerprint, vs `git rev-parse HEAD^{tree}`:
|
||||
#
|
||||
# - Committing identical content does NOT change the fingerprint, so a
|
||||
# record made on a dirty tree stays valid after the exact same content is
|
||||
# committed (the /ship Step 5 -> Step 16 case).
|
||||
# - Untracked new source files DO change the fingerprint, so "tests passed"
|
||||
# can't stay FRESH after a new file appears.
|
||||
# - Rebase/amend/squash that preserve content do not change it.
|
||||
#
|
||||
# The real repo index is never touched. Staged blobs land in the object store
|
||||
# as unreachable objects and get gc'd like stash churn. Exit 1 outside a git
|
||||
# repo or in a repo with no commits — callers treat that as "no fingerprint".
|
||||
set -euo pipefail
|
||||
|
||||
TOP=$(git rev-parse --show-toplevel 2>/dev/null) || exit 1
|
||||
TMPIDX=$(mktemp "${TMPDIR:-/tmp}/gstack-wtree-XXXXXX")
|
||||
trap 'rm -f "$TMPIDX"' EXIT
|
||||
export GIT_INDEX_FILE="$TMPIDX"
|
||||
git -C "$TOP" read-tree HEAD 2>/dev/null || exit 1
|
||||
git -C "$TOP" add -A 2>/dev/null || exit 1
|
||||
git -C "$TOP" write-tree 2>/dev/null
|
||||
Reference in New Issue
Block a user