#!/usr/bin/env bash # Phase 14.2.1 fail-closed gate (lean Translate plugin, cabana ML fields, # proof host). Every stage exits non-zero on a failing command, a go test # run that fails, skips, matches zero tests, prints "no tests to run", a # named required test that did not pass, a data race, stale generated # artifacts, a forbidden deferred surface, or an unmitigated high threat. # --self-test proves the detector fails closed on planted inputs. --all # runs every stage and must end with "Phase 14.2.1 gate passed". # # Sibling repositories are invoked with `go -C`. Full mode runs Postgres # integration and treats Docker unavailability as failure; -short is not # final evidence. set -euo pipefail unset FORCE_COLOR ROOT="${PHASE1421_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" PLUGIN="${PHASE1421_PLUGIN:-$ROOT/../sm-translate-plugin}" HOST="${PHASE1421_HOST:-$ROOT/../sm-grzybyfunkcjonalne-app}" PHP="${PHASE1421_PHP:-/media/nvme/dev/golem15/fonoteka/plugins/golem15/translate}" PHP_SHA="725d547ec839f02b5fdc0f0a6faaed601a414d50" PHASE_DIR="${PHASE1421_PHASE_DIR:-$ROOT/.planning/phases/14.2.1-translate-plugin}" REVIEW="$PHASE_DIR/14.2.1-SECURITY-REVIEW.md" VALIDATION="$PHASE_DIR/14.2.1-VALIDATION.md" PLUGIN_REQUIRE=( TestTranslateEndToEnd TestLocalesAdminForbidden TestTranslatorResolve TestTranslatorConcurrentIsolation TestInvalidLocaleRejected TestTranslatableGetSet TestFallbackDefaultLocale TestTranslateTables TestSeedEnPl TestTranslateMigrationsRollbackAndRemigrate TestTranslateEndToEndFixtureAbsentFromProduction ) FRAMEWORK_REQUIRE=( TestLocaleResolver TestML TestMLFieldTypes TestMLNestedSave TestMarkdownRejectsUnsafeHTML TestMLOpenAPIConformance ) HOST_REQUIRE=(TestBootUserTranslate) HIGH_THREATS=( T-14.2.1-01 T-14.2.1-02 T-14.2.1-04 T-14.2.1-05 T-14.2.1-06 T-14.2.1-07 T-14.2.1-09 T-14.2.1-10 T-14.2.1-11 T-14.2.1-12 T-14.2.1-14 T-14.2.1-15 T-14.2.1-18 T-14.2.1-SC ) ALL_THREATS=( T-14.2.1-01 T-14.2.1-02 T-14.2.1-03 T-14.2.1-04 T-14.2.1-05 T-14.2.1-06 T-14.2.1-07 T-14.2.1-08 T-14.2.1-09 T-14.2.1-10 T-14.2.1-11 T-14.2.1-12 T-14.2.1-13 T-14.2.1-14 T-14.2.1-15 T-14.2.1-16 T-14.2.1-17 T-14.2.1-18 T-14.2.1-SC ) usage() { cat >&2 <<'EOF' usage: check-phase14.2.1.sh --self-test check-phase14.2.1.sh --php check-phase14.2.1.sh --layout check-phase14.2.1.sh --plugin check-phase14.2.1.sh --framework check-phase14.2.1.sh --docs check-phase14.2.1.sh --admin check-phase14.2.1.sh --host check-phase14.2.1.sh --forbidden check-phase14.2.1.sh --security check-phase14.2.1.sh --all EOF exit 2 } # detect reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests, # 4 non-JSON, 5 missing required name, 6 data race. detect() { python3 - "$1" <<'PY' import json, os, sys path = sys.argv[1] require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n] passed = set() failed = [] 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 ev.get("ImportPath") or "" if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")): print(f"refuse: build failed {pkg}", file=sys.stderr) sys.exit(1) text = ev.get("Output") or "" if action == "output": 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": failed.append(f"{pkg} {test}".strip()) if action == "pass" and test: passed.add(test) if failed: print("refuse: failed " + ", ".join(failed), file=sys.stderr) sys.exit(1) if not passed: print("refuse: zero tests", file=sys.stderr) sys.exit(3) top = {name for name in passed if "/" not in name} missing = [n for n in require if n not in top and not any(p.startswith(n + "/") or p == n for p in passed)] if missing: print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr) sys.exit(5) PY } go_json() { local dir="$1" shift local log err rc=0 dc=0 log="$(mktemp)" err="$(mktemp)" (cd "$dir" && go test -json "$@") >"$log" 2>"$err" || rc=$? detect "$log" || dc=$? if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then cat "$err" >&2 || true grep -v '^{' "$log" | tail -n 40 >&2 || true rm -f "$log" "$err" echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2 return 1 fi rm -f "$log" "$err" } expect_detect() { local name="$1" want="$2" payload="$3" log dc=0 log="$(mktemp)" printf '%s\n' "$payload" >"$log" 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 return 1 fi } run_self_test() { bash -n "${BASH_SOURCE[0]}" expect_detect pass 0 '{"Action":"pass","Package":"p","Test":"TestTranslateEndToEnd"}' expect_detect fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p","Test":"TestLocalesAdminForbidden"}' expect_detect package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"} {"Action":"fail","Package":"p"}' expect_detect build 1 '{"Action":"build-fail","ImportPath":"p"}' expect_detect skip 2 '{"Action":"skip","Package":"p","Test":"TestTranslateEndToEnd"}' expect_detect zero 3 '{"Action":"pass","Package":"p"}' expect_detect no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"} {"Action":"pass","Package":"p"}' expect_detect nonjson 4 '{"Action":"pass",' expect_detect race 6 '{"Action":"output","Package":"p","Test":"TestA","Output":"WARNING: DATA RACE\n"} {"Action":"pass","Package":"p","Test":"TestA"}' REQUIRE_TESTS="TestTranslateEndToEnd TestML" expect_detect missing-named 5 \ '{"Action":"pass","Package":"p","Test":"TestTranslateEndToEnd"}' local flag for flag in --self-test --php --layout --plugin --framework --docs --admin --host --forbidden --security --all; do grep -q -- "^ $flag)" "${BASH_SOURCE[0]}" || { echo "refuse: missing mode $flag" >&2 return 1 } done echo "phase14.2.1 self-test passed" } run_php() { [[ -d "$PHP" ]] || { echo "refuse: PHP pin tree $PHP is missing" >&2 return 1 } local sha sha="$(git -C "$PHP" rev-parse HEAD)" if [[ "$sha" != "$PHP_SHA" ]]; then echo "refuse: PHP SHA $sha, want $PHP_SHA" >&2 return 1 fi if [[ -n "$(git -C "$PHP" status --porcelain)" ]]; then git -C "$PHP" status --short >&2 echo "refuse: PHP pin tree has a diff" >&2 return 1 fi echo "phase14.2.1 php passed ($sha)" } run_layout() { [[ -f "$PLUGIN/go.mod" ]] || { echo "refuse: plugin go.mod missing" >&2 return 1 } grep -q '^module git.golem15.com/golem15/sm-translate-plugin$' "$PLUGIN/go.mod" || { echo "refuse: plugin module path" >&2 return 1 } [[ -f "$HOST/go.work" && -f "$HOST/plugins.gen.go" && -f "$HOST/summer.yaml" ]] || { echo "refuse: host layout is incomplete" >&2 return 1 } local line for path in plugins/golem15/user plugins/golem15/translate; do line="$(git -C "$HOST" ls-files -s "$path")" [[ "$line" == 160000* ]] || { echo "refuse: $path is not a gitlink: $line" >&2 return 1 } done if grep -E 'golem15\.journal|acme\.fixture' "$HOST/plugins.gen.go" >/dev/null; then echo "refuse: production plugin list contains journal or fixture" >&2 return 1 fi echo "phase14.2.1 layout passed" } run_plugin() { [[ -d "$PLUGIN" ]] || { echo "refuse: plugin repository $PLUGIN not found" >&2 return 1 } go -C "$PLUGIN" vet ./... REQUIRE_TESTS="${PLUGIN_REQUIRE[*]}" go_json "$PLUGIN" ./... -count=1 -timeout 20m REQUIRE_TESTS="${PLUGIN_REQUIRE[*]}" go_json "$PLUGIN" ./... -count=1 -race -timeout 20m echo "phase14.2.1 plugin passed" } run_framework() { (cd "$ROOT" && go vet ./modules/cabana ./modules/surf) REQUIRE_TESTS="${FRAMEWORK_REQUIRE[*]}" go_json "$ROOT" ./modules/cabana ./modules/surf -count=1 -timeout 20m REQUIRE_TESTS="${FRAMEWORK_REQUIRE[*]}" go_json "$ROOT" ./modules/cabana ./modules/surf -count=1 -race -timeout 30m echo "phase14.2.1 framework passed" } run_docs() { REQUIRE_TESTS="TestDocsTree" go_json "$ROOT" ./cmd/summer -count=1 -run '^TestDocsTree$' local out out="$(cd "$ROOT" && go run ./cmd/summer docs:build --check 2>&1)" || { echo "$out" >&2 echo "refuse: docs:build --check failed" >&2 return 1 } echo "phase14.2.1 docs passed" } run_admin() { npm --prefix "$ROOT/admin" run typecheck local log rc=0 log="$(mktemp)" npm --prefix "$ROOT/admin" test -- --run tests/form/registry.test.ts tests/form/MLFields.test.ts tests/form/MarkdownField.test.ts >"$log" 2>&1 || rc=$? if [[ "$rc" -ne 0 ]] || grep -qE 'No test files found|Unhandled (Errors|Rejection)|FAIL ' "$log"; then tail -n 60 "$log" >&2 rm -f "$log" echo "refuse: admin Vitest run failed (exit $rc)" >&2 return 1 fi grep -E 'Test Files|Tests ' "$log" || true rm -f "$log" "$ROOT/scripts/check-admin-openapi.sh" --check "$ROOT/scripts/check-admin-dist.sh" echo "phase14.2.1 admin passed" } run_host() { [[ -d "$HOST" ]] || { echo "refuse: proof host $HOST not found" >&2 return 1 } go -C "$HOST" vet ./... REQUIRE_TESTS="${HOST_REQUIRE[*]}" go_json "$HOST" ./... -count=1 -timeout 5m go -C "$HOST" build -o /tmp/phase1421-host ./... rm -f /tmp/phase1421-host echo "phase14.2.1 host passed" } run_forbidden() { local bad=0 hits hits="$(cd "$PLUGIN" && grep -RInE 'winter_translate_|rainlab_translate_' --include='*.go' . | grep -vE '_test\.go:' || true)" if [[ -n "$hits" ]]; then echo "refuse: Winter/RainLab table names in plugin Go: $hits" >&2 bad=1 fi hits="$(cd "$PLUGIN" && grep -RInE 'manage_messages|golem15\.translate\.messages' --include='*.go' . | grep -vE '_test\.go:' || true)" if [[ -n "$hits" ]]; then echo "refuse: Messages admin surface in production plugin: $hits" >&2 bad=1 fi hits="$(cd "$PLUGIN" && grep -RInE 'plugin\.Open|yaegi|AutoMigrate' --include='*.go' . | grep -vE '_test\.go:' || true)" if [[ -n "$hits" ]]; then echo "refuse: runtime loading or AutoMigrate in production plugin: $hits" >&2 bad=1 fi hits="$(cd "$PLUGIN" && grep -RInE 'acme\.fixture|Acme\\\\Fixture\\\\Models\\\\Post' --include='*.go' . | grep -vE '_test\.go:' || true)" if [[ -n "$hits" ]]; then echo "refuse: test fixture in production plugin: $hits" >&2 bad=1 fi hits="$(cd "$ROOT" && grep -RInE 'fonoteka|p[lł]ytarium|grzybyfunkcjonalne' modules/cabana/README.md modules/surf/README.md docs --include='*.md' || true)" if [[ -n "$hits" ]]; then echo "refuse: consuming-application name in framework docs: $hits" >&2 bad=1 fi hits="$(cd "$ROOT/admin/src" && grep -RInE 'v-html=|innerHTML|outerHTML|insertAdjacentHTML' . || true)" if [[ -n "$hits" ]]; then echo "refuse: raw-HTML sink in admin/src: $hits" >&2 bad=1 fi hits="$(gofmt -l "$PLUGIN"/*.go "$PLUGIN"/classes/*.go "$PLUGIN"/updates/*.go "$ROOT"/modules/cabana/ml_test.go "$ROOT"/modules/cabana/markdown_test.go "$ROOT"/modules/surf/locale_resolver_test.go 2>/dev/null || true)" if [[ -n "$hits" ]]; then echo "refuse: gofmt: $hits" >&2 bad=1 fi [[ "$bad" -eq 0 ]] || return 1 echo "phase14.2.1 forbidden passed" } run_security() { [[ -f "$REVIEW" ]] || { echo "refuse: missing $REVIEW" >&2 return 1 } [[ -f "$VALIDATION" ]] || { echo "refuse: missing $VALIDATION" >&2 return 1 } local id count for id in "${ALL_THREATS[@]}"; do count="$(grep -c -- "$id" "$REVIEW" || true)" if [[ "$count" -lt 1 ]]; then echo "refuse: security review missing $id" >&2 return 1 fi done if grep -qiE 'unmitigated high' "$REVIEW"; then echo "refuse: security review still has an unmitigated high finding" >&2 return 1 fi for id in "${HIGH_THREATS[@]}"; do grep -q -- "$id" "$REVIEW" || { echo "refuse: high threat $id missing" >&2 return 1 } grep -A2 -- "$id" "$REVIEW" | grep -qi mitigate || { echo "refuse: high threat $id is not marked mitigate" >&2 return 1 } done if ! grep -q 'No external API integration' "$REVIEW"; then echo "refuse: security review must state there is no external API integration" >&2 return 1 fi if ! grep -q 'nyquist_compliant: true' "$VALIDATION"; then echo "refuse: VALIDATION is not signed off" >&2 return 1 fi echo "phase14.2.1 security passed" } run_all() { local stage for stage in self-test php layout plugin framework docs admin host forbidden security; do if bash "${BASH_SOURCE[0]}" "--$stage"; then echo "PASS $stage" else echo "FAIL $stage" exit 1 fi done echo "Phase 14.2.1 gate passed" } case "${1:---all}" in --self-test) run_self_test ;; --php) run_php ;; --layout) run_layout ;; --plugin) run_plugin ;; --framework) run_framework ;; --docs) run_docs ;; --admin) run_admin ;; --host) run_host ;; --forbidden) run_forbidden ;; --security) run_security ;; --all) run_all ;; *) usage ;; esac