#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

[ "$(id -un)" = "ops" ] || {
  echo "STOP: запускать на router-ops пользователем ops"
  exit 1
}

STEP="STEP_050M07R05_GENERATE_COMPLETE_CANONICAL_CANDIDATES"
PASS_DECISION="PASS_${STEP}"

TOKEN="e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4"
PUBLIC_BASE="https://helena-background-beam-harry.trycloudflare.com/r/${TOKEN}"

ROOT="/opt/router-ops"
STATE_ROOT="${ROOT}/state"
PUBROOT="${ROOT}/public/r/${TOKEN}"

CANONICAL_STATE="${STATE_ROOT}/current-vm101-canonical.env"
CANONICAL_PRIVATE_ROOT="${STATE_ROOT}/canonical/vm101"
CANONICAL_PUBLIC_ROOT="${PUBROOT}/vm101-canonical"
SOURCE_PUBLIC_CURRENT="${CANONICAL_PUBLIC_ROOT}/current"

BUILD_TS="$(date -u +%Y%m%d-%H%M%S)"
CANONICAL_ID="${BUILD_TS}_vm101_m07_canonical_v2_candidates"

WORKSPACE="${CANONICAL_PRIVATE_ROOT}/snapshots/${CANONICAL_ID}"

PUBLIC_SNAPSHOT="${CANONICAL_PUBLIC_ROOT}/snapshots/${CANONICAL_ID}"
PUBLIC_CURRENT="${CANONICAL_PUBLIC_ROOT}/current"

REPORT_SLUG="${BUILD_TS}_step050m07r05_generate_complete_canonical_candidates"
REPORT_DIR="${PUBROOT}/${REPORT_SLUG}"

TRYCF_REPORT="${PUBLIC_BASE}/${REPORT_SLUG}/"
REPORT_TXT="${TRYCF_REPORT}report.txt"
FACTS_JSON="${TRYCF_REPORT}facts.json"

ARCHITECTURE_PLAN="${PUBLIC_BASE}/20260711-181158_local_architecture_plan_vm101_autonomous_hmn_recovery/"
XS_MAP="${PUBLIC_BASE}/20260711-120734_xs_map_audit_repair_publish/"
GLOBAL_PROJECT_PLAN="${PUBLIC_BASE}/20260711-123348_global_project_plan_wg_paid/"
VM101_REFERENCE="${PUBLIC_BASE}/vm101-reference/current/"
VM101_CANONICAL="${PUBLIC_BASE}/vm101-canonical/current/"
VM101_CANONICAL_SNAPSHOT="${PUBLIC_BASE}/vm101-canonical/snapshots/${CANONICAL_ID}/"

CURRENT_STAGE="initialization"
LAST_SUCCESS="step_saved"

SOURCE_CANONICAL_ID="UNRESOLVED"
SOURCE_WORKSPACE="UNRESOLVED"
REFERENCE_ID="UNRESOLVED"
SOURCE_REFERENCE_ID="UNRESOLVED"

TARGET_COUNT=0
GENERATED_COUNT=0
CHANGED_COUNT=0
CONTRACT_PASS_COUNT=0
CONTRACT_FAILURES=0
SYNTAX_FAILURES=0
SECRET_FINDINGS=0
PUBLIC_FILES=0

CURRENT_ID_BEFORE="UNRESOLVED"
CURRENT_ID_AFTER="UNRESOLVED"

PRIVATE_SHA_BUILT=false
PUBLIC_SNAPSHOT_PUBLISHED=false
PUBLIC_CURRENT_UPDATED=false
STATE_UPDATED=false
VALIDATION_PASSED=false

VM101_CONTACTED=false
VM101_MODIFIED=false

mkdir -p "$REPORT_DIR"

cp -a "$0" "$REPORT_DIR/step.sh"
chmod 600 "$REPORT_DIR/step.sh"

PROGRESS_LOG="$REPORT_DIR/progress.log"
: > "$PROGRESS_LOG"

stage() {
  CURRENT_STAGE="$1"

  echo
  echo ">>> [$1] $2" | tee -a "$PROGRESS_LOG"

  date -u '+    utc=%Y-%m-%dT%H:%M:%SZ' |
    tee -a "$PROGRESS_LOG"
}

mark_success() {
  LAST_SUCCESS="$1"
  echo "last_success=$LAST_SUCCESS" >> "$PROGRESS_LOG"
}

state_value() {
  local key="$1"

  sed -n "s/^${key}=//p" "$CANONICAL_STATE" |
    tail -n1
}

json_value() {
  local file="$1"
  local path="$2"

  python3 - "$file" "$path" <<'PY'
import json
import sys

file_path, dotted_path = sys.argv[1:]
value = json.load(open(file_path, encoding="utf-8"))

for part in dotted_path.split("."):
    value = value[part]

if isinstance(value, bool):
    print("true" if value else "false")
else:
    print(value)
PY
}

print_links() {
  echo
  echo "TRYCF_REPORT=$TRYCF_REPORT"
  echo "REPORT_TXT=$REPORT_TXT"
  echo "FACTS_JSON=$FACTS_JSON"
  echo "ARCHITECTURE_PLAN=$ARCHITECTURE_PLAN"
  echo "XS_MAP=$XS_MAP"
  echo "GLOBAL_PROJECT_PLAN=$GLOBAL_PROJECT_PLAN"
  echo "VM101_REFERENCE=$VM101_REFERENCE"
  echo "VM101_CANONICAL=$VM101_CANONICAL"
}

create_index() {
  cat > "$REPORT_DIR/index.html" <<EOF
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>${STEP}</title>
</head>
<body style="font-family:system-ui;max-width:1120px;margin:40px auto;padding:0 20px">
<h1>${STEP}</h1>

<ul>
<li><a href="report.txt">report.txt</a></li>
<li><a href="facts.json">facts.json</a></li>
<li><a href="assessment.json">assessment.json</a></li>
<li><a href="candidate-summary.json">candidate-summary.json</a></li>
<li><a href="contract-tests.json">contract-tests.json</a></li>
<li><a href="diff-summary.json">diff-summary.json</a></li>
<li><a href="syntax-checks.txt">syntax-checks.txt</a></li>
<li><a href="secret-scan.json">secret-scan.json</a></li>
<li><a href="step.sh">step.sh</a></li>
</ul>

<h2>Постоянные ссылки</h2>
<ul>
<li><a href="${VM101_REFERENCE}">VM101 Reference</a></li>
<li><a href="${VM101_CANONICAL}">VM101 Canonical</a></li>
<li><a href="${VM101_CANONICAL_SNAPSHOT}">Immutable canonical snapshot</a></li>
</ul>
</body>
</html>
EOF
}

write_stop() {
  local reason="$1"
  local rc="$2"
  local line="$3"

  cat > "$REPORT_DIR/report.txt" <<EOF
=== ${STEP} RESULT ===
step=${STEP}
decision=STOP_${STEP}_${reason}
step_execution=STOP
operation_result=CANONICAL_CANDIDATES_NOT_PUBLISHED
production_health=UNCHANGED
milestone_status=M07_IN_PROGRESS
all_ok=false

failure:
  rc=${rc}
  line=${line}
  stage=${CURRENT_STAGE}
  last_success=${LAST_SUCCESS}

canonical:
  canonical_id=${CANONICAL_ID}
  source_canonical_id=${SOURCE_CANONICAL_ID}
  source_workspace=${SOURCE_WORKSPACE}
  workspace=${WORKSPACE}
  reference_id=${REFERENCE_ID}
  source_reference_id=${SOURCE_REFERENCE_ID}

counts:
  target_count=${TARGET_COUNT}
  generated_count=${GENERATED_COUNT}
  changed_count=${CHANGED_COUNT}
  contract_pass_count=${CONTRACT_PASS_COUNT}
  contract_failures=${CONTRACT_FAILURES}
  syntax_failures=${SYNTAX_FAILURES}
  secret_findings=${SECRET_FINDINGS}

state:
  private_sha_built=${PRIVATE_SHA_BUILT}
  public_snapshot_published=${PUBLIC_SNAPSHOT_PUBLISHED}
  public_current_updated=${PUBLIC_CURRENT_UPDATED}
  state_updated=${STATE_UPDATED}
  validation_passed=${VALIDATION_PASSED}

safety:
  local_only=true
  vm101_contacted=false
  vm101_modified=false
  installation_allowed=false
  network_changed=false
  services_changed=false
  runtime_state_changed=false
  refresh_ran=false
  rebalance_ran=false

TRYCF_REPORT=${TRYCF_REPORT}
REPORT_TXT=${REPORT_TXT}
FACTS_JSON=${FACTS_JSON}
ARCHITECTURE_PLAN=${ARCHITECTURE_PLAN}
XS_MAP=${XS_MAP}
GLOBAL_PROJECT_PLAN=${GLOBAL_PROJECT_PLAN}
VM101_REFERENCE=${VM101_REFERENCE}
VM101_CANONICAL=${VM101_CANONICAL}
EOF

  python3 - \
    "$STEP" \
    "$reason" \
    "$rc" \
    "$line" \
    "$CURRENT_STAGE" \
    "$LAST_SUCCESS" \
    "$CANONICAL_ID" \
    "$SOURCE_CANONICAL_ID" \
    "$SOURCE_WORKSPACE" \
    "$WORKSPACE" \
    "$REFERENCE_ID" \
    "$SOURCE_REFERENCE_ID" \
    "$TARGET_COUNT" \
    "$GENERATED_COUNT" \
    "$CHANGED_COUNT" \
    "$CONTRACT_PASS_COUNT" \
    "$CONTRACT_FAILURES" \
    "$SYNTAX_FAILURES" \
    "$SECRET_FINDINGS" \
    "$PRIVATE_SHA_BUILT" \
    "$PUBLIC_SNAPSHOT_PUBLISHED" \
    "$PUBLIC_CURRENT_UPDATED" \
    "$STATE_UPDATED" \
    "$VALIDATION_PASSED" \
    "$TRYCF_REPORT" \
    "$REPORT_TXT" \
    "$FACTS_JSON" \
    "$ARCHITECTURE_PLAN" \
    "$XS_MAP" \
    "$GLOBAL_PROJECT_PLAN" \
    "$VM101_REFERENCE" \
    "$VM101_CANONICAL" \
    > "$REPORT_DIR/facts.json" <<'PY'
import json
import sys

(
    step,
    reason,
    rc,
    line,
    stage,
    last_success,
    canonical_id,
    source_canonical_id,
    source_workspace,
    workspace,
    reference_id,
    source_reference_id,
    target_count,
    generated_count,
    changed_count,
    contract_pass_count,
    contract_failures,
    syntax_failures,
    secret_findings,
    private_sha_built,
    snapshot_published,
    current_updated,
    state_updated,
    validation_passed,
    report,
    report_txt,
    facts_json,
    architecture,
    xs_map,
    global_plan,
    vm101_reference,
    vm101_canonical,
) = sys.argv[1:]

to_bool = lambda value: value == "true"

print(json.dumps({
    "schema": "router-step-facts-v1",
    "step": step,
    "assessment": {
        "decision": f"STOP_{step}_{reason}",
        "step_execution": "STOP",
        "operation_result":
            "CANONICAL_CANDIDATES_NOT_PUBLISHED",
        "production_health": "UNCHANGED",
        "milestone_status": "M07_IN_PROGRESS",
        "all_ok": False,
        "failure": {
            "reason": reason,
            "rc": int(rc),
            "line": int(line),
            "stage": stage,
            "last_success": last_success,
        },
    },
    "canonical": {
        "canonical_id": canonical_id,
        "source_canonical_id": source_canonical_id,
        "source_workspace": source_workspace,
        "workspace": workspace,
        "reference_id": reference_id,
        "source_reference_id": source_reference_id,
    },
    "counts": {
        "target_count": int(target_count),
        "generated_count": int(generated_count),
        "changed_count": int(changed_count),
        "contract_pass_count": int(contract_pass_count),
        "contract_failures": int(contract_failures),
        "syntax_failures": int(syntax_failures),
        "secret_findings": int(secret_findings),
    },
    "state": {
        "private_sha_built":
            to_bool(private_sha_built),
        "public_snapshot_published":
            to_bool(snapshot_published),
        "public_current_updated":
            to_bool(current_updated),
        "state_updated":
            to_bool(state_updated),
        "validation_passed":
            to_bool(validation_passed),
    },
    "safety": {
        "local_only": True,
        "vm101_contacted": False,
        "vm101_modified": False,
        "installation_allowed": False,
        "network_changed": False,
        "services_changed": False,
        "runtime_state_changed": False,
        "refresh_ran": False,
        "rebalance_ran": False,
    },
    "publish": {
        "trycf_report": report,
        "report_txt": report_txt,
        "facts_json": facts_json,
        "architecture_plan": architecture,
        "xs_map": xs_map,
        "global_project_plan": global_plan,
        "vm101_reference": vm101_reference,
        "vm101_canonical": vm101_canonical,
    },
}, ensure_ascii=False, indent=2))
PY

  create_index

  find "$REPORT_DIR" \
    -type f \
    ! -name SHA256SUMS \
    -print0 |
    sort -z |
    xargs -0 sha256sum \
    > "$REPORT_DIR/SHA256SUMS"

  print_links
  exit "$rc"
}

fatal() {
  local reason="$1"
  local rc="${2:-1}"
  local line="${3:-$LINENO}"

  trap - ERR
  write_stop "$reason" "$rc" "$line"
}

trap 'rc=$?; fatal "UNEXPECTED_ERROR" "$rc" "$LINENO"' ERR

stage "01/09" "Проверяю finalized canonical edit map"

[ -x "$ROOT/bin/vm101-canonical-validate" ] ||
  fatal "CANONICAL_VALIDATOR_MISSING" 2 "$LINENO"

"$ROOT/bin/vm101-canonical-validate" \
  > "$REPORT_DIR/source-canonical-validation.txt" ||
  fatal "SOURCE_CANONICAL_VALIDATION_FAILED" 3 "$LINENO"

[ -s "$CANONICAL_STATE" ] ||
  fatal "CANONICAL_STATE_MISSING" 4 "$LINENO"

SOURCE_CANONICAL_ID="$(
  state_value VM101_CANONICAL_ID
)"

SOURCE_WORKSPACE="$(
  state_value VM101_CANONICAL_PRIVATE_WORKSPACE
)"

REFERENCE_ID="$(
  state_value VM101_CANONICAL_REFERENCE_ID
)"

SOURCE_REFERENCE_ID="$(
  state_value VM101_CANONICAL_SOURCE_REFERENCE_ID
)"

[ -n "$SOURCE_CANONICAL_ID" ] ||
  fatal "SOURCE_CANONICAL_ID_EMPTY" 5 "$LINENO"

[ -d "$SOURCE_WORKSPACE" ] ||
  fatal "SOURCE_WORKSPACE_MISSING" 6 "$LINENO"

for required in \
  "$SOURCE_WORKSPACE/targets.tsv" \
  "$SOURCE_WORKSPACE/contracts/source-locks.tsv" \
  "$SOURCE_WORKSPACE/contracts/edit-map-private.json" \
  "$SOURCE_WORKSPACE/contracts/replacement-blueprints.json" \
  "$SOURCE_WORKSPACE/canonical-manifest-private.json" \
  "$SOURCE_WORKSPACE/source" \
  "$SOURCE_WORKSPACE/candidate" \
  "$SOURCE_WORKSPACE/SHA256SUMS"
do
  [ -e "$required" ] || {
    echo "MISSING_SOURCE_COMPONENT=$required"
    fatal "SOURCE_COMPONENT_MISSING" 7 "$LINENO"
  }
done

(
  cd "$SOURCE_WORKSPACE"
  sha256sum -c SHA256SUMS
) > "$REPORT_DIR/source-private-sha256.txt" 2>&1 ||
  fatal "SOURCE_PRIVATE_SHA256_FAILED" 8 "$LINENO"

mark_success "source_canonical_verified"

stage "02/09" "Клонирую private workspace для candidate v2"

[ ! -e "$WORKSPACE" ] ||
  fatal "TARGET_WORKSPACE_ALREADY_EXISTS" 9 "$LINENO"

mkdir -p "$(dirname "$WORKSPACE")"

cp -a "$SOURCE_WORKSPACE" "$WORKSPACE"

rm -f "$WORKSPACE/SHA256SUMS"

rm -rf "$WORKSPACE/public-summary"

mkdir -p \
  "$WORKSPACE/tests" \
  "$WORKSPACE/diffs" \
  "$WORKSPACE/contracts" \
  "$WORKSPACE/public-summary"

mark_success "candidate_workspace_cloned"

stage "03/09" "Генерирую полные candidate-файлы"

python3 - \
  "$WORKSPACE" \
  "$CANONICAL_ID" \
  "$SOURCE_CANONICAL_ID" \
  "$REFERENCE_ID" \
  "$SOURCE_REFERENCE_ID" \
  "$REPORT_DIR/generator-log.json" <<'PY'
import difflib
import hashlib
import json
import re
import sys
from pathlib import Path

(
    workspace_arg,
    canonical_id,
    source_canonical_id,
    reference_id,
    source_reference_id,
    generator_log_arg,
) = sys.argv[1:]

workspace = Path(workspace_arg)
generator_log_path = Path(generator_log_arg)

source_root = workspace / "source"
candidate_root = workspace / "candidate"
diff_root = workspace / "diffs"

locks_path = workspace / "contracts/source-locks.tsv"
edit_map_path = workspace / "contracts/edit-map-private.json"
blueprints_path = (
    workspace
    / "contracts/replacement-blueprints.json"
)

edit_map = json.loads(
    edit_map_path.read_text(encoding="utf-8")
)

blueprints = json.loads(
    blueprints_path.read_text(encoding="utf-8")
)


def sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def read_text(path: Path) -> str:
    return path.read_text(
        encoding="utf-8",
        errors="strict",
    )


def write_text(path: Path, text: str) -> None:
    path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    path.write_text(
        text,
        encoding="utf-8",
    )


def replace_exact_once(
    text: str,
    old: str,
    new: str,
    label: str,
) -> str:
    count = text.count(old)

    if count != 1:
        raise RuntimeError(
            f"{label}: expected exact count 1, got {count}"
        )

    return text.replace(old, new, 1)


def replace_regex_once(
    text: str,
    pattern: str,
    replacement: str,
    label: str,
    flags: int = 0,
) -> str:
    compiled = re.compile(pattern, flags)
    result, count = compiled.subn(
        replacement,
        text,
        count=1,
    )

    if count != 1:
        raise RuntimeError(
            f"{label}: expected regex count 1, got {count}"
        )

    return result


RUNTIME_LOADER = """# BEGIN CANONICAL_M07_RUNTIME
VM101_RUNTIME_LIB="${VM101_RUNTIME_LIB:-/usr/local/lib/router-egress-vm101-runtime.sh}"

if [ ! -r "$VM101_RUNTIME_LIB" ]; then
  echo "ERROR: missing runtime library $VM101_RUNTIME_LIB" >&2
  exit 70
fi

. "$VM101_RUNTIME_LIB"
# END CANONICAL_M07_RUNTIME
"""


def insert_runtime_loader(
    text: str,
    label: str,
) -> str:
    if "BEGIN CANONICAL_M07_RUNTIME" in text:
        return text

    match = re.search(
        r"(?m)^set -u[ \t]*$",
        text,
    )

    if match:
        insert_at = match.end()

        return (
            text[:insert_at]
            + "\n\n"
            + RUNTIME_LOADER.rstrip()
            + text[insert_at:]
        )

    if text.startswith("#!"):
        newline = text.find("\n")

        if newline >= 0:
            return (
                text[:newline + 1]
                + "\n"
                + RUNTIME_LOADER
                + "\n"
                + text[newline + 1:]
            )

    raise RuntimeError(
        f"{label}: cannot locate shell preamble"
    )


def assignment_span(
    text: str,
    variable: str,
    require_text: str | None = None,
):
    pattern = re.compile(
        rf"\b{re.escape(variable)}"
        rf"[ \t]*=[ \t]*[\"']?\$\(",
    )

    matches = list(pattern.finditer(text))

    candidates = []

    for match in matches:
        command_start = text.find(
            "$(",
            match.start(),
            match.end(),
        )

        if command_start < 0:
            continue

        index = command_start + 2
        depth = 1
        single_quote = False
        double_quote = False
        escaped = False

        while index < len(text):
            char = text[index]

            if escaped:
                escaped = False
                index += 1
                continue

            if char == "\\" and not single_quote:
                escaped = True
                index += 1
                continue

            if char == "'" and not double_quote:
                single_quote = not single_quote
                index += 1
                continue

            if char == '"' and not single_quote:
                double_quote = not double_quote
                index += 1
                continue

            if not single_quote:
                if char == "(":
                    depth += 1
                elif char == ")":
                    depth -= 1

                    if depth == 0:
                        end = index + 1

                        while (
                            end < len(text)
                            and text[end] in "\"'"
                        ):
                            end += 1

                        block = text[
                            match.start():end
                        ]

                        if (
                            require_text is None
                            or require_text in block
                        ):
                            candidates.append(
                                (
                                    match.start(),
                                    end,
                                    block,
                                )
                            )

                        break

            index += 1

    if len(candidates) != 1:
        raise RuntimeError(
            f"assignment {variable}: expected one "
            f"matching span, got {len(candidates)}"
        )

    return candidates[0]


def replace_assignment(
    text: str,
    variable: str,
    replacement: str,
    require_text: str | None = None,
) -> str:
    start, end, _ = assignment_span(
        text,
        variable,
        require_text,
    )

    return text[:start] + replacement + text[end:]


def inject_after_function_open(
    text: str,
    function_name: str,
    body: str,
    label: str,
) -> str:
    pattern = re.compile(
        rf"\b{re.escape(function_name)}"
        rf"[ \t]*\(\)[ \t]*\{{",
    )

    matches = list(pattern.finditer(text))

    if len(matches) != 1:
        raise RuntimeError(
            f"{label}: function {function_name} "
            f"count={len(matches)}"
        )

    match = matches[0]

    return (
        text[:match.end()]
        + "\n"
        + body.rstrip()
        + "\n"
        + text[match.end():]
    )


def replace_uci_runtime_current(
    text: str,
    label: str,
) -> str:
    pattern = re.compile(
        r'\b(?P<var>cur|current)'
        r'="\$\('
        r'uci[ \t]+-q[ \t]+get[ \t]+'
        r'network\.\$\{iface\}\.hmn_endpoint'
        r'[ \t]+2>/dev/null'
        r'[ \t]+\|\|[ \t]+true'
        r'\)"'
    )

    matches = list(pattern.finditer(text))

    if len(matches) != 1:
        raise RuntimeError(
            f"{label}: stale UCI current count="
            f"{len(matches)}"
        )

    match = matches[0]
    variable = match.group("var")

    replacement = (
        f'{variable}="$(vm101_runtime_endpoint '
        f'"$iface" 2>/dev/null || '
        f'uci -q get network.${{iface}}.'
        f'hmn_endpoint 2>/dev/null || true)"'
    )

    return (
        text[:match.start()]
        + replacement
        + text[match.end():]
    )


def transform_bootstrap_script(
    text: str,
    label: str,
) -> tuple[str, list[str]]:
    text = insert_runtime_loader(
        text,
        label,
    )

    notes = [
        "runtime_library_loaded",
    ]

    variables = ["ACTIVE", "TABLE_DEV"]
    replaced = []

    for variable in variables:
        try:
            start, end, block = assignment_span(
                text,
                variable,
                "table 200",
            )
        except RuntimeError:
            continue

        replacement = (
            f'{variable}="$('
            f'vm101_healthy_bootstrap_iface || true'
            f')"'
        )

        text = (
            text[:start]
            + replacement
            + text[end:]
        )

        replaced.append(variable)

    if not replaced:
        try:
            text = replace_assignment(
                text,
                "ACTIVE",
                'ACTIVE="$(vm101_healthy_bootstrap_iface || true)"',
            )

            replaced.append("ACTIVE")
        except RuntimeError as error:
            raise RuntimeError(
                f"{label}: bootstrap assignment "
                f"not found: {error}"
            )

    guard_variable = (
        "ACTIVE"
        if "ACTIVE" in replaced
        else replaced[0]
    )

    guard = (
        f'\n[ -n "${guard_variable}" ] || {{\n'
        f'  echo "ERROR: no strict healthy VPN '
        f'interface among vpn1-vpn5" >&2\n'
        f'  exit 71\n'
        f'}}\n'
    )

    assignment_text = (
        f'{guard_variable}="$('
        f'vm101_healthy_bootstrap_iface || true'
        f')"'
    )

    location = text.find(assignment_text)

    if location < 0:
        raise RuntimeError(
            f"{label}: replacement assignment absent"
        )

    end_line = text.find(
        "\n",
        location,
    )

    if end_line < 0:
        end_line = len(text)

    if "no strict healthy VPN interface" not in text:
        text = (
            text[:end_line]
            + guard
            + text[end_line:]
        )

    notes.append(
        "healthy_bootstrap_assignment:"
        + ",".join(replaced)
    )

    return text, notes


def transform_refresh_pool_safe(
    text: str,
) -> tuple[str, list[str]]:
    label = "refresh_pool_safe"

    text = insert_runtime_loader(
        text,
        label,
    )

    old_root = "/root/hmn/backups/refresh-safe-before-"
    new_root = "/tmp/hmn-refresh-backups/refresh-safe-before-"

    root_count = text.count(old_root)

    if root_count < 1:
        raise RuntimeError(
            f"{label}: backup path count={root_count}"
        )

    text = text.replace(
        old_root,
        new_root,
    )

    functions = """# BEGIN CANONICAL_M07_REFRESH_GUARDS
CANONICAL_BACKUP_ROOT="${CANONICAL_BACKUP_ROOT:-/tmp/hmn-refresh-backups}"

mkdir -p "$CANONICAL_BACKUP_ROOT" || {
  echo "ERROR: cannot create $CANONICAL_BACKUP_ROOT" >&2
  exit 72
}

vm101_require_free_kb / 16384 || {
  echo "ERROR: root storage preflight failed" >&2
  exit 72
}

vm101_require_free_kb /tmp 32768 || {
  echo "ERROR: /tmp storage preflight failed" >&2
  exit 73
}

canonical_backup_dir() {
  printf '%s\\n' "${BACKUPDIR:-${BACK:-}}"
}

canonical_prepare_backup_manifest() {
  backup_dir="$(canonical_backup_dir)"

  [ -n "$backup_dir" ] || {
    echo "ERROR: backup directory variable unresolved" >&2
    return 74
  }

  [ -d "$backup_dir" ] || {
    echo "ERROR: backup directory missing: $backup_dir" >&2
    return 74
  }

  manifest="$backup_dir/manifest.tsv"
  complete="$backup_dir/manifest.complete"

  rm -f "$manifest" "$complete"

  for name in \
    configs-latest-readlink.txt \
    configs-latest-realpath.txt \
    ok-awg1-strict-all-latest.tsv \
    ok-awg1-strict-foreign-latest.tsv \
    selected-awg1-latest.tsv
  do
    source_file="/root/hmn/cache/$name"
    backup_file="$backup_dir/$name"

    if [ -e "$source_file" ]; then
      [ -s "$backup_file" ] || {
        echo "ERROR: incomplete backup artifact $name" >&2
        return 74
      }
    fi
  done

  find "$backup_dir" \
    -type f \
    ! -name manifest.tsv \
    ! -name manifest.complete \
    -print |
  sort |
  while IFS= read -r file; do
    [ -s "$file" ] || {
      echo "ERROR: empty backup artifact $file" >&2
      exit 75
    }

    hash="$(vm101_file_sha256 "$file")" || exit 75
    size="$(wc -c < "$file" | tr -d '[:space:]')"

    printf '%s\\t%s\\t%s\\n' \
      "$hash" \
      "$size" \
      "${file#${backup_dir}/}"
  done > "$manifest" || return 75

  [ -s "$manifest" ] || {
    echo "ERROR: empty backup manifest" >&2
    return 75
  }

  printf 'complete=true\\n' > "$complete"
}

canonical_download_all_awg() {
  canonical_prepare_backup_manifest || return $?

  /root/hmn/hmn-download-all-awg.sh "$@"
}
# END CANONICAL_M07_REFRESH_GUARDS
"""

    loader_end = text.find(
        "# END CANONICAL_M07_RUNTIME"
    )

    if loader_end < 0:
        raise RuntimeError(
            f"{label}: runtime marker absent"
        )

    loader_end = text.find(
        "\n",
        loader_end,
    )

    if loader_end < 0:
        loader_end = len(text)

    text = (
        text[:loader_end]
        + "\n\n"
        + functions
        + text[loader_end:]
    )

    downloader_count = text.count(
        "/root/hmn/hmn-download-all-awg.sh"
    )

    if downloader_count < 2:
        raise RuntimeError(
            f"{label}: expected original downloader "
            f"plus wrapper, count={downloader_count}"
        )

    first_original = text.find(
        "/root/hmn/hmn-download-all-awg.sh",
        text.find("# END CANONICAL_M07_REFRESH_GUARDS"),
    )

    if first_original < 0:
        raise RuntimeError(
            f"{label}: original downloader invocation absent"
        )

    wrapper_definition = text.find(
        "/root/hmn/hmn-download-all-awg.sh",
        text.find("canonical_download_all_awg()"),
    )

    search_start = wrapper_definition + 1

    invocation = text.find(
        "/root/hmn/hmn-download-all-awg.sh",
        search_start,
    )

    if invocation < 0:
        raise RuntimeError(
            f"{label}: external downloader invocation absent"
        )

    text = (
        text[:invocation]
        + "canonical_download_all_awg"
        + text[
            invocation
            + len("/root/hmn/hmn-download-all-awg.sh"):
        ]
    )

    restore_gate = """  backup_dir="$(canonical_backup_dir)"

  [ -s "$backup_dir/manifest.complete" ] || {
    echo "ERROR: rollback refused; backup manifest incomplete" >&2
    return 90
  }
"""

    text = inject_after_function_open(
        text,
        "restore_backup",
        restore_gate,
        label,
    )

    return text, [
        "backup_root_moved_to_tmp",
        "root_storage_preflight",
        "tmp_storage_preflight",
        "backup_manifest_gate",
        "rollback_manifest_gate",
        "download_wrapped_by_manifest_validation",
    ]


def transform_planner(
    text: str,
) -> tuple[str, list[str]]:
    text = insert_runtime_loader(
        text,
        "planner",
    )

    text = replace_uci_runtime_current(
        text,
        "planner",
    )

    return text, [
        "runtime_library_loaded",
        "current_endpoint_from_amneziawg_runtime",
        "uci_endpoint_fallback_only",
    ]


def transform_rebalance_apply(
    text: str,
) -> tuple[str, list[str]]:
    label = "rebalance_apply"

    text = insert_runtime_loader(
        text,
        label,
    )

    text = replace_uci_runtime_current(
        text,
        label,
    )

    strict_override = """  vm101_strict_iface "$1" 3 1
  return $?
"""

    text = inject_after_function_open(
        text,
        "strict_ping",
        strict_override,
        label,
    )

    exit_contract = """
# BEGIN CANONICAL_M07_EXIT_CONTRACT
CANONICAL_EXIT_RC=0

case "$decision" in
  commit_failed)
    CANONICAL_EXIT_RC=41
    ;;
  refuse)
    if [ "$MODE" = "--commit" ]; then
      CANONICAL_EXIT_RC=42
    fi
    ;;
esac

exit "$CANONICAL_EXIT_RC"
# END CANONICAL_M07_EXIT_CONTRACT
"""

    if "BEGIN CANONICAL_M07_EXIT_CONTRACT" in text:
        raise RuntimeError(
            f"{label}: exit contract already present"
        )

    text = text.rstrip() + "\n" + exit_contract

    return text, [
        "runtime_current_endpoint",
        "strict_ping_three_attempts",
        "commit_failed_exit_41",
        "commit_refuse_exit_42",
        "json_shell_status_agreement",
    ]


def transform_runner(
    text: str,
) -> tuple[str, list[str]]:
    label = "emergency_runner"

    helpers = """json_output_string() {
  file="$1"
  key="$2"

  sed -n \
    "s/.*\\"${key}\\"[[:space:]]*:[[:space:]]*\\"\\([^\\"]*\\)\\".*/\\1/p" \
    "$file" |
  tail -n1
}

json_output_bool() {
  file="$1"
  key="$2"

  sed -n \
    "s/.*\\"${key}\\"[[:space:]]*:[[:space:]]*\\(true\\|false\\).*/\\1/p" \
    "$file" |
  tail -n1
}

"""

    marker = re.compile(
        r"\bacquire_lock[ \t]*\(\)[ \t]*\{"
    )

    matches = list(marker.finditer(text))

    if len(matches) != 1:
        raise RuntimeError(
            f"{label}: acquire_lock count={len(matches)}"
        )

    match = matches[0]

    text = (
        text[:match.start()]
        + helpers
        + text[match.start():]
    )

    apply_pattern = re.compile(
        r'"\$EMERGENCY_REBALANCE_APPLY_CMD"'
        r'[ \t\r\n]+--commit'
        r'[ \t\r\n]+--confirm'
        r'[ \t\r\n]+REBALANCE_TOP5_DAILY'
        r'[ \t\r\n]+>>'
        r'[ \t\r\n]*"\$EMERGENCY_LOG"'
        r'[ \t\r\n]+2>&1'
        r'[ \t\r\n]+rebalance_rc=\$\?'
        r'[ \t\r\n]+set_state'
        r'[ \t\r\n]+last_emergency_rebalance_rc'
        r'[ \t\r\n]+"\$rebalance_rc"',
        re.MULTILINE,
    )

    matches = list(apply_pattern.finditer(text))

    if len(matches) != 1:
        raise RuntimeError(
            f"{label}: apply block count={len(matches)}"
        )

    replacement = """rebalance_output="/tmp/emergency-rebalance-output.$$"
          : > "$rebalance_output"

          if "$EMERGENCY_REBALANCE_APPLY_CMD" \
            --commit \
            --confirm REBALANCE_TOP5_DAILY \
            > "$rebalance_output" 2>&1
          then
            rebalance_rc=0
          else
            rebalance_rc=$?
          fi

          cat "$rebalance_output" \
            >> "$EMERGENCY_LOG" 2>/dev/null ||
            true

          rebalance_decision="$(
            json_output_string \
              "$rebalance_output" \
              decision
          )"

          rebalance_apply_ok="$(
            json_output_bool \
              "$rebalance_output" \
              apply_ok
          )"

          case "$rebalance_decision" in
            commit_ok|noop)
              ;;
            *)
              rebalance_rc=65
              ;;
          esac

          [ "$rebalance_apply_ok" = "true" ] ||
            rebalance_rc=65

          rm -f "$rebalance_output"

          set_state \
            last_emergency_rebalance_rc \
            "$rebalance_rc"
"""

    text = (
        text[:matches[0].start()]
        + replacement
        + text[matches[0].end():]
    )

    return text, [
        "rebalance_output_captured",
        "shell_rc_checked",
        "decision_commit_ok_or_noop_required",
        "apply_ok_true_required",
        "false_success_exit_65",
    ]


def transform_slots_apply(
    text: str,
) -> tuple[str, list[str]]:
    label = "slots_apply"

    text = insert_runtime_loader(
        text,
        label,
    )

    pattern = re.compile(
        r'ip route show table 200'
        r'[ \t\r\n]*\|'
        r'[ \t\r\n]*grep -F'
        r'[ \t\r\n]*"default dev vpn1"'
        r'[ \t\r\n]*>/dev/null'
        r'[ \t\r\n]*\|\|'
        r'[ \t\r\n]*\{'
        r'.*?'
        r'legacy_table200_default_vpn1_missing'
        r'.*?'
        r'exit 20'
        r'.*?'
        r'\}',
        re.DOTALL,
    )

    matches = list(pattern.finditer(text))

    if len(matches) != 1:
        raise RuntimeError(
            f"{label}: legacy gate count={len(matches)}"
        )

    replacement = """if vm101_table200_default_present; then
    echo "legacy_table200_default_present=1"
  else
    echo "legacy_table200_default_present=0"
  fi"""

    text = (
        text[:matches[0].start()]
        + replacement
        + text[matches[0].end():]
    )

    return text, [
        "runtime_library_loaded",
        "tables_201_205_remain_authoritative",
        "table_200_informational_only",
        "legacy_exit_20_removed",
    ]


locks = {}

for number, line in enumerate(
    locks_path.read_text(
        encoding="utf-8"
    ).splitlines()
):
    if number == 0:
        continue

    (
        label,
        source_path,
        source_sha256,
        size_bytes,
        sensitive,
    ) = line.split("\t")

    locks[label] = {
        "source_path": source_path,
        "source_sha256": source_sha256,
        "size_bytes": int(size_bytes),
        "sensitive": sensitive == "true",
    }

edit_targets = {
    item["label"]: item
    for item in edit_map["targets"]
}

blueprint_targets = {
    item["label"]: item
    for item in blueprints["targets"]
}

expected_labels = {
    "refresh_pool_safe",
    "code_test",
    "download_all_awg",
    "refresh_awg",
    "validate_current_pool",
    "planner",
    "rebalance_apply",
    "emergency_runner",
    "slots_apply",
}

if set(locks) != expected_labels:
    raise RuntimeError(
        "source lock labels mismatch: "
        + ",".join(sorted(set(locks)))
    )

if set(edit_targets) != expected_labels:
    raise RuntimeError(
        "edit map labels mismatch"
    )

if set(blueprint_targets) != expected_labels:
    raise RuntimeError(
        "blueprint labels mismatch"
    )

results = []

for label in sorted(expected_labels):
    lock = locks[label]
    source_path = lock["source_path"]
    relative = source_path.lstrip("/")

    source_file = source_root / relative
    candidate_file = candidate_root / relative

    source_bytes = source_file.read_bytes()
    source_hash = sha256_bytes(source_bytes)

    if source_hash != lock["source_sha256"]:
        raise RuntimeError(
            f"{label}: source lock mismatch"
        )

    if len(source_bytes) != lock["size_bytes"]:
        raise RuntimeError(
            f"{label}: source size mismatch"
        )

    source_text = source_bytes.decode("utf-8")

    edit_target = edit_targets[label]

    if edit_target["source_sha256"] != source_hash:
        raise RuntimeError(
            f"{label}: edit map source mismatch"
        )

    if (
        blueprint_targets[label]["source_sha256"]
        != source_hash
    ):
        raise RuntimeError(
            f"{label}: blueprint source mismatch"
        )

    source_lines = source_text.splitlines()

    for region in edit_target["regions"]:
        start = int(region["start_line"])
        end = int(region["end_line"])

        exact_text = "\n".join(
            f"{number:04d}: {source_lines[number - 1]}"
            for number in range(start, end + 1)
        ) + "\n"

        actual = sha256_bytes(
            exact_text.encode("utf-8")
        )

        if actual != region["exact_sha256"]:
            raise RuntimeError(
                f"{label}: region hash mismatch "
                f"{region['region_id']}"
            )

    if label == "refresh_pool_safe":
        candidate_text, notes = (
            transform_refresh_pool_safe(
                source_text
            )
        )
    elif label in {
        "code_test",
        "download_all_awg",
        "refresh_awg",
        "validate_current_pool",
    }:
        candidate_text, notes = (
            transform_bootstrap_script(
                source_text,
                label,
            )
        )

        if (
            label == "validate_current_pool"
            and "hmn_endpoint" in candidate_text
        ):
            try:
                candidate_text = (
                    replace_uci_runtime_current(
                        candidate_text,
                        label,
                    )
                )

                notes.append(
                    "runtime_endpoint_before_uci_fallback"
                )
            except RuntimeError:
                notes.append(
                    "no_unique_uci_endpoint_assignment"
                )
    elif label == "planner":
        candidate_text, notes = (
            transform_planner(
                source_text
            )
        )
    elif label == "rebalance_apply":
        candidate_text, notes = (
            transform_rebalance_apply(
                source_text
            )
        )
    elif label == "emergency_runner":
        candidate_text, notes = (
            transform_runner(
                source_text
            )
        )
    elif label == "slots_apply":
        candidate_text, notes = (
            transform_slots_apply(
                source_text
            )
        )
    else:
        raise RuntimeError(
            f"unsupported label: {label}"
        )

    if candidate_text == source_text:
        raise RuntimeError(
            f"{label}: candidate unchanged"
        )

    if not candidate_text.endswith("\n"):
        candidate_text += "\n"

    write_text(
        candidate_file,
        candidate_text,
    )

    candidate_hash = sha256_bytes(
        candidate_text.encode("utf-8")
    )

    diff_lines = list(
        difflib.unified_diff(
            source_text.splitlines(),
            candidate_text.splitlines(),
            fromfile=source_path + ".source",
            tofile=source_path + ".candidate",
            lineterm="",
        )
    )

    diff_text = "\n".join(diff_lines) + "\n"

    write_text(
        diff_root / f"{label}.private.diff",
        diff_text,
    )

    added = sum(
        1
        for line in diff_lines
        if line.startswith("+")
        and not line.startswith("+++")
    )

    removed = sum(
        1
        for line in diff_lines
        if line.startswith("-")
        and not line.startswith("---")
    )

    results.append({
        "label": label,
        "source_path": source_path,
        "sensitive": lock["sensitive"],
        "source_sha256": source_hash,
        "candidate_sha256": candidate_hash,
        "source_size_bytes": len(source_bytes),
        "candidate_size_bytes":
            len(candidate_text.encode("utf-8")),
        "source_line_count":
            len(source_text.splitlines()),
        "candidate_line_count":
            len(candidate_text.splitlines()),
        "diff_added_lines": added,
        "diff_removed_lines": removed,
        "notes": notes,
        "changed": True,
    })

runtime_library = (
    candidate_root
    / "usr/local/lib/router-egress-vm101-runtime.sh"
)

if not runtime_library.is_file():
    raise RuntimeError(
        "canonical runtime library missing"
    )

runtime_text = read_text(runtime_library)

required_runtime_tokens = [
    "vm101_runtime_cli()",
    "vm101_runtime_endpoint()",
    "vm101_strict_iface()",
    "vm101_healthy_bootstrap_iface()",
    "vm101_routes_201_205_ok()",
    "vm101_table200_default_present()",
    "vm101_require_free_kb()",
    "vm101_file_sha256()",
]

for token in required_runtime_tokens:
    if token not in runtime_text:
        raise RuntimeError(
            f"runtime library token missing: {token}"
        )

generator_result = {
    "schema":
        "vm101-canonical-candidate-generator-v1",
    "canonical_id": canonical_id,
    "source_canonical_id":
        source_canonical_id,
    "reference_id": reference_id,
    "source_reference_id":
        source_reference_id,
    "target_count": len(results),
    "generated_count": len(results),
    "changed_count":
        sum(1 for item in results if item["changed"]),
    "results": results,
    "runtime_library": {
        "path":
            "/usr/local/lib/router-egress-vm101-runtime.sh",
        "sha256":
            sha256_bytes(runtime_library.read_bytes()),
        "required_tokens":
            required_runtime_tokens,
    },
    "installation_allowed": False,
    "vm101_contacted": False,
    "vm101_modified": False,
}

generator_log_path.write_text(
    json.dumps(
        generator_result,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(
    workspace
    / "contracts/candidate-generator.json"
).write_text(
    json.dumps(
        generator_result,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)
PY

TARGET_COUNT="$(
  json_value \
    "$REPORT_DIR/generator-log.json" \
    target_count
)"

GENERATED_COUNT="$(
  json_value \
    "$REPORT_DIR/generator-log.json" \
    generated_count
)"

CHANGED_COUNT="$(
  json_value \
    "$REPORT_DIR/generator-log.json" \
    changed_count
)"

[ "$TARGET_COUNT" -eq 9 ] ||
  fatal "TARGET_COUNT_INVALID" 10 "$LINENO"

[ "$GENERATED_COUNT" -eq 9 ] ||
  fatal "GENERATION_INCOMPLETE" 11 "$LINENO"

[ "$CHANGED_COUNT" -eq 9 ] ||
  fatal "NOT_ALL_CANDIDATES_CHANGED" 12 "$LINENO"

mark_success "complete_candidates_generated"

stage "04/09" "Проверяю shell-синтаксис всех candidates"

: > "$REPORT_DIR/syntax-checks.txt"

while IFS= read -r file; do
  relative="${file#${WORKSPACE}/candidate/}"

  case "$relative" in
    *.sh|etc/init.d/*|usr/local/sbin/*|usr/local/lib/*)
      if sh -n "$file" \
        >> "$REPORT_DIR/syntax-checks.txt" \
        2>&1
      then
        echo "PASS $relative" \
          >> "$REPORT_DIR/syntax-checks.txt"
      else
        echo "FAIL $relative" \
          >> "$REPORT_DIR/syntax-checks.txt"

        SYNTAX_FAILURES=$((SYNTAX_FAILURES + 1))
      fi
      ;;
  esac
done < <(
  find "$WORKSPACE/candidate" \
    -type f |
    sort
)

cp -a \
  "$REPORT_DIR/syntax-checks.txt" \
  "$WORKSPACE/tests/syntax-checks-r05.txt"

[ "$SYNTAX_FAILURES" -eq 0 ] ||
  fatal "CANDIDATE_SYNTAX_FAILED" 13 "$LINENO"

mark_success "candidate_syntax_passed"

stage "05/09" "Запускаю статические contract tests"

python3 - \
  "$WORKSPACE" \
  "$REPORT_DIR/contract-tests.json" \
  "$REPORT_DIR/diff-summary.json" <<'PY'
import json
import sys
from pathlib import Path

workspace = Path(sys.argv[1])
contract_output = Path(sys.argv[2])
diff_output = Path(sys.argv[3])

generator = json.loads(
    (
        workspace
        / "contracts/candidate-generator.json"
    ).read_text(encoding="utf-8")
)

candidate_root = workspace / "candidate"

paths = {
    item["label"]:
        candidate_root / item["source_path"].lstrip("/")
    for item in generator["results"]
}

texts = {
    label: path.read_text(
        encoding="utf-8",
        errors="strict",
    )
    for label, path in paths.items()
}

checks = []


def check(
    label: str,
    name: str,
    passed: bool,
    detail: str,
) -> None:
    checks.append({
        "label": label,
        "name": name,
        "passed": bool(passed),
        "detail": detail,
    })


for label in [
    "code_test",
    "download_all_awg",
    "refresh_awg",
    "validate_current_pool",
]:
    text = texts[label]

    check(
        label,
        "runtime_loader",
        "BEGIN CANONICAL_M07_RUNTIME" in text,
        "common runtime library is sourced",
    )

    check(
        label,
        "healthy_bootstrap",
        "vm101_healthy_bootstrap_iface" in text,
        "bootstrap uses strict healthy vpn1-vpn5",
    )

    check(
        label,
        "no_legacy_active_error",
        "no strict healthy VPN interface among vpn1-vpn5"
        in text,
        "explicit failure remains when no healthy slot exists",
    )

planner = texts["planner"]

check(
    "planner",
    "runtime_loader",
    "BEGIN CANONICAL_M07_RUNTIME" in planner,
    "runtime library loaded",
)

check(
    "planner",
    "runtime_current",
    'vm101_runtime_endpoint "$iface"' in planner,
    "planner current endpoint comes from runtime",
)

check(
    "planner",
    "uci_fallback",
    "hmn_endpoint" in planner,
    "UCI remains fallback only",
)

apply_text = texts["rebalance_apply"]

check(
    "rebalance_apply",
    "runtime_current",
    'vm101_runtime_endpoint "$iface"' in apply_text,
    "apply current endpoint comes from runtime",
)

check(
    "rebalance_apply",
    "strict_retry",
    'vm101_strict_iface "$1" 3 1'
    in apply_text,
    "strict health uses three attempts",
)

check(
    "rebalance_apply",
    "commit_failed_nonzero",
    "CANONICAL_EXIT_RC=41" in apply_text,
    "commit_failed returns 41",
)

check(
    "rebalance_apply",
    "commit_refuse_nonzero",
    "CANONICAL_EXIT_RC=42" in apply_text,
    "commit refusal returns 42",
)

check(
    "rebalance_apply",
    "exit_contract",
    'exit "$CANONICAL_EXIT_RC"' in apply_text,
    "JSON decision and shell status are coupled",
)

runner = texts["emergency_runner"]

check(
    "emergency_runner",
    "output_capture",
    'rebalance_output="/tmp/emergency-rebalance-output.$$"'
    in runner,
    "rebalance output is captured",
)

check(
    "emergency_runner",
    "decision_validation",
    "rebalance_decision" in runner
    and "commit_ok|noop" in runner,
    "decision must be commit_ok or noop",
)

check(
    "emergency_runner",
    "apply_ok_validation",
    "rebalance_apply_ok" in runner
    and '[ "$rebalance_apply_ok" = "true" ]'
    in runner,
    "apply_ok must be true",
)

check(
    "emergency_runner",
    "false_success_rc",
    "rebalance_rc=65" in runner,
    "false-success is converted to rc 65",
)

slots = texts["slots_apply"]

check(
    "slots_apply",
    "table200_optional",
    "legacy_table200_default_present=0"
    in slots
    and "legacy_table200_default_present=1"
    in slots,
    "table 200 is informational",
)

check(
    "slots_apply",
    "legacy_exit_removed",
    "legacy_table200_default_vpn1_missing"
    not in slots
    and "exit 20" not in slots,
    "legacy table-200 hard gate removed",
)

check(
    "slots_apply",
    "tables_201_205_preserved",
    all(
        f"table {table}" in slots
        for table in [201, 202, 203, 204, 205]
    ),
    "five authoritative route tables remain",
)

refresh = texts["refresh_pool_safe"]

check(
    "refresh_pool_safe",
    "tmp_backup_root",
    "/tmp/hmn-refresh-backups/refresh-safe-before-"
    in refresh,
    "large refresh backup moved to tmpfs",
)

check(
    "refresh_pool_safe",
    "root_preflight",
    "vm101_require_free_kb / 16384"
    in refresh,
    "root free-space preflight present",
)

check(
    "refresh_pool_safe",
    "tmp_preflight",
    "vm101_require_free_kb /tmp 32768"
    in refresh,
    "tmp free-space preflight present",
)

check(
    "refresh_pool_safe",
    "manifest_creation",
    "canonical_prepare_backup_manifest()"
    in refresh
    and "manifest.complete" in refresh,
    "backup manifest and completion marker present",
)

check(
    "refresh_pool_safe",
    "download_manifest_gate",
    "canonical_download_all_awg()"
    in refresh,
    "download is wrapped by manifest gate",
)

check(
    "refresh_pool_safe",
    "rollback_manifest_gate",
    "rollback refused; backup manifest incomplete"
    in refresh,
    "rollback refuses incomplete backup",
)

runtime = (
    candidate_root
    / "usr/local/lib/router-egress-vm101-runtime.sh"
).read_text(encoding="utf-8")

for token in [
    "vm101_runtime_endpoint()",
    "vm101_healthy_bootstrap_iface()",
    "vm101_require_free_kb()",
    "vm101_file_sha256()",
]:
    check(
        "runtime_library",
        token,
        token in runtime,
        f"required runtime helper {token}",
    )

failed = [
    item
    for item in checks
    if not item["passed"]
]

contract_result = {
    "schema":
        "vm101-canonical-contract-tests-v1",
    "passed": not failed,
    "test_count": len(checks),
    "pass_count":
        sum(1 for item in checks if item["passed"]),
    "failure_count": len(failed),
    "checks": checks,
    "failures": failed,
}

contract_output.write_text(
    json.dumps(
        contract_result,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(
    workspace
    / "tests/contract-tests-r05.json"
).write_text(
    json.dumps(
        contract_result,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

diff_summary = {
    "schema":
        "vm101-canonical-diff-summary-v1",
    "candidate_count":
        len(generator["results"]),
    "changed_count":
        generator["changed_count"],
    "candidates": [
        {
            "label": item["label"],
            "source_path": item["source_path"],
            "sensitive": item["sensitive"],
            "source_sha256": (
                "PRIVATE_ONLY"
                if item["sensitive"]
                else item["source_sha256"]
            ),
            "candidate_sha256": (
                "PRIVATE_ONLY"
                if item["sensitive"]
                else item["candidate_sha256"]
            ),
            "source_size_bytes":
                item["source_size_bytes"],
            "candidate_size_bytes":
                item["candidate_size_bytes"],
            "source_line_count":
                item["source_line_count"],
            "candidate_line_count":
                item["candidate_line_count"],
            "diff_added_lines":
                item["diff_added_lines"],
            "diff_removed_lines":
                item["diff_removed_lines"],
            "notes": item["notes"],
            "full_private_diff":
                f"diffs/{item['label']}.private.diff",
        }
        for item in generator["results"]
    ],
    "private_diff_contents_published": False,
}

diff_output.write_text(
    json.dumps(
        diff_summary,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

if failed:
    raise SystemExit(20)
PY

CONTRACT_PASS_COUNT="$(
  json_value \
    "$REPORT_DIR/contract-tests.json" \
    pass_count
)"

CONTRACT_FAILURES="$(
  json_value \
    "$REPORT_DIR/contract-tests.json" \
    failure_count
)"

[ "$CONTRACT_FAILURES" -eq 0 ] ||
  fatal "CONTRACT_TESTS_FAILED" 14 "$LINENO"

mark_success "contract_tests_passed"

stage "06/09" "Обновляю private candidate manifest и hashes"

python3 - \
  "$WORKSPACE" \
  "$CANONICAL_ID" \
  "$SOURCE_CANONICAL_ID" \
  "$REFERENCE_ID" \
  "$SOURCE_REFERENCE_ID" <<'PY'
import hashlib
import json
import sys
from pathlib import Path

(
    workspace_arg,
    canonical_id,
    source_canonical_id,
    reference_id,
    source_reference_id,
) = sys.argv[1:]

workspace = Path(workspace_arg)

generator = json.loads(
    (
        workspace
        / "contracts/candidate-generator.json"
    ).read_text(encoding="utf-8")
)

contract_tests = json.loads(
    (
        workspace
        / "tests/contract-tests-r05.json"
    ).read_text(encoding="utf-8")
)

syntax_path = (
    workspace
    / "tests/syntax-checks-r05.txt"
)


def sha256(path: Path) -> str:
    return hashlib.sha256(
        path.read_bytes()
    ).hexdigest()


targets = []

for item in generator["results"]:
    targets.append({
        "label": item["label"],
        "destination":
            item["source_path"],
        "source_sha256":
            item["source_sha256"],
        "candidate_sha256":
            item["candidate_sha256"],
        "sensitive":
            item["sensitive"],
        "changed":
            item["changed"],
        "diff_added_lines":
            item["diff_added_lines"],
        "diff_removed_lines":
            item["diff_removed_lines"],
        "notes":
            item["notes"],
    })

runtime_path = (
    workspace
    / "candidate/usr/local/lib/"
    / "router-egress-vm101-runtime.sh"
)

manifest = {
    "schema":
        "vm101-canonical-private-manifest-v2",
    "canonical_id": canonical_id,
    "source_canonical_id":
        source_canonical_id,
    "reference_id": reference_id,
    "source_reference_id":
        source_reference_id,
    "candidate_status":
        "COMPLETE_CANDIDATES_GENERATED_NOT_INSTALLED",
    "target_count": len(targets),
    "generated_count":
        generator["generated_count"],
    "changed_count":
        generator["changed_count"],
    "targets": targets,
    "new_files": [{
        "destination":
            "/usr/local/lib/router-egress-vm101-runtime.sh",
        "candidate_sha256":
            sha256(runtime_path),
        "sensitive": False,
    }],
    "tests": {
        "syntax_passed":
            syntax_path.is_file(),
        "contract_tests_passed":
            contract_tests["passed"],
        "contract_test_count":
            contract_tests["test_count"],
        "contract_pass_count":
            contract_tests["pass_count"],
        "contract_failure_count":
            contract_tests["failure_count"],
    },
    "installation": {
        "allowed": False,
        "performed": False,
        "vm101_contacted": False,
        "vm101_modified": False,
    },
    "next_required_step":
        "REVIEW_PRIVATE_DIFFS_AND_BUILD_INSTALL_BUNDLE",
}

(
    workspace
    / "canonical-manifest-private.json"
).write_text(
    json.dumps(
        manifest,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)
PY

cp -a \
  "$REPORT_DIR/syntax-checks.txt" \
  "$WORKSPACE/tests/syntax-checks-r05.txt"

(
  cd "$WORKSPACE"

  find . \
    -type f \
    ! -name SHA256SUMS \
    -print0 |
    sort -z |
    xargs -0 sha256sum \
    > SHA256SUMS

  sha256sum -c SHA256SUMS
) > "$REPORT_DIR/private-sha256-check.txt" 2>&1 ||
  fatal "PRIVATE_SHA256_FAILED" 15 "$LINENO"

PRIVATE_SHA_BUILT=true

mark_success "private_candidate_manifest_finalized"

stage "07/09" "Строю sanitized public candidate summary"

PUBLIC_STAGE="$WORKSPACE/public-summary"

rm -rf "$PUBLIC_STAGE"

mkdir -p \
  "$PUBLIC_STAGE/contracts" \
  "$PUBLIC_STAGE/tests" \
  "$PUBLIC_STAGE/docs"

python3 - \
  "$WORKSPACE" \
  "$REPORT_DIR/diff-summary.json" \
  "$REPORT_DIR/contract-tests.json" \
  "$CANONICAL_ID" \
  "$SOURCE_CANONICAL_ID" \
  "$REFERENCE_ID" \
  "$SOURCE_REFERENCE_ID" <<'PY'
import hashlib
import json
import sys
from pathlib import Path

(
    workspace_arg,
    diff_summary_arg,
    contract_tests_arg,
    canonical_id,
    source_canonical_id,
    reference_id,
    source_reference_id,
) = sys.argv[1:]

workspace = Path(workspace_arg)
public_root = workspace / "public-summary"

generator = json.loads(
    (
        workspace
        / "contracts/candidate-generator.json"
    ).read_text(encoding="utf-8")
)

diff_summary = json.load(
    open(diff_summary_arg, encoding="utf-8")
)

contract_tests = json.load(
    open(contract_tests_arg, encoding="utf-8")
)


def sha256(path: Path) -> str:
    return hashlib.sha256(
        path.read_bytes()
    ).hexdigest()


public_targets = []

for item in generator["results"]:
    public_targets.append({
        "label": item["label"],
        "destination":
            item["source_path"],
        "sensitive":
            item["sensitive"],
        "source_sha256": (
            "PRIVATE_ONLY"
            if item["sensitive"]
            else item["source_sha256"]
        ),
        "candidate_sha256": (
            "PRIVATE_ONLY"
            if item["sensitive"]
            else item["candidate_sha256"]
        ),
        "source_size_bytes":
            item["source_size_bytes"],
        "candidate_size_bytes":
            item["candidate_size_bytes"],
        "diff_added_lines":
            item["diff_added_lines"],
        "diff_removed_lines":
            item["diff_removed_lines"],
        "notes":
            item["notes"],
        "complete_candidate_generated":
            True,
        "full_candidate_public":
            False,
        "full_diff_public":
            False,
    })

runtime_path = (
    workspace
    / "candidate/usr/local/lib/"
    / "router-egress-vm101-runtime.sh"
)

manifest = {
    "schema":
        "vm101-canonical-public-manifest-v2",
    "canonical_id": canonical_id,
    "source_canonical_id":
        source_canonical_id,
    "reference_id": reference_id,
    "source_reference_id":
        source_reference_id,
    "candidate_status":
        "COMPLETE_CANDIDATES_GENERATED_NOT_INSTALLED",
    "target_count": len(public_targets),
    "generated_count":
        generator["generated_count"],
    "changed_count":
        generator["changed_count"],
    "targets": public_targets,
    "new_files": [{
        "destination":
            "/usr/local/lib/router-egress-vm101-runtime.sh",
        "candidate_sha256":
            sha256(runtime_path),
        "sensitive": False,
        "full_candidate_public": False,
    }],
    "tests": {
        "syntax_passed": True,
        "contract_tests_passed":
            contract_tests["passed"],
        "contract_test_count":
            contract_tests["test_count"],
        "contract_pass_count":
            contract_tests["pass_count"],
        "contract_failure_count":
            contract_tests["failure_count"],
    },
    "publication": {
        "private_source_contents_published":
            False,
        "private_candidate_contents_published":
            False,
        "private_diff_contents_published":
            False,
        "sensitive_hashes_published":
            False,
    },
    "installation": {
        "allowed": False,
        "performed": False,
        "vm101_contacted": False,
        "vm101_modified": False,
    },
    "next_required_step":
        "REVIEW_PRIVATE_DIFFS_AND_BUILD_INSTALL_BUNDLE",
}

(public_root / "canonical-manifest.json").write_text(
    json.dumps(
        manifest,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(
    public_root
    / "contracts/candidate-summary.json"
).write_text(
    json.dumps(
        {
            "schema":
                "vm101-canonical-public-candidate-summary-v1",
            "canonical_id":
                canonical_id,
            "source_canonical_id":
                source_canonical_id,
            "candidate_status":
                manifest["candidate_status"],
            "targets":
                public_targets,
        },
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(
    public_root
    / "contracts/diff-summary.json"
).write_text(
    json.dumps(
        diff_summary,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(
    public_root
    / "tests/contract-tests.json"
).write_text(
    json.dumps(
        contract_tests,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

syntax_source = (
    workspace
    / "tests/syntax-checks-r05.txt"
)

(
    public_root
    / "tests/syntax-checks.txt"
).write_text(
    syntax_source.read_text(
        encoding="utf-8"
    ),
    encoding="utf-8",
)

status_doc = f"""# VM101 canonical candidate status

Canonical ID: {canonical_id}

Source canonical ID: {source_canonical_id}

Nine complete candidate files were generated from exact source locks.

The common runtime library is included as a new candidate file.

Validation:

- generated targets: {generator['generated_count']}/9
- changed targets: {generator['changed_count']}/9
- shell syntax: passed
- contract tests: {contract_tests['pass_count']}/{contract_tests['test_count']}
- VM101 contacted: no
- VM101 modified: no
- installation allowed: no

Full source files, candidate files and private diffs remain only in
the private canonical workspace on VM130.

The next step must review source-to-candidate diffs and build an
installation bundle with live hash checks, surgical backup, atomic
replacement and automatic rollback.
"""

(
    public_root
    / "docs/candidate-status.md"
).write_text(
    status_doc,
    encoding="utf-8",
)
PY

cp -a \
  "$PUBLIC_STAGE/contracts/candidate-summary.json" \
  "$REPORT_DIR/candidate-summary.json"

python3 - \
  "$PUBLIC_STAGE" \
  "$REPORT_DIR/secret-scan.json" <<'PY'
import json
import re
import sys
from pathlib import Path

root = Path(sys.argv[1])
output = Path(sys.argv[2])

patterns = [
    (
        "private_pem",
        re.compile(
            r"-----BEGIN "
            r"(?:RSA |EC |OPENSSH )?"
            r"PRIVATE KEY-----",
            re.IGNORECASE,
        ),
    ),
    (
        "wireguard_private_value",
        re.compile(
            r"^\s*(?:PrivateKey|PresharedKey)"
            r"\s*=\s*(?!REDACTED\b)\S+",
            re.IGNORECASE,
        ),
    ),
    (
        "authorization_header",
        re.compile(
            r"Authorization\s*:\s*"
            r"(?:Bearer|Basic)\s+"
            r"(?!REDACTED\b)\S+",
            re.IGNORECASE,
        ),
    ),
    (
        "credential_url",
        re.compile(
            r"[a-z][a-z0-9+.-]*://"
            r"[^/\s:@]+:[^/\s@]+@",
            re.IGNORECASE,
        ),
    ),
    (
        "long_base64_blob",
        re.compile(
            r"\b[A-Za-z0-9+/]{80,}={0,2}\b"
        ),
    ),
]

findings = []

for path in sorted(root.rglob("*")):
    if not path.is_file():
        continue

    if path.name == "secret-scan.json":
        continue

    try:
        text = path.read_text(
            encoding="utf-8",
            errors="strict",
        )
    except UnicodeDecodeError:
        findings.append({
            "file": str(path.relative_to(root)),
            "line": 0,
            "type": "non_utf8_file",
        })
        continue

    for line_number, line in enumerate(
        text.splitlines(),
        start=1,
    ):
        for finding_type, pattern in patterns:
            if pattern.search(line):
                findings.append({
                    "file":
                        str(path.relative_to(root)),
                    "line": line_number,
                    "type": finding_type,
                    "sample": line[:180],
                })

result = {
    "schema":
        "vm101-canonical-public-secret-scan-v1",
    "passed": not findings,
    "finding_count": len(findings),
    "findings": findings,
}

payload = json.dumps(
    result,
    ensure_ascii=False,
    indent=2,
) + "\n"

(root / "secret-scan.json").write_text(
    payload,
    encoding="utf-8",
)

output.write_text(
    payload,
    encoding="utf-8",
)

if findings:
    raise SystemExit(40)
PY

SECRET_FINDINGS="$(
  json_value \
    "$REPORT_DIR/secret-scan.json" \
    finding_count
)"

[ "$SECRET_FINDINGS" -eq 0 ] ||
  fatal "PUBLIC_SECRET_SCAN_FAILED" 16 "$LINENO"

cat > "$PUBLIC_STAGE/index.html" <<EOF
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>VM101 Canonical ${CANONICAL_ID}</title>
</head>
<body style="font-family:system-ui;max-width:1120px;margin:40px auto;padding:0 20px">
<h1>VM101 Canonical candidates</h1>

<p>Canonical ID: <code>${CANONICAL_ID}</code></p>
<p>Source canonical ID: <code>${SOURCE_CANONICAL_ID}</code></p>

<ul>
<li><a href="canonical-manifest.json">canonical-manifest.json</a></li>
<li><a href="contracts/candidate-summary.json">candidate-summary.json</a></li>
<li><a href="contracts/diff-summary.json">diff-summary.json</a></li>
<li><a href="tests/contract-tests.json">contract-tests.json</a></li>
<li><a href="tests/syntax-checks.txt">syntax-checks.txt</a></li>
<li><a href="docs/candidate-status.md">candidate-status.md</a></li>
<li><a href="secret-scan.json">secret-scan.json</a></li>
<li><a href="SHA256SUMS">SHA256SUMS</a></li>
</ul>

<p>Complete candidate files and private diffs remain private on VM130.</p>
<p>VM101 was not contacted or modified.</p>
</body>
</html>
EOF

(
  cd "$PUBLIC_STAGE"

  find . \
    -type f \
    ! -name SHA256SUMS \
    -print0 |
    sort -z |
    xargs -0 sha256sum \
    > SHA256SUMS

  sha256sum -c SHA256SUMS
) > "$REPORT_DIR/public-stage-sha256.txt" 2>&1 ||
  fatal "PUBLIC_STAGE_SHA256_FAILED" 17 "$LINENO"

PUBLIC_FILES="$(
  find "$PUBLIC_STAGE" \
    -type f |
    wc -l |
    tr -d '[:space:]'
)"

mark_success "sanitized_public_summary_built"

stage "08/09" "Публикую candidate canonical snapshot и current"

[ ! -e "$PUBLIC_SNAPSHOT" ] ||
  fatal "PUBLIC_SNAPSHOT_ALREADY_EXISTS" 18 "$LINENO"

PUBLIC_TMP="${PUBLIC_SNAPSHOT}.new.$$"
CURRENT_NEW="${CANONICAL_PUBLIC_ROOT}/current.new.$$"
CURRENT_OLD="${CANONICAL_PUBLIC_ROOT}/current.old.$$"

mkdir -p "$CANONICAL_PUBLIC_ROOT/snapshots"

rm -rf \
  "$PUBLIC_TMP" \
  "$CURRENT_NEW" \
  "$CURRENT_OLD"

cp -a "$PUBLIC_STAGE" "$PUBLIC_TMP"
chmod -R a+rX "$PUBLIC_TMP"

(
  cd "$PUBLIC_TMP"
  sha256sum -c SHA256SUMS
) > "$REPORT_DIR/public-snapshot-precheck.txt" 2>&1 ||
  fatal "PUBLIC_SNAPSHOT_PRECHECK_FAILED" 19 "$LINENO"

mv "$PUBLIC_TMP" "$PUBLIC_SNAPSHOT"
PUBLIC_SNAPSHOT_PUBLISHED=true

if [ -s "$PUBLIC_CURRENT/canonical-manifest.json" ]; then
  CURRENT_ID_BEFORE="$(
    json_value \
      "$PUBLIC_CURRENT/canonical-manifest.json" \
      canonical_id
  )"
else
  CURRENT_ID_BEFORE="MISSING"
fi

cp -a "$PUBLIC_SNAPSHOT" "$CURRENT_NEW"

(
  cd "$CURRENT_NEW"
  sha256sum -c SHA256SUMS
) > "$REPORT_DIR/current-new-sha256.txt" 2>&1 ||
  fatal "CURRENT_NEW_SHA256_FAILED" 20 "$LINENO"

if [ -e "$PUBLIC_CURRENT" ]; then
  mv "$PUBLIC_CURRENT" "$CURRENT_OLD"
fi

mv "$CURRENT_NEW" "$PUBLIC_CURRENT"
rm -rf "$CURRENT_OLD"

PUBLIC_CURRENT_UPDATED=true

CURRENT_ID_AFTER="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    canonical_id
)"

[ "$CURRENT_ID_AFTER" = "$CANONICAL_ID" ] ||
  fatal "CURRENT_ID_AFTER_MISMATCH" 21 "$LINENO"

rm -f "${CANONICAL_PRIVATE_ROOT}/current"

ln -s \
  "snapshots/${CANONICAL_ID}" \
  "${CANONICAL_PRIVATE_ROOT}/current"

cat > "$CANONICAL_STATE" <<EOF
VM101_CANONICAL_ID=${CANONICAL_ID}
VM101_CANONICAL=${VM101_CANONICAL}
VM101_CANONICAL_SNAPSHOT=${VM101_CANONICAL_SNAPSHOT}
VM101_CANONICAL_PRIVATE_WORKSPACE=${WORKSPACE}
VM101_CANONICAL_REFERENCE_ID=${REFERENCE_ID}
VM101_CANONICAL_SOURCE_REFERENCE_ID=${SOURCE_REFERENCE_ID}
VM101_CANONICAL_SOURCE_CANONICAL_ID=${SOURCE_CANONICAL_ID}
VM101_CANONICAL_GENERATED_AT_UTC=${BUILD_TS}
VM101_CANONICAL_STATUS=COMPLETE_CANDIDATES_GENERATED_NOT_INSTALLED
EOF

chmod 600 "$CANONICAL_STATE"
STATE_UPDATED=true

mark_success "candidate_canonical_published"

stage "09/09" "Проверяю опубликованный candidate canonical"

(
  cd "$PUBLIC_CURRENT"
  sha256sum -c SHA256SUMS
) > "$REPORT_DIR/current-sha256-check.txt" 2>&1 ||
  fatal "PUBLISHED_CURRENT_SHA256_FAILED" 22 "$LINENO"

PUBLISHED_ID="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    canonical_id
)"

PUBLISHED_STATUS="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    candidate_status
)"

PUBLISHED_GENERATED="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    generated_count
)"

PUBLISHED_CHANGED="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    changed_count
)"

PUBLISHED_CONTRACT_PASS="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    tests.contract_tests_passed
)"

PUBLISHED_INSTALL_ALLOWED="$(
  json_value \
    "$PUBLIC_CURRENT/canonical-manifest.json" \
    installation.allowed
)"

[ "$PUBLISHED_ID" = "$CANONICAL_ID" ] ||
  fatal "PUBLISHED_ID_MISMATCH" 23 "$LINENO"

[ "$PUBLISHED_STATUS" = "COMPLETE_CANDIDATES_GENERATED_NOT_INSTALLED" ] ||
  fatal "PUBLISHED_STATUS_MISMATCH" 24 "$LINENO"

[ "$PUBLISHED_GENERATED" = "9" ] ||
  fatal "PUBLISHED_GENERATED_COUNT_INVALID" 25 "$LINENO"

[ "$PUBLISHED_CHANGED" = "9" ] ||
  fatal "PUBLISHED_CHANGED_COUNT_INVALID" 26 "$LINENO"

[ "$PUBLISHED_CONTRACT_PASS" = "true" ] ||
  fatal "PUBLISHED_CONTRACT_STATUS_INVALID" 27 "$LINENO"

[ "$PUBLISHED_INSTALL_ALLOWED" = "false" ] ||
  fatal "INSTALLATION_UNEXPECTEDLY_ALLOWED" 28 "$LINENO"

VALIDATION_PASSED=true

PRIVATE_MANIFEST_SHA="$(
  sha256sum \
    "$WORKSPACE/canonical-manifest-private.json" |
    cut -d' ' -f1
)"

PUBLIC_MANIFEST_SHA="$(
  sha256sum \
    "$PUBLIC_CURRENT/canonical-manifest.json" |
    cut -d' ' -f1
)"

GENERATOR_SHA="$(
  sha256sum \
    "$WORKSPACE/contracts/candidate-generator.json" |
    cut -d' ' -f1
)"

CONTRACT_SHA="$(
  sha256sum \
    "$WORKSPACE/tests/contract-tests-r05.json" |
    cut -d' ' -f1
)"

DIFF_SUMMARY_SHA="$(
  sha256sum \
    "$REPORT_DIR/diff-summary.json" |
    cut -d' ' -f1
)"

python3 - \
  "$STEP" \
  "$CANONICAL_ID" \
  "$SOURCE_CANONICAL_ID" \
  "$REFERENCE_ID" \
  "$SOURCE_REFERENCE_ID" \
  "$WORKSPACE" \
  "$PUBLIC_SNAPSHOT" \
  "$VM101_CANONICAL_SNAPSHOT" \
  "$CURRENT_ID_BEFORE" \
  "$CURRENT_ID_AFTER" \
  "$TARGET_COUNT" \
  "$GENERATED_COUNT" \
  "$CHANGED_COUNT" \
  "$CONTRACT_PASS_COUNT" \
  "$CONTRACT_FAILURES" \
  "$SYNTAX_FAILURES" \
  "$SECRET_FINDINGS" \
  "$PUBLIC_FILES" \
  "$PRIVATE_MANIFEST_SHA" \
  "$PUBLIC_MANIFEST_SHA" \
  "$GENERATOR_SHA" \
  "$CONTRACT_SHA" \
  "$DIFF_SUMMARY_SHA" \
  "$VM101_CANONICAL" \
  > "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

(
    step,
    canonical_id,
    source_canonical_id,
    reference_id,
    source_reference_id,
    workspace,
    snapshot_path,
    snapshot_url,
    current_before,
    current_after,
    target_count,
    generated_count,
    changed_count,
    contract_pass_count,
    contract_failures,
    syntax_failures,
    secret_findings,
    public_files,
    private_manifest_sha,
    public_manifest_sha,
    generator_sha,
    contract_sha,
    diff_summary_sha,
    current_url,
) = sys.argv[1:]

print(json.dumps({
    "schema":
        "vm101-canonical-candidate-assessment-v1",
    "decision": f"PASS_{step}",
    "step_execution": "PASS",
    "operation_result":
        "COMPLETE_CANONICAL_CANDIDATES_GENERATED",
    "production_health": "UNCHANGED",
    "milestone_status": "M07_IN_PROGRESS",
    "all_ok": True,
    "canonical": {
        "canonical_id": canonical_id,
        "source_canonical_id":
            source_canonical_id,
        "reference_id": reference_id,
        "source_reference_id":
            source_reference_id,
        "private_workspace": workspace,
        "snapshot_path": snapshot_path,
        "snapshot_url": snapshot_url,
        "current_url": current_url,
        "current_id_before": current_before,
        "current_id_after": current_after,
        "status":
            "COMPLETE_CANDIDATES_GENERATED_NOT_INSTALLED",
    },
    "counts": {
        "target_count": int(target_count),
        "generated_count": int(generated_count),
        "changed_count": int(changed_count),
        "contract_pass_count":
            int(contract_pass_count),
        "contract_failures":
            int(contract_failures),
        "syntax_failures":
            int(syntax_failures),
        "secret_findings":
            int(secret_findings),
        "public_files":
            int(public_files),
    },
    "integrity": {
        "private_manifest_sha256":
            private_manifest_sha,
        "public_manifest_sha256":
            public_manifest_sha,
        "generator_sha256":
            generator_sha,
        "contract_tests_sha256":
            contract_sha,
        "diff_summary_sha256":
            diff_summary_sha,
    },
    "candidate_state": {
        "complete_files_generated": True,
        "private_diffs_generated": True,
        "syntax_passed": True,
        "contract_tests_passed": True,
        "installation_allowed": False,
        "installation_performed": False,
    },
    "safety": {
        "local_only": True,
        "vm101_contacted": False,
        "vm101_modified": False,
        "network_changed": False,
        "services_changed": False,
        "runtime_state_changed": False,
        "refresh_ran": False,
        "rebalance_ran": False,
    },
    "next_step":
        "REVIEW_PRIVATE_DIFFS_AND_BUILD_INSTALL_BUNDLE",
}, ensure_ascii=False, indent=2))
PY

cat > "$REPORT_DIR/report.txt" <<EOF
=== ${STEP} RESULT ===
step=${STEP}
decision=${PASS_DECISION}
step_execution=PASS
operation_result=COMPLETE_CANONICAL_CANDIDATES_GENERATED
production_health=UNCHANGED
milestone_status=M07_IN_PROGRESS
all_ok=true

canonical:
  canonical_id=${CANONICAL_ID}
  source_canonical_id=${SOURCE_CANONICAL_ID}
  reference_id=${REFERENCE_ID}
  source_reference_id=${SOURCE_REFERENCE_ID}
  private_workspace=${WORKSPACE}
  public_snapshot=${PUBLIC_SNAPSHOT}
  public_snapshot_url=${VM101_CANONICAL_SNAPSHOT}
  public_current=${VM101_CANONICAL}
  current_id_before=${CURRENT_ID_BEFORE}
  current_id_after=${CURRENT_ID_AFTER}
  status=COMPLETE_CANDIDATES_GENERATED_NOT_INSTALLED

counts:
  target_count=${TARGET_COUNT}
  generated_count=${GENERATED_COUNT}
  changed_count=${CHANGED_COUNT}
  contract_pass_count=${CONTRACT_PASS_COUNT}
  contract_failures=${CONTRACT_FAILURES}
  syntax_failures=${SYNTAX_FAILURES}
  secret_findings=${SECRET_FINDINGS}
  public_files=${PUBLIC_FILES}

integrity:
  private_manifest_sha256=${PRIVATE_MANIFEST_SHA}
  public_manifest_sha256=${PUBLIC_MANIFEST_SHA}
  generator_sha256=${GENERATOR_SHA}
  contract_tests_sha256=${CONTRACT_SHA}
  diff_summary_sha256=${DIFF_SUMMARY_SHA}

candidate_state:
  complete_files_generated=true
  private_diffs_generated=true
  syntax_passed=true
  contract_tests_passed=true
  installation_allowed=false
  installation_performed=false

safety:
  local_only=true
  vm101_contacted=false
  vm101_modified=false
  network_changed=false
  services_changed=false
  runtime_state_changed=false
  refresh_ran=false
  rebalance_ran=false

plan:
  current_milestone=M07
  milestone_completed=false

next_step:
  REVIEW_PRIVATE_DIFFS_AND_BUILD_INSTALL_BUNDLE

TRYCF_REPORT=${TRYCF_REPORT}
REPORT_TXT=${REPORT_TXT}
FACTS_JSON=${FACTS_JSON}
ARCHITECTURE_PLAN=${ARCHITECTURE_PLAN}
XS_MAP=${XS_MAP}
GLOBAL_PROJECT_PLAN=${GLOBAL_PROJECT_PLAN}
VM101_REFERENCE=${VM101_REFERENCE}
VM101_CANONICAL=${VM101_CANONICAL}
EOF

python3 - \
  "$REPORT_DIR/assessment.json" \
  "$STEP" \
  "$BUILD_TS" \
  "$TRYCF_REPORT" \
  "$REPORT_TXT" \
  "$FACTS_JSON" \
  "$ARCHITECTURE_PLAN" \
  "$XS_MAP" \
  "$GLOBAL_PROJECT_PLAN" \
  "$VM101_REFERENCE" \
  "$VM101_CANONICAL" \
  > "$REPORT_DIR/facts.json" <<'PY'
import json
import sys

(
    assessment_path,
    step,
    generated_at,
    report,
    report_txt,
    facts_json,
    architecture,
    xs_map,
    global_plan,
    vm101_reference,
    vm101_canonical,
) = sys.argv[1:]

assessment = json.load(
    open(assessment_path, encoding="utf-8")
)

print(json.dumps({
    "schema": "router-step-facts-v1",
    "step": step,
    "generated_at_utc": generated_at,
    "assessment": assessment,
    "safety": assessment["safety"],
    "plan": {
        "current_milestone": "M07",
        "milestone_completed": False,
    },
    "next_step":
        "REVIEW_PRIVATE_DIFFS_AND_BUILD_INSTALL_BUNDLE",
    "publish": {
        "trycf_report": report,
        "report_txt": report_txt,
        "facts_json": facts_json,
        "architecture_plan": architecture,
        "xs_map": xs_map,
        "global_project_plan": global_plan,
        "vm101_reference": vm101_reference,
        "vm101_canonical": vm101_canonical,
    },
}, ensure_ascii=False, indent=2))
PY

create_index

find "$REPORT_DIR" \
  -type f \
  ! -name SHA256SUMS \
  -print0 |
  sort -z |
  xargs -0 sha256sum \
  > "$REPORT_DIR/SHA256SUMS"

trap - ERR

echo "decision=$PASS_DECISION"
echo "step_execution=PASS"
echo "operation_result=COMPLETE_CANONICAL_CANDIDATES_GENERATED"
echo "production_health=UNCHANGED"
echo "canonical_id=$CANONICAL_ID"
echo "source_canonical_id=$SOURCE_CANONICAL_ID"
echo "target_count=$TARGET_COUNT"
echo "generated_count=$GENERATED_COUNT"
echo "changed_count=$CHANGED_COUNT"
echo "contract_pass_count=$CONTRACT_PASS_COUNT"
echo "contract_failures=$CONTRACT_FAILURES"
echo "syntax_failures=$SYNTAX_FAILURES"
echo "secret_findings=$SECRET_FINDINGS"
echo "installation_allowed=false"
echo "vm101_contacted=false"
echo "vm101_modified=false"

print_links
