#!/usr/bin/env bash # Phase 13 fail-closed gate (wishlist, notifications, CSV import and export, # credentials, onboarding and public routes: API-03 to API-07). # # Every stage exits non-zero on a failing command, a go test run that fails, # skips, matches zero tests or prints "no tests to run", a named test that # did not pass, a data race, a parity count other than the expected one, a # coverage floor missed, a corpus secret or an evidence gap. --self-test # proves each detector fails closed on planted inputs. # # --removal is the anchor-exact mutation harness behind the RC rows of # 13-SECURITY-REVIEW.md: it removes one protection at a time, requires its # named test to fail on an assertion, and restores the file byte for byte # (checked with cmp). It refuses a file with uncommitted changes and edits # tracked source while it runs, so it is not part of --all. # # Framework commands run in summercms.go; application commands run in the # sibling repository named by PHASE13_APP (default ../fonoteka.go). set -euo pipefail # A colour-forcing shell variable changes the output some framework tests # compare byte for byte; the gate runs without it. unset FORCE_COLOR ROOT="${PHASE13_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" APP="${PHASE13_APP:-$(cd "$ROOT/../fonoteka.go" && pwd)}" PHASE_DIR="${PHASE13_PHASE_DIR:-$ROOT/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public}" REVIEW="$PHASE_DIR/13-SECURITY-REVIEW.md" VALIDATION="$PHASE_DIR/13-VALIDATION.md" APP_PLUGINS=(./plugins/golem15/fonoteka/... ./plugins/golem15/user/...) EXPECTED_PORTED=157 EXPECTED_PENDING=14 COVERAGE_FLOOR=80 # The user plugin's controllers package before Phase 13 (measured at # sm-user-plugin c258e9f); Phase 13 may not lower it. USER_CONTROLLERS_PRE=68.8 XTEXT_VERSION="v0.42.0" FUZZ_CORPUS="plugins/golem15/fonoteka/testdata/fuzz" usage() { cat >&2 <<'EOF' usage: check-phase13.sh --self-test check-phase13.sh --go check-phase13.sh --parity check-phase13.sh --named check-phase13.sh --removal check-phase13.sh --coverage check-phase13.sh --evidence check-phase13.sh --all EOF exit 2 } # phase13_detect reads go test -json. Exit 1 fail, 2 skip, 3 zero tests or # "no tests to run", 4 non-JSON, 5 a required test did not pass, 6 a data # race was reported. PHASE13_REQUIRE lists tests that must pass. phase13_detect() { python3 - "$1" <<'PY' import json, os, sys path = sys.argv[1] require = set(os.environ.get("PHASE13_REQUIRE", "").split()) passed = set() failed_tests, failed_pkgs = {}, [] build_failed = False with open(path, encoding="utf-8", errors="replace") as fh: for raw in fh: line = raw.strip() if not line.startswith("{"): continue try: ev = json.loads(line) except json.JSONDecodeError: print("refuse: non-json test output", file=sys.stderr) sys.exit(4) action = ev.get("Action") test = ev.get("Test") or "" pkg = ev.get("Package") or "" if action == "build-fail": build_failed = True if action == "output": text = ev.get("Output") or "" if "no tests to run" in text: print(f"refuse: no tests to run in {pkg}", file=sys.stderr) sys.exit(3) if "WARNING: DATA RACE" in text: print(f"refuse: data race in {pkg} {test}", file=sys.stderr) sys.exit(6) if action == "skip" and test: print(f"refuse: skipped {pkg} {test}", file=sys.stderr) sys.exit(2) if action == "fail": if ev.get("FailedBuild"): build_failed = True if test: failed_tests.setdefault(pkg, []).append(test) else: failed_pkgs.append(pkg) if action == "pass" and test: passed.add(test) if build_failed: print("refuse: build failed", file=sys.stderr) sys.exit(1) for pkg, tests in failed_tests.items(): for test in tests: print(f"refuse: failed {pkg} {test}", file=sys.stderr) sys.exit(1) for pkg in failed_pkgs: print(f"refuse: failed {pkg or 'unknown package'}", file=sys.stderr) sys.exit(1) missing = sorted(name for name in require if name not in passed) if missing: print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr) sys.exit(5) if not passed: print("refuse: zero tests", file=sys.stderr) sys.exit(3) PY } # phase13_go DIR ARGS... runs go test -json -count=1 ARGS through the # detector. With PHASE13_KEEP set, the JSON log is copied there. phase13_go() { local dir="$1" shift local log err log="$(mktemp)" err="$(mktemp)" set +e (cd "$dir" && go test -json -count=1 "$@") >"$log" 2>"$err" local rc=$? set -e local dc=0 phase13_detect "$log" || dc=$? if [[ "$dc" -ne 0 || "$rc" -ne 0 ]]; then cat "$err" >&2 || true tail -n 40 "$log" >&2 || true rm -f "$log" "$err" echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 exit 1 fi if [[ -n "${PHASE13_KEEP:-}" ]]; then cp "$log" "$PHASE13_KEEP" fi rm -f "$log" "$err" } # phase13_tests DIR PKG [-race] TEST... requires every named test to run and # pass, each matched by its exact name. phase13_tests() { local dir="$1" pkg="$2" shift 2 local extra=() if [[ "${1:-}" == "-race" ]]; then extra=(-race) shift fi local names="$*" local regex="^($(tr ' ' '|' <<<"$names"))\$" PHASE13_REQUIRE="$names" phase13_go "$dir" "$pkg" "${extra[@]}" -run "$regex" } expect_detect() { local name="$1" want="$2" payload="$3" local log dc=0 log="$(mktemp)" printf '%s\n' "$payload" >"$log" phase13_detect "$log" 2>/dev/null || dc=$? rm -f "$log" if [[ "$dc" -ne "$want" ]]; then echo "refuse: self-test $name: detector exit $dc, want $want" >&2 exit 1 fi } # The named tests: every test 13-VALIDATION.md names, by package. The # evidence stage refuses a validation or review row naming a test missing # here. NAMED_ROOT_SURF="TestOverlappingConstrainedRoutes TestOverlapConstraintFallsThrough TestOverlapFamilyOfThree TestOverlapHeadAndAllow TestOverlapFamilyAcrossPlugins TestOverlapUnsupportedShapes" NAMED_ROOT_CONGA="TestUnregisteredKindWithWorker TestUnregisteredKindWithoutWorker TestUnregisteredKindRefusalAndDelay" NAMED_ROOT_LAGOON="TestValidateRequestProhibited TestValidateRequestProhibitedNested" NAMED_ROOT_TIDE="TestNormalizeContentDispositionDate TestNormalizeNotificationPublication TestNormalizePhase13Edges" NAMED_APP_USER="TestRegisterEventPayload TestRegisterUserExports" NAMED_APP_FONOTEKA="TestWishlistOverlapPatternsDispatch TestJobContractDispatchWhileWorkerRuns TestNotificationsRoutes TestCredentialsCRUD TestCredentialSecretsNeverSerialized TestResolveAIConfigPrecedence TestDiscogsSharedMirror TestBootstrapConcurrent TestRegisterInvitationListener TestInspectInvitation TestWishlistOwnListAndShow TestWishlistItemAddedOncePerPath TestDigestCoalescing TestWishlistShareSettingsHousehold TestReserveConcurrent TestRevealIdempotent TestReservationMask TestWishlistSubscriptions TestPurchaseSideEffects TestPurchaseMailAfterCommit TestWishlistOverlapRoutesAssembled TestCsvExport TestCsvStoreAndShow TestCsvImportScope TestCsvCommitCAS TestCsvJobRows TestCsvCancel TestCsvRowPickSeam TestPublicResolve TestPubfailCounter TestPubfailCounterPerApp TestPublicBucketsPerRoute TestPublicAlbumFieldSet TestPublicAlbumsIndex TestPublicAlbumsEngine TestRouteTablePhase13 TestRouteTablePhase12 TestFullRouteTableAuthGroupMutualExclusivity FuzzWriteEndpoints TestPhase13Threats TestPhase13EmptyBodies TestPhase13Boundaries TestPhase13HandlersFailClosed TestPhase09SecurityRoutes" NAMED_APP_CLASSES="TestJobContract TestPhase13ReservationMask TestPhase13PubfailWindow TestPhase13SmallHelpers TestPhase13NilHandles" NAMED_APP_CSV="TestPHPFputcsv TestCsvParserTruthTable TestCsvDetectorTruthTable TestCsvPHPCasts TestCsvOrderedMap" NAMED_APP_API="TestCsvMappingInput" NAMED_APP_MIDDLEWARE="TestPublicShareHeadersRewrites429 TestPublicShareHeadersLeaves200Body TestPublicShareHeadersExactBytes" NAMED_APP_PARITY="TestCheckCorpusPortedCaseStatus TestParityCorpus TestBroadcastGoldens TestFonotekaNuxtFlows TestUserAPINuxtFlows" all_named() { echo "$NAMED_ROOT_SURF $NAMED_ROOT_CONGA $NAMED_ROOT_LAGOON $NAMED_ROOT_TIDE $NAMED_APP_USER $NAMED_APP_FONOTEKA $NAMED_APP_CLASSES $NAMED_APP_CSV $NAMED_APP_API $NAMED_APP_MIDDLEWARE $NAMED_APP_PARITY" } # module_pin MODLIST: golang.org/x/text stays at the audited version # (T-13-SC: the CSV decoder's charmap). REASON_PIN="golang.org/x/text is not pinned at $XTEXT_VERSION" module_pin() { local modlist="$1" hits hits="$(grep -E '^golang\.org/x/text ' "$modlist" | sort -u || true)" if [[ -z "$hits" ]] || grep -vqE "^golang\.org/x/text $XTEXT_VERSION\$" <<<"$hits"; then echo "refuse: hygiene: $REASON_PIN: ${hits:-}" >&2 return 1 fi return 0 } run_go() { (cd "$ROOT" && go vet ./...) phase13_go "$ROOT" ./... (cd "$APP" && go vet ./... "${APP_PLUGINS[@]}") phase13_go "$APP" ./... "${APP_PLUGINS[@]}" local modlist modlist="$(mktemp)" (cd "$ROOT" && go list -m all) >"$modlist" (cd "$APP" && go list -m all) >>"$modlist" if ! module_pin "$modlist"; then rm -f "$modlist" exit 1 fi rm -f "$modlist" echo "phase13 go passed" } # corpus_scan DIR: the fuzz seed corpus holds synthetic values only # (T-13-36): no 64-hex token, inv_ personal token, JWT or bearer header. corpus_scan() { python3 - "$1" <<'PY' import os, re, sys root = sys.argv[1] if not os.path.isdir(root): print(f"refuse: fuzz corpus {root} is missing", file=sys.stderr) sys.exit(1) patterns = [ ("64-hex value", re.compile(r"(?&2 exit 1 fi local log log="$(mktemp)" PHASE13_KEEP="$log" PHASE13_REQUIRE="$PARITY_REQUIRE" \ phase13_go "$APP" ./parity -run '^(TestParityCorpus|TestBroadcastGoldens|TestFonotekaNuxtFlows|TestUserAPINuxtFlows|TestCheckCorpusPortedCaseStatus)$' if ! corpus_coverage "$log"; then rm -f "$log" exit 1 fi rm -f "$log" (cd "$APP" && go run ./parity/check_corpus.go --manifest parity/manifest.yaml --require-recorded --check-secrets) corpus_scan "$APP/$FUZZ_CORPUS" PHASE13_REQUIRE="TestDocsTree" phase13_go "$ROOT" ./cmd/summer -run '^TestDocsTree$' (cd "$ROOT" && go run ./cmd/summer docs:build --check) echo "phase13 parity passed ($n ported, 0 failing, $p pending)" } run_named() { phase13_tests "$ROOT" ./modules/surf $NAMED_ROOT_SURF phase13_tests "$ROOT" ./modules/conga $NAMED_ROOT_CONGA phase13_tests "$ROOT" ./modules/lagoon $NAMED_ROOT_LAGOON phase13_tests "$ROOT" ./modules/tide $NAMED_ROOT_TIDE phase13_tests "$APP" ./plugins/golem15/user $NAMED_APP_USER phase13_tests "$APP" ./plugins/golem15/fonoteka -race $NAMED_APP_FONOTEKA phase13_tests "$APP" ./plugins/golem15/fonoteka/classes $NAMED_APP_CLASSES phase13_tests "$APP" ./plugins/golem15/fonoteka/classes/csv $NAMED_APP_CSV phase13_tests "$APP" ./plugins/golem15/fonoteka/controllers/api $NAMED_APP_API phase13_tests "$APP" ./plugins/golem15/fonoteka/middleware $NAMED_APP_MIDDLEWARE phase13_tests "$APP" ./parity $NAMED_APP_PARITY echo "phase13 named passed" } # coverage_report FLOOR PROFILE... prints one line per package of the merged # profiles (a block counts as covered when any profile covered it) and # refuses any package below FLOOR percent. COVERAGE_ONLY limits the report # to packages whose import path ends with one of its words. coverage_report() { python3 - "$@" <<'PY' import collections, os, sys floor = float(sys.argv[1]) only = os.environ.get("COVERAGE_ONLY", "").split() blocks = {} for path in sys.argv[2:]: for line in open(path): if line.startswith("mode:") or not line.strip(): continue loc, n, c = line.rsplit(" ", 2) n, c = int(n), int(c) prev = blocks.get(loc, (n, 0)) blocks[loc] = (n, max(prev[1], c)) total, covered = collections.Counter(), collections.Counter() for loc, (n, c) in blocks.items(): pkg = loc.split(":")[0].rsplit("/", 1)[0] if only and not any(pkg.endswith(o) for o in only): continue total[pkg] += n if c: covered[pkg] += n if not total: print("refuse: coverage profile is empty", file=sys.stderr) sys.exit(1) low = [] for pkg in sorted(total): pct = 100.0 * covered[pkg] / total[pkg] print(f"coverage {pkg} {pct:.1f}%") if pct < floor: low.append(f"{pkg} {pct:.1f}%") if low: print(f"refuse: below the {floor:.1f}% coverage floor: " + ", ".join(low), file=sys.stderr) sys.exit(1) PY } # func_floor FLOOR FILE: reads go tool cover -func output on stdin and # refuses any function of FILE below FLOOR percent, or none at all. func_floor() { python3 -c ' import sys floor, suffix = float(sys.argv[1]), sys.argv[2] seen = 0 low = [] for line in sys.stdin: parts = line.split() if len(parts) < 3 or not parts[0].split(":")[0].endswith(suffix): continue seen += 1 pct = float(parts[-1].rstrip("%")) print(f"coverage {parts[0]} {parts[1]} {pct:.1f}%") if pct < floor: low.append(f"{parts[1]} {pct:.1f}%") if not seen: print(f"refuse: no function of {suffix} in the profile", file=sys.stderr) sys.exit(1) if low: print(f"refuse: below the {floor:.0f}% function floor in {suffix}: " + ", ".join(low), file=sys.stderr) sys.exit(1) ' "$@" } # cover_profile DIR OUT ARGS... writes a coverage profile of go test ARGS. cover_profile() { local dir="$1" out="$2" shift 2 local log log="$(mktemp)" if ! (cd "$dir" && go test -count=1 -coverprofile="$out" "$@") >"$log" 2>&1; then tail -n 40 "$log" >&2 rm -f "$log" echo "refuse: go test -coverprofile $* in $dir" >&2 exit 1 fi rm -f "$log" } run_coverage() { local dir dir="$(mktemp -d)" trap 'rm -rf "$dir"' RETURN local pkg i=0 # Framework packages changed in Phase 13: each package's own tests. for pkg in ./modules/surf ./modules/conga ./modules/lagoon ./modules/tide; do i=$((i + 1)) cover_profile "$ROOT" "$dir/root$i.out" "$pkg" done coverage_report "$COVERAGE_FLOOR" "$dir"/root*.out # Application packages: every test of the plugin that exercises them. cover_profile "$APP" "$dir/app.out" ./plugins/golem15/fonoteka/... \ -coverpkg=./plugins/golem15/fonoteka/classes,./plugins/golem15/fonoteka/classes/csv,./plugins/golem15/fonoteka/controllers/api,./plugins/golem15/fonoteka/middleware coverage_report "$COVERAGE_FLOOR" "$dir/app.out" # The shared user plugin: classes at the floor, every registration # export at the floor, the controllers package not below its pre-phase # value. cover_profile "$APP" "$dir/user.out" ./plugins/golem15/user/... \ -coverpkg=./plugins/golem15/user/classes,./plugins/golem15/user/controllers COVERAGE_ONLY="/classes" coverage_report "$COVERAGE_FLOOR" "$dir/user.out" COVERAGE_ONLY="/controllers" coverage_report "$USER_CONTROLLERS_PRE" "$dir/user.out" (cd "$APP" && go tool cover -func="$dir/user.out") | func_floor "$COVERAGE_FLOOR" controllers/registration.go echo "phase13 coverage passed" } # removal_table: the RC rows of 13-SECURITY-REVIEW.md. Fields: id, threat, # repo (root|app|script, or rootapp for a framework file whose test runs in # the application), file, anchor, replacement, package, test regex. # Anchors must occur exactly once. removal_table() { cat <<'EOF' [ ["RC-01", "T-13-23", "root", "modules/surf/overlap.go", "\t\tif !m.constraintsMatch(vals) {\n\t\t\tcontinue\n\t\t}\n", "", "./modules/surf", "^TestOverlapConstraintFallsThrough$"], ["RC-02", "T-13-23", "root", "modules/surf/overlap.go", "\t\tif s.param == \"\" && s.lit != vals[i] {\n\t\t\treturn false\n\t\t}", "\t\tif false && s.param == \"\" && s.lit != vals[i] {\n\t\t\treturn false\n\t\t}", "./modules/surf", "^TestOverlappingConstrainedRoutes$"], ["RC-03", "T-13-23", "rootapp", "modules/surf/overlap.go", "\t\tif s.param == \"\" && s.lit != vals[i] {\n\t\t\treturn false\n\t\t}", "\t\tif false && s.param == \"\" && s.lit != vals[i] {\n\t\t\treturn false\n\t\t}", "./plugins/golem15/fonoteka", "^TestRouteTablePhase13$"], ["RC-04", "T-13-22", "root", "modules/conga/conga.go", "\t}\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\treturn m.insertOnlyLocked()\n}\n\n// insertClient", "\t}\n\treturn m.insertClient()\n}\n\n// insertClient", "./modules/conga", "^TestUnregisteredKindWithWorker$"], ["RC-05", "T-13-05", "app", "plugins/golem15/fonoteka/classes/reservations.go", "if rc.IsOwner && !revealed {", "if false && rc.IsOwner && !revealed {", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-05$"], ["RC-06", "T-13-03", "app", "plugins/golem15/fonoteka/classes/share_service.go", "Where(\"LOWER(public_token) = ? AND public_enabled = ? AND kind = ?\", strings.ToLower(token), true, kind)", "Where(\"LOWER(public_token) = ? AND ? AND kind = ?\", strings.ToLower(token), true, kind)", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-03$"], ["RC-07", "T-13-29", "app", "plugins/golem15/fonoteka/classes/share_service.go", "subtle.ConstantTimeCompare([]byte(*stored), []byte(token)) == 1", "subtle.ConstantTimeCompare([]byte(strings.ToLower(*stored)), []byte(strings.ToLower(token))) == 1", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-29$"], ["RC-08", "T-13-01", "app", "plugins/golem15/fonoteka/classes/public_share.go", "\tif w.hits+w.inflight >= c.limit {\n\t\tc.release(key, w)\n\t\treturn nil, false\n\t}", "\tif false && w.hits+w.inflight >= c.limit {\n\t\tc.release(key, w)\n\t\treturn nil, false\n\t}", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-01$"], ["RC-09", "T-13-01", "app", "plugins/golem15/fonoteka/classes/public_share.go", "too := w.hits+w.inflight >= c.limit", "too := false && w.hits+w.inflight >= c.limit", "./plugins/golem15/fonoteka", "^TestPubfailCounter$"], ["RC-10", "T-13-20", "app", "plugins/golem15/user/controllers/registration.go", "\tdelete(payload, \"password_confirmation\")\n", "", "./plugins/golem15/user", "^TestRegisterEventPayload$"], ["RC-11", "T-13-20", "app", "plugins/golem15/user/controllers/registration.go", "\tdelete(payload, \"password_confirmation\")\n", "", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-20$"], ["RC-12", "T-13-18", "app", "plugins/golem15/fonoteka/classes/onboarding.go", "\t\tn, err := countLiveUsers(tx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif n != 0 {\n\t\t\treturn ErrOnboardingCompleted\n\t\t}\n", "", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-18$"], ["RC-13", "T-13-12", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "\tif n == 0 {\n\t\treturn nil, nil\n\t}\n\treturn &imps[0], nil", "\t_ = n\n\treturn &imps[0], nil", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-12$"], ["RC-14", "T-13-17", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "WHERE id = ? AND status = ?`", "WHERE id = ? AND ? <> ''`", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-17$"], ["RC-15", "T-13-10", "app", "plugins/golem15/fonoteka/classes/credential_write_service.go", "var CredentialFillFields = []string{\"provider\", \"model\", \"base_url\"}", "var CredentialFillFields = []string{\"provider\", \"model\", \"base_url\", \"user_id\"}", "./plugins/golem15/fonoteka", "^FuzzWriteEndpoints$"], ["RC-16", "T-13-10", "app", "plugins/golem15/fonoteka/classes/credential_write_service.go", "var CredentialFillFields = []string{\"provider\", \"model\", \"base_url\"}", "var CredentialFillFields = []string{\"provider\", \"model\", \"base_url\", \"user_id\"}", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-10$"], ["RC-17", "T-13-21", "app", "plugins/golem15/fonoteka/classes/notifications.go", "WHERE user_id = ? AND id = ?`, userID, id)", "WHERE ? > 0 AND id = ?`, userID, id)", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-21$"], ["RC-18", "T-13-16", "app", "plugins/golem15/fonoteka/classes/csv_import_service.go", "func (unavailableReleaseFetcher) FetchRelease(context.Context, *usermodels.User, string) (map[string]any, error) {\n\treturn nil, ErrDiscogsUnavailable", "func (unavailableReleaseFetcher) FetchRelease(context.Context, *usermodels.User, string) (map[string]any, error) {\n\treturn map[string]any{\"name\": \"Forged\"}, nil", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-16$"], ["RC-19", "T-13-28", "app", "plugins/golem15/fonoteka/classes/wishlist_notifications.go", "if len(inserted) != 1 || !inserted[0] {", "if len(inserted) != 1 {", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-28$"], ["RC-20", "T-13-02", "app", "plugins/golem15/fonoteka/classes/serialize_public_album.go", "\tCreatedAt *wire.Time `json:\"created_at\"`\n}", "\tCreatedAt *wire.Time `json:\"created_at\"`\n\tShelf *string `json:\"shelf\"`\n}", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-02$"], ["RC-21", "T-13-08", "app", "plugins/golem15/fonoteka/controllers/api/credentials_controller.go", "err := gdb.WithContext(r.Context()).Select(\"id\", \"provider\", \"model\", \"base_url\").Where(\"user_id = ?\", user.ID).Take(&cred).Error\n\t\twriteAICredentialStatus(w, cred.Provider, cred.Model, cred.BaseURL, err)", "err := gdb.WithContext(r.Context()).Where(\"user_id = ?\", user.ID).Take(&cred).Error\n\t\t_ = err\n\t\twriteJSON(w, http.StatusOK, map[string]any{\"configured\": true, \"provider\": cred.Provider, \"api_key\": cred.APIKey.Reveal()})", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-08$"], ["RC-22", "T-13-07", "app", "plugins/golem15/fonoteka/routes.go", "g.Get(\"/wishlist/albums\", wishlistIndex, \"inv.scope:read\")", "g.Get(\"/wishlist/albums\", wishlistIndex, \"inv.scope:read\", \"inv.scope:read\")", "./plugins/golem15/fonoteka", "^TestRouteTablePhase13$"], ["RC-23", "T-13-04", "app", "plugins/golem15/fonoteka/classes/reservations.go", "WHERE album_id = ? AND user_id = ?`, albumID, userID)", "WHERE album_id = ? AND ? > 0`, albumID, userID)", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-04$"], ["RC-24", "T-13-06", "app", "plugins/golem15/fonoteka/controllers/api/wishlist_subscriptions_controller.go", "Model(&models.Collection{}).Scopes(classes.WishlistsVisibleTo(user.ID)).\n", "Model(&models.Collection{}).Where(\"kind = 'wishlist' AND ? > 0\", user.ID).\n", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-06$"], ["RC-25", "T-13-09", "app", "plugins/golem15/fonoteka/controllers/api/credentials_controller.go", "\t\tif !mayManageOrg(w, r, gdb, user) {\n\t\t\treturn\n\t\t}\n\t\tfields, apiKey, ok := aiCredentialInput(w, r, app, gdb)", "\t\tfields, apiKey, ok := aiCredentialInput(w, r, app, gdb)", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-09$"], ["RC-26", "T-13-13", "app", "plugins/golem15/fonoteka/controllers/api/csv_import_controller.go", "\t\tbucket, err := csvBucket(app)\n\t\tif err != nil {\n\t\t\twriteOpaque500(w)\n\t\t\treturn\n\t\t}\n\t\timp, err := classes.StoreCsvImport(", "\t\tbucket, err := uploadBucket(app), error(nil)\n\t\tif err != nil {\n\t\t\twriteOpaque500(w)\n\t\t\treturn\n\t\t}\n\t\timp, err := classes.StoreCsvImport(", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-13$"], ["RC-27", "T-13-19", "app", "plugins/golem15/fonoteka/classes/onboarding.go", "expires_at > NOW() AND LOWER(email) = ?", "expires_at > NOW() AND (LOWER(email) = ? OR TRUE)", "./plugins/golem15/fonoteka", "^TestPhase13Threats$/^T-13-19$"], ["RC-28", "T-13-SC", "script", "scripts/check-phase13.sh", "hits=\"$(grep -E '^golang\\.org/x/text ' \"$modlist\" | sort -u || true)\"", "hits=\"golang.org/x/text $XTEXT_VERSION\"", "", "--self-test"], ["RC-29", "T-13-36", "script", "scripts/check-phase13.sh", "holds a {label}\", file=sys.stderr)\n sys.exit(1)", "holds a {label}\", file=sys.stderr)\n pass", "", "--self-test"], ["RC-30", "T-13-35", "script", "scripts/check-phase13.sh", " if not any(re.match(r\"^\\| RC-\\d+ \\| \" + re.escape(tid) + r\" \\|\", l) for l in removal):", " if False:", "", "--self-test"], ["RC-31", "T-13-34", "script", "scripts/check-phase13.sh", " if dirty:\n", " if False:\n", "", "--self-test"] ] EOF } # removal_harness TABLE_FILE: for each row, refuse a file with uncommitted # changes, save it, apply the anchor-exact mutation, run the named test (or, # for the gate script, its --self-test on a mutated copy) and require it to # fail on an assertion, then restore the file and require cmp to match. removal_harness() { python3 - "$1" "$ROOT" "$APP" <<'PY' import json, os, shutil, signal, subprocess, sys, tempfile table = json.load(open(sys.argv[1])) root, app = sys.argv[2], sys.argv[3] only = set(os.environ.get("PHASE13_RC", "").split()) current = {} def restore(*_): # A signal mid-run still puts the file back. if current: with open(current["path"], "wb") as fh: fh.write(current["original"]) sys.exit(1) signal.signal(signal.SIGINT, restore) signal.signal(signal.SIGTERM, restore) failures = 0 for rc, threat, repo, rel, anchor, repl, pkg, run in table: if only and rc not in only: continue base = {"root": root, "app": app, "script": root, "rootapp": root}[repo] run_dir = {"root": root, "app": app, "script": root, "rootapp": app}[repo] path = os.path.join(base, rel) tracked = subprocess.run(["git", "-C", os.path.dirname(path), "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True).returncode == 0 if tracked and repo != "script": dirty = subprocess.run(["git", "-C", os.path.dirname(path), "status", "--porcelain", "--", os.path.basename(path)], capture_output=True, text=True).stdout.strip() if dirty: print(f"refuse: {rc}: {rel} is dirty; commit or restore it first", file=sys.stderr) sys.exit(1) original = open(path, "rb").read() text = original.decode() n = text.count(anchor) if n != 1: print(f"refuse: {rc} {threat}: anchor occurs {n} times in {rel}", file=sys.stderr) sys.exit(1) mutated = text.replace(anchor, repl, 1) scratch = tempfile.mkdtemp(prefix="phase13-rc-") saved = os.path.join(scratch, "saved") shutil.copyfile(path, saved) try: if repo == "script": copy = os.path.join(scratch, os.path.basename(rel)) open(copy, "w").write(mutated) env = dict(os.environ, PHASE13_ROOT=root, PHASE13_APP=app) proc = subprocess.run(["bash", copy, run], cwd=root, env=env, capture_output=True, text=True, timeout=900) out = proc.stdout + proc.stderr ok = proc.returncode != 0 and "refuse:" in out evidence = next((l for l in out.splitlines() if l.startswith("refuse:")), "") else: current.update(path=path, original=original) with open(path, "w") as fh: fh.write(mutated) proc = subprocess.run(["go", "test", pkg, "-run", run, "-count=1"], cwd=run_dir, capture_output=True, text=True, timeout=1800) out = proc.stdout + proc.stderr build = "[build failed]" in out or "[setup failed]" in out ok = proc.returncode != 0 and "--- FAIL" in out and not build fails = [l.strip() for l in out.splitlines() if l.strip().startswith("--- FAIL")] names = [l.split()[2] for l in fails if len(l.split()) > 2] evidence = ", ".join(names[:5]) + (f" (+{len(names) - 5} more)" if len(names) > 5 else "") if names else ("build failed" if build else "no failure") finally: with open(path, "wb") as fh: fh.write(original) current.clear() same = subprocess.run(["cmp", "-s", saved, path]).returncode == 0 shutil.rmtree(scratch, ignore_errors=True) if not same: print(f"refuse: {rc}: {rel} was not restored byte for byte", file=sys.stderr) sys.exit(1) status = "fails as required" if ok else "SURVIVED" print(f"{rc} {threat} {rel}: {status}: {evidence}", flush=True) if not ok: failures += 1 if failures: print(f"refuse: {failures} removal check(s) survived", file=sys.stderr) sys.exit(1) PY } run_removal() { local table table="$(mktemp)" removal_table >"$table" if ! removal_harness "$table"; then rm -f "$table" exit 1 fi rm -f "$table" echo "phase13 removal passed" } # removal_harness_in ROOT TABLE runs the harness against another root. removal_harness_in() { local root="$1" table="$2" ( ROOT="$root" APP="$root" export GOWORK=off GOFLAGS=-mod=mod removal_harness "$table" ) } # evidence_check PHASE_DIR REVIEW VALIDATION NAMED: every T-13 threat the # plans declare has exactly one review row copying its strictest severity # and disposition (a threat several plans declare takes the strictest); # a mitigated threat names a test the --named stage runs or a gate stage; # a high mitigated threat has a removal row; the validation file is # validated, Nyquist-compliant, Wave 0 complete, without a pending or TBD # row, names API-03 to API-07, and every test it names is run by --named. evidence_check() { python3 - "$@" <<'PY' import glob, os, re, sys phase_dir, review_path, validation_path, named = sys.argv[1], sys.argv[2], sys.argv[3], set(sys.argv[4].split()) for p in (review_path, validation_path): if not os.path.isfile(p): print(f"refuse: {p} is missing", file=sys.stderr) sys.exit(1) review = open(review_path).read() validation = open(validation_path).read() sev_rank = {"low": 0, "medium": 1, "high": 2} declared = {} for plan in sorted(glob.glob(os.path.join(phase_dir, "13-0*-PLAN.md"))): for line in open(plan): m = re.match(r"^\| (T-13-(?:\d\d|SC)) \|", line) if not m: continue cells = [c.strip().lower() for c in line.strip().strip("|").split("|")] prev = declared.get(m.group(1)) if prev is None: declared[m.group(1)] = cells continue sev = max(prev[3], cells[3], key=lambda s: sev_rank.get(s, -1)) disp = "mitigate" if "mitigate" in (prev[4], cells[4]) else prev[4] declared[m.group(1)] = prev[:3] + [sev, disp] + prev[5:] if not declared: print("refuse: no plan declares a T-13 threat", file=sys.stderr) sys.exit(1) lines = review.splitlines() removal = [l for l in lines if re.match(r"^\| RC-\d+ \| T-13-", l)] for tid, cells in sorted(declared.items()): rows = [l for l in lines if l.startswith("| " + tid + " |")] if len(rows) != 1: print(f"refuse: review has {len(rows)} threat rows for {tid}, want 1", file=sys.stderr) sys.exit(1) row = [c.strip().lower() for c in rows[0].strip().strip("|").split("|")] severity, disposition = cells[3], cells[4] if severity not in row or disposition not in row: print(f"refuse: review row {tid} does not copy severity {severity!r} and disposition {disposition!r}", file=sys.stderr) sys.exit(1) if disposition == "mitigate": tests = set(re.findall(r"\b(?:Test|Fuzz)[A-Z][A-Za-z0-9_]*", rows[0])) if not tests and "check-phase13.sh" not in rows[0]: print(f"refuse: mitigated threat {tid} names no test or gate stage", file=sys.stderr) sys.exit(1) unrun = sorted(t for t in tests if t not in named) if unrun: print(f"refuse: threat {tid} names {', '.join(unrun)}, which the --named stage does not run", file=sys.stderr) sys.exit(1) if severity == "high" and disposition == "mitigate": if not any(re.match(r"^\| RC-\d+ \| " + re.escape(tid) + r" \|", l) for l in removal): print(f"refuse: high threat {tid} has no removal check row", file=sys.stderr) sys.exit(1) for flag in ("nyquist_compliant: true", "wave_0_complete: true", "status: validated"): if not re.search(r"^" + re.escape(flag) + r"$", validation, re.M): print(f"refuse: validation lacks {flag!r}", file=sys.stderr) sys.exit(1) status_word = re.compile(r"(?&2 exit 1 } done if [[ -n "${FORCE_COLOR+x}" ]]; then echo "refuse: self-test FORCE_COLOR is still set" >&2 exit 1 fi local scratch scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' RETURN # The module pin refuses a changed or missing x/text and accepts the # audited line. printf 'golang.org/x/text %s\n' "$XTEXT_VERSION" >"$scratch/mods" module_pin "$scratch/mods" 2>/dev/null || { echo "refuse: self-test module_pin rejected the audited version" >&2 exit 1 } for plant in 'golang.org/x/text v0.41.0' ''; do printf '%s\n' "$plant" >"$scratch/mods" if module_pin "$scratch/mods" 2>/dev/null; then echo "refuse: self-test module_pin accepted ${plant:-a missing x/text}" >&2 exit 1 fi done # The corpus scan refuses each planted secret shape and accepts # synthetic values. mkdir -p "$scratch/corpus" printf 'go test fuzz v1\nstring("POST /x")\nstring("{\\"status\\":\\"imported\\",\\"pad\\":\\"QQQQ\\"}")\n' >"$scratch/corpus/seed" corpus_scan "$scratch/corpus" 2>/dev/null || { echo "refuse: self-test corpus_scan rejected a synthetic seed" >&2 exit 1 } local secret for secret in "$(printf 'a%.0s' $(seq 64))" "inv_ABCDEFGHIJKLMNOPQRST" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig" "Bearer abcdefghijklmnop"; do printf 'string("%s")\n' "$secret" >"$scratch/corpus/planted" if corpus_scan "$scratch/corpus" 2>/dev/null; then echo "refuse: self-test corpus_scan accepted a planted secret ${secret:0:12}" >&2 exit 1 fi done rm -f "$scratch/corpus/planted" "$scratch/corpus/seed" if corpus_scan "$scratch/corpus" 2>/dev/null; then echo "refuse: self-test corpus_scan accepted an empty corpus" >&2 exit 1 fi # The manifest counter reads only status lines. printf 'routes:\n - id: a\n status: ported\n - id: b\n status: pending\n - id: c\n status: ported\n# status: ported\n' >"$scratch/manifest.yaml" if [[ "$(manifest_count "$scratch/manifest.yaml" ported)" -ne 2 || "$(manifest_count "$scratch/manifest.yaml" pending)" -ne 1 ]]; then echo "refuse: self-test manifest_count miscounted" >&2 exit 1 fi # The corpus coverage line must show the expected counts. local line="recorded $((EXPECTED_PORTED + EXPECTED_PENDING))/$((EXPECTED_PORTED + EXPECTED_PENDING)) passing $EXPECTED_PORTED failing 0 unrecorded 0 pending $EXPECTED_PENDING" printf '{"Action":"output","Package":"p","Test":"TestParityCorpus/coverage","Output":"%s\\n"}\n' "$line" >"$scratch/cov.json" corpus_coverage "$scratch/cov.json" >/dev/null 2>&1 || { echo "refuse: self-test corpus_coverage rejected the expected counts" >&2 exit 1 } local total=$((EXPECTED_PORTED + EXPECTED_PENDING)) planted for planted in \ "recorded $total/$total passing $((EXPECTED_PORTED - 1)) failing 1 unrecorded 0 pending $EXPECTED_PENDING" \ "recorded $((total + 1))/$((total + 1)) passing $EXPECTED_PORTED failing 0 unrecorded 0 pending $((EXPECTED_PENDING + 1))" \ "recorded $((total - 1))/$total passing $EXPECTED_PORTED failing 0 unrecorded 1 pending $((EXPECTED_PENDING - 1))" \ "recorded $total/$total passing $((EXPECTED_PORTED + 1)) failing 0 unrecorded 0 pending $((EXPECTED_PENDING - 1))"; do printf '{"Action":"output","Package":"p","Output":"%s\\n"}\n' "$planted" >"$scratch/cov.json" if corpus_coverage "$scratch/cov.json" >/dev/null 2>&1; then echo "refuse: self-test corpus_coverage accepted: $planted" >&2 exit 1 fi done printf '{"Action":"pass","Package":"p","Test":"TestParityCorpus"}\n' >"$scratch/cov.json" if corpus_coverage "$scratch/cov.json" >/dev/null 2>&1; then echo "refuse: self-test corpus_coverage accepted a log without a coverage line" >&2 exit 1 fi # The coverage report refuses a package under the floor and accepts one # over it; a block covered by any profile counts once; COVERAGE_ONLY # narrows the report. printf 'mode: set\nexample.test/a/x.go:1.1,2.2 8 1\nexample.test/a/x.go:3.1,4.2 2 0\n' >"$scratch/p1" printf 'mode: set\nexample.test/a/x.go:3.1,4.2 2 1\nexample.test/b/y.go:1.1,2.2 5 0\nexample.test/b/y.go:3.1,4.2 5 1\n' >"$scratch/p2" local out out="$(coverage_report 80 "$scratch/p1" 2>&1)" || { echo "refuse: self-test coverage_report refused 80% at an 80% floor: $out" >&2 exit 1 } if out="$(coverage_report 80 "$scratch/p1" "$scratch/p2" 2>&1)"; then echo "refuse: self-test coverage_report accepted a 50% package" >&2 exit 1 fi grep -q "coverage example.test/a 100.0%" <<<"$out" || { echo "refuse: self-test coverage_report did not merge profiles: $out" >&2 exit 1 } COVERAGE_ONLY="/a" coverage_report 80 "$scratch/p1" "$scratch/p2" >/dev/null 2>&1 || { echo "refuse: self-test COVERAGE_ONLY did not narrow the report" >&2 exit 1 } printf 'mode: set\n' >"$scratch/empty" if coverage_report 80 "$scratch/empty" 2>/dev/null; then echo "refuse: self-test coverage_report accepted an empty profile" >&2 exit 1 fi # The function floor refuses a function below it and a file with no # functions in the profile. printf 'example.test/c/registration.go:10:\tRegisterUser\t84.4%%\nexample.test/c/registration.go:40:\tIssueToken\t100.0%%\ntotal:\t(statements)\t90.0%%\n' >"$scratch/func" func_floor 80 c/registration.go <"$scratch/func" >/dev/null 2>&1 || { echo "refuse: self-test func_floor rejected functions over the floor" >&2 exit 1 } printf 'example.test/c/registration.go:10:\tRegisterUser\t79.9%%\n' >"$scratch/func" if func_floor 80 c/registration.go <"$scratch/func" >/dev/null 2>&1; then echo "refuse: self-test func_floor accepted a 79.9% function" >&2 exit 1 fi if func_floor 80 c/other.go <"$scratch/func" >/dev/null 2>&1; then echo "refuse: self-test func_floor accepted a file without functions" >&2 exit 1 fi # The evidence check refuses a missing threat row, a wrong disposition, # a high threat without a removal row, a test the named stage does not # run, a pending validation row, a missing Wave 0 flag and a validation # test the named stage does not run; a threat two plans declare takes # the stricter severity and disposition. mkdir -p "$scratch/phase" printf '| T-13-90 | Spoofing | x | high | mitigate | y |\n| T-13-91 | Tampering | x | low | accept | y |\n' >"$scratch/phase/13-01-PLAN.md" printf '| T-13-91 | Tampering | x | medium | mitigate | y |\n' >"$scratch/phase/13-02-PLAN.md" cat >"$scratch/review.md" <<'EOR' | T-13-90 | Spoofing | x | high | mitigate | y | TestAlpha | pass | none | | T-13-91 | Tampering | x | medium | mitigate | y | TestAlpha | pass | none | | RC-90 | T-13-90 | f | a | b | c | fails | EOR cat >"$scratch/validation.md" <<'EOV' status: validated nyquist_compliant: true wave_0_complete: true | 13-01-T1 | API-03, API-04, API-05, API-06, API-07 | `go test -run '^TestAlpha$'` | ✅ green | EOV evidence_check "$scratch/phase" "$scratch/review.md" "$scratch/validation.md" "TestAlpha" >/dev/null 2>&1 || { echo "refuse: self-test evidence_check rejected a complete record" >&2 exit 1 } local case for case in missing-row disposition removal unrun pending wave0 unnamed; do cp "$scratch/review.md" "$scratch/review.case" cp "$scratch/validation.md" "$scratch/validation.case" local named="TestAlpha" case "$case" in missing-row) sed -i '/^| T-13-91 /d' "$scratch/review.case" ;; disposition) sed -i 's/| medium | mitigate |/| low | accept |/' "$scratch/review.case" ;; removal) sed -i '/^| RC-90 /d' "$scratch/review.case" ;; unrun) sed -i 's/| TestAlpha | pass | none |$/| TestGamma | pass | none |/' "$scratch/review.case" ;; pending) printf '| 13-02-T1 | API-03 | x | ⬜ pending |\n' >>"$scratch/validation.case" ;; wave0) sed -i '/^wave_0_complete: true$/d' "$scratch/validation.case" ;; unnamed) printf '| 13-02-T1 | API-03 | `go test -run TestBeta` | ✅ green |\n' >>"$scratch/validation.case" ;; esac if evidence_check "$scratch/phase" "$scratch/review.case" "$scratch/validation.case" "$named" >/dev/null 2>&1; then echo "refuse: self-test evidence_check accepted the $case plant" >&2 exit 1 fi done # The removal harness refuses an anchor that is not unique and a dirty # tracked file, restores the file byte for byte, and reports a mutation # whose test passes. local fake="$scratch/fake" mkdir -p "$fake/modules/acme" printf 'module example.test/acme\n\ngo 1.27\n' >"$fake/go.mod" printf 'package acme\n\nfunc Guard(n int) bool {\n\tif n > 3 {\n\t\treturn false\n\t}\n\treturn true\n}\n' >"$fake/modules/acme/acme.go" printf 'package acme\n\nimport "testing"\n\nfunc TestGuard(t *testing.T) {\n\tif Guard(4) {\n\t\tt.Fatal("guard removed")\n\t}\n}\n\nfunc TestOther(t *testing.T) {}\n' >"$fake/modules/acme/acme_test.go" cp "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" local table="$scratch/table.json" printf '[["RC-T1","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./modules/acme","^TestGuard$"]]' >"$table" out="$(removal_harness_in "$fake" "$table" 2>&1)" || { echo "refuse: self-test removal harness did not catch a guarded mutation: $out" >&2 exit 1 } grep -q "RC-T1 T-X modules/acme/acme.go: fails as required" <<<"$out" || { echo "refuse: self-test removal harness output: $out" >&2 exit 1 } cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || { echo "refuse: self-test removal harness did not restore the file" >&2 exit 1 } printf '[["RC-T2","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./modules/acme","^TestOther$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness accepted a mutation whose test passes: $out" >&2 exit 1 fi grep -q "RC-T2 T-X modules/acme/acme.go: SURVIVED" <<<"$out" || { echo "refuse: self-test removal harness refused a surviving mutation for the wrong reason: $out" >&2 exit 1 } printf '[["RC-T3","T-X","root","modules/acme/acme.go","return","x","./modules/acme","^TestGuard$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness accepted a non-unique anchor" >&2 exit 1 fi grep -q "anchor occurs 2 times" <<<"$out" || { echo "refuse: self-test removal harness refused a non-unique anchor for the wrong reason: $out" >&2 exit 1 } printf '[["RC-T5","T-X","root","modules/acme/acme.go","if n > 3 {","if n > 3 {\\n\\tundefinedCall()","./modules/acme","^TestGuard$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness counted a build failure as a failing test" >&2 exit 1 fi cmp -s "$fake/modules/acme/acme.go" "$scratch/acme.go.saved" || { echo "refuse: self-test removal harness did not restore after a build failure" >&2 exit 1 } (cd "$fake" && git init -q && git add -A && git -c user.email=gate@example.test -c user.name=gate commit -qm init) >/dev/null printf '// local edit\n' >>"$fake/modules/acme/acme.go" printf '[["RC-T4","T-X","root","modules/acme/acme.go","if n > 3 {","if false {","./modules/acme","^TestGuard$"]]' >"$table" if out="$(removal_harness_in "$fake" "$table" 2>&1)"; then echo "refuse: self-test removal harness mutated a dirty file" >&2 exit 1 fi grep -q "is dirty" <<<"$out" || { echo "refuse: self-test removal harness refused a dirty file for the wrong reason: $out" >&2 exit 1 } # Every row of the real removal table names a unique anchor in the # current tree (the --removal stage would refuse it otherwise). removal_table >"$table" python3 - "$table" "$ROOT" "$APP" <<'PY' || exit 1 import json, os, sys table, root, app = json.load(open(sys.argv[1])), sys.argv[2], sys.argv[3] for rc, threat, repo, rel, anchor, repl, pkg, run in table: base = {"root": root, "app": app, "script": root, "rootapp": root}[repo] path = os.path.join(base, rel) if not os.path.isfile(path): print(f"refuse: self-test {rc}: {rel} is missing", file=sys.stderr) sys.exit(1) n = open(path).read().count(anchor) if n != 1: print(f"refuse: self-test {rc}: anchor occurs {n} times in {rel}", file=sys.stderr) sys.exit(1) PY echo "phase13 self-test passed" } case "${1:-}" in --self-test) run_self_test ;; --go) run_go ;; --parity) run_parity ;; --named) run_named ;; --removal) run_removal ;; --coverage) run_coverage ;; --evidence) run_evidence ;; --all) run_self_test run_go run_parity run_named run_coverage run_evidence echo "phase13 all passed" ;; *) usage ;; esac