',
- document,
- re.DOTALL,
- )
- if match is None:
- return "missing"
- return match.group("value").strip()
-
-
-def validate_html_report(
- document: str,
- path: Path,
- backend_image: str | None,
- errors: list[str],
-) -> None:
- if backend_image is None:
- return
- append_mismatch(
- errors,
- path,
- "backend version",
- backend_runtime_version(backend_image),
- html_code_claim(document, "production-backend-version"),
- )
- append_mismatch(
- errors,
- path,
- "backend image",
- backend_image,
- html_code_claim(document, "production-backend-image"),
- )
-
-
-def local_env_reference(
- compose_path: str, reference: str
-) -> tuple[str | None, bool]:
- expression = ENV_REFERENCE_RE.fullmatch(reference)
- unresolved = expression is not None or "$" in reference
- candidate = expression.group("default") if expression is not None else reference
- if not candidate or "$" in candidate or candidate.startswith(("/", "\\")):
- return None, unresolved or bool(candidate)
- compose_directory = Path(compose_path).parent
- relative = (compose_directory / Path(candidate)).as_posix()
- while relative.startswith("./"):
- relative = relative[2:]
- if relative == ".." or relative.startswith("../"):
- return None, True
- return relative, unresolved
-
-
-def protected_invariant(key: str) -> str:
- if key == "MICROSERVICE_METADATA_ENABLED":
- return "metadata enabled"
- return f"BENS disabled ({key})"
-
-
-def validate_protected_values(
- values: dict[str, str | None],
- path: Path,
- errors: list[str],
- require_metadata: bool,
- invariant_prefix: str = "",
-) -> None:
- for key, expected in PROTECTED_BACKEND_ENV.items():
- actual = values.get(key)
- if actual is None and (not require_metadata or key != "MICROSERVICE_METADATA_ENABLED"):
- continue
- if actual != expected:
- invariant = protected_invariant(key)
- if invariant_prefix:
- invariant = f"{invariant_prefix} {invariant}"
- errors.append(
- diagnostic(
- path,
- invariant,
- expected,
- actual if actual is not None else "missing",
- )
- )
-
-
-def validate_backend_sources(
- root: Path,
- compose_path: str,
- backend: dict[str, object],
- env_cache: dict[str, dict[str, str] | None],
- errors: list[str],
-) -> None:
- effective_values: dict[str, str | None] = {}
- canonical_source_referenced = False
- for reference in backend["env_files"]:
- local_path, unresolved = local_env_reference(compose_path, reference)
- if local_path == COMMON_BLOCKSCOUT_ENV and not unresolved:
- canonical_source_referenced = True
- if local_path is None:
- for key in PROTECTED_BACKEND_ENV:
- effective_values[key] = "unresolved"
- continue
- if local_path not in env_cache:
- try:
- env_cache[local_path] = read_env(root / local_path)
- except FileNotFoundError:
- env_cache[local_path] = None
- errors.append(
- diagnostic(
- Path(local_path), "missing required file", "present", "missing"
- )
- )
- except StructureError as error:
- env_cache[local_path] = None
- errors.append(
- diagnostic(Path(local_path), "env structure", "supported env", str(error))
- )
- values = env_cache.get(local_path)
- if values is not None and local_path != COMMON_BLOCKSCOUT_ENV:
- validate_protected_values(values, Path(local_path), errors, False)
- if unresolved:
- for key in PROTECTED_BACKEND_ENV:
- effective_values[key] = "unresolved"
- elif values is not None:
- for key in PROTECTED_BACKEND_ENV:
- if key in values:
- effective_values[key] = values[key]
-
- inline_environment = backend["environment"]
- validate_protected_values(
- inline_environment, Path(compose_path), errors, require_metadata=False
- )
- for key in PROTECTED_BACKEND_ENV:
- if key in inline_environment:
- inline_value = inline_environment[key]
- effective_values[key] = (
- inline_value if inline_value is not None else "unresolved"
- )
-
- if not canonical_source_referenced:
- errors.append(
- diagnostic(
- Path(compose_path),
- "canonical backend env source",
- COMMON_BLOCKSCOUT_ENV,
- "missing",
- )
- )
-
- validate_protected_values(
- effective_values,
- Path(compose_path),
- errors,
- require_metadata=True,
- invariant_prefix="effective",
- )
-
-
-def dependency_workflow_errors(text: str, path: Path) -> list[str]:
- errors: list[str] = []
- lines = yaml_lines(text)
- on_start, on_end = mapping_block(lines, "on", 0)
- push_start, push_end = mapping_block(lines, "push", 2, on_start, on_end)
- paths_start, paths_end = mapping_block(
- lines, "paths", 4, push_start, push_end
- )
- actual_paths: set[str] = set()
- for index in range(paths_start, paths_end):
- line_number, indent, content = lines[index]
- if indent != 6 or not content.startswith("- "):
- raise StructureError(f"unsupported push path at line {line_number}")
- actual_paths.add(parse_scalar(content[2:]))
- missing_paths = sorted(REQUIRED_DEPENDENCY_PUSH_PATHS - actual_paths)
- if missing_paths:
- errors.append(
- diagnostic(
- path,
- "required push paths",
- "complete guard source set",
- f"missing: {', '.join(missing_paths)}",
- )
- )
-
- jobs_start, jobs_end = mapping_block(lines, "jobs", 0)
- workflow_start, workflow_end = mapping_block(
- lines, "workflow-scripts", 2, jobs_start, jobs_end
- )
- job_properties = direct_mapping_children(
- lines,
- workflow_start,
- workflow_end,
- 4,
- "workflow-scripts job property",
- )
- if "steps" not in job_properties:
- raise StructureError("missing workflow-scripts steps mapping")
- steps_value, steps_start, steps_end = job_properties["steps"]
- if steps_value:
- raise StructureError("unsupported inline workflow-scripts steps")
-
- def guard_control_reasons(
- properties: dict[str, tuple[str, int, int]], scope: str
- ) -> list[str]:
- reasons: list[str] = []
- controls = (("if", "true"), ("continue-on-error", "false"))
- for key, safe_value in controls:
- if key not in properties:
- continue
- value, child_start, child_end = properties[key]
- if child_start != child_end:
- raise StructureError(f"unsupported {scope} {key} structure")
- actual = parse_scalar(value).lower()
- if actual != safe_value:
- reasons.append(f"{scope} {key}: {actual or 'missing'}")
- return reasons
-
- job_reasons = guard_control_reasons(job_properties, "job")
- step_markers = [
- index
- for index in range(steps_start, steps_end)
- if lines[index][1] == 6
- ]
- for index in step_markers:
- if not lines[index][2].startswith("- "):
- raise StructureError(
- f"unsupported workflow step at line {lines[index][0]}"
- )
-
- active_command = False
- inactive_reasons: list[str] = []
- for marker_position, marker_index in enumerate(step_markers):
- step_end = steps_end
- if marker_position + 1 < len(step_markers):
- step_end = step_markers[marker_position + 1]
- inline_entry = split_yaml_mapping(lines[marker_index][2][2:].strip())
- if inline_entry is None:
- raise StructureError(
- f"unsupported workflow step at line {lines[marker_index][0]}"
- )
- step_properties = direct_mapping_children(
- lines,
- marker_index + 1,
- step_end,
- 8,
- "workflow step property",
- )
- inline_key, inline_value = inline_entry
- if inline_key in step_properties:
- raise StructureError(f"duplicate workflow step property {inline_key!r}")
- step_properties[inline_key] = (
- inline_value,
- marker_index + 1,
- marker_index + 1,
- )
- if "run" not in step_properties:
- continue
- run_value, run_start, run_end = step_properties["run"]
- if run_value in ("|", ">", "|-", ">-"):
- block_commands = [
- lines[index][2].strip()
- for index in range(run_start, run_end)
- if lines[index][1] > 8
- ]
- runs_validator = VALIDATOR_COMMAND in block_commands
- else:
- if run_start != run_end:
- raise StructureError("unsupported validator run step structure")
- runs_validator = parse_scalar(run_value) == VALIDATOR_COMMAND
- if not runs_validator:
- continue
- step_reasons = guard_control_reasons(step_properties, "step")
- if not job_reasons and not step_reasons:
- active_command = True
- break
- inactive_reasons.extend(job_reasons + step_reasons)
- if not active_command:
- actual = ", ".join(dict.fromkeys(inactive_reasons)) or "missing"
- errors.append(
- diagnostic(path, "active validator run step", VALIDATOR_COMMAND, actual)
- )
- return errors
-
-
-def validate_repository(root: Path) -> list[str]:
- errors: list[str] = []
- images: dict[str, dict[str, str]] = {"backend": {}, "frontend": {}}
- compose_services: dict[str, dict[str, dict[str, object]]] = {}
- documents: dict[str, str] = {}
-
- for compose_path in COMPOSE_FILES:
- try:
- compose = read_required(root, compose_path)
- except FileNotFoundError:
- errors.append(
- diagnostic(
- Path(compose_path), "missing required file", "present", "missing"
- )
- )
- continue
- try:
- services = parse_compose_services(compose)
- except StructureError as error:
- errors.append(
- diagnostic(
- Path(compose_path),
- "Compose structure",
- "one supported top-level services mapping",
- str(error),
- )
- )
- continue
- compose_services[compose_path] = services
- for service in images:
- image = services.get(service, {}).get("image")
- if image is None:
- errors.append(
- diagnostic(
- Path(compose_path),
- f"{service} image",
- "service image key",
- "missing",
- )
- )
- elif parse_immutable_image(image) is None:
- errors.append(
- diagnostic(
- Path(compose_path),
- f"{service} immutable image pin",
- "tag@sha256:<64 lowercase hexadecimal characters>",
- image,
- )
- )
- else:
- images[service][compose_path] = image
-
- canonical_images: dict[str, str] = {}
- mainnet_path = COMPOSE_FILES[0]
- for service, service_images in images.items():
- canonical_image = service_images.get(mainnet_path)
- if canonical_image is None:
- continue
- canonical_images[service] = canonical_image
- for compose_path, image in service_images.items():
- if image != canonical_image:
- errors.append(
- diagnostic(
- Path(compose_path), f"{service} image", canonical_image, image
- )
- )
-
- common_env_values: dict[str, str] | None = None
- try:
- common_env_values = read_env(root / COMMON_BLOCKSCOUT_ENV)
- except FileNotFoundError:
- errors.append(
- diagnostic(
- Path(COMMON_BLOCKSCOUT_ENV), "missing required file", "present", "missing"
- )
- )
- except StructureError as error:
- errors.append(
- diagnostic(
- Path(COMMON_BLOCKSCOUT_ENV), "env structure", "supported env", str(error)
- )
- )
- else:
- validate_protected_values(
- common_env_values,
- Path(COMMON_BLOCKSCOUT_ENV),
- errors,
- require_metadata=True,
- )
-
- env_cache: dict[str, dict[str, str] | None] = {
- COMMON_BLOCKSCOUT_ENV: common_env_values
- }
- for compose_path, services in compose_services.items():
- backend = services.get("backend")
- if backend is not None:
- validate_backend_sources(
- root, compose_path, backend, env_cache, errors
- )
-
- for frontend_env_file in FRONTEND_ENV_FILES:
- try:
- frontend_env = read_env(root / frontend_env_file)
- except FileNotFoundError:
- errors.append(
- diagnostic(
- Path(frontend_env_file), "missing required file", "present", "missing"
- )
- )
- continue
- except StructureError as error:
- errors.append(
- diagnostic(
- Path(frontend_env_file), "env structure", "supported env", str(error)
- )
- )
- continue
- name_service_api_host = frontend_env.get(NAME_SERVICE_API_HOST)
- if name_service_api_host:
- errors.append(
- diagnostic(
- Path(frontend_env_file),
- f"{NAME_SERVICE_API_HOST} disabled",
- "unset",
- name_service_api_host,
- )
- )
-
- workflow_env: dict[str, str] = {}
- try:
- workflow_text = read_required(root, DEPLOY_WORKFLOW)
- except FileNotFoundError:
- errors.append(
- diagnostic(Path(DEPLOY_WORKFLOW), "missing required file", "present", "missing")
- )
- else:
- try:
- workflow_env = read_workflow_env(workflow_text, GCP_KEYS)
- except StructureError as error:
- errors.append(
- diagnostic(
- Path(DEPLOY_WORKFLOW),
- "workflow structure",
- "one supported top-level env mapping",
- str(error),
- )
- )
- else:
- for key in GCP_KEYS:
- if not workflow_env.get(key):
- errors.append(
- diagnostic(
- Path(DEPLOY_WORKFLOW),
- key,
- "defined workflow value",
- "missing",
- )
- )
-
- for document_path in STATUS_DOCUMENTS:
- try:
- documents[document_path] = read_required(root, document_path)
- except FileNotFoundError:
- errors.append(
- diagnostic(
- Path(document_path), "missing required file", "present", "missing"
- )
- )
-
- backend_image = canonical_images.get("backend")
- frontend_image = canonical_images.get("frontend")
- frontend_version = None
- if frontend_image is not None:
- parsed_frontend = parse_immutable_image(frontend_image)
- if parsed_frontend is not None:
- frontend_version = parsed_frontend["tag"]
-
- if FEATURES_DOCUMENT in documents:
- validate_features_document(
- documents[FEATURES_DOCUMENT],
- Path(FEATURES_DOCUMENT),
- backend_image,
- frontend_version,
- errors,
- )
- if CHANGELOG_DOCUMENT in documents:
- validate_changelog_document(
- documents[CHANGELOG_DOCUMENT],
- Path(CHANGELOG_DOCUMENT),
- backend_image,
- frontend_version,
- errors,
- )
- if ARCHITECTURE_DOCUMENT in documents:
- validate_architecture_document(
- documents[ARCHITECTURE_DOCUMENT],
- Path(ARCHITECTURE_DOCUMENT),
- backend_image,
- frontend_image,
- workflow_env,
- errors,
- )
- if HTML_REPORT_DOCUMENT in documents:
- validate_html_report(
- documents[HTML_REPORT_DOCUMENT],
- Path(HTML_REPORT_DOCUMENT),
- backend_image,
- errors,
- )
-
- try:
- dependency_workflow = read_required(root, DEPENDENCY_WORKFLOW)
- except FileNotFoundError:
- errors.append(
- diagnostic(
- Path(DEPENDENCY_WORKFLOW), "missing required file", "present", "missing"
- )
- )
- else:
- try:
- errors.extend(
- dependency_workflow_errors(
- dependency_workflow, Path(DEPENDENCY_WORKFLOW)
- )
- )
- except StructureError as error:
- errors.append(
- diagnostic(
- Path(DEPENDENCY_WORKFLOW),
- "workflow structure",
- "supported workflow-scripts job and push paths",
- str(error),
- )
- )
-
- return errors
-
-
-def main() -> int:
- errors = validate_repository(ROOT)
- if errors:
- print("Production documentation drift validation failed:")
- for error in errors:
- print(f"- {error}")
- return 1
- print("Production documentation drift validation passed.")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/.github/workflows/dependency-build.yml b/.github/workflows/dependency-build.yml
index 6890bcfc9a82..69f2b5aa4d38 100644
--- a/.github/workflows/dependency-build.yml
+++ b/.github/workflows/dependency-build.yml
@@ -13,24 +13,12 @@ on:
- "apps/**/yarn.lock"
- "docker/Dockerfile"
- "docker/oldUI.Dockerfile"
- - "docker-compose/docker-compose-mainnet.yml"
- - "docker-compose/docker-compose-testnet.yml"
- - "docker-compose/docker-compose-beta.yml"
- - "docker-compose/envs/common-blockscout.env"
- "docker-compose/envs/common-blockscout-mainnet.env"
- "docker-compose/envs/common-blockscout-testnet.env"
- - "docker-compose/envs/common-blockscout-beta.env"
- - "docker-compose/envs/common-frontend*.env"
- - "docs/FEATURES.md"
- - "docs/CHANGELOG.md"
- - "docs/DOScan-ARCHITECTURE.md"
- - "docs/reports/doscan-frontend-env-audit.vi.html"
- - ".github/workflows/deploy-config.yml"
- ".github/workflows/dependency-build.yml"
- ".github/workflows/publish-regular-docker-image-on-demand.yml"
- ".github/workflows/sync-upstream.yml"
- ".github/scripts/check-upstream-sync.py"
- - ".github/scripts/validate-docs-production-status.py"
- ".github/scripts/tests/**"
workflow_dispatch:
@@ -57,9 +45,6 @@ jobs:
- name: Test upstream sync state detection
run: python -m unittest discover -s .github/scripts/tests -p 'test_*.py' -v
- - name: Validate production documentation status
- run: python .github/scripts/validate-docs-production-status.py
-
token-lists:
name: Validate DOS Chain token list reference
runs-on: ubuntu-latest
diff --git a/docs/superpowers/plans/2026-08-04-docs-production-status-drift-guard.md b/docs/superpowers/plans/2026-08-04-docs-production-status-drift-guard.md
deleted file mode 100644
index fbbae2a693f3..000000000000
--- a/docs/superpowers/plans/2026-08-04-docs-production-status-drift-guard.md
+++ /dev/null
@@ -1,305 +0,0 @@
-# Production Status Documentation Drift Guard Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Add an offline CI validator that rejects production documentation when its critical deployment status disagrees with Compose, environment, or deployment workflow configuration.
-
-**Architecture:** A Python standard-library script reads three Compose files, the active backend and frontend environment files, and the deployment workflow. It derives immutable image pins, required Metadata and BENS states, and GCP topology, then checks the three Markdown production status documents and returns every detected error in one run.
-
-**Tech Stack:** Python 3, `unittest`, GitHub Actions YAML, Markdown source documents.
-
-## Global Constraints
-
-- Treat Compose, environment files, and `.github/workflows/deploy-config.yml` as canonical.
-- Validate only critical production invariants: Backend and Frontend pins, Metadata enabled, BENS disabled, and Mainnet/Testnet GCP topology.
-- Validate Mainnet, Testnet, and Beta image equality.
-- Use only the Python standard library and make no network requests.
-- Aggregate diagnostics with file, invariant, expected value, and actual value.
-- Keep the Blockscout v11.2.4 upgrade outside this change.
-- Preserve all existing deployment and environment validation behavior.
-
----
-
-### Task 1: Parse canonical deployment state and validate immutable image pins
-
-**Files:**
-- Create: `.github/scripts/validate-docs-production-status.py`
-- Create: `.github/scripts/tests/test_validate_docs_production_status.py`
-
-**Interfaces:**
-- Consumes: A repository root as `pathlib.Path`.
-- Produces: `validate_repository(root: Path) -> list[str]` and `main() -> int`.
-- Produces: Helper functions `read_required`, `read_env`, `extract_service_image`, `parse_immutable_image`, and `backend_runtime_version`.
-
-- [ ] **Step 1: Write baseline and image drift tests**
-
-Create a temporary repository fixture containing minimal Mainnet, Testnet, and Beta Compose files. Each file must define the same immutable Backend and Frontend images. Create synchronized Markdown fixtures and assert:
-
-```python
-def test_synchronized_repository_passes(self):
- self.assertEqual([], self.module.validate_repository(self.repo))
-
-def test_backend_digest_drift_names_the_compose_file(self):
- self.replace(
- "docker-compose/docker-compose-testnet.yml",
- BACKEND_IMAGE,
- BACKEND_IMAGE_WITH_DIFFERENT_DIGEST,
- )
- errors = self.module.validate_repository(self.repo)
- self.assertTrue(any("docker-compose-testnet.yml" in error for error in errors))
- self.assertTrue(any("backend image" in error for error in errors))
-
-def test_frontend_version_drift_names_the_document(self):
- self.replace("docs/FEATURES.md", "2.10.0", "2.9.0")
- errors = self.module.validate_repository(self.repo)
- self.assertTrue(any("docs/FEATURES.md" in error for error in errors))
- self.assertTrue(any("frontend version" in error for error in errors))
-```
-
-- [ ] **Step 2: Run the focused tests and verify RED**
-
-Run:
-
-```text
-python -m unittest .github/scripts/tests/test_validate_docs_production_status.py -v
-```
-
-Expected: import or attribute failure because the validator does not exist yet.
-
-- [ ] **Step 3: Implement minimal source and image validation**
-
-Implement these exact public interfaces:
-
-```python
-def validate_repository(root: Path) -> list[str]:
- errors: list[str] = []
- # Read required files, derive canonical state, and append all mismatches.
- return errors
-
-def main() -> int:
- errors = validate_repository(ROOT)
- if errors:
- print("Production documentation drift validation failed:")
- for error in errors:
- print(f"- {error}")
- return 1
- print("Production documentation drift validation passed.")
- return 0
-```
-
-Parse each Compose file by locating the requested service block and its `image:` key. Accept an immutable image only when it matches:
-
-```python
-IMMUTABLE_IMAGE_RE = re.compile(
- r"^(?P[^\s:@]+(?:/[^\s:@]+)*):"
- r"(?P[^\s@]+)@sha256:(?P[0-9a-f]{64})$"
-)
-```
-
-Require the same Backend image and the same Frontend image in all three Compose files. Require the full Backend pin in all three production status documents, the Frontend tag version in all three documents, and the full Frontend pin in `docs/DOScan-ARCHITECTURE.md`.
-
-- [ ] **Step 4: Run the focused tests and verify GREEN**
-
-Run:
-
-```text
-python -m unittest .github/scripts/tests/test_validate_docs_production_status.py -v
-```
-
-Expected: baseline and image drift tests pass.
-
-- [ ] **Step 5: Commit the image invariant slice**
-
-```text
-git add .github/scripts/validate-docs-production-status.py .github/scripts/tests/test_validate_docs_production_status.py
-git commit -m "ci: validate production image documentation"
-```
-
----
-
-### Task 2: Validate Metadata, BENS, GCP topology, and aggregated diagnostics
-
-**Files:**
-- Modify: `.github/scripts/validate-docs-production-status.py`
-- Modify: `.github/scripts/tests/test_validate_docs_production_status.py`
-
-**Interfaces:**
-- Consumes: `validate_repository(root: Path) -> list[str]` from Task 1.
-- Produces: `read_env(path: Path) -> dict[str, str]`, `read_workflow_env(text: str, keys: tuple[str, ...]) -> dict[str, str]`, and formatted diagnostic strings.
-
-- [ ] **Step 1: Add failing state and error aggregation tests**
-
-Add these behaviors to the temporary fixture suite:
-
-```python
-def test_metadata_must_remain_enabled(self):
- self.replace(
- "docker-compose/envs/common-blockscout.env",
- "MICROSERVICE_METADATA_ENABLED=true",
- "MICROSERVICE_METADATA_ENABLED=false",
- )
- errors = self.module.validate_repository(self.repo)
- self.assertTrue(any("metadata enabled" in error for error in errors))
-
-def test_bens_configuration_is_rejected(self):
- self.append(
- "docker-compose/envs/common-blockscout.env",
- "MICROSERVICE_BENS_ENABLED=true\n",
- )
- errors = self.module.validate_repository(self.repo)
- self.assertTrue(any("BENS disabled" in error for error in errors))
-
-def test_gcp_topology_drift_names_the_architecture_document(self):
- self.replace("docs/DOScan-ARCHITECTURE.md", "dos-testnet-r0", "stale-host")
- errors = self.module.validate_repository(self.repo)
- self.assertTrue(any("GCP_TESTNET_INSTANCE" in error for error in errors))
-
-def test_missing_sources_and_multiple_drifts_are_aggregated(self):
- (self.repo / "docker-compose/docker-compose-beta.yml").unlink()
- self.replace("docs/FEATURES.md", "2.10.0", "2.9.0")
- errors = self.module.validate_repository(self.repo)
- self.assertGreaterEqual(len(errors), 2)
- self.assertTrue(any("missing required file" in error for error in errors))
- self.assertTrue(any("frontend version" in error for error in errors))
-```
-
-- [ ] **Step 2: Run the focused tests and verify RED**
-
-Run:
-
-```text
-python -m unittest .github/scripts/tests/test_validate_docs_production_status.py -v
-```
-
-Expected: the new Metadata, BENS, topology, and aggregation assertions fail.
-
-- [ ] **Step 3: Implement the remaining critical invariants**
-
-Read active env assignments while ignoring blank and commented lines. Require `MICROSERVICE_METADATA_ENABLED=true`. Reject an active BENS enablement, URL, or protocols value, and reject active `NEXT_PUBLIC_NAME_SERVICE_API_HOST` in every `common-frontend*.env` file.
-
-Read these exact workflow variables:
-
-```python
-GCP_KEYS = (
- "GCP_INSTANCE",
- "GCP_ZONE",
- "GCP_TESTNET_INSTANCE",
- "GCP_TESTNET_ZONE",
-)
-```
-
-Require every derived value in `docs/DOScan-ARCHITECTURE.md`. Format every mismatch through one helper:
-
-```python
-def diagnostic(path: Path, invariant: str, expected: object, actual: object) -> str:
- return (
- f"{path.as_posix()}: {invariant}; "
- f"expected {expected!r}; actual {actual!r}"
- )
-```
-
-When a file is missing, append a diagnostic and continue validating every source that remains readable.
-
-- [ ] **Step 4: Run focused and existing script tests**
-
-Run:
-
-```text
-python -m unittest discover -s .github/scripts/tests -p "test_*.py" -v
-python .github/scripts/validate-docs-production-status.py
-python scripts/validate-blockscout-env-parity.py
-```
-
-Expected: all unit tests pass, the documentation validator passes on the real repository, and env parity passes.
-
-- [ ] **Step 5: Commit the state invariant slice**
-
-```text
-git add .github/scripts/validate-docs-production-status.py .github/scripts/tests/test_validate_docs_production_status.py
-git commit -m "ci: guard production status invariants"
-```
-
----
-
-### Task 3: Wire the guard into Dependency build and verify the complete change
-
-**Files:**
-- Modify: `.github/workflows/dependency-build.yml`
-
-**Interfaces:**
-- Consumes: `.github/scripts/validate-docs-production-status.py` from Tasks 1 and 2.
-- Produces: A PR and path-filtered `main` push CI gate.
-
-- [ ] **Step 1: Add a failing workflow source assertion**
-
-Add a unit test that reads `.github/workflows/dependency-build.yml` from a supplied fixture root and requires the command:
-
-```text
-python .github/scripts/validate-docs-production-status.py
-```
-
-Expected before workflow modification: FAIL because the command is absent.
-
-- [ ] **Step 2: Run the workflow assertion and verify RED**
-
-Run:
-
-```text
-python -m unittest .github/scripts/tests/test_validate_docs_production_status.py -v
-```
-
-Expected: only the workflow integration assertion fails.
-
-- [ ] **Step 3: Add the CI command and push path coverage**
-
-In `workflow-scripts`, add:
-
-```yaml
- - name: Validate production documentation status
- run: python .github/scripts/validate-docs-production-status.py
-```
-
-Add these push paths without changing the Elixir build change detector:
-
-```yaml
- - "docker-compose/docker-compose-mainnet.yml"
- - "docker-compose/docker-compose-testnet.yml"
- - "docker-compose/docker-compose-beta.yml"
- - "docker-compose/envs/common-blockscout.env"
- - "docker-compose/envs/common-frontend*.env"
- - "docs/FEATURES.md"
- - "docs/CHANGELOG.md"
- - "docs/DOScan-ARCHITECTURE.md"
- - ".github/workflows/deploy-config.yml"
- - ".github/scripts/validate-docs-production-status.py"
-```
-
-- [ ] **Step 4: Run full local verification**
-
-Run:
-
-```text
-python -m unittest discover -s .github/scripts/tests -p "test_*.py" -v
-python .github/scripts/validate-docs-production-status.py
-python scripts/validate-blockscout-env-parity.py
-git diff --check
-```
-
-Expected: every command exits 0.
-
-- [ ] **Step 5: Commit workflow integration**
-
-```text
-git add .github/workflows/dependency-build.yml .github/scripts/tests/test_validate_docs_production_status.py
-git commit -m "ci: run production documentation drift guard"
-```
-
-- [ ] **Step 6: Review and publish**
-
-Request an independent code review, fix all Critical and Important findings, rerun the complete verification set, push the branch, open a PR, wait for required CI, and merge only after review and CI are green.
-
----
-
-## Execution Mode
-
-JOY authorized implementation without an execution choice. Use inline execution with `superpowers:executing-plans`, preserving TDD evidence at every RED and GREEN boundary.
diff --git a/docs/superpowers/specs/2026-08-04-docs-production-status-drift-guard-design.vi.html b/docs/superpowers/specs/2026-08-04-docs-production-status-drift-guard-design.vi.html
deleted file mode 100644
index 2ec54664ed2a..000000000000
--- a/docs/superpowers/specs/2026-08-04-docs-production-status-drift-guard-design.vi.html
+++ /dev/null
@@ -1,308 +0,0 @@
-
-
-
-
-
-
-
- Thiết kế CI guard chống lệch trạng thái production
-
-
-
-
-
DOScan · Đặc tả thiết kế
-
CI guard chống lệch trạng thái production
-
Thiết kế đã được JOY duyệt ở mức invariant quan trọng
-
- Guard mới sẽ lấy Compose, env và workflow làm nguồn canonical, sau đó chặn PR nếu
- FEATURES.md, CHANGELOG.md hoặc DOScan-ARCHITECTURE.md
- còn ghi version, image pin, Metadata, BENS hoặc topology GCP mâu thuẫn.
-
-
-
1. Mục tiêu và phạm vi
-
-
-
Trong phạm vi
-
-
Frontend và Backend version, tag, digest trên Mainnet, Testnet và Beta.
-
Trạng thái Metadata Service đang bật.
-
BENS không được cấu hình và tài liệu phải ghi là tắt.
-
GCP host và zone của Mainnet, Testnet.
-
Tính nhất quán giữa ba file tài liệu production.
-
Thông báo lỗi chỉ rõ file, invariant, expected và actual.
-
-
-
-
Ngoài phạm vi
-
-
Không gọi public HTTP production trong guard.
-
Không kiểm tra mọi câu chữ hoặc mọi dòng trong bảng feature.
-
Không generate lại Markdown.
-
Không thêm manifest JSON làm nguồn trạng thái thứ ba.
-
Không nâng cấp Backend lên v11.2.4 trong cùng PR.
-
-
-
-
-
2. Phương án đã chọn
-
-
-
Validator semantic, đã chọn
-
Đọc nguồn deployment thật, suy ra invariant canonical rồi kiểm tra token cấu trúc trong tài liệu.
-
Ưu điểm: không tạo nguồn trạng thái mới, patch nhỏ, lỗi CI có ý nghĩa.
-
-
-
Manifest trung gian, không chọn
-
Dễ parse nhưng tạo thêm một file phải đồng bộ với Compose và docs.
-
-
-
Generate docs, không chọn
-
Chặt nhưng quá xâm lấn, dễ làm tài liệu khó đọc và vượt yêu cầu hiện tại.
-
-
-
-
3. Kiến trúc
-
Compose + env + deploy-config.yml
- |
- v
-validate-docs-production-status.py
- |
- +-- suy ra Frontend/Backend pins
- +-- suy ra Metadata và BENS state
- +-- suy ra GCP host/zone
- |
- v
-FEATURES.md + CHANGELOG.md + DOScan-ARCHITECTURE.md
- |
- +-- pass: CI tiếp tục
- +-- fail: báo invariant, expected, actual và file
Dùng fixture tạm để kiểm tra hành vi pass/fail và nội dung lỗi.
-
-
-
.github/workflows/dependency-build.yml
-
Chạy unittest và validator trên PR; thêm path phù hợp cho push vào main.
-
-
-
-
-
4. Nguồn canonical và invariant
-
-
-
Invariant
Nguồn canonical
Tài liệu phải khớp
-
-
-
-
Backend image tag và digest
-
Service backend trong ba file docker-compose/docker-compose-{mainnet,testnet,beta}.yml
-
Cả ba docs phải chứa pin production hiện tại ở phần trạng thái liên quan.
-
-
-
Frontend image version và digest
-
Service frontend trong ba file Compose trên
-
Ba docs phải chứa version; architecture phải chứa toàn bộ image pin gồm digest.
-
-
-
Metadata bật
-
MICROSERVICE_METADATA_ENABLED=true trong docker-compose/envs/common-blockscout.env
-
Feature và architecture phải ghi Enabled, changelog phải ghi baseline đúng.
-
-
-
BENS tắt
-
Không có active MICROSERVICE_BENS_ENABLED=true và các frontend env không có Name Service host
-
Feature và architecture phải ghi Disabled hoặc not configured.
-
-
-
GCP Mainnet/Testnet
-
Bốn biến GCP_* trong .github/workflows/deploy-config.yml
-
DOScan-ARCHITECTURE.md.
-
-
-
-
-
- Nguyên tắc: guard bảo vệ trạng thái đã deploy, không tự động so sánh với release upstream mới nhất.
- Khi DOScan nâng lên v11.2.4, Compose/env được cập nhật trước hoặc cùng PR với docs; guard sẽ buộc ba nguồn Markdown đi cùng thay đổi đó.
-
-
-
5. Xử lý lỗi
-
Validator trả exit code 1 nếu có bất kỳ drift nào và gom toàn bộ lỗi trong một lần chạy.
-
Production documentation drift validation failed:
-- docs/FEATURES.md: backend image expected 'ghcr.io/...@sha256:...', actual token not found
-- docs/DOScan-ARCHITECTURE.md: GCP_TESTNET_ZONE expected 'asia-southeast1-a', found stale value
-
File thiếu, YAML không có service cần thiết hoặc image không có digest đều là lỗi rõ ràng, không được bỏ qua.
-
-
6. TDD và test matrix
-
-
-
Test
Mutation phải bị bắt
-
-
-
Baseline pass
Fixture đồng bộ hoàn toàn trả exit code 0.
-
Backend digest drift
Một Compose hoặc docs còn digest cũ.
-
Frontend version drift
Docs còn version cũ sau khi Compose đổi.
-
Metadata disabled
Env đổi thành false hoặc tài liệu còn ghi Disabled.
-
BENS enabled
Env bật BENS nhưng tài liệu vẫn ghi tắt.
-
GCP topology drift
Host hoặc zone trong architecture không khớp workflow.
-
Missing source
Thiếu Compose, env, workflow hoặc docs cần thiết.
-
Aggregated diagnostics
Nhiều drift được báo cùng lúc, không fail ở lỗi đầu tiên.
-
-
-
Mỗi hành vi mới phải đi qua RED, GREEN, REFACTOR. Test chạy trên fixture thật trong thư mục tạm, không mock parser.
-
-
7. CI và tiêu chí hoàn tất
-
-
Unittest mới pass cùng toàn bộ test workflow script hiện có.
-
Validator pass trên repo production hiện tại.
-
Mỗi mutation trong test matrix làm validator fail vì đúng lý do.
-
Dependency build chạy guard trên PR và push liên quan.
-
Không kích hoạt Deploy Config chỉ vì thay đổi guard hoặc spec.
-
Superpowers reviewer không còn Critical hoặc Important issue.
-
PR CI xanh trước merge.
-
-
-
8. Trình tự với Blockscout v11.2.4
-
-
Merge drift guard.
-
Thực hiện task upgrade v11.2.4 riêng.
-
Xác minh custom NFT video patch 86fd0dd5 còn cần reapply hay đã được upstream thay thế.
-
Build image immutable, smoke test NFT image/video, deploy Beta trước production.
-
Cập nhật Compose và ba docs trong cùng chuỗi upgrade để guard bảo vệ trạng thái mới.