#!/usr/bin/env bash # doc-drift.sh — Plane-1 / Tier-1 checker for the R720 agent-team. # # Design refs: docs/r720-agent-team-design.md §4 (Tier 1 roster: doc-drift — # "Flags repos whose architecture moved but Confluence/README did not") and §7 Phase 3 # ("doc-drift + step-ca/Roles Anywhere + aws-posture"). This is the THIRD Plane-1 checker # built on the Phase-0 shared substrate (lib/sweep_substrate.sh); it mirrors # compliance-drift.sh / dependency-cve.sh conventions VERBATIM so the coordinator (§5) can # drive all of them identically. doc-drift is UNGATED (only aws-posture in this phase is # hard-gated behind the GPT-4.1 IAM cross-review; that checker is NOT built here). # # WHAT IT DOES (read-only): # Scans the SAME shallow clean clones nightly_sweep.sh already produced in $MIRROR_DIR — it # does NOT re-clone (mirrors-first; an optional --refresh re-runs discovery+mirror via the # shared substrate). In each mirror it flags repos whose ARCHITECTURE MOVED but the README # DID NOT — i.e. documentation drift. The checklist is DETERMINISTIC and GROUNDED in the # global CLAUDE.md README obligation; it does NOT invent fuzzy judgments. See "CHECKLIST". # # This phase is the deterministic core ONLY. The design's "Gemini (large context)" judge # layer (§4) is a LATER enhancement: a clearly-marked inert stub hook (maybe_judge) marks # the future seam; it does NOTHING offline and NOTHING in this phase. # # REPORTING (matches secrev sweep conventions): # - Writes a per-run JSON + text report under $REPORT_ROOT//, mode 600 (umask 077). # - Slack ALARM-ONLY: a clean run (no confirmed drift) posts NOTHING (memory # feedback_cloudwatch_alarms). Secret-shaped values are redacted from the Slack string. # - Reuses the substrate's redact() + post_slack_alarm() verbatim. # # SUBSTRATE REUSE (lib/sweep_substrate.sh, sourced — bash dynamic scoping): # redact, post_slack_alarm -> Slack delivery (reads SLACK_WEBHOOK_URL, REPORT_DIR, SWEEP_LOG) # discover_repos, mirror_repo-> ONLY on --refresh (reads GH_TOKEN, GH_ORG, MIRROR_DIR, REPORT_DIR) # Default path enumerates EXISTING $MIRROR_DIR/*/.git dirs — zero clones, zero network. # # CANARY / DRY-RUN (offline, no network, no token): # --canary runs the checklist against a planted-drift fixture (checkers/fixtures/doc-drift/) # and asserts the known drift count. This is the anti-complacency floor (design §6.4) AND the # routing dry-run (§7 Phase 3): with --dry-run, the Slack alarm is composed + printed but NOT # POSTed. Fully offline-smoke-testable (the checks are filesystem + `git log`, no network). # # SCOPE / SAFETY: # Read-only. All checks are filesystem + local `git log`; NO network, NO token, NO GitHub API # (doc-drift has no API-only checks — it is purely tree+history). Fixtures ship git metadata as # dotgit/ (renamed to .git/ at run time) so they commit into THIS repo without becoming # submodules — the SAME trick compliance-drift / dependency-cve use. A repo with NO README is # SKIPPED (compliance-drift owns readme-present); doc-drift never double-flags a missing README. # # This script does NOT touch agent_team/ or agent-team/, is NOT wired into systemd, and does NOT # stand up step-ca / Roles Anywhere / aws-posture — that is Phase-3/6 provisioning (gated). See # the "PROVISIONING (NOT DONE HERE)" note at the bottom. # # Exit: 0 = ran (whether or not it alarmed); 2 = setup/usage error; 3 = canary assertion FAILED. set -euo pipefail export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" log() { echo "[doc-drift] $*" >&2; } die() { echo "[doc-drift] FATAL: $*" >&2; exit 2; } # --- Shared substrate --------------------------------------------------------- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SUBSTRATE="$HERE/../lib/sweep_substrate.sh" [ -f "$SUBSTRATE" ] || die "shared substrate not found: $SUBSTRATE" # shellcheck source=../lib/sweep_substrate.sh . "$SUBSTRATE" # --- Config + defaults (env, all optional) ------------------------------------ GH_ORG="${GH_ORG:-Sea-Haven-Industries}" MIRROR_DIR="${MIRROR_DIR:-$HOME/repo-mirrors}" REPORT_ROOT="${REPORT_ROOT:-$HOME/sweep-reports/doc-drift}" # Docs-only repos describe themselves differently (a handbook is its own doc); skip the # architecture-omission scan for them. They still get the staleness check. DOCS_ONLY_REPOS="${DOCS_ONLY_REPOS:-engineering-handbook}" # Staleness thresholds: README must lag the newest code by BOTH at least this many days AND # this many substantial code commits before we call it drift (two-factor = no false alarm on a # single quick fix landed after a doc commit; memory feedback_cloudwatch_alarms). DOC_DRIFT_STALE_DAYS="${DOC_DRIFT_STALE_DAYS:-60}" DOC_DRIFT_STALE_COMMITS="${DOC_DRIFT_STALE_COMMITS:-3}" REFRESH=0 # --refresh: re-run discovery+mirror via substrate (network). Default: reuse mirrors. DO_API=1 # --no-api: accepted for interface-parity with the other checkers; doc-drift makes # NO API calls, so this flag is a documented no-op (kept so the coordinator # can pass a uniform flag set to every Tier-1 checker). DRY_RUN=0 # --dry-run: compose the Slack alarm but DO NOT post it (routing dry-run). CANARY=0 # --canary: run against the planted-drift fixture + assert the known count. TARGETS_OVERRIDE="" # --targets "p1 p2": scan explicit dirs instead of the mirror set. usage() { cat >&2 </dev/null || die "jq is required" command -v git >/dev/null || die "git is required" # --- Report dir (mode 600 reports; matches sweep conventions) ----------------- umask 077 UTC_DATE="$(date -u +%Y-%m-%d)" UTC_STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" REPORT_DIR="$REPORT_ROOT/$UTC_DATE" mkdir -p "$REPORT_DIR"; chmod 700 "$REPORT_ROOT" "$REPORT_DIR" 2>/dev/null || true # shellcheck disable=SC2034 # read by the sourced substrate (post_slack_alarm) via dynamic scope SWEEP_LOG="$REPORT_DIR/doc-drift.log" # name the substrate's post_slack_alarm() references REPORT_JSON="$REPORT_DIR/doc-drift.json" REPORT_TXT="$REPORT_DIR/doc-drift.txt" # doc-drift makes NO API calls, so DO_API is a documented no-op kept only for coordinator # flag-parity; surface it in the run banner so the chosen value is auditable (and used). log "=== doc-drift $UTC_STAMP (canary=$CANARY dry_run=$DRY_RUN refresh=$REFRESH api=${DO_API}[no-op] stale_days=$DOC_DRIFT_STALE_DAYS stale_commits=$DOC_DRIFT_STALE_COMMITS) ===" # ------------------------------------------------------------------------------ # CHECKLIST (grounded — every item cites the README obligation; nothing invented): # # readme-omits-component README exists but omits a major existing component # present in the tree (top-level service dir, SAM/CDK stack, # Lambda handler dir, openapi/docs API spec) # -> global CLAUDE.md: "README must accurately describe # architecture, services, data flow, and configuration" # readme-stale-vs-code README last-touched commit far older than the newest code # commit (>= DOC_DRIFT_STALE_DAYS) AND >= DOC_DRIFT_STALE_COMMITS # substantial code commits landed after the README was touched # -> global CLAUDE.md: "update the README in the same commit" # # A repo with NO README is SKIPPED (compliance-drift owns readme-present; double-flagging would # be a false alarm). Each emitted finding follows the spirit of finding.schema.json # (id/title/severity/category/proof/status) so the coordinator can route it like an agentic # finding. category="other" (doc drift is not one of the schema's security categories). # status="confirmed" only for deterministic filesystem/git-history facts. The future Gemini # judge (design §4) is an inert stub (maybe_judge) — never invoked offline / in this phase. # ------------------------------------------------------------------------------ # Drift accumulator: one JSON object per finding, appended to a bash array. declare -a FINDINGS=() add_finding() { # repo id title severity check proof local repo="$1" id="$2" title="$3" sev="$4" check="$5" proof="$6" FINDINGS+=( "$(jq -n \ --arg repo "$repo" --arg id "$id" --arg title "$title" --arg sev "$sev" \ --arg check "$check" --arg proof "$proof" \ '{repo:$repo, id:($repo+"-"+$id), title:$title, severity:$sev, category:"other", check:$check, status:"confirmed", proof:{outcome:$proof}}')" ) } declare -a SKIPPED_CHECKS=() # (repo:check) checks skipped on missing data — reported, never alarmed note_skip() { SKIPPED_CHECKS+=( "$1" ); } in_csv() { # needle csv -> 0 if present local n="$1" csv="$2"; case ",$csv," in *",$n,"*) return 0 ;; *) return 1 ;; esac } # Inert future seam (design §4 "Gemini (large context)" judge): in LIVE mode an ambiguous # omission ("is this component material enough to require a README mention?") could be escalated # to a large-context judge. This phase keeps the deterministic core ONLY — the stub does nothing # and is never reached offline / in canary / dry-run. maybe_judge() { # candidate_json (no-op stub; Phase-3 intentionally inert) return 0 } # --- Does a README mention a component name? (case-insensitive, word-ish, deterministic) ---- # Matches the bare name OR the name with a trailing slash (how a dir is usually cited). Strips # a leading "the " never matters; we test the literal token. Pure grep, no fuzzy matching. readme_mentions() { # readme_file name local rf="$1" name="$2" # Escape regex metacharacters in the component name (defensive; dir names are usually plain). local esc; esc="$(printf '%s' "$name" | sed -E 's/[][(){}.*+?^$|\\/]/\\&/g')" grep -qiE "(^|[^A-Za-z0-9_-])${esc}([^A-Za-z0-9_-]|/|$)" "$rf" 2>/dev/null } # --- Enumerate the major components present in a repo tree (deterministic) ------ # Emits "TYPElabelmention_token" lines. mention_token is what the README must contain. # service-dir a top-level directory whose name ends in -service or -api, or named api/web/worker # sam-cdk-stack a SAM/CDK stack root (template.yaml | app.py at a stack root | cdk.json) # lambda-dir a Lambda handler dir (a dir named handlers/ or containing handler.* / app.py under handlers/) # api-spec an openapi/ or docs/ directory or an openapi.* / swagger.* spec file enumerate_components() { # repo_dir -> TSV lines local dir="$1" d nm # 1) top-level service-ish directories (the unit a README is expected to name) for d in "$dir"/*/; do [ -d "$d" ] || continue nm="$(basename "$d")" case "$nm" in .git|.github|node_modules|dist|build|vendor|__pycache__|.venv) continue ;; esac case "$nm" in *-service|*-api|api|web|worker|backend|frontend) printf 'service-dir\t%s\t%s\n' "$nm" "$nm" ;; esac done # 2) SAM / CDK stack roots if [ -f "$dir/template.yaml" ] || [ -f "$dir/template.yml" ]; then printf 'sam-cdk-stack\t%s\t%s\n' "template.yaml (SAM stack)" "template.yaml" fi if [ -f "$dir/cdk.json" ]; then printf 'sam-cdk-stack\t%s\t%s\n' "cdk.json (CDK app)" "cdk.json" fi # 3) Lambda handler dirs: a top-level/handlers-rooted dir literally named "handlers" while IFS= read -r d; do [ -n "$d" ] || continue printf 'lambda-dir\t%s\t%s\n' "handlers/ (Lambda handlers)" "handlers" break # one mention requirement for the handlers tree is enough done < <(find "$dir" -maxdepth 2 -type d -name handlers -not -path '*/.git/*' 2>/dev/null) # 4) API spec: an openapi/ or docs/ dir, or an openapi.*/swagger.* file if [ -d "$dir/openapi" ]; then printf 'api-spec\t%s\t%s\n' "openapi/ (API spec)" "openapi" elif find "$dir" -maxdepth 2 \( -iname 'openapi.*' -o -iname 'swagger.*' \) -not -path '*/.git/*' -print -quit 2>/dev/null | grep -q .; then printf 'api-spec\t%s\t%s\n' "openapi/swagger spec" "openapi" fi } # --- README last-touch epoch vs newest code commit (staleness, deterministic git log) ------- # Returns the staleness facts on stdout as TSV "readme_epochnewest_code_epochcommits_after". # commits_after = count of commits that touched code (non-doc) files AFTER the README's last touch. # Code = anything that is NOT a README/markdown/LICENSE/.gitignore/docs file. Prints nothing if # the repo has no git history or no README in history (caller treats that as "cannot assess"). readme_staleness_facts() { # repo_dir local dir="$1" command -v git >/dev/null || return 0 git -C "$dir" rev-parse --git-dir >/dev/null 2>&1 || return 0 # README last-touch (committer epoch of the most recent commit touching README.md). local rd_epoch rd_epoch="$(git -C "$dir" log -1 --format='%ct' -- README.md 2>/dev/null || true)" [ -n "$rd_epoch" ] || return 0 # README not in history -> cannot assess staleness # Newest commit touching a CODE path (exclude docs/markdown/license/config-noise). local code_epoch code_epoch="$(git -C "$dir" log -1 --format='%ct' -- \ ':(exclude)README.md' ':(exclude)*.md' ':(exclude)docs/**' \ ':(exclude)LICENSE' ':(exclude).gitignore' ':(exclude).github/**' \ 2>/dev/null || true)" [ -n "$code_epoch" ] || return 0 # no code commits -> nothing to be stale against # Count CODE commits strictly AFTER the README's last touch. local commits_after commits_after="$(git -C "$dir" rev-list --count "--since=@${rd_epoch}" HEAD -- \ ':(exclude)README.md' ':(exclude)*.md' ':(exclude)docs/**' \ ':(exclude)LICENSE' ':(exclude).gitignore' ':(exclude).github/**' \ 2>/dev/null || echo 0)" printf '%s\t%s\t%s\n' "$rd_epoch" "$code_epoch" "${commits_after:-0}" } # ============================================================================== # PER-REPO CHECK (offline; filesystem + local git log only) # ============================================================================== check_repo() { # repo_name repo_dir local repo="$1" dir="$2" local docs_only=0; in_csv "$repo" "$DOCS_ONLY_REPOS" && docs_only=1 # No README -> doc-drift cannot assess drift; compliance-drift owns readme-present. SKIP. if [ ! -f "$dir/README.md" ]; then note_skip "$repo:doc-drift(no-readme — compliance-drift owns readme-present)" return fi local readme="$dir/README.md" # --- readme-omits-component (skip for docs-only repos: they document differently) --- if [ "$docs_only" -eq 0 ]; then local type label token while IFS=$'\t' read -r type label token; do [ -n "$token" ] || continue if ! readme_mentions "$readme" "$token"; then add_finding "$repo" "readme-omits-$(printf '%s' "$type-$token" | tr -c 'A-Za-z0-9-' '-')" \ "README omits existing component: $label" "medium" "readme-omits-component" \ "global CLAUDE.md: README must accurately describe architecture/services (present in tree, absent from README: $label)" fi done < <(enumerate_components "$dir") else note_skip "$repo:readme-omits-component(docs-only)" fi # --- readme-stale-vs-code (two-factor: age in days AND code-commits-after) --- local facts; facts="$(readme_staleness_facts "$dir")" if [ -z "$facts" ]; then note_skip "$repo:readme-stale-vs-code(no-history-or-no-readme-in-history)" else local rd_epoch code_epoch commits_after age_days IFS=$'\t' read -r rd_epoch code_epoch commits_after <<< "$facts" age_days=$(( (code_epoch - rd_epoch) / 86400 )) [ "$age_days" -lt 0 ] && age_days=0 if [ "$age_days" -ge "$DOC_DRIFT_STALE_DAYS" ] && [ "$commits_after" -ge "$DOC_DRIFT_STALE_COMMITS" ]; then add_finding "$repo" "readme-stale" \ "README is stale: ${age_days}d behind newest code, ${commits_after} code commit(s) since last README touch" \ "medium" "readme-stale-vs-code" \ "global CLAUDE.md: update the README in the same commit as functionality changes (thresholds: >=${DOC_DRIFT_STALE_DAYS}d AND >=${DOC_DRIFT_STALE_COMMITS} code commits)" fi fi maybe_judge "" # inert in this phase (future Gemini large-context seam) } # ============================================================================== # TARGET RESOLUTION # ============================================================================== declare -a REPO_NAMES=(); declare -A REPO_DIR=() if [ "$CANARY" -eq 1 ]; then FIXTURE_ROOT="$HERE/fixtures/doc-drift" [ -d "$FIXTURE_ROOT" ] || die "canary fixture missing: $FIXTURE_ROOT" # Pin the exception lists + thresholds the fixtures were authored against, so the canary is # self-contained and deterministic regardless of the operator's env. DOCS_ONLY_REPOS="" DOC_DRIFT_STALE_DAYS=60 DOC_DRIFT_STALE_COMMITS=3 # Fixtures ship their git metadata as `dotgit/` (not `.git/`) so they are committable into THIS # repo without becoming nested submodules. Materialize them into a temp work area — copy each # fixture and rename dotgit -> .git — so the README/git-log checks run against a real git # checkout. The temp area is mode 700 and removed on exit (same trick as compliance-drift.sh). FIXTURE_WORK="$(mktemp -d "${TMPDIR:-/tmp}/doc-drift-canary.XXXXXX")" trap 'rm -rf "$FIXTURE_WORK"' EXIT log "canary: materializing planted-drift fixtures from $FIXTURE_ROOT into $FIXTURE_WORK" for d in "$FIXTURE_ROOT"/*/; do [ -d "$d/dotgit" ] || continue # only fixture repos (skip README.md, EXPECTED_* etc.) nm="$(basename "$d")" cp -R "$d" "$FIXTURE_WORK/$nm" mv "$FIXTURE_WORK/$nm/dotgit" "$FIXTURE_WORK/$nm/.git" REPO_NAMES+=( "$nm" ); REPO_DIR["$nm"]="$FIXTURE_WORK/$nm" done elif [ -n "$TARGETS_OVERRIDE" ]; then # shellcheck disable=SC2206 # intentional word-split of the space-separated --targets list arr=( $TARGETS_OVERRIDE ) for p in "${arr[@]}"; do p="${p/#\~/$HOME}"; nm="$(basename "$p")"; REPO_NAMES+=( "$nm" ); REPO_DIR["$nm"]="$p"; done log "explicit targets: ${REPO_NAMES[*]}" else if [ "$REFRESH" -eq 1 ]; then [ -n "${GH_TOKEN:-}" ] || die "--refresh needs GH_TOKEN" command -v curl >/dev/null || die "--refresh needs curl" mkdir -p "$MIRROR_DIR" log "refresh: re-discovering + mirroring via shared substrate (no separate clone path)" DISCOVERED="$REPORT_DIR/discovered.tsv" if discover_repos > "$DISCOVERED" 2>>"$REPORT_DIR/discover.log" && [ -s "$DISCOVERED" ]; then while IFS=$'\t' read -r name url branch; do [ -n "$name" ] || continue mirror_repo "$name" "$url" "$branch" || log " mirror FAILED: $name (will use stale mirror if present)" done < "$DISCOVERED" else log "discovery failed — falling back to existing mirrors (coverage may be stale)" fi fi # Default + post-refresh: enumerate EXISTING mirrors. No clone here — reuse the sweep's clones. [ -d "$MIRROR_DIR" ] || die "mirror dir not found: $MIRROR_DIR (run nightly_sweep.sh first, or use --refresh/--targets)" for d in "$MIRROR_DIR"/*/; do [ -d "$d/.git" ] || continue nm="$(basename "$d")"; REPO_NAMES+=( "$nm" ); REPO_DIR["$nm"]="${d%/}" done log "reusing ${#REPO_NAMES[@]} existing mirror(s) in $MIRROR_DIR (no re-clone)" fi [ "${#REPO_NAMES[@]}" -gt 0 ] || die "no repos to scan" # ============================================================================== # RUN CHECKS # ============================================================================== for nm in "${REPO_NAMES[@]}"; do check_repo "$nm" "${REPO_DIR[$nm]}" done # ============================================================================== # ASSEMBLE REPORT (JSON + text), mode 600 (identical shape to compliance-drift) # ============================================================================== if [ "${#FINDINGS[@]}" -gt 0 ]; then FINDINGS_JSON="$(printf '%s\n' "${FINDINGS[@]}" | jq -cs .)" else FINDINGS_JSON="[]" fi if [ "${#SKIPPED_CHECKS[@]}" -gt 0 ]; then SKIPPED_JSON="$(printf '%s\n' "${SKIPPED_CHECKS[@]}" | jq -R . | jq -cs .)" else SKIPPED_JSON="[]" fi N_DRIFT="$(echo "$FINDINGS_JSON" | jq 'length')" N_HIGH="$(echo "$FINDINGS_JSON" | jq '[.[]|select(.severity=="high")] | length')" N_REPOS_DRIFTED="$(echo "$FINDINGS_JSON" | jq '[.[].repo] | unique | length')" jq -n \ --arg checker "doc-drift" --arg ts "$UTC_STAMP" --arg org "$GH_ORG" \ --argjson scanned "${#REPO_NAMES[@]}" \ --argjson findings "$FINDINGS_JSON" --argjson skipped "$SKIPPED_JSON" \ '{checker:$checker, generated:$ts, org:$org, repos_scanned:$scanned, drift_count:($findings|length), repos_with_drift:([$findings[].repo]|unique|length), findings:$findings, skipped_checks:$skipped}' > "$REPORT_JSON" { echo "doc-drift report — $UTC_STAMP" echo "org=$GH_ORG repos_scanned=${#REPO_NAMES[@]} stale_thresholds=${DOC_DRIFT_STALE_DAYS}d/${DOC_DRIFT_STALE_COMMITS}commits" echo "drift findings: $N_DRIFT ($N_HIGH high) across $N_REPOS_DRIFTED repo(s)" echo echo "$FINDINGS_JSON" | jq -r '.[] | "• [\(.severity)] \(.repo): \(.title)\n rule: \(.proof.outcome)"' if [ "$(echo "$SKIPPED_JSON" | jq 'length')" -gt 0 ]; then echo; echo "skipped checks (missing data / not doc-drift's job — NOT counted as drift):" echo "$SKIPPED_JSON" | jq -r '.[] | " - \(.)"' fi } > "$REPORT_TXT" chmod 600 "$REPORT_JSON" "$REPORT_TXT" 2>/dev/null || true log "report: $REPORT_JSON ($N_DRIFT drift finding(s), $N_REPOS_DRIFTED repo(s))" # ============================================================================== # CANARY ASSERTION (anti-complacency floor, design §6.4) # ============================================================================== if [ "$CANARY" -eq 1 ]; then EXPECT_FILE="$HERE/fixtures/doc-drift/EXPECTED_DRIFT_COUNT" [ -f "$EXPECT_FILE" ] || die "canary expected-count file missing: $EXPECT_FILE" EXPECTED="$(tr -dc '0-9' < "$EXPECT_FILE")" log "canary assertion: expected drift=$EXPECTED, got=$N_DRIFT" if [ "$N_DRIFT" -ne "$EXPECTED" ]; then echo "[doc-drift] CANARY FAIL: planted-drift count mismatch (expected $EXPECTED, got $N_DRIFT)" >&2 echo " -> the checklist regressed (a check stopped firing) or the fixture changed. See $REPORT_TXT." >&2 exit 3 fi log "canary PASS: all $EXPECTED planted drifts detected." fi # ============================================================================== # ALARM-ONLY ROUTING (clean = silent; memory feedback_cloudwatch_alarms) # ============================================================================== if [ "$N_DRIFT" -eq 0 ]; then log "no confirmed drift — posting NOTHING to Slack (ALARM-only policy)." exit 0 fi ALARM_BODY="$(echo "$FINDINGS_JSON" | jq -r ' group_by(.repo)[] | "*\(.[0].repo)*: " + ([.[] | "[\(.severity)] \(.title)"] | join("; "))' | sed 's/^/• /')" SLACK_TEXT=":memo: *Sea Haven doc-drift — ALARM* ($UTC_STAMP) $N_DRIFT documentation-drift finding(s) across $N_REPOS_DRIFTED repo(s) ($N_HIGH high): $ALARM_BODY Checks: README-omits-component · README-stale-vs-code (architecture moved, docs did not) Report (mode 600): \`$REPORT_JSON\` (on R720)" SLACK_TEXT="$(echo "$SLACK_TEXT" | redact)" echo "$SLACK_TEXT" >&2 if [ "$DRY_RUN" -eq 1 ]; then log "DRY-RUN: alarm composed but NOT posted (routing dry-run, design §7 Phase 3)." exit 0 fi post_slack_alarm "$SLACK_TEXT" exit 0 # ============================================================================== # PROVISIONING (NOT DONE HERE — gated, Phase 3 / Phase 6): # - No systemd unit / timer is installed by this script. Wiring it into the live # sea-haven-secrev schedule (or a sibling timer) is provisioning and is gated. # - This script is NOT registered in checker_coordinator.sh; the coordinator registry is # integrated centrally (separate change), so doc-drift is not yet driven by the coordinator. # - step-ca / IAM Roles Anywhere / the read-only AWS role / aws-posture are NOT stood up or # built here. The IAM artifacts authored alongside this checker (security-review/iam/) are # FILES for the mandatory GPT-4.1 cross-review; aws-posture itself is hard-gated behind that # review and is built only after it is recorded (design §7, B3). # - The LIVE "Gemini (large context)" doc-drift judge (design §4) is the only LLM seam; it is # an inert stub here (maybe_judge) and stays off in canary / dry-run / offline. # - Confluence + project_r720_agent_team memory updates are docs-as-you-go obligations for the # build session, tracked outside this script. # ==============================================================================