carlos / todomvc-bakeoff Public
feat: post-build pipeline (review, fix, security round, readability) and bakeoff record
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+96
−12
builders/post-build.sh+45 −0builders/run-readability.sh+1 −1harness/cmd/bakeoff/main.go+12 −0harness/harness_test.go+8 −0harness/results.go+19 −0results/go.json+11 −11
diff --git a/builders/post-build.sh b/builders/post-build.sh| new file mode 100755 |
| index 0000000..bc072dd |
| --- /dev/null |
| +++ b/builders/post-build.sh |
| @@ -0,0 +1,45 @@ |
| +#!/usr/bin/env bash |
| +# Everything after a build, identically for every app: tokens, live suite, |
| +# Astra review, one fix round, re-review, a security-only round if the |
| +# re-review still finds security problems (then a final review, record |
| +# only), and Fable's blind readability score. |
| +# builders/post-build.sh <slug> |
| +set -uo pipefail |
| +slug=$1 |
| +repo=$(cd "$(dirname "$0")/.." && pwd) |
| +cd "$repo" |
| +app="$repo/.worktrees/$slug/apps/$slug" |
| +status=$(head -1 "$app/STATUS.md" 2>/dev/null || echo "BLOCKED no STATUS.md written") |
| +bakeoff tokens --slug "$slug" --phase build |
| +lang=$(cd harness && go run ./cmd/language "$slug") |
| +case $status in |
| + LIVE*) ;; |
| + *) bakeoff record --slug "$slug" --language "$lang" --status "$status"; echo "$slug: $status"; exit 0 ;; |
| +esac |
| +read -r _ url version <<<"$status" |
| +bakeoff record --slug "$slug" --language "$lang" --status LIVE --app-url "$url" --version "$version" |
| +suite() { scripts/test-app.sh "$url" "$TMPDIR/$slug-suite.json" >/dev/null 2>&1; bakeoff behaviour --slug "$slug" --file "$TMPDIR/$slug-suite.json"; rm -f "$TMPDIR/$slug-suite.json"; } |
| +suite |
| +builders/run-review.sh "$slug" 1 |
| +if [ "$(tr -d '[:space:]' < "builders/reviews/$slug.1.md")" != "Nofindings." ]; then |
| + builders/run-fix.sh "$slug" fix "builders/reviews/$slug.1.md" |
| + bakeoff tokens --slug "$slug" --phase fix |
| +fi |
| +builders/run-review.sh "$slug" 2 |
| +last=2 |
| +if ! bakeoff findings --slug "$slug" --file "builders/reviews/$slug.2.md"; then |
| + grep -E '^\s*[0-9]+\.\s+\[security\]' -A20 "builders/reviews/$slug.2.md" | awk '/^\s*[0-9]+\.\s+\[/{keep=/\[security\]/} keep' > "$TMPDIR/$slug.security.md" |
| + builders/run-fix.sh "$slug" security "$TMPDIR/$slug.security.md" |
| + rm -f "$TMPDIR/$slug.security.md" |
| + bakeoff tokens --slug "$slug" --phase security |
| + builders/run-review.sh "$slug" 3 |
| + last=3 |
| + bakeoff findings --slug "$slug" --file "builders/reviews/$slug.3.md" |
| +fi |
| +bakeoff tokens --slug "$slug" --phase review |
| +status=$(head -1 "$app/STATUS.md") |
| +read -r _ url version <<<"$status" |
| +bakeoff record --slug "$slug" --version "$version" |
| +suite |
| +APP_DIR="$app" builders/run-readability.sh "$slug" |
| +echo "$slug: post-build done (reviews: $last)" |
diff --git a/builders/run-readability.sh b/builders/run-readability.sh| index 902649f..e8362e3 100755 |
| --- a/builders/run-readability.sh |
| +++ b/builders/run-readability.sh |
| @@ -12,7 +12,7 @@ trap 'rm -rf "$work"' EXIT |
| { |
| sed "s/{LANGUAGE}/$language/g" builders/prompts/readability.md |
| echo |
| - bakeoff source --slug "$slug" | sed -e "s/todo-$slug/todo-app/g" -e "s|apps/$slug|apps/app|g" |
| + bakeoff source --slug "$slug" --dir "${APP_DIR:-apps/$slug}" | sed -e "s/todo-$slug/todo-app/g" -e "s|apps/$slug|apps/app|g" |
| } > "$work/prompt.md" |
| replies=() |
| for i in 1 2 3; do |
diff --git a/harness/cmd/bakeoff/main.go b/harness/cmd/bakeoff/main.go| index 1883503..398c420 100644 |
| --- a/harness/cmd/bakeoff/main.go |
| +++ b/harness/cmd/bakeoff/main.go |
| @@ -36,6 +36,13 @@ func main() { |
| phase := fs.String("phase", "", "tokens: build|fix|security|review") |
| logs := fs.String("logs", "builders/logs", "tokens: log directory") |
| file := fs.String("file", "", "behaviour: Playwright JSON report") |
| + var rec b.Record |
| + fs.StringVar(&rec.Language, "language", "", "record: display name") |
| + fs.StringVar(&rec.Status, "status", "", "record: LIVE or BLOCKED <reason>") |
| + fs.StringVar(&rec.Version, "version", "", "record: deployed version") |
| + fs.StringVar(&rec.Stack, "stack", "", "record: one-line stack summary") |
| + fs.StringVar(&rec.Source, "source", "", "record: source URL") |
| + recURL := fs.String("app-url", "", "record: live URL") |
| replies := fs.String("replies", "", "readability: comma-separated files, one scorer reply each") |
| fs.Parse(os.Args[2:]) |
| if *slug == "" && cmd != "report" { |
| @@ -70,6 +77,11 @@ func main() { |
| err = renderReport(*file) |
| case "findings": |
| err = findings(*slug, *file) |
| + case "record": |
| + if rec.URL = *recURL; rec.URL == "" && strings.HasPrefix(rec.Status, "LIVE") { |
| + rec.URL = *url |
| + } |
| + err = b.Update(*slug, func(r *b.AppResult) { b.ApplyRecord(r, rec) }) |
| case "source": |
| err = source(*dir) |
| case "readability": |
diff --git a/harness/harness_test.go b/harness/harness_test.go| index 0a4618c..c5a5953 100644 |
| --- a/harness/harness_test.go |
| +++ b/harness/harness_test.go |
| @@ -254,3 +254,11 @@ func TestTokensSince(t *testing.T) { |
| t.Fatalf("got %+v", got) |
| } |
| } |
| + |
| +func TestApplyRecordKeepsUnsetFields(t *testing.T) { |
| + r := AppResult{Language: "Go", Status: "LIVE", URL: "u", Version: "v1"} |
| + ApplyRecord(&r, Record{Version: "v2"}) |
| + if r.Language != "Go" || r.Status != "LIVE" || r.URL != "u" || r.Version != "v2" { |
| + t.Fatalf("got %+v", r) |
| + } |
| +} |
diff --git a/harness/results.go b/harness/results.go| index 4ecb228..8e810ad 100644 |
| --- a/harness/results.go |
| +++ b/harness/results.go |
| @@ -72,3 +72,22 @@ func UpdateIn(dir, slug string, f func(*AppResult)) error { |
| // Update applies f to results/<slug>.json. |
| func Update(slug string, f func(*AppResult)) error { return UpdateIn(ResultsDir, slug, f) } |
| + |
| +// Record is a partial update of an app's identity fields; empty fields |
| +// leave the stored value alone. |
| +type Record struct{ Language, Status, URL, Version, Stack, Source string } |
| + |
| +// ApplyRecord copies the non-empty fields of rec into r. |
| +func ApplyRecord(r *AppResult, rec Record) { |
| + set := func(dst *string, v string) { |
| + if v != "" { |
| + *dst = v |
| + } |
| + } |
| + set(&r.Language, rec.Language) |
| + set(&r.Status, rec.Status) |
| + set(&r.URL, rec.URL) |
| + set(&r.Version, rec.Version) |
| + set(&r.Stack, rec.Stack) |
| + set(&r.Source, rec.Source) |
| +} |
diff --git a/results/go.json b/results/go.json| index 389aa63..d8ff348 100644 |
| --- a/results/go.json |
| +++ b/results/go.json |
| @@ -1,9 +1,9 @@ |
| { |
| "Slug": "go", |
| - "Language": "", |
| - "Status": "", |
| - "URL": "", |
| - "Version": "", |
| + "Language": "Go", |
| + "Status": "LIVE", |
| + "URL": "https://todo-go.bfj.oncarlos.com", |
| + "Version": "8a42c8d", |
| "Stack": "", |
| "Source": "", |
| "BehaviourJSOn": { |
| @@ -89,13 +89,13 @@ |
| "CostUSD": 0 |
| }, |
| "Readability": { |
| - "Naming": 0, |
| - "Structure": 0, |
| - "Traceability": 0, |
| - "Errors": 0, |
| - "Comments": 0, |
| - "Overall": 0, |
| - "Notes": "" |
| + "Naming": 4, |
| + "Structure": 4, |
| + "Traceability": 5, |
| + "Errors": 4, |
| + "Comments": 5, |
| + "Overall": 8, |
| + "Notes": "Strongest: comments consistently explain why (migration policy, single-writer pool, Origin check, focusout during swap), and the package docs state the security model up front. Request flow is short: route in New -\u003e itemAction -\u003e action -\u003e Store method -\u003e render \"list\". Error path is explicit via ErrNotFound and fail, with buffered rendering for clean 500s. Weakest: action/itemAction wrappers with inline closures and two differing callback signatures add a layer to step through; `one`, `w`/`r` fields and `bumpMarker` are terse or odd. loadStatic ignores fs.Sub and WalkDir errors, and the view struct mixes filter state, counts and Version. app.js swap/draft restoration is dense." |
| }, |
| "ReviewFindings": [ |
| "[spec] [apps/go/main.go:114](/home/paulca/amadan.net/carlos/todomvc-bakeoff/.worktrees/go/apps/go/main.go:114): `checkLoopback` accepts any loopback address, including `127.0.0.2` and `::1`. The brief explicitly requires binding only to `127.0.0.1`. These alternative `--listen` addresses pass validation and start a listener; restrict validation to the required address." |