diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml
index e96f8def..3723292d 100644
--- a/.github/workflows/docs-ci.yml
+++ b/.github/workflows/docs-ci.yml
@@ -13,10 +13,12 @@ jobs:
timeout-minutes: 15
steps:
- name: Check out repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: pip
@@ -40,3 +42,6 @@ jobs:
- name: Validate search index
run: python scripts/docs/validate_search_index.py
+
+ - name: Audit pre-Pages content preservation
+ run: python scripts/docs/audit_pre_pages.py
diff --git a/.github/workflows/oryx-python-build-test.yml b/.github/workflows/oryx-python-build-test.yml
index f1a7b7b5..aac6d7f9 100644
--- a/.github/workflows/oryx-python-build-test.yml
+++ b/.github/workflows/oryx-python-build-test.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
- name: Pull Oryx build image
run: docker pull mcr.microsoft.com/oryx/build:github-actions-debian-bullseye
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 9942f783..caab42b8 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -22,10 +22,12 @@ jobs:
timeout-minutes: 15
steps:
- name: Check out repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v7
with:
python-version: "3.13"
cache: pip
@@ -47,11 +49,14 @@ jobs:
- name: Validate search index
run: python scripts/docs/validate_search_index.py
+ - name: Audit pre-Pages content preservation
+ run: python scripts/docs/audit_pre_pages.py
+
- name: Configure Pages
- uses: actions/configure-pages@v5
+ uses: actions/configure-pages@v6
- name: Upload Pages artifact
- uses: actions/upload-pages-artifact@v4
+ uses: actions/upload-pages-artifact@v5
with:
path: site
@@ -68,4 +73,4 @@ jobs:
steps:
- name: Deploy to Pages
id: deployment
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@v5
diff --git a/AGENTS.md b/AGENTS.md
index 183350df..d1b5608a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -108,8 +108,10 @@ python scripts/docs/validate_links.py
python scripts/docs/validate_public_safety.py
mkdocs build --strict
python scripts/docs/validate_search_index.py
+python scripts/docs/audit_pre_pages.py
```
테스트나 검증 실패를 무시하거나 생성된 `site/` 및 검색 인덱스를 커밋하지
-않는다. sample 애플리케이션의 별도 테스트나 의존성은 해당 sample 변경
-범위에서만 실행한다.
+않는다. `python scripts/docs/audit_pre_pages.py`는 전체 Git 이력이
+필요하며 자세한 기준과 실행 지침은 `docs/contributing/index.md`를 따른다.
+sample 애플리케이션의 별도 테스트나 의존성은 해당 sample 변경 범위에서만 실행한다.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5514b230..5a4e8db6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -39,4 +39,9 @@ python scripts/docs/validate_links.py
python scripts/docs/validate_public_safety.py
mkdocs build --strict
python scripts/docs/validate_search_index.py
+python scripts/docs/audit_pre_pages.py
```
+
+이 감사는 과거 공개 콘텐츠와 호환 URL 보존을 확인하며 전체 Git 이력이
+필요합니다. 자세한 기준과 실행 지침은
+[상세 기여 지침](docs/contributing/index.md)을 따릅니다.
diff --git a/README.md b/README.md
index 6e5e3ba8..3800aa2a 100644
--- a/README.md
+++ b/README.md
@@ -37,5 +37,8 @@ python -m venv .venv
자세한 작성·검증 규칙은
[공개 문서 작성 계약](docs/contributing/index.md)을 확인하세요.
+검색 인덱스 검증 다음에는 `python scripts/docs/audit_pre_pages.py`를
+실행하며, 이 감사는 전체 Git 이력이 필요합니다. 자세한 기준과 실행 지침도
+[공개 문서 작성 계약](docs/contributing/index.md)에서 확인하세요.
> 이 저장소와 Pages는 공개되어 있습니다. 고객·사용자 식별자, 구독·테넌트 ID, 비밀, 내부 URL 또는 승인되지 않은 화면 캡처를 커밋하지 마세요.
diff --git a/docs/contributing/index.md b/docs/contributing/index.md
index 79b98475..4dea1061 100644
--- a/docs/contributing/index.md
+++ b/docs/contributing/index.md
@@ -112,8 +112,8 @@ publish:
`downloads/index.md` 같은 원본 자산은 해당 파일 경로로 직접 링크합니다.
`downloads/`처럼 문서 디렉터리로 줄여 쓰면 다운로드 자산을 가리키지 않습니다.
공개 target의 경로 구성 요소는 어느 깊이에서도 `.`으로 시작할 수 없습니다.
- [Pages artifact 패키징](https://github.com/actions/upload-pages-artifact/blob/v4/action.yml)이
- 숨김 파일과 숨김 디렉터리를 제외하므로, 이런 target은 빌드 전에 거부합니다.
+ [Pages artifact 패키징](https://github.com/actions/upload-pages-artifact/blob/v5/action.yml)이
+ 기본적으로 숨김 파일과 숨김 디렉터리를 제외하므로, 이런 target은 빌드 전에 거부합니다.
sample 원본 payload 내부의 숨김 파일은 허용하며 원본 폴더는 계속 게시에서 제외합니다.
페이지 본문과 링크 텍스트는 기존처럼 검색됩니다. `used_by`는 sample card의
표시 위치만 결정하며 `publish`와 독립적입니다.
@@ -300,7 +300,49 @@ python scripts/docs/validate_links.py
python scripts/docs/validate_public_safety.py
mkdocs build --strict
python scripts/docs/validate_search_index.py
+python scripts/docs/audit_pre_pages.py
```
+`audit_pre_pages.py`는 고정된 pre-Pages 기준선과 현재 built site를 함께
+대조해 과거 공개 콘텐츠와 호환 URL이 계속 보존되는지 검사합니다.
+보존 inventory는 `a4e6801` 시점의 **기준선 문서 62개**를 현재 canonical
+문서에 일대일로 연결합니다. 신규 문서는 이 기준선 inventory에 추가하지
+않으며, 현재 문서 수가 늘어났다는 이유로 감사가 실패하지 않습니다.
+각 연결은 `rename_similarity`로 계산한 기준선 → 최초 Pages commit의
+실제 Git `R` 기록과 원본·대상 경로가 일치해야 합니다. 양쪽 파일이
+존재한다는 사실만으로는 이관 계보를 인정하지 않습니다.
+과거 구조 비교와 JSON의 `document_count`·`details.documents`는 이 62개만
+대상으로 합니다. `current_document_count`는 현재 카탈로그에서 계산합니다.
+검토된 disposition은 Git에서 실제로 삭제된 기준선 경로에만 적용합니다.
+비어 있지 않은 `current_paths`는 `HEAD`에 저장된 `docs/services/**` 또는
+`tests/docs/**`의 일반 소스 파일이어야 합니다. 추적되지 않은 파일, ignore된
+파일, 생성 결과물과 심볼릭 링크는 대체 근거로 사용할 수 없습니다.
+빈 `current_paths`는 삭제된 로컬 상태를 제외할 때만 허용합니다.
+가시성 감사는 닫힌 ``를 첫 직접 자식 `` 또는 브라우저의
+기본 컨트롤로 펼칠 수 있는지 검사합니다. 작성된 summary가 없어도
+컨트롤이 보이고 상호작용 가능하면 본문을 인정합니다. 상속된 `inert`,
+`hidden`, `display:none`, `aria-hidden` 등의 접근 제한은 계속 반영합니다.
+다른 요소 뒤에 오는 첫 ``도 인정하며, 이미 `open`인 본문은
+별도로 가시성을 확인합니다.
+
+전체 감사는 기준선 밖의 신규 문서까지 포함해 현재 canonical 문서 **전체**의
+본문 가시성, 검색, 서비스별 주제 진입점, 글 찾기 연결과 로컬 자산을 검사합니다.
+각 문서가 선언한 모든 `redirect_from`도 확인하며, inventory에 기록된
+기준선 Pages 경로 62개는 계속 필수입니다. 현재 문서가 66개인 경우의 성공
+출력은 다음과 같습니다. 현재 문서 수는 고정된 제한이 아닙니다.
+
+```text
+Audited 359 baseline files and 73 Markdown files: 62/62 baseline documents mapped and preserved; 66 current documents searchable and visible; declared redirects and local assets verified.
+```
+
+`--content-only`는 기준선 구조 보존만 검사하며 built site를 확인했다고
+표현하지 않습니다. 이 검사는
+`a4e6801`과 `9ace9667` commit object를 직접 읽으므로 전체 Git 이력이
+필수입니다. 로컬 clone이 얕으면 검사를 건너뛰지 말고
+`git fetch --unshallow` 또는 동등한 전체 이력 fetch를 수행한 뒤 다시
+실행합니다. CI에서는 checkout `fetch-depth: 0`으로 같은 조건을 보장합니다.
+필수 감사 step과 이를 포함한 validate/build job에는 실행을 건너뛸 수 있는
+`if`를 두지 않으며, `continue-on-error`로 감사 실패를 무시하지 않습니다.
+
생성된 `site/`과 검색 인덱스는 커밋하지 않습니다. 실패한 검증을 무시하지
않습니다.
diff --git a/docs/services/azure-database-for-mysql/blue-green-upgrade/index.md b/docs/services/azure-database-for-mysql/blue-green-upgrade/index.md
index cc9c4b3d..0a22ca74 100644
--- a/docs/services/azure-database-for-mysql/blue-green-upgrade/index.md
+++ b/docs/services/azure-database-for-mysql/blue-green-upgrade/index.md
@@ -323,7 +323,7 @@ mysql -h -e "
> **Phase 5 (재배포) 불필요** — `DB_HOST=primary.db..internal` 은 CNAME 교체 후에도 동일.
-
+
Rollback 절차 보기
> Rollback 가능 조건: new-db에 write가 발생하지 않은 시점(= super_read_only ON 오류로만 발생). new-db에 이미 데이터가 작성된 경우는 데이터 동기화 후 교체해야 하며, 데이터 팀 개입이 필요합니다.
@@ -351,7 +351,7 @@ mysql -h -e "SET GLOBAL super_read_only = OFF;"
## 시뮬레이션 (`simulation/`)
-
+
시뮬레이션 구성, 설계 결정, 실행 순서 보기
### 목적
@@ -622,7 +622,7 @@ LIMIT 1;
→ **사전에 new-db에서 전체 쿼리 regression test 수행 필수**
-
+
5. Rollback 절차
커트오버 후 문제 발생 시 아래 순서로 원복합니다.
diff --git a/docs/services/microsoft-foundry/agent-memory/index.md b/docs/services/microsoft-foundry/agent-memory/index.md
index 44a8276e..4797c77a 100644
--- a/docs/services/microsoft-foundry/agent-memory/index.md
+++ b/docs/services/microsoft-foundry/agent-memory/index.md
@@ -53,6 +53,10 @@ redirect_from:
## 목적별 빠른 경로
+이관 전 문서별 상세 읽기 목표와 대상 독자는
+[원문 독자 지도](https://github.com/hellices/devguidesample/blob/main/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md#historical-reader-map-pre-pages)에
+역사적 기록으로 보존한다.
+
**시간이 없다면** → [06. 커머스 적용 설계](commerce/index.md) 만 읽는다. 나머지 문서의 결론이 여기 수렴한다.
**설계를 시작한다면**
diff --git a/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md b/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
index 25294a31..4049c787 100644
--- a/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+++ b/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
@@ -3,3 +3,22 @@
This sample contains the editable SVG diagrams owned by the Agent Memory topic
package. They support its memory taxonomy, architecture patterns, retrieval
pipeline, production tiers, and commerce blueprint.
+
+## Historical reader map (pre-Pages)
+
+The pre-Pages snapshot `a4e680116db1016a698e64baae9efde858a8eafa`
+included the following detailed reading objectives and audiences. The topic's
+generated navigation replaced the old document list, but did not retain these
+audience assignments. This is a historical research map, not a current product-support statement.
+Links now point to the corresponding canonical documents.
+
+### 문서 구성
+
+| 문서 | 내용 | 대상 |
+|------|------|------|
+| **[01. 메모리 분류 체계](../../taxonomy/index.md)** | Agent Memory **8유형 통합 정의**, 유형별 **저장소 선택지**와 Azure 매핑, Retention 정책, 30기법 6패밀리 지도 | 전원 |
+| **[02. 아키텍처 패턴](../../architecture-patterns/index.md)** | Single-Store / Dual-Store / Tiered / Graph-Augmented / Full Cognitive 5패턴, 비교표, 선택 결정 트리 | 아키텍트 |
+| **[03. 파이프라인과 검색](../../pipeline-retrieval/index.md)** | 쓰기 경로(추출→중복·모순 해소→라우팅) / 읽기 경로(hybrid search→RRF→rerank→MMR→temporal), 단계별 지연 비용 | 개발자 |
+| **[04. 프레임워크 비교](../../frameworks/index.md)** | Mem0 · Zep · Graphiti · Letta(MemGPT) · Cognee · Azure AI Search, **자체 구현 vs 도입 판단 기준** | 의사결정자 |
+| **[05. 프로덕션과 평가](../../production-evaluation/index.md)** | Tiered storage, PII·GDPR, TTL·샤딩·관측성·비용, 평가 지표, LoCoMo/LongMemEval 벤치마크 | SRE / 개발자 |
+| **[06. 커머스 적용 설계](../../commerce/index.md)** | **메모리 스키마 · 지연 예산 · 개인화 강도 단계화 · 안티패턴 · Phase 0~4 로드맵 · 기술+비즈니스 지표** | 전원 (핵심) |
diff --git a/scripts/docs/audit_pre_pages.py b/scripts/docs/audit_pre_pages.py
new file mode 100644
index 00000000..f44a2cc3
--- /dev/null
+++ b/scripts/docs/audit_pre_pages.py
@@ -0,0 +1,86 @@
+"""Audit fixed pre-Pages preservation and visibility of the current canonical corpus."""
+
+from __future__ import annotations
+
+import argparse
+from collections.abc import Mapping
+from dataclasses import fields, is_dataclass
+import json
+from pathlib import Path, PurePath
+import sys
+from uuid import uuid4
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+if str(REPO_ROOT) not in sys.path:
+ sys.path.insert(0, str(REPO_ROOT))
+
+from scripts.docs.pre_pages import PreservationAuditResult, audit_repository
+
+
+def _json_value(value: object) -> object:
+ if is_dataclass(value):
+ return {field.name: _json_value(getattr(value, field.name)) for field in fields(value)}
+ if isinstance(value, Mapping):
+ return {str(key): _json_value(item) for key, item in value.items()}
+ if isinstance(value, (tuple, list)):
+ return [_json_value(item) for item in value]
+ if isinstance(value, PurePath):
+ return value.as_posix()
+ return value
+
+
+def write_audit_json(path: Path, result: PreservationAuditResult) -> None:
+ """Replace a report only after the entire new JSON document has been written."""
+ path.parent.mkdir(parents=True, exist_ok=True)
+ sibling = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
+ try:
+ with sibling.open("x", encoding="utf-8") as stream:
+ stream.write(json.dumps(_json_value(result), ensure_ascii=False, indent=2, sort_keys=True) + "\n")
+ sibling.replace(path)
+ finally:
+ sibling.unlink(missing_ok=True)
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--repo-root", type=Path, default=REPO_ROOT)
+ parser.add_argument("--site-dir", type=Path)
+ parser.add_argument("--inventory", type=Path)
+ parser.add_argument("--json-output", type=Path)
+ parser.add_argument("--content-only", action="store_true")
+ args = parser.parse_args(argv)
+ root = args.repo_root.resolve()
+ site = args.site_dir if args.site_dir is not None else Path("site")
+ inventory = args.inventory if args.inventory is not None else Path("scripts/docs/pre_pages_inventory.yml")
+ result = audit_repository(
+ root, root / site, root / inventory, content_only=args.content_only,
+ )
+ if args.json_output:
+ try:
+ write_audit_json(args.json_output, result)
+ except (OSError, ValueError) as error:
+ print(f"{args.json_output}: cannot write audit JSON: {error}")
+ return 1
+ for warning in result.warnings:
+ print(f"Warning: {warning}")
+ if result.errors:
+ for error in result.errors:
+ print(error.replace("\r", "\\r").replace("\n", "\\n"))
+ return 1
+ summary = (
+ f"Audited {result.baseline_file_count} baseline files and "
+ f"{result.baseline_markdown_count} Markdown files: "
+ f"{result.preserved_documents}/{result.document_count} baseline documents mapped and preserved"
+ )
+ print(summary + (
+ f" (content-only; {result.current_document_count} current documents; built site not inspected)."
+ if args.content_only else
+ f"; {result.current_document_count} current documents searchable and visible; "
+ "declared redirects and local assets verified."
+ ))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/docs/hooks.py b/scripts/docs/hooks.py
index 353a32f7..446f4d77 100644
--- a/scripts/docs/hooks.py
+++ b/scripts/docs/hooks.py
@@ -10,6 +10,7 @@
import sys
from typing import Any, Mapping
+from jinja2 import ChoiceLoader, DictLoader
from mkdocs.structure.files import File, InclusionLevel
from mkdocs.structure import StructureItem
from mkdocs.structure.nav import Navigation, Section
@@ -376,6 +377,25 @@ def on_page_markdown(markdown: str, page: Any, config: Mapping[str, Any], files:
return markdown
+def on_env(env: Any, config: Mapping[str, Any], files: Any) -> Any:
+ """Keep Material's sharing anchor without its executable placeholder URL."""
+ theme = config.get("theme", {})
+ if theme.get("name") != "material" or "search.share" not in theme.get("features", []):
+ return env
+ template = "partials/search.html"
+ source, _, _ = env.loader.get_source(env, template)
+ safe_source = re.sub(
+ r']*data-md-component="search-share"[^>]*>',
+ lambda match: match[0].replace('href="javascript:void(0)"', 'href="#"'),
+ source,
+ )
+ if safe_source != source:
+ env.loader = ChoiceLoader([DictLoader({template: safe_source}), env.loader])
+ if env.cache is not None:
+ env.cache.clear()
+ return env
+
+
def on_post_page(output: str, page: Any, config: Mapping[str, Any]) -> str:
"""Move generated redirect metadata into the final HTML head."""
page_file = getattr(page, "file", None)
diff --git a/scripts/docs/pre_pages.py b/scripts/docs/pre_pages.py
new file mode 100644
index 00000000..afdd8660
--- /dev/null
+++ b/scripts/docs/pre_pages.py
@@ -0,0 +1,739 @@
+"""Fixed pre-Pages inventory, reviewed-evidence schema, and Git lineage."""
+
+from __future__ import annotations
+
+from collections.abc import Hashable
+from dataclasses import asdict, dataclass, field
+from pathlib import Path, PurePosixPath, PureWindowsPath
+import re
+import stat
+import subprocess
+from types import MappingProxyType
+from typing import Mapping
+import unicodedata
+
+import yaml
+
+from scripts.docs.content import Document, DocumentFormatError, load_taxonomy
+from scripts.docs.topics import build_topic_catalog
+
+
+class AuditFormatError(ValueError):
+ """Raised when the inventory or its Git lineage cannot be validated."""
+
+
+class _InventorySafeLoader(yaml.SafeLoader):
+ def construct_mapping(self, node: yaml.MappingNode, deep: bool = False) -> dict:
+ self.flatten_mapping(node)
+ keys = set()
+ for key_node, _ in node.value:
+ key = self.construct_object(key_node, deep=deep)
+ if not isinstance(key, Hashable):
+ raise yaml.constructor.ConstructorError(
+ "while constructing an inventory mapping", node.start_mark,
+ "found unhashable key", key_node.start_mark,
+ )
+ if key in keys:
+ raise yaml.constructor.ConstructorError(
+ "while constructing an inventory mapping", node.start_mark,
+ f"duplicate mapping key: {key!r}", key_node.start_mark,
+ )
+ keys.add(key)
+ return super().construct_mapping(node, deep=deep)
+
+
+@dataclass(frozen=True)
+class StructureEvidence:
+ """A normalized current structure with exact (not minimum) multiplicity."""
+
+ path: PurePosixPath
+ category: str
+ fingerprint: str
+ count: int
+ kind: str = field(init=False, default="structure")
+
+
+@dataclass(frozen=True)
+class FileEvidence:
+ """Exact current file bytes, including binary artifacts."""
+
+ path: PurePosixPath
+ sha256: str
+ kind: str = field(init=False, default="file")
+
+
+@dataclass(frozen=True)
+class ReviewedChange:
+ """A bounded baseline loss justified by immutable current evidence references."""
+
+ missing_count: int
+ reason: str
+ evidence: tuple[StructureEvidence | FileEvidence, ...]
+ link_change: str | None = None
+
+
+@dataclass(frozen=True)
+class BaselineDocument:
+ baseline_path: PurePosixPath
+ pages_path: PurePosixPath
+ reviewed_changes: Mapping[str, Mapping[str, ReviewedChange]]
+
+ def __post_init__(self) -> None:
+ object.__setattr__(
+ self,
+ "reviewed_changes",
+ MappingProxyType({
+ category: MappingProxyType(dict(approvals))
+ for category, approvals in validate_reviewed_changes(self.reviewed_changes).items()
+ }),
+ )
+
+
+@dataclass(frozen=True)
+class ReviewedDisposition:
+ status: str
+ current_paths: tuple[PurePosixPath, ...]
+ reason: str
+
+
+@dataclass(frozen=True)
+class PrePagesInventory:
+ baseline_commit: str
+ pages_commit: str
+ rename_similarity: int
+ documents: tuple[BaselineDocument, ...]
+ dispositions: Mapping[PurePosixPath, ReviewedDisposition]
+
+ def __post_init__(self) -> None:
+ object.__setattr__(self, "dispositions", MappingProxyType(dict(self.dispositions)))
+
+
+@dataclass(frozen=True)
+class GitFileDisposition:
+ baseline_path: PurePosixPath
+ status: str
+ current_paths: tuple[PurePosixPath, ...]
+
+
+@dataclass(frozen=True)
+class PreservationAuditResult:
+ """Fixed-baseline preservation and dynamically discovered current coverage."""
+
+ baseline_file_count: int
+ baseline_markdown_count: int
+ document_count: int
+ current_document_count: int
+ preserved_documents: int
+ reviewed_documents: int
+ errors: tuple[str, ...]
+ warnings: tuple[str, ...]
+ details: Mapping[str, object]
+
+
+def _fields(value: object, required: set[str], label: str, optional: set[str] = frozenset()) -> dict:
+ if not isinstance(value, dict):
+ raise AuditFormatError(f"{label} must be a mapping")
+ missing = required - value.keys()
+ extra = value.keys() - required - optional
+ if missing or extra:
+ raise AuditFormatError(
+ f"{label}: missing fields {sorted(missing)}; unexpected fields {sorted(extra, key=str)}"
+ )
+ return value
+
+
+def _nonempty_string(value: object, label: str) -> str:
+ if not isinstance(value, str) or not value.strip():
+ raise AuditFormatError(f"{label} must be a non-empty string")
+ return value
+
+
+def _commit_hash(value: object, label: str) -> str:
+ if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{40}", value):
+ raise AuditFormatError(f"{label} must be a full lowercase 40-character commit hash: {value!r}")
+ return value
+
+
+def _relative_path(value: object, label: str) -> PurePosixPath:
+ raw = _nonempty_string(value, label)
+ path = PurePosixPath(raw)
+ if (
+ path.is_absolute()
+ or PureWindowsPath(raw).drive
+ or "\\" in raw
+ or "\0" in raw
+ or ".." in path.parts
+ or raw != path.as_posix()
+ or not path.parts
+ ):
+ raise AuditFormatError(f"{label} must be a normalized repository-relative POSIX path: {raw!r}")
+ return path
+
+
+STRUCTURE_CATEGORIES = (
+ "title", "headings", "prose", "code", "tables", "images", "local_link_labels",
+)
+EVIDENCE_CATEGORIES = (*STRUCTURE_CATEGORIES, "links")
+
+
+def validate_public_evidence_path(path: PurePosixPath, label: str) -> None:
+ if any(part.casefold() in {".git", ".superpowers"} for part in path.parts):
+ raise AuditFormatError(f"{label}: forbidden evidence path: {path}")
+ if not (
+ len(path.parts) > 2 and path.parts[:2] == ("docs", "services")
+ or path == PurePosixPath(".devcontainer/README.md")
+ ):
+ raise AuditFormatError(f"{label}: evidence path is outside the public source allowlist: {path}")
+
+
+def _positive_count(value: object, label: str) -> int:
+ if type(value) is not int or value <= 0:
+ raise AuditFormatError(f"{label} must be a positive integer")
+ return value
+
+
+def _sha256(value: object, label: str) -> str:
+ if not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value):
+ raise AuditFormatError(f"{label} must be lowercase SHA-256")
+ return value
+
+
+def _review_reason(
+ value: object, label: str, references: tuple[StructureEvidence | FileEvidence, ...],
+) -> str:
+ reason = _nonempty_string(value, label)
+ text = unicodedata.normalize("NFKC", reason).casefold()
+ for reference in references:
+ text = text.replace(unicodedata.normalize("NFKC", str(reference.path)).casefold(), " ")
+ text = re.sub(r"(?:https?://|(?:docs|samples)/)\S+", " ", text)
+ text = re.sub(
+ r"\b(?:safety|reason|review(?:ed)?|approval|approved?|preservation|replacement|migration|updated?|revised?|"
+ r"changed?|removed?|deleted?|retained?|preserved?|replaced?|migrated?|"
+ r"needed|necessary|unnecessary|longer|anymore|during|before|after|because|content|document|documentation|"
+ r"block|material|stuff|latest|current|previous|old|new|evidence|references?|"
+ r"obsolete|irrelevant|therefore|nothing|significant|suitable|equivalent|"
+ r"improved?|presentation|has|have|"
+ r"a|an|the|and|or|as|at|in|on|to|of|for|from|with|by|"
+ r"this|that|these|those|it|its|is|was|are|were|be|been|not|no|now)\b",
+ " ", text,
+ )
+ text = re.sub(
+ r"\b(?:안전|검토|사유|이유|보존|대체|이관|갱신|업데이트|수정|변경|삭제|제거|"
+ r"유지|불필요|필요|내용|문서|콘텐츠|최신|현재|해당|않|없|기존|이전|"
+ r"과정|일반적|쓰이지|부분|항목|정리|적절|대한|거쳐)[가-힣]*"
+ r"|\b(?:이|그|더|이상|위해|때문에)\b",
+ " ", text,
+ )
+ concrete = re.findall(r"[a-z가-힣][a-z0-9가-힣_.-]*", text)
+ if len(reason.strip()) < 40 or len(concrete) < 2 or sum(map(len, concrete)) < 12:
+ raise AuditFormatError(
+ f"{label} must describe concrete replacement/preservation details, not generic-only approval phrases"
+ )
+ if not any(
+ re.search(
+ r"(? StructureEvidence | FileEvidence:
+ if isinstance(value, (StructureEvidence, FileEvidence)):
+ value = {**asdict(value), "path": str(value.path)}
+ if not isinstance(value, dict):
+ raise AuditFormatError(f"{label} must be a mapping")
+ kind = value.get("kind")
+ if kind not in ("structure", "file"):
+ raise AuditFormatError(f"{label}.kind must be structure or file")
+ fields = {"kind", "path", "category", "fingerprint", "count"} if kind == "structure" else {
+ "kind", "path", "sha256",
+ }
+ raw = _fields(value, fields, label)
+ path = _relative_path(raw["path"], f"{label}.path")
+ validate_public_evidence_path(path, f"{label}.path")
+ if kind == "file":
+ return FileEvidence(path, _sha256(raw["sha256"], f"{label}.sha256"))
+ category = raw["category"]
+ if category not in EVIDENCE_CATEGORIES:
+ raise AuditFormatError(f"{label}.category is not a known structure category")
+ return StructureEvidence(
+ path, category, _sha256(raw["fingerprint"], f"{label}.fingerprint"),
+ _positive_count(raw["count"], f"{label}.count"),
+ )
+
+
+def validate_reviewed_changes(
+ value: object, label: str = "reviewed_changes",
+) -> Mapping[str, Mapping[str, ReviewedChange]]:
+ """Parse immutable approvals; current file/structure evidence is checked by the content auditor."""
+ if not isinstance(value, Mapping):
+ raise AuditFormatError(f"{label} must be a mapping")
+ parsed = {}
+ for category, approvals in value.items():
+ if category not in STRUCTURE_CATEGORIES:
+ raise AuditFormatError(f"{label}: unknown structure category: {category!r}")
+ if not isinstance(approvals, Mapping):
+ raise AuditFormatError(f"{label}.{category} must be a mapping")
+ parsed[category] = {}
+ for fingerprint, approval in approvals.items():
+ _sha256(fingerprint, f"{label}.{category}.fingerprint")
+ entry_label = f"{label}.{category}.{fingerprint}.approval"
+ if isinstance(approval, ReviewedChange):
+ change = approval
+ approval = {
+ "missing_count": change.missing_count,
+ "reason": change.reason,
+ "evidence": change.evidence,
+ }
+ if change.link_change is not None:
+ approval["link_change"] = change.link_change
+ raw = _fields(
+ approval, {"missing_count", "reason", "evidence"}, entry_label, {"link_change"},
+ )
+ count = _positive_count(raw["missing_count"], f"{entry_label}.missing_count")
+ if not isinstance(raw["evidence"], (list, tuple)) or not raw["evidence"]:
+ raise AuditFormatError(f"{entry_label}.evidence must be a non-empty sequence")
+ references = []
+ seen = set()
+ for index, reference in enumerate(raw["evidence"]):
+ reference = _evidence_reference(reference, f"{entry_label}.evidence[{index}]")
+ key = (reference.kind, reference.path)
+ if isinstance(reference, StructureEvidence):
+ key += (reference.category, reference.fingerprint)
+ if key in seen:
+ raise AuditFormatError(f"{entry_label}: duplicate evidence reference: {reference.path}")
+ seen.add(key)
+ references.append(reference)
+ references = tuple(references)
+ reason = _review_reason(raw["reason"], f"{entry_label}.reason", references)
+ link_change = raw.get("link_change")
+ if "link_change" in raw and category != "local_link_labels":
+ raise AuditFormatError(f"{entry_label}.link_change is only valid for local_link_labels")
+ if category == "local_link_labels":
+ link_change = raw.get("link_change", "replacement")
+ if link_change not in ("replacement", "redundant-self-link"):
+ raise AuditFormatError(f"{entry_label}.link_change is unknown")
+ evidence_categories = {
+ ref.category for ref in references if isinstance(ref, StructureEvidence)
+ }
+ if link_change == "replacement":
+ if "links" not in evidence_categories:
+ raise AuditFormatError(f"{entry_label}: replacement approvals require exact links evidence")
+ else:
+ if not {"title", "headings"} <= evidence_categories or "links" in evidence_categories:
+ raise AuditFormatError(
+ f"{entry_label}: redundant self-link removal requires document identity/topic "
+ "structure, not replacement links"
+ )
+ if not re.search(r"\bself[- ]link\b|자기.*링크|자체.*링크", reason, re.IGNORECASE) or not re.search(
+ r"\b(?:remov\w*|omit\w*|drop\w*)\b|삭제|제거|생략", reason, re.IGNORECASE,
+ ):
+ raise AuditFormatError(f"{entry_label}.reason must specifically explain self-link removal")
+ parsed[category][fingerprint] = ReviewedChange(count, reason, references, link_change)
+ return parsed
+
+
+def load_inventory(path: Path) -> PrePagesInventory:
+ try:
+ data = yaml.load(path.read_text(encoding="utf-8"), Loader=_InventorySafeLoader)
+ except (OSError, UnicodeError, yaml.YAMLError) as error:
+ raise AuditFormatError(f"{path}: cannot read inventory: {error}") from error
+ data = _fields(
+ data,
+ {"version", "baseline_commit", "pages_commit", "rename_similarity", "documents", "dispositions"},
+ str(path),
+ )
+ if type(data["version"]) is not int or data["version"] != 3:
+ raise AuditFormatError("version must be 3 (exact links and public HEAD-tracked evidence)")
+ baseline = _commit_hash(data["baseline_commit"], "baseline_commit")
+ pages = _commit_hash(data["pages_commit"], "pages_commit")
+ similarity = data["rename_similarity"]
+ if type(similarity) is not int or not 1 <= similarity <= 100:
+ raise AuditFormatError("rename_similarity must be an integer between 1 and 100")
+ if not isinstance(data["documents"], list):
+ raise AuditFormatError("documents must be a list")
+
+ documents: list[BaselineDocument] = []
+ seen: dict[str, set[PurePosixPath]] = {"baseline_path": set(), "pages_path": set()}
+ for index, raw in enumerate(data["documents"]):
+ label = f"documents[{index}]"
+ raw = _fields(raw, {"baseline_path", "pages_path"}, label, {"reviewed_changes"})
+ paths = {}
+ for field in seen:
+ value = _relative_path(raw[field], f"{label}.{field}")
+ if value in seen[field]:
+ raise AuditFormatError(f"{label}: duplicate {field}: {value}")
+ seen[field].add(value)
+ paths[field] = value
+ documents.append(BaselineDocument(
+ **paths,
+ reviewed_changes=validate_reviewed_changes(
+ raw.get("reviewed_changes", {}), f"{label}.reviewed_changes",
+ ),
+ ))
+
+ if not isinstance(data["dispositions"], dict):
+ raise AuditFormatError("dispositions must be a mapping")
+ dispositions = {}
+ for raw_path, raw in data["dispositions"].items():
+ baseline_path = _relative_path(raw_path, "dispositions path")
+ label = f"dispositions.{raw_path}"
+ raw = _fields(raw, {"status", "current_paths", "reason"}, label)
+ status = raw["status"]
+ if not isinstance(status, str) or status not in {
+ "excluded-local-state", "replaced-summary", "replaced-test",
+ }:
+ raise AuditFormatError(f"{label}.status is not a reviewed disposition status")
+ reason = _nonempty_string(raw["reason"], f"{label}.reason")
+ current = raw["current_paths"]
+ if not isinstance(current, list):
+ raise AuditFormatError(f"{label}.current_paths must be a list")
+ if (status == "excluded-local-state") != (not current):
+ raise AuditFormatError(
+ f"{label}.current_paths must be empty only for excluded-local-state"
+ )
+ paths = tuple(_relative_path(item, f"{label}.current_paths") for item in current)
+ seen_paths = set()
+ for current_path in paths:
+ if current_path in seen_paths:
+ raise AuditFormatError(f"{label}: duplicate current_paths entry: {current_path}")
+ seen_paths.add(current_path)
+ dispositions[baseline_path] = ReviewedDisposition(status, paths, reason)
+ return PrePagesInventory(baseline, pages, similarity, tuple(documents), dispositions)
+
+
+def _git(repo_root: Path, *arguments: str) -> bytes:
+ result = subprocess.run(
+ ["git", "-C", str(repo_root), *arguments],
+ check=False,
+ capture_output=True,
+ )
+ if result.returncode:
+ detail = result.stderr.decode("utf-8", errors="replace").strip()
+ raise AuditFormatError(
+ f"Git {' '.join(arguments)} failed: {detail}. "
+ "Ensure the exact commit is available with full history (fetch-depth: 0)."
+ )
+ return result.stdout
+
+
+def _require_commit(repo_root: Path, commit: str) -> None:
+ _commit_hash(commit, "commit")
+ _require_full_history(repo_root, commit)
+ if _git(repo_root, "cat-file", "-t", commit).strip() != b"commit":
+ raise AuditFormatError(f"{commit}: expected a Git commit object")
+
+
+def _require_full_history(repo_root: Path, *commits: str) -> None:
+ if _git(repo_root, "rev-parse", "--is-shallow-repository").strip() == b"true":
+ raise AuditFormatError(
+ f"Shallow Git repository cannot audit required commits: {', '.join(commits)}. "
+ "Fetch full history before auditing (git fetch --unshallow; checkout fetch-depth: 0)."
+ )
+
+
+def git_bytes(repo_root: Path, commit: str, path: PurePosixPath) -> bytes:
+ _require_commit(repo_root, commit)
+ path = _relative_path(path.as_posix(), "Git path")
+ return _git(repo_root, "cat-file", "blob", f"{commit}:{path.as_posix()}")
+
+
+def git_text(repo_root: Path, commit: str, path: PurePosixPath) -> str:
+ return git_bytes(repo_root, commit, path).decode("utf-8")
+
+
+def resolve_public_evidence_files(
+ repo_root: Path, paths: tuple[PurePosixPath, ...],
+) -> Mapping[PurePosixPath, Path]:
+ """Authorize HEAD-tracked public paths, but return their live working-tree files."""
+ root = repo_root.resolve()
+ tracked = {
+ PurePosixPath(path)
+ for path in _git(root, "ls-tree", "-r", "--name-only", "-z", "HEAD").decode("utf-8").split("\0")
+ if path
+ }
+ resolved = {}
+ to_check = set()
+ for reference in paths:
+ validate_public_evidence_path(reference, "evidence")
+ if reference not in tracked:
+ raise AuditFormatError(f"evidence path is not tracked at HEAD: {reference}")
+ try:
+ current = (root / reference).resolve(strict=True)
+ if not current.is_relative_to(root):
+ raise AuditFormatError(f"evidence path is outside repository: {reference}")
+ target = PurePosixPath(current.relative_to(root).as_posix())
+ validate_public_evidence_path(target, f"evidence target for {reference}")
+ if target not in tracked:
+ raise AuditFormatError(f"resolved evidence target is not tracked at HEAD: {target}")
+ if not current.is_file():
+ raise AuditFormatError(f"evidence path is not a file: {reference}")
+ except OSError as error:
+ raise AuditFormatError(f"cannot resolve current evidence {reference}: {error}") from error
+ resolved[reference] = current
+ to_check.update((reference, target))
+ if to_check:
+ result = subprocess.run(
+ ["git", "-C", str(root), "check-ignore", "--no-index", "-z", "--stdin"],
+ input="".join(f"{path}\0" for path in sorted(to_check)).encode("utf-8"),
+ capture_output=True,
+ check=False,
+ )
+ if result.returncode not in (0, 1):
+ raise AuditFormatError(f"cannot check ignored evidence paths: {result.stderr.decode('utf-8', errors='replace')}")
+ ignored = [path for path in result.stdout.decode("utf-8").split("\0") if path]
+ if ignored:
+ raise AuditFormatError("ignored evidence paths are ineligible: " + ", ".join(ignored))
+ return MappingProxyType(resolved)
+
+
+def baseline_paths(repo_root: Path, inventory: PrePagesInventory) -> tuple[PurePosixPath, ...]:
+ _require_full_history(repo_root, inventory.baseline_commit, inventory.pages_commit)
+ _require_commit(repo_root, inventory.baseline_commit)
+ output = _git(repo_root, "ls-tree", "-r", "--name-only", "-z", inventory.baseline_commit)
+ return tuple(PurePosixPath(path) for path in output.decode("utf-8").split("\0") if path)
+
+
+def _parse_name_status_z(data: bytes) -> tuple[tuple[str, PurePosixPath, PurePosixPath | None], ...]:
+ if not data:
+ return ()
+ if not data.endswith(b"\0"):
+ raise AuditFormatError("Truncated Git name-status output: missing final NUL")
+ try:
+ fields = data.decode("utf-8").split("\0")[:-1]
+ except UnicodeError as error:
+ raise AuditFormatError("Git name-status output is not UTF-8") from error
+ records = []
+ before, after = set(), set()
+ position = 0
+ while position < len(fields):
+ status = fields[position]
+ rename = re.fullmatch(r"R([0-9]{1,3})", status)
+ if status not in {"A", "M", "D", "T"} and not (rename and int(rename[1]) <= 100):
+ raise AuditFormatError(f"Unexpected Git name-status record: {status!r}")
+ width = 3 if rename else 2
+ if position + width > len(fields):
+ raise AuditFormatError(f"Truncated Git name-status record: {status}")
+ source = _relative_path(fields[position + 1], "Git name-status source")
+ target = _relative_path(fields[position + 2], "Git rename target") if rename else None
+ if status != "A":
+ if source in before:
+ raise AuditFormatError(f"Duplicate Git name-status source: {source}")
+ before.add(source)
+ if status != "D":
+ destination = target if rename else source
+ if destination in after:
+ raise AuditFormatError(f"Duplicate Git name-status target: {destination}")
+ after.add(destination)
+ records.append((status, source, target))
+ position += width
+ return tuple(records)
+
+
+def resolve_current_documents(
+ repo_root: Path, inventory: PrePagesInventory
+) -> Mapping[PurePosixPath, Document]:
+ _require_full_history(repo_root, inventory.baseline_commit, inventory.pages_commit)
+ if len(inventory.documents) != 62:
+ raise AuditFormatError(f"Expected 62 inventory documents, found {len(inventory.documents)}")
+ _require_commit(repo_root, inventory.baseline_commit)
+ _require_commit(repo_root, inventory.pages_commit)
+ initial_changes = _parse_name_status_z(_git(
+ repo_root, "diff", f"--find-renames={inventory.rename_similarity}%",
+ "--name-status", "-z", inventory.baseline_commit, inventory.pages_commit, "--",
+ ))
+ initial_renames = {
+ source: target for status, source, target in initial_changes
+ if status.startswith("R") and int(status[1:]) >= inventory.rename_similarity
+ }
+ try:
+ taxonomy = load_taxonomy(repo_root / "docs-taxonomy.yml")
+ catalog = build_topic_catalog(repo_root / "docs", taxonomy)
+ except (DocumentFormatError, OSError, yaml.YAMLError) as error:
+ raise AuditFormatError(f"Cannot resolve current documents: {error}") from error
+ by_path = {document.relative_path: document for document in catalog.documents}
+ resolved: dict[PurePosixPath, Document] = {}
+ used: set[PurePosixPath] = set()
+ for document in inventory.documents:
+ git_bytes(repo_root, inventory.baseline_commit, document.baseline_path)
+ pages_path = PurePosixPath("docs") / document.pages_path
+ git_bytes(repo_root, inventory.pages_commit, pages_path)
+ if initial_renames.get(document.baseline_path) != pages_path:
+ raise AuditFormatError(
+ f"{document.baseline_path}: initial rename lineage from {inventory.baseline_commit} "
+ f"to {inventory.pages_commit} at {inventory.rename_similarity}% must be R to {pages_path}; "
+ f"observed {initial_renames.get(document.baseline_path)}"
+ )
+ canonical = catalog.redirects.get(document.pages_path)
+ if canonical is None:
+ raise AuditFormatError(f"{document.pages_path}: missing initial Pages redirect_from")
+ if canonical in used:
+ raise AuditFormatError(f"{canonical}: current public document used more than once")
+ if document.baseline_path in resolved:
+ raise AuditFormatError(f"duplicate baseline_path: {document.baseline_path}")
+ resolved[document.baseline_path] = by_path[canonical]
+ used.add(canonical)
+ return MappingProxyType(resolved)
+
+
+def _validate_disposition_replacements(repo_root: Path, inventory: PrePagesInventory) -> None:
+ replacements = [
+ (baseline, current)
+ for baseline, disposition in inventory.dispositions.items()
+ for current in disposition.current_paths
+ ]
+ if not replacements:
+ return
+ root = repo_root.resolve()
+ tracked = {}
+ for record in _git(root, "ls-tree", "-r", "-z", "HEAD").decode("utf-8").split("\0"):
+ if record:
+ metadata, _, name = record.partition("\t")
+ mode, kind, _ = metadata.split()
+ tracked[PurePosixPath(name)] = (mode, kind)
+ owners = {}
+ for baseline, reference in replacements:
+ label = f"{baseline}: reviewed replacement {reference}"
+ reference = _relative_path(str(reference), label)
+ if len(reference.parts) < 3 or reference.parts[:2] not in {("docs", "services"), ("tests", "docs")}:
+ raise AuditFormatError(f"{label}: outside allowed roots docs/services/** and tests/docs/**")
+ if any(part.casefold() in {
+ ".git", ".superpowers", "site", "__pycache__", ".pytest_cache", "node_modules", ".venv",
+ } for part in reference.parts):
+ raise AuditFormatError(f"{label}: generated or private paths are ineligible")
+ if reference not in tracked:
+ raise AuditFormatError(f"{label}: not tracked at HEAD")
+ if tracked[reference] not in {("100644", "blob"), ("100755", "blob")}:
+ raise AuditFormatError(f"{label}: not a regular file at HEAD (mode {tracked[reference][0]})")
+ try:
+ current = root
+ for part in reference.parts:
+ current /= part
+ mode = current.lstat().st_mode
+ if stat.S_ISLNK(mode):
+ raise AuditFormatError(f"{label}: symlink components are ineligible")
+ if not stat.S_ISREG(mode):
+ raise AuditFormatError(f"{label}: current path is not a regular file")
+ if not current.resolve(strict=True).is_relative_to(root):
+ raise AuditFormatError(f"{label}: current path escapes the repository")
+ except (OSError, RuntimeError) as error:
+ raise AuditFormatError(f"{label}: cannot resolve current file: {error}") from error
+ owners.setdefault(reference, baseline)
+ ignored = subprocess.run(
+ ["git", "-C", str(root), "check-ignore", "--no-index", "-z", "--stdin"],
+ input="".join(f"{path}\0" for path in sorted(owners)).encode("utf-8"),
+ capture_output=True, check=False,
+ )
+ if ignored.returncode not in (0, 1):
+ raise AuditFormatError(f"cannot check ignored replacement paths: {ignored.stderr.decode('utf-8', errors='replace')}")
+ for value in ignored.stdout.decode("utf-8").split("\0"):
+ if value:
+ reference = PurePosixPath(value)
+ raise AuditFormatError(f"{owners[reference]}: reviewed replacement {reference}: ignored paths are ineligible")
+
+
+def classify_baseline_files(
+ repo_root: Path, inventory: PrePagesInventory
+) -> tuple[GitFileDisposition, ...]:
+ paths = baseline_paths(repo_root, inventory)
+ classified = {
+ path: GitFileDisposition(path, "unchanged", (path,))
+ for path in paths
+ }
+ output = _git(
+ repo_root, "diff", f"--find-renames={inventory.rename_similarity}%",
+ "--name-status", "-z", inventory.baseline_commit, "HEAD", "--",
+ )
+ deleted = set()
+ for status, source, target in _parse_name_status_z(output):
+ if status == "A":
+ continue
+ if source not in classified:
+ raise AuditFormatError(f"Git diff path is not in baseline: {source}")
+ if status == "D":
+ deleted.add(source)
+ elif status in {"M", "T"}:
+ classified[source] = GitFileDisposition(source, "modified", (source,))
+ elif status.startswith("R"):
+ assert target is not None
+ classified[source] = GitFileDisposition(source, "renamed", (target,))
+ else:
+ raise AuditFormatError(f"{source}: unsupported Git status {status}")
+
+ for path, disposition in inventory.dispositions.items():
+ if path not in classified:
+ raise AuditFormatError(f"Reviewed disposition is not in baseline: {path}")
+ if path not in deleted:
+ raise AuditFormatError(
+ f"{path}: reviewed disposition requires a deleted baseline path; "
+ f"Git status is {classified[path].status}"
+ )
+ if (disposition.status == "excluded-local-state") != (not disposition.current_paths):
+ raise AuditFormatError(f"{path}: current_paths must be empty only for excluded-local-state")
+ _validate_disposition_replacements(repo_root, inventory)
+ for path, disposition in inventory.dispositions.items():
+ classified[path] = GitFileDisposition(path, "reviewed", disposition.current_paths)
+ unreviewed = deleted - inventory.dispositions.keys()
+ if unreviewed:
+ raise AuditFormatError(
+ "unreviewed deleted baseline paths: " + ", ".join(str(path) for path in sorted(unreviewed))
+ )
+ return tuple(classified[path] for path in paths)
+
+
+def audit_repository(
+ repo_root: Path,
+ site_dir: Path,
+ inventory_path: Path,
+ *,
+ content_only: bool = False,
+) -> PreservationAuditResult:
+ """Audit fixed Git lineage, live source preservation, and optionally built output."""
+ from scripts.docs.pre_pages_content import audit_document_content
+ from scripts.docs.pre_pages_site import inspect_built_site
+
+ errors: list[str] = []
+ details: dict[str, object] = {"content_only": content_only}
+ file_count = markdown_count = document_count = current_document_count = preserved = reviewed = 0
+ root = repo_root.resolve()
+ try:
+ inventory = load_inventory(inventory_path)
+ paths = baseline_paths(root, inventory)
+ file_count = len(paths)
+ markdown_count = sum(path.suffix.casefold() == ".md" for path in paths)
+ document_count = len(inventory.documents)
+ if (file_count, markdown_count, document_count) != (359, 73, 62):
+ raise AuditFormatError(
+ f"{inventory_path}: expected 359 baseline files, 73 Markdown files, and 62 documents; "
+ f"found {file_count}, {markdown_count}, and {document_count}"
+ )
+ details["baseline_commit"] = inventory.baseline_commit
+ details["pages_commit"] = inventory.pages_commit
+ catalog = build_topic_catalog(root / "docs", load_taxonomy(root / "docs-taxonomy.yml"))
+ current_document_count = len(catalog.documents)
+ details["current_document_count"] = current_document_count
+ details["files"] = [asdict(item) for item in classify_baseline_files(root, inventory)]
+ content = audit_document_content(root, inventory)
+ details["documents"] = [asdict(item) for item in content]
+ preserved = sum(item.status != "missing" for item in content)
+ reviewed = sum(item.status == "reviewed" for item in content)
+ for item in content:
+ for finding in item.missing:
+ errors.append(
+ f"{item.current_path}: unreviewed missing {finding.category} "
+ f"{finding.fingerprint} from {item.baseline_path}: {finding.excerpt}"
+ )
+ if not content_only:
+ site_result = inspect_built_site(root, site_dir, inventory, catalog)
+ errors.extend(site_result.errors)
+ details["site"] = site_result.details
+ except (OSError, ValueError, RuntimeError, yaml.YAMLError) as error:
+ errors.append(str(error))
+ return PreservationAuditResult(
+ file_count, markdown_count, document_count, current_document_count, preserved, reviewed,
+ tuple(errors), (), MappingProxyType(details),
+ )
diff --git a/scripts/docs/pre_pages_content.py b/scripts/docs/pre_pages_content.py
new file mode 100644
index 00000000..b4f6b238
--- /dev/null
+++ b/scripts/docs/pre_pages_content.py
@@ -0,0 +1,903 @@
+"""Source-structure preservation against the fixed pre-Pages Git baseline."""
+
+from __future__ import annotations
+
+from collections import Counter
+from dataclasses import asdict, dataclass
+from difflib import SequenceMatcher
+from hashlib import sha256
+from html import escape, unescape
+from html.parser import HTMLParser
+import json
+from pathlib import Path, PurePosixPath
+import posixpath
+import re
+from typing import Mapping
+from types import MappingProxyType
+import unicodedata
+from urllib.parse import unquote, urlsplit
+
+import markdown
+from markdown.blockprocessors import ReferenceProcessor
+
+from scripts.docs.pre_pages import (
+ AuditFormatError,
+ FileEvidence,
+ PrePagesInventory,
+ ReviewedChange,
+ STRUCTURE_CATEGORIES,
+ StructureEvidence,
+ git_text,
+ resolve_current_documents,
+ resolve_public_evidence_files,
+ validate_reviewed_changes,
+)
+from scripts.docs.pre_pages_visibility import AuthoredContent
+
+
+@dataclass(frozen=True)
+class MarkdownStructure:
+ title: str
+ headings: tuple[str, ...]
+ prose: tuple[str, ...]
+ code: tuple[str, ...]
+ tables: tuple[str, ...]
+ images: tuple[str, ...]
+ local_link_labels: tuple[str, ...]
+ links: tuple[str, ...] = ()
+
+
+@dataclass(frozen=True)
+class StructureFinding:
+ category: str
+ fingerprint: str
+ excerpt: str
+
+
+@dataclass(frozen=True)
+class DocumentPreservation:
+ baseline_path: PurePosixPath
+ current_path: PurePosixPath
+ status: str
+ text_similarity: float
+ missing: tuple[StructureFinding, ...]
+ reviewed: tuple[StructureFinding, ...]
+
+
+_DESTINATION = r'(<[^>\n]*>|(?:\\.|[^\s()\\]|\([^()\n]*\))*)'
+_LINK_END = r'''(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)'''
+_LINK = re.compile(
+ r'(!?)\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\]\(\s*' + _DESTINATION
+ + _LINK_END
+)
+_IMAGE = re.compile(r'!\[([^\[\]]*)\]\(\s*' + _DESTINATION + _LINK_END)
+_REFERENCE = re.compile(r"(!?)\[([^\[\]]+)\](?:\[([^\[\]]*)\])?")
+_DEFINITION = ReferenceProcessor.RE
+_HTML_TAG = re.compile(
+ r"""?([A-Za-z][A-Za-z0-9:_-]*)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^'">])*>""",
+ re.DOTALL,
+)
+_HTML_OPEN = re.compile(r"?[A-Za-z][A-Za-z0-9:_-]*(?=[\s/>])")
+_AUTOLINK = re.compile(r"<((?:https?://|mailto:)[^<>\s]+)>", re.IGNORECASE)
+_FENCE_OPEN = re.compile(r"((?:[ \t]{0,3}>[ \t]?)*)([ \t]*)(`{3,}|~{3,})(.*)")
+_PRESENTATION_TAGS = frozenset((
+ "div", "section", "article", "aside", "nav", "p", "span", "strong",
+ "em", "b", "i", "a", "ol", "ul", "li", "br",
+))
+_RAW_TAGS = frozenset((
+ "script", "style", "code", "pre", "textarea", "title", "xmp", "iframe",
+ "noembed", "noframes", "noscript", "template", "plaintext",
+))
+_TEXT_RAW_TAGS = _RAW_TAGS - {"code", "pre", "template"}
+_WRAPPER = re.compile(
+ r"?(?:div|section|article|aside|nav|p|span|strong|em|b|i|a|ol|ul|li|br)\b[^>]*>",
+ re.IGNORECASE,
+)
+# Source-semantic subset of mkdocs.yml, shared with renderer-oracle tests:
+# attr_list changes final href/src/alt; md_in_html enables nested Markdown.
+# tables/admonition/details/tabbed/tasklist expose source content in containers.
+# highlight/inlinehilite/superfences identify code that must not supply evidence.
+# Coloring is disabled only for speed; generated code markup is ignored either way.
+SEMANTIC_MARKDOWN_CONFIG = MappingProxyType({
+ "extensions": (
+ "admonition", "attr_list", "md_in_html", "tables", "pymdownx.details",
+ "pymdownx.highlight", "pymdownx.inlinehilite", "pymdownx.superfences",
+ "pymdownx.tabbed", "pymdownx.tasklist",
+ ),
+ "extension_configs": MappingProxyType({
+ "pymdownx.highlight": MappingProxyType({"anchor_linenums": True, "use_pygments": False}),
+ "pymdownx.tabbed": MappingProxyType({"alternate_style": True}),
+ "pymdownx.tasklist": MappingProxyType({"custom_checkbox": True}),
+ }),
+})
+SEMANTIC_MARKDOWN_EXCLUSIONS = MappingProxyType({
+ "toc": "Generated TOC/permalink navigation is not authored evidence; source headings are compared separately.",
+ "pymdownx.snippets": "File inclusion has side effects; no snippet directives occur in the audited source corpus.",
+})
+_HIDDEN_TAGS = frozenset(("script", "style", "template", "iframe", "noembed", "noframes", "noscript", "title"))
+
+
+def create_semantic_renderer() -> markdown.Markdown:
+ """Build a fresh renderer from the single audited, side-effect-free site configuration."""
+ return markdown.Markdown(
+ extensions=list(SEMANTIC_MARKDOWN_CONFIG["extensions"]),
+ extension_configs={
+ name: dict(options)
+ for name, options in SEMANTIC_MARKDOWN_CONFIG["extension_configs"].items()
+ },
+ )
+
+
+def _space(text: str) -> str:
+ return " ".join(text.split())
+
+
+def _escaped_match(match: re.Match) -> bool:
+ prefix = match.string[:match.start()]
+ return (len(prefix) - len(prefix.rstrip("\\"))) % 2 == 1
+
+
+@dataclass(frozen=True)
+class _CodeSpan:
+ start: int
+ end: int
+ value: str
+
+
+def _backtick_run_end(text: str, start: int) -> int:
+ end = start
+ while end < len(text) and text[end] == "`":
+ end += 1
+ return end
+
+
+def _code_span_at(text: str, start: int, limit: int | None = None) -> _CodeSpan | None:
+ if text[start:start + 1] != "`" or start and text[start - 1] == "`":
+ return None
+ prefix = text[:start]
+ if (len(prefix) - len(prefix.rstrip("\\"))) % 2:
+ return None
+ opening_end = _backtick_run_end(text, start)
+ width = opening_end - start
+ stop = len(text) if limit is None else limit
+ position = opening_end
+ while position < stop:
+ closing = text.find("`", position, stop)
+ if closing < 0:
+ return None
+ end = _backtick_run_end(text, closing)
+ if end - closing == width and end <= stop:
+ value = text[opening_end:closing].replace("\r\n", "\n").replace("\r", "\n").replace("\n", " ")
+ if value.startswith(" ") and value.endswith(" ") and value.strip(" "):
+ value = value[1:-1]
+ return _CodeSpan(start, end, value)
+ position = end
+ return None
+
+
+def _code_spans(text: str) -> tuple[_CodeSpan, ...]:
+ spans = []
+ position = 0
+ while position < len(text):
+ start = text.find("`", position)
+ if start < 0:
+ break
+ span = _code_span_at(text, start)
+ if span is None:
+ position = _backtick_run_end(text, start)
+ else:
+ spans.append(span)
+ position = span.end
+ return tuple(spans)
+
+
+@dataclass(frozen=True)
+class _RenderedSemantics:
+ links: tuple[str, ...]
+ local_link_labels: tuple[str, ...]
+ images: tuple[str, ...]
+
+
+def _literal_markdown_characters(text: str) -> str:
+ return "".join(f"{ord(char)};" if char in "\\`*_{}[]!" else char for char in text)
+
+
+class _RendererHtmlBoundary(HTMLParser):
+ """Keep raw HTML literal while the pinned renderer owns all Markdown grammar."""
+
+ CDATA_CONTENT_ELEMENTS = tuple(_RAW_TAGS - {"template"})
+
+ def __init__(self) -> None:
+ super().__init__(convert_charrefs=False)
+ self.output: list[str] = []
+ self.raw: list[str] = []
+
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ original = self.get_starttag_text()
+ if any(value and any(char in value for char in "\\`*_{}[]!<>\n\r") for _, value in attrs):
+ pieces = ["<", tag]
+ for name, value in attrs:
+ pieces.append(f" {name}")
+ if value is not None:
+ value = _literal_markdown_characters(escape(value, quote=True))
+ value = value.replace("\n", "
").replace("\r", "
")
+ pieces.append(f'="{value}"')
+ pieces.append("/>" if original.endswith("/>") else ">")
+ self.output.append("".join(pieces))
+ else:
+ self.output.append(original)
+ if tag in _RAW_TAGS:
+ self.raw.append(tag)
+
+ def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ self.handle_starttag(tag, attrs)
+ if tag in _RAW_TAGS:
+ if tag != "template":
+ self.set_cdata_mode(tag)
+ elif tag in self.raw:
+ self.raw.remove(tag)
+
+ def handle_endtag(self, tag: str) -> None:
+ self.output.append(f"{tag}>")
+ if tag in self.raw:
+ index = len(self.raw) - 1 - self.raw[::-1].index(tag)
+ del self.raw[index:]
+
+ def handle_data(self, data: str) -> None:
+ self.output.append(_literal_markdown_characters(data) if self.raw else data)
+
+ def handle_entityref(self, name: str) -> None:
+ self.output.append(f"&{name};")
+
+ def handle_charref(self, name: str) -> None:
+ self.output.append(f"{name};")
+
+ def handle_comment(self, data: str) -> None:
+ self.output.append(f"")
+
+
+def _rendered_semantics(
+ text: str, html_spans: list[tuple[int, int]],
+) -> tuple[_RenderedSemantics, dict]:
+ for start, end in reversed(html_spans):
+ reader = _RendererHtmlBoundary()
+ reader.feed(text[start:end])
+ reader.close()
+ text = text[:start] + "".join(reader.output) + text[end:]
+ renderer = create_semantic_renderer()
+ output = renderer.convert(text)
+ authored = AuthoredContent()
+ authored.feed(output)
+ authored.close()
+ if authored.hidden:
+ raise AuditFormatError(f"persistently hidden authored content: {authored.hidden}")
+ evidence = authored.semantic_evidence(_RAW_TAGS, _HIDDEN_TAGS)
+ links, local_labels, images = [], [], []
+ for label, target in evidence.links:
+ label = _space(unicodedata.normalize("NFKC", label))
+ links.append(f"{label}\n{unicodedata.normalize('NFKC', target)}")
+ if not re.match(r"^(?:[a-z][\w+.-]*:|//)", target, re.IGNORECASE):
+ local_labels.append(label)
+ for alt, source in evidence.images:
+ alt = _space(unicodedata.normalize("NFKC", alt))
+ basename = PurePosixPath(unquote(urlsplit(source).path)).name
+ images.append(f"{alt}\n{unicodedata.normalize('NFKC', basename)}")
+ return _RenderedSemantics(tuple(links), tuple(local_labels), tuple(images)), renderer.references
+
+
+@dataclass(frozen=True)
+class _HtmlFragment:
+ raw: str
+ rendered: str
+ label: str
+ wrapper: bool = False
+
+
+class _HtmlElementReader(HTMLParser):
+ """Read source prose text; semantic links/images are collected only after rendering."""
+
+ CDATA_CONTENT_ELEMENTS = tuple(_RAW_TAGS - {"template"})
+
+ def __init__(self) -> None:
+ super().__init__(convert_charrefs=True)
+ self.text: list[str] = []
+ self.raw_tag: str | None = None
+ self.template_depth = 0
+
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ if self.template_depth:
+ self.template_depth += tag == "template"
+ return
+ if tag == "template":
+ self.template_depth = 1
+ return
+ if tag in _RAW_TAGS:
+ self.raw_tag = tag
+ return
+ if tag in {"br", "hr", "p", "div", "li", "tr"}:
+ self.text.append(" ")
+
+ def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ self.handle_starttag(tag, attrs)
+ if tag in _RAW_TAGS:
+ if tag != "template":
+ self.set_cdata_mode(tag)
+ return
+ self.handle_endtag(tag)
+
+ def handle_endtag(self, tag: str) -> None:
+ if self.template_depth:
+ self.template_depth -= tag == "template"
+ elif self.raw_tag == tag:
+ self.raw_tag = None
+ elif tag in {"p", "div", "li", "tr"}:
+ self.text.append(" ")
+
+ def handle_data(self, data: str) -> None:
+ if not self.template_depth and self.raw_tag not in {"script", "style", "iframe", "noembed", "noframes", "noscript"}:
+ self.text.append(unescape(data) if self.raw_tag else data)
+
+
+def _html_element_end(text: str, start: int, tag: str) -> int | None:
+ if tag == "plaintext":
+ return len(text)
+ if tag in _TEXT_RAW_TAGS:
+ closing = re.search(r"" + re.escape(tag) + r"\s*>", text[start:], re.IGNORECASE)
+ return start + closing.end() if closing else None
+ depth = 1
+ position = start
+ while position < len(text):
+ position = text.find("<", position)
+ if position < 0:
+ return None
+ if text.startswith("", position + 4)
+ position = len(text) if end < 0 else end + 3
+ continue
+ token = _HTML_TAG.match(text, position)
+ if token is None:
+ position += 1
+ continue
+ name = token[1].lower()
+ closing = token[0].startswith("")
+ position = token.end()
+ if name == tag:
+ depth += -1 if closing else 1
+ if not depth:
+ return position
+ elif not closing and name in _RAW_TAGS:
+ position = _html_element_end(text, position, name) or len(text)
+ return None
+
+
+def _html_fragment(text: str, position: int) -> tuple[int, _HtmlFragment] | None:
+ token = _HTML_TAG.match(text, position)
+ if token is None:
+ if _HTML_OPEN.match(text, position):
+ raw = text[position:]
+ return len(text), _HtmlFragment(raw, raw, raw)
+ return None
+ raw, tag, end = token[0], token[1].lower(), token.end()
+ escaped = _escaped_match(token)
+ opening = not raw.startswith("")
+ if opening and not escaped and (tag == "a" or tag in _RAW_TAGS):
+ complete = _html_element_end(text, end, tag)
+ if complete is not None or tag in _RAW_TAGS:
+ end = complete or len(text)
+ raw = text[position:end]
+ reader = _HtmlElementReader()
+ reader.feed(raw)
+ reader.close()
+ label = _space("".join(reader.text))
+ if tag == "a":
+ return end, _HtmlFragment(raw, f" {label} ", label)
+ return end, _HtmlFragment(raw, raw, label)
+ if opening and tag == "img" and not escaped:
+ return end, _HtmlFragment(raw, "", "")
+ if opening and tag == "details" and not escaped and re.fullmatch(
+ r"""""", raw, re.IGNORECASE,
+ ):
+ # The rendering opt-in is not historical prose; keep raw HTML for link semantics.
+ return end, _HtmlFragment(raw, "", "")
+ wrapper = tag in _PRESENTATION_TAGS
+ return end, _HtmlFragment(raw, " " if wrapper else raw, raw if escaped else "", wrapper=wrapper)
+
+
+def _source_line(
+ text: str, position: int, html: dict[str, _HtmlFragment], prefix: str,
+ literals: dict[str, str],
+ html_spans: list[tuple[int, int]],
+) -> tuple[str, int]:
+ parts = []
+ while position < len(text) and text[position] != "\n":
+ if text.startswith("", position + 4)
+ position = len(text) if end < 0 else end + 3
+ continue
+ if text[position] == "`":
+ boundary = re.search(r"\n(?:[ \t]*\n| {0,3}#{1,6}[ \t]+| {0,3}(?:`{3,}|~{3,}))", text[position:])
+ limit = position + boundary.start() if boundary else len(text)
+ span = _code_span_at(text, position, limit)
+ if span:
+ key = f"{prefix}code{len(literals)}\ue101"
+ literals[key] = span.value
+ parts.append(key)
+ position = span.end
+ else:
+ end = _backtick_run_end(text, position)
+ parts.append(text[position:end])
+ position = end
+ continue
+ autolink = _AUTOLINK.match(text, position)
+ if autolink:
+ parts.append(autolink[0])
+ position = autolink.end()
+ continue
+ if text[position] == "<":
+ fragment = _html_fragment(text, position)
+ if fragment:
+ start = position
+ position, value = fragment
+ key = f"{prefix}{len(html)}\ue101"
+ html[key] = value
+ parts.append(key)
+ if not (len(text[:start]) - len(text[:start].rstrip("\\"))) % 2:
+ html_spans.append((start, position))
+ continue
+ parts.append(text[position])
+ position += 1
+ return "".join(parts), position + (position < len(text))
+
+
+def _unquote(line: str, depth: int) -> str | None:
+ for _ in range(depth):
+ prefix = re.match(r"^[ \t]{0,3}>[ \t]?", line)
+ if prefix is None:
+ return None
+ line = line[prefix.end():]
+ return line
+
+
+def _segments(
+ text: str, html: dict[str, _HtmlFragment] | None = None,
+ literals: dict[str, str] | None = None,
+ html_spans: list[tuple[int, int]] | None = None,
+) -> list[tuple[str | None, str]]:
+ """Separate fences before applying any prose-only normalization."""
+ segments = []
+ normal: list[str] = []
+ code: list[str] = []
+ fence = ""
+ language = ""
+ indent = ""
+ quote_depth = 0
+ html = html if html is not None else {}
+ literals = literals if literals is not None else {}
+ html_spans = html_spans if html_spans is not None else []
+ prefix = "\ue100html"
+ while prefix in text:
+ prefix += "x"
+ position = 0
+ while position < len(text):
+ end = text.find("\n", position)
+ end = len(text) if end < 0 else end
+ line = text[position:end]
+ if fence:
+ code_line = _unquote(line, quote_depth)
+ if code_line is None:
+ segments.append((language, "\n".join(code).strip("\n")))
+ fence, code = "", []
+ elif re.fullmatch(
+ r"\s*" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}\s*", code_line,
+ ):
+ segments.append((language, "\n".join(code).strip("\n")))
+ fence, code = "", []
+ position = end + 1
+ continue
+ else:
+ code.append(code_line.removeprefix(indent).rstrip())
+ position = end + 1
+ continue
+ opening = _FENCE_OPEN.fullmatch(line)
+ if opening and opening[3].startswith("`") and "`" in opening[4]:
+ opening = None
+ if opening:
+ position = end + 1
+ else:
+ line, position = _source_line(text, position, html, prefix, literals, html_spans)
+ opening = _FENCE_OPEN.fullmatch(line)
+ if opening and opening[3].startswith("`") and "`" in opening[4]:
+ opening = None
+ if opening:
+ segments.append((None, "\n".join(normal)))
+ normal = []
+ quotes, indent, fence, language = opening.groups()
+ quote_depth = quotes.count(">")
+ language = language.strip()
+ else:
+ normal.append(line)
+ if fence:
+ segments.append((language, "\n".join(code).strip("\n")))
+ segments.append((None, "\n".join(normal)))
+ return segments
+
+
+def _table_cells(line: str) -> list[str]:
+ line = line.strip()
+ cells = []
+ start = 0
+ spans = {span.start: span for span in _code_spans(line)}
+ position = 0
+ while position < len(line):
+ if position in spans:
+ position = spans[position].end
+ continue
+ if line[position] == "\\":
+ position += 2
+ continue
+ if line[position] == "|":
+ cells.append(line[start:position])
+ start = position + 1
+ position += 1
+ cells.append(line[start:])
+ if line.startswith("|") and not cells[0]:
+ cells.pop(0)
+ if line.endswith("|") and not cells[-1]:
+ cells.pop()
+ return [cell.strip() for cell in cells]
+
+
+def _separator(line: str) -> bool:
+ cells = _table_cells(line)
+ return "|" in line and bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells)
+
+
+def extract_markdown_structure(text: str) -> MarkdownStructure:
+ text = text.replace("\r\n", "\n").replace("\r", "\n").lstrip("\ufeff")
+ text = re.sub(r"\A---[ \t]*\n.*?\n---[ \t]*(?:\n|$)", "", text, count=1, flags=re.DOTALL)
+ literal_prefix = "\ue000code"
+ while literal_prefix in text:
+ literal_prefix += "x"
+ html: dict[str, _HtmlFragment] = {}
+ source_literals: dict[str, str] = {}
+ html_spans: list[tuple[int, int]] = []
+ segments = _segments(text, html, source_literals, html_spans)
+ semantics, rendered_references = _rendered_semantics(text, html_spans)
+ definitions = {key: value[0] for key, value in rendered_references.items()}
+ title = ""
+ headings: list[str] = []
+ prose: list[str] = []
+ code: list[str] = []
+ tables: list[str] = []
+
+ def html_text(value: str, field: str) -> str:
+ for token, fragment in html.items():
+ value = value.replace(token, getattr(fragment, field))
+ return value
+
+ def visible(value: str) -> str:
+ value = _WRAPPER.sub(" ", unescape(value))
+ for _ in range(2):
+ value = re.sub(r"(\*\*|\*)(.+?)\1", r"\2", value)
+ value = re.sub(r"(? str:
+ # Source-only prose normalization; semantic evidence never comes from these regexes.
+ markup = visible(label)
+ if image:
+ return ""
+ return markup
+
+ def inline(value: str) -> str:
+ literals: dict[str, str] = {}
+
+ def restore(text: str) -> str:
+ for token, literal in literals.items():
+ text = text.replace(token, literal)
+ for token, literal in source_literals.items():
+ text = text.replace(token, literal)
+ return text
+
+ for span in reversed(_code_spans(value)):
+ token = f"{literal_prefix}{len(literals)}\ue001"
+ literals[token] = span.value
+ value = value[:span.start] + token + value[span.end:]
+
+ def markdown_image(match: re.Match) -> str:
+ return match[0] if _escaped_match(match) else link(match[1], match[2], True)
+
+ def reference_image(match: re.Match) -> str:
+ if not match[1] or _escaped_match(match):
+ return match[0]
+ key = _space(match[3] or match[2]).lower()
+ return link(match[2], definitions[key], True) if key in definitions else match[0]
+
+ value = _IMAGE.sub(markdown_image, value)
+ value = _REFERENCE.sub(reference_image, value)
+
+ def markdown_link(match: re.Match) -> str:
+ if _escaped_match(match):
+ if match[1]:
+ return "!" + link(match[2], match[3], False)
+ return match[0]
+ return link(match[2], match[3], bool(match[1]))
+
+ value = _LINK.sub(markdown_link, value)
+
+ def reference(match: re.Match) -> str:
+ escaped = _escaped_match(match)
+ if escaped and not match[1]:
+ return match[0]
+ key = _space(match[3] or match[2]).lower()
+ if key not in definitions:
+ return match[0]
+ if escaped:
+ return "!" + link(match[2], definitions[key], False)
+ return link(match[2], definitions[key], bool(match[1]))
+
+ value = _REFERENCE.sub(reference, value)
+
+ result = _space(html_text(visible(value), "rendered"))
+ return restore(result)
+
+ def heading(value: str, level: int) -> None:
+ nonlocal title
+ value = re.sub(r"\s+#+\s*$", "", value)
+ value = re.sub(
+ r"^(?:\d+[.)]|\d+(?:\.\d+)+\.?)\s+", "",
+ unicodedata.normalize("NFKC", inline(value)),
+ )
+ if level == 1 and not title:
+ title = value
+ else:
+ headings.append(value)
+
+ for language, body in segments:
+ if language is not None:
+ code.append(f"{language}\n{body}")
+ continue
+ body = _DEFINITION.sub("", body)
+ lines = body.split("\n")
+ paragraph: list[str] = []
+
+ def flush() -> None:
+ value = inline(" ".join(paragraph))
+ if value:
+ prose.append(value)
+ paragraph.clear()
+
+ in_table = False
+ index = 0
+ while index < len(lines):
+ line = lines[index].strip()
+ if line in html and html[line].wrapper:
+ line = ""
+ following = lines[index + 1].strip() if index + 1 < len(lines) else ""
+ index += 1
+ if not line:
+ flush()
+ in_table = False
+ continue
+ atx = re.match(r"^(#{1,6})[ \t]+(.*)", line)
+ if atx:
+ flush()
+ heading(atx[2], len(atx[1]))
+ in_table = False
+ elif re.fullmatch(r"={3,}|-{3,}", following) and not re.match(r"[-*+] ", line):
+ flush()
+ heading(line, 1 if following.startswith("=") else 2)
+ index += 1
+ in_table = False
+ elif "|" in line and (line.startswith("|") or in_table or _separator(following)):
+ flush()
+ in_table = True
+ if not _separator(line):
+ tables.append(" | ".join(
+ unicodedata.normalize("NFKC", inline(cell).replace(r"\|", "|"))
+ .replace("\\", "\\\\").replace("|", r"\|")
+ for cell in _table_cells(line)
+ ))
+ elif re.fullmatch(r"(?:[-*_]\s*){3,}", line):
+ flush()
+ in_table = False
+ else:
+ in_table = False
+ line = re.sub(r"^(?:>\s*)+", "", line)
+ if re.match(r"^(?:\d+[.)]|[-*+])\s+", line):
+ flush()
+ line = re.sub(r"^\d+[.)]\s+", "1. ", line)
+ line = re.sub(r"^[-*+]\s+", "- ", line)
+ paragraph.append(line)
+ flush()
+ return MarkdownStructure(
+ unicodedata.normalize("NFKC", title),
+ tuple(unicodedata.normalize("NFKC", value) for value in headings),
+ tuple(unicodedata.normalize("NFKC", value) for value in prose),
+ tuple(unicodedata.normalize("NFKC", value) for value in code), tuple(tables),
+ tuple(semantics.images), tuple(semantics.local_link_labels),
+ tuple(semantics.links),
+ )
+
+
+def _values(structure: MarkdownStructure, category: str) -> tuple[str, ...]:
+ if category == "title":
+ return (structure.title,) if structure.title else ()
+ return getattr(structure, category)
+
+
+def _verify_current_evidence(
+ repo_root: Path,
+ baseline_path: PurePosixPath,
+ reviewed_changes: Mapping[str, Mapping[str, ReviewedChange]],
+) -> None:
+ files = resolve_public_evidence_files(repo_root, tuple({
+ ref.path for group in reviewed_changes.values() for approval in group.values()
+ for ref in approval.evidence
+ }))
+ contents: dict[PurePosixPath, bytes] = {}
+ structures: dict[PurePosixPath, MarkdownStructure] = {}
+ for category, approvals in reviewed_changes.items():
+ for fingerprint, approval in approvals.items():
+ for index, reference in enumerate(approval.evidence):
+ label = f"{baseline_path}.{category}.{fingerprint}.evidence[{index}] ({reference.path})"
+ if reference.path not in contents:
+ try:
+ contents[reference.path] = files[reference.path].read_bytes()
+ except OSError as error:
+ raise AuditFormatError(f"{label}: cannot read current evidence: {error}") from error
+ data = contents[reference.path]
+ if isinstance(reference, FileEvidence):
+ actual_hash = sha256(data).hexdigest()
+ if actual_hash != reference.sha256:
+ raise AuditFormatError(f"{label}: evidence SHA-256 changed: {actual_hash}")
+ continue
+ if reference.path not in structures:
+ try:
+ structures[reference.path] = extract_markdown_structure(data.decode("utf-8"))
+ except UnicodeError as error:
+ raise AuditFormatError(f"{label}: structure evidence is not UTF-8: {error}") from error
+ except AuditFormatError as error:
+ raise AuditFormatError(f"{label}: {error}") from error
+ counts = Counter(
+ sha256(f"{reference.category}\0{value}".encode("utf-8")).hexdigest()
+ for value in _values(structures[reference.path], reference.category)
+ )
+ actual_count = counts[reference.fingerprint]
+ if actual_count != reference.count:
+ raise AuditFormatError(
+ f"{label}: evidence {reference.category} fingerprint {reference.fingerprint} "
+ f"count changed: expected {reference.count}, found {actual_count}"
+ )
+
+
+def _verify_self_link_removal(
+ baseline_path: PurePosixPath,
+ current_path: PurePosixPath,
+ baseline: MarkdownStructure,
+ current: MarkdownStructure,
+ fingerprint: str,
+ approval: ReviewedChange,
+) -> None:
+ labels = {
+ label for label in baseline.local_link_labels
+ if sha256(f"local_link_labels\0{label}".encode("utf-8")).hexdigest() == fingerprint
+ }
+ links = [value.partition("\n")[2] for value in baseline.links if value.partition("\n")[0] in labels]
+ if len(links) < approval.missing_count:
+ raise AuditFormatError(f"{baseline_path}: self-link removal has no matching baseline link evidence")
+ for target in links:
+ url = urlsplit(target)
+ if (
+ url.scheme or url.netloc or url.query or url.fragment or not url.path or url.path.startswith("/")
+ or PurePosixPath(posixpath.normpath(str(baseline_path.parent / unquote(url.path)))) != baseline_path
+ ):
+ raise AuditFormatError(f"{baseline_path}: removal is not a redundant document self-link")
+ identities = [
+ ref for ref in approval.evidence
+ if isinstance(ref, StructureEvidence) and ref.path == current_path
+ and ref.fingerprint in {
+ sha256(f"{ref.category}\0{value}".encode("utf-8")).hexdigest()
+ for value in _values(current, ref.category)
+ }
+ ]
+ if not {"title", "headings"} <= {ref.category for ref in identities}:
+ raise AuditFormatError(f"{baseline_path}: self-link removal must bind current document identity/topic structure")
+
+
+def compare_markdown(
+ baseline_path: PurePosixPath,
+ current_path: PurePosixPath,
+ baseline: MarkdownStructure,
+ current: MarkdownStructure,
+ reviewed_changes: Mapping[str, Mapping[str, ReviewedChange]],
+ *,
+ repo_root: Path | None = None,
+) -> DocumentPreservation:
+ """Compare source structures; approvals require repo_root for fresh evidence reads."""
+ reviewed_changes = validate_reviewed_changes(reviewed_changes, f"{baseline_path}.reviewed_changes")
+ raw_missing: list[StructureFinding] = []
+ for category in STRUCTURE_CATEGORIES:
+ remaining = Counter(_values(current, category))
+ for value in _values(baseline, category):
+ if remaining[value]:
+ remaining[value] -= 1
+ continue
+ fingerprint = sha256(f"{category}\0{value}".encode("utf-8")).hexdigest()
+ raw_missing.append(StructureFinding(category, fingerprint, value[:160]))
+ missing_counts = Counter((finding.category, finding.fingerprint) for finding in raw_missing)
+ approved = set()
+ for category, approvals in reviewed_changes.items():
+ for fingerprint, approval in approvals.items():
+ actual = missing_counts[(category, fingerprint)]
+ if not actual:
+ raise AuditFormatError(
+ f"{baseline_path}: stale reviewed_changes.{category}.{fingerprint}; "
+ "the exact baseline structure is not missing"
+ )
+ if actual != approval.missing_count:
+ raise AuditFormatError(
+ f"{baseline_path}.{category}.{fingerprint}: missing_count changed: "
+ f"approved {approval.missing_count}, found {actual}"
+ )
+ if approval.link_change == "redundant-self-link":
+ _verify_self_link_removal(
+ baseline_path, current_path, baseline, current, fingerprint, approval,
+ )
+ approved.add((category, fingerprint))
+ if approved:
+ if repo_root is None:
+ raise AuditFormatError(f"{baseline_path}: repo_root is required to verify current approval evidence")
+ _verify_current_evidence(repo_root, baseline_path, reviewed_changes)
+ missing = tuple(f for f in raw_missing if (f.category, f.fingerprint) not in approved)
+ reviewed = tuple(f for f in raw_missing if (f.category, f.fingerprint) in approved)
+ before_text = "\n".join((baseline.title, *baseline.headings, *baseline.prose))
+ after_text = "\n".join((current.title, *current.headings, *current.prose))
+ similarity = SequenceMatcher(None, before_text, after_text, autojunk=False).ratio()
+ status = "missing" if missing else "reviewed" if reviewed else "preserved"
+ return DocumentPreservation(
+ baseline_path, current_path, status, similarity, missing, reviewed,
+ )
+
+
+def audit_document_content(
+ repo_root: Path, inventory: PrePagesInventory,
+) -> tuple[DocumentPreservation, ...]:
+ documents = resolve_current_documents(repo_root, inventory)
+ results = []
+ for entry in inventory.documents:
+ document = documents[entry.baseline_path]
+ current_path = PurePosixPath("docs") / document.relative_path
+ try:
+ results.append(compare_markdown(
+ entry.baseline_path, current_path,
+ extract_markdown_structure(git_text(repo_root, inventory.baseline_commit, entry.baseline_path)),
+ extract_markdown_structure(document.body),
+ entry.reviewed_changes, repo_root=repo_root,
+ ))
+ except AuditFormatError as error:
+ raise AuditFormatError(f"{current_path}: {error}") from error
+ return tuple(results)
+
+
+def write_content_review_json(
+ path: Path, results: tuple[DocumentPreservation, ...],
+) -> None:
+ documents = [
+ {
+ **asdict(result),
+ "baseline_path": result.baseline_path.as_posix(),
+ "current_path": result.current_path.as_posix(),
+ }
+ for result in results
+ ]
+ data = {
+ "summary": {
+ "documents": len(results),
+ "missing": sum(len(result.missing) for result in results),
+ "reviewed": sum(len(result.reviewed) for result in results),
+ "statuses": dict(sorted(Counter(result.status for result in results).items())),
+ },
+ "documents": documents,
+ }
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
diff --git a/scripts/docs/pre_pages_css.py b/scripts/docs/pre_pages_css.py
new file mode 100644
index 00000000..c361c0a0
--- /dev/null
+++ b/scripts/docs/pre_pages_css.py
@@ -0,0 +1,146 @@
+"""Token-aware inline CSS subset for preservation visibility checks."""
+
+from __future__ import annotations
+
+from scripts.docs.pre_pages import AuditFormatError
+
+
+_SPACE = " \t\n\r\f"
+_HEX = "0123456789abcdefABCDEF"
+_LOWER = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")
+_VALUES = {
+ "display": frozenset((
+ "none", "block", "inline", "inline-block", "flex", "inline-flex", "grid", "inline-grid",
+ "table", "inline-table", "table-row-group", "table-header-group", "table-footer-group",
+ "table-row", "table-cell", "table-column-group", "table-column", "table-caption",
+ "list-item", "contents", "flow-root", "initial", "inherit", "unset",
+ )),
+ "visibility": frozenset(("visible", "hidden", "collapse", "initial", "inherit", "unset")),
+}
+
+
+def _error(style: str, reason: str) -> AuditFormatError:
+ return AuditFormatError(f"cannot safely resolve inline CSS ({reason}): {style[:160]!r}")
+
+
+def _escape(style: str, position: int) -> tuple[str, int]:
+ position += 1
+ if position == len(style) or style[position] in "\n\r\f":
+ raise _error(style, "invalid escape")
+ end = position
+ while end < min(position + 6, len(style)) and style[end] in _HEX:
+ end += 1
+ if end > position:
+ codepoint = int(style[position:end], 16)
+ value = chr(codepoint) if 0 < codepoint <= 0x10FFFF and not 0xD800 <= codepoint <= 0xDFFF else "\ufffd"
+ if end < len(style) and style[end] in _SPACE:
+ end += 2 if style[end:end + 2] == "\r\n" else 1
+ return value, end
+ return style[position], position + 1
+
+
+def _name_character(char: str) -> bool:
+ return char.isascii() and (char.isalnum() or char in "-_") or ord(char) >= 128
+
+
+def _tokens(style: str) -> list[tuple[str, str]]:
+ tokens = []
+ position = 0
+ while position < len(style):
+ char = style[position]
+ if char in _SPACE:
+ position += 1
+ elif char == "\0":
+ raise _error(style, "NUL character")
+ elif style.startswith("/*", position):
+ end = style.find("*/", position + 2)
+ if end < 0:
+ raise _error(style, "unterminated comment")
+ position = end + 2
+ elif char in "\"'":
+ quote = char
+ position += 1
+ value = []
+ while position < len(style) and style[position] != quote:
+ if style[position] in "\n\r\f":
+ raise _error(style, "newline in string")
+ if style[position] == "\\":
+ if style[position + 1:position + 2] in {"\n", "\r", "\f"}:
+ position += 3 if style[position + 1:position + 3] == "\r\n" else 2
+ continue
+ escaped, position = _escape(style, position)
+ value.append(escaped)
+ else:
+ value.append(style[position])
+ position += 1
+ if position == len(style):
+ raise _error(style, "unterminated string")
+ tokens.append(("string", "".join(value)))
+ position += 1
+ elif char == "\\" or _name_character(char) and not char.isdecimal():
+ value = []
+ while position < len(style):
+ if style[position] == "\\":
+ escaped, position = _escape(style, position)
+ value.append(escaped)
+ elif _name_character(style[position]):
+ value.append(style[position])
+ position += 1
+ else:
+ break
+ tokens.append(("ident", "".join(value).translate(_LOWER)))
+ else:
+ tokens.append((char, char))
+ position += 1
+ return tokens
+
+
+def inline_visibility(style: str) -> dict[str, str]:
+ declarations: list[list[tuple[str, str]]] = [[]]
+ brackets: list[str] = []
+ for token in _tokens(style):
+ kind = token[0]
+ if kind in {"(", "[", "{"}:
+ brackets.append({"(": ")", "[": "]", "{": "}"}[kind])
+ elif kind in {")", "]", "}"}:
+ if not brackets or brackets.pop() != kind:
+ raise _error(style, "unmatched block delimiter")
+ if kind == ";" and not brackets:
+ declarations.append([])
+ else:
+ declarations[-1].append(token)
+ if brackets:
+ raise _error(style, "unterminated block")
+
+ resolved: dict[str, tuple[bool, str]] = {}
+ for declaration in declarations:
+ if not declaration:
+ continue
+ if len(declaration) < 2 or declaration[0][0] != "ident" or declaration[1][0] != ":":
+ raise _error(style, "unsupported declaration")
+ name, value = declaration[0][1], declaration[2:]
+ if name == "all":
+ raise _error(style, "all requires the full cascade")
+ if name not in _VALUES:
+ continue
+ important = value[-2:] == [("!", "!"), ("ident", "important")]
+ if important:
+ value = value[:-2]
+ if len(value) != 1 or value[0][0] != "ident" or value[0][1] not in _VALUES[name]:
+ raise _error(style, f"unsupported {name} value")
+ if important or not resolved.get(name, (False, ""))[0]:
+ resolved[name] = (important, value[0][1])
+ return {name: value for name, (_, value) in resolved.items()}
+
+
+def presentation_visibility(attributes: dict[str, str | None]) -> dict[str, str]:
+ resolved = {}
+ for name, allowed in _VALUES.items():
+ if name not in attributes:
+ continue
+ value = attributes[name] or ""
+ tokens = _tokens(value)
+ if len(tokens) != 1 or tokens[0][0] != "ident" or tokens[0][1] not in allowed:
+ raise _error(f"{name}={value}", "unsupported SVG presentation value")
+ resolved[name] = tokens[0][1]
+ return resolved
diff --git a/scripts/docs/pre_pages_html.py b/scripts/docs/pre_pages_html.py
new file mode 100644
index 00000000..82be567f
--- /dev/null
+++ b/scripts/docs/pre_pages_html.py
@@ -0,0 +1,125 @@
+"""Bounded HTML implied-end rules shared by source and built-page readers."""
+
+from collections.abc import Sequence
+from html.parser import HTMLParser
+
+from scripts.docs.pre_pages import AuditFormatError
+
+
+_P_CLOSERS = frozenset((
+ "address", "article", "aside", "blockquote", "center", "details", "dialog", "dir",
+ "div", "dl", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2",
+ "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", "dt", "dd", "listing",
+ "main", "menu", "nav", "ol", "p", "pre", "search", "section", "summary", "table", "ul",
+))
+_P_SCOPE = frozenset(("applet", "caption", "html", "table", "td", "th", "marquee", "object", "template", "button", "select", "svg", "math", "foreignobject"))
+_LIST_SCOPE = (_P_CLOSERS | _P_SCOPE | {"body", "select", "option", "optgroup"}) - {"address", "div", "p"}
+_TABLE_GROUPS = frozenset(("thead", "tbody", "tfoot"))
+_TABLE_CONTENT = _TABLE_GROUPS | {"tr", "td", "th", "p"}
+_FORMATTING = frozenset(("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u"))
+_END_CHILDREN = {
+ **{tag: {"p"} for tag in _P_CLOSERS | {"body", "foreignobject"}},
+ **{tag: {"li", "p"} for tag in ("ul", "ol", "menu")},
+ "dl": {"dt", "dd", "p"},
+ "tr": {"td", "th", "p"},
+ **{tag: {"tr", "td", "th", "p"} for tag in _TABLE_GROUPS},
+ "table": _TABLE_CONTENT | {"colgroup"},
+ "select": {"option", "optgroup"},
+ "optgroup": {"option"},
+}
+_TABLE_ALLOWED = _TABLE_CONTENT - {"p"} | {"caption", "colgroup", "col", "script", "style", "template"}
+
+
+class AuditHTMLParser(HTMLParser):
+ """Pin parent-page raw/RCDATA rules, including ignored non-void slashes."""
+
+ CDATA_CONTENT_ELEMENTS = ("script", "style", "xmp", "iframe", "noembed", "noframes")
+ RCDATA_CONTENT_ELEMENTS = ("textarea", "title")
+
+ def __init__(self, *, convert_charrefs: bool = True) -> None:
+ super().__init__(convert_charrefs=convert_charrefs)
+ self.scripting = True
+
+ def enter_raw_content(self, tag: str) -> bool:
+ if tag in self.CDATA_CONTENT_ELEMENTS or tag in {"plaintext", "noscript"}:
+ self.set_cdata_mode(tag, escapable=False)
+ return True
+ if tag in self.RCDATA_CONTENT_ELEMENTS:
+ self.set_cdata_mode(tag, escapable=True)
+ return True
+ return False
+
+
+def table_foster_context(stack: Sequence[str]) -> bool:
+ for tag in reversed(stack):
+ if tag in {"td", "th", "caption", "template", "script", "style", "svg", "math", "foreignobject"}:
+ return False
+ if tag in {"table", "tr"} | _TABLE_GROUPS:
+ return True
+ return False
+
+
+def validate_table_text(stack: Sequence[str], text: str) -> None:
+ if text.strip(" \t\n\r\f") and table_foster_context(stack):
+ raise AuditFormatError("unsupported table foster-parenting text")
+
+
+def _in_scope(stack: Sequence[str], targets: set[str] | frozenset[str], boundaries: set[str] | frozenset[str]) -> int | None:
+ for index in range(len(stack) - 1, -1, -1):
+ if stack[index] in targets:
+ return index
+ if stack[index] in boundaries:
+ break
+ return None
+
+
+def open_p_in_scope(stack: Sequence[str]) -> bool:
+ return _in_scope(stack, {"p"}, _P_SCOPE) is not None
+
+
+def implied_end_on_start(stack: Sequence[str], tag: str) -> int | None:
+ if table_foster_context(stack) and tag not in _TABLE_ALLOWED:
+ raise AuditFormatError(f"unsupported table foster-parenting flow content: <{tag}>")
+ index = None
+ if tag in _TABLE_CONTENT - {"p"}:
+ table = _in_scope(stack, {"table"}, {"template", "svg", "math"})
+ if table is None or any(name not in _TABLE_CONTENT for name in stack[table + 1:]):
+ raise AuditFormatError(f"unsupported ambiguous table context for <{tag}>")
+ if tag in {"td", "th"} and "tr" not in stack[table + 1:]:
+ raise AuditFormatError(f"unsupported table cell without a row: <{tag}>")
+ if tag == "li":
+ index = _in_scope(stack, {"li"}, _LIST_SCOPE)
+ elif tag in {"dt", "dd"}:
+ index = _in_scope(stack, {"dt", "dd"}, _LIST_SCOPE)
+ elif tag in {"td", "th"}:
+ index = _in_scope(stack, {"td", "th"}, {"tr", "table"} | _TABLE_GROUPS)
+ elif tag == "tr":
+ index = _in_scope(stack, {"tr"}, {"table"} | _TABLE_GROUPS)
+ elif tag in _TABLE_GROUPS:
+ index = _in_scope(stack, _TABLE_GROUPS, {"table"})
+ if index is None:
+ index = _in_scope(stack, {"tr"}, {"table"})
+ elif tag == "option":
+ index = _in_scope(stack, {"option"}, {"select", "optgroup"})
+ elif tag == "optgroup":
+ index = _in_scope(stack, {"optgroup"}, {"select"})
+ if index is None:
+ index = _in_scope(stack, {"option"}, {"select"})
+ remaining = stack[:index] if index is not None else stack
+ if tag in _P_CLOSERS:
+ paragraph = _in_scope(remaining, {"p"}, _P_SCOPE)
+ if paragraph is not None:
+ index = paragraph
+ if index is not None and any(name in _FORMATTING for name in stack[index + 1:]):
+ raise AuditFormatError(f"unsupported ambiguous formatting adoption before <{tag}>")
+ return index
+
+
+def implied_end_on_end(stack: Sequence[str], tag: str) -> int | None:
+ index = next((i for i in range(len(stack) - 1, -1, -1) if stack[i] == tag), None)
+ if index is None:
+ return None
+ children = stack[index + 1:]
+ if children and all(child in _END_CHILDREN.get(tag, ()) for child in children):
+ return index + 1
+ return None
diff --git a/scripts/docs/pre_pages_inventory.yml b/scripts/docs/pre_pages_inventory.yml
new file mode 100644
index 00000000..d322fe43
--- /dev/null
+++ b/scripts/docs/pre_pages_inventory.yml
@@ -0,0 +1,4125 @@
+# Reviewed exact links and public HEAD-tracked evidence; no snapshot or prose-only link approvals.
+version: 3
+baseline_commit: a4e680116db1016a698e64baae9efde858a8eafa
+pages_commit: 9ace966742ff92a00fc84bcc21529af47b8661af
+rename_similarity: 20
+documents:
+ - baseline_path: aifoundry/agent_framework_2026_customer_brief.md
+ pages_path: research/microsoft-foundry/agent-framework-2026/index.md
+ reviewed_changes: {}
+ - baseline_path: aifoundry/codex_closednetwork.md
+ pages_path: guides/microsoft-foundry/codex-closed-network/index.md
+ reviewed_changes:
+ prose:
+ 118b96a5a8992998657f86c734faad1c91c60b434bf3dc872cd57ed891601410:
+ missing_count: 1
+ reason: 'In docs/services/microsoft-foundry/codex-closed-network/index.md: Safety: the hosts
+ example, Codex TOML base_url and nginx server_name/Host now consistently use example-openai.openai.azure.com
+ instead of the private endpoint hostname. Loopback addresses, ports, model and proxy configuration
+ are retained. Current prose replacement begins: ''예: Private Endpoint 도메인이 example-openai.openai.azure.com인
+ 경우''.'
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/codex-closed-network/index.md
+ category: prose
+ fingerprint: 0c962db546318c617da81cb569680dacf2a63978a150c40d28211edb5ff7f769
+ count: 1
+ code:
+ d33ebf7645206700ee597fa0faf3667d1042274f1de382a2b3fca47be72eda91:
+ missing_count: 1
+ reason: 'In docs/services/microsoft-foundry/codex-closed-network/index.md: Safety: the hosts
+ example, Codex TOML base_url and nginx server_name/Host now consistently use example-openai.openai.azure.com
+ instead of the private endpoint hostname. Loopback addresses, ports, model and proxy configuration
+ are retained. Current code replacement begins: ''127.0.0.1 example-openai.openai.azure.com''.'
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/codex-closed-network/index.md
+ category: code
+ fingerprint: 8800cd6f8474d4cde2fbf104287480472fb9351dd61ba6ffe56ed3dbca6077cc
+ count: 1
+ ce8dbbc8cd77131c31f326888caeffd8b6df484eb4b7190d44a05159984b00ca:
+ missing_count: 1
+ reason: 'In docs/services/microsoft-foundry/codex-closed-network/index.md: Safety: the hosts
+ example, Codex TOML base_url and nginx server_name/Host now consistently use example-openai.openai.azure.com
+ instead of the private endpoint hostname. Loopback addresses, ports, model and proxy configuration
+ are retained. Current code replacement begins: ''model = "gpt-5.3-codex" model_provider =
+ "custom-provider" personality = "pragmatic" model_reasoning_effort = "medium" preferred_auth_method''.'
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/codex-closed-network/index.md
+ category: code
+ fingerprint: 17522a8569ba814cf6f0332803b5f4459f6f07624d92d130eeff41169f1c6357
+ count: 1
+ 2e05e66284addd0c672dbe1245bb8761cba1fcc25fbf0797c7b5f7324f7ffd1a:
+ missing_count: 1
+ reason: 'In docs/services/microsoft-foundry/codex-closed-network/index.md: Safety: the hosts
+ example, Codex TOML base_url and nginx server_name/Host now consistently use example-openai.openai.azure.com
+ instead of the private endpoint hostname. Loopback addresses, ports, model and proxy configuration
+ are retained. Current code replacement begins: ''worker_processes 1; events { worker_connections 1024;
+ } http { include mime.types; default_type application/octet-stream; sendfile ''.'
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/codex-closed-network/index.md
+ category: code
+ fingerprint: d3cd1322cefb1098c42bc0d471dbeb009b350a5005323a04100daa6663d292ab
+ count: 1
+ - baseline_path: aifoundry/foundry_local.md
+ pages_path: guides/microsoft-foundry/foundry-local-air-gapped/index.md
+ reviewed_changes: {}
+ - baseline_path: aisearch-v2/README.md
+ pages_path: guides/azure-ai-search/korean-analyzer-comparison/index.md
+ reviewed_changes:
+ prose:
+ b22a799dcc9854d4f5d04429a05a7d6422303d141148dab593bb00acce598c0e:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/korean-analyzer-comparison/index.md: Safety: the validation/Portal/resource
+ table and cleanup command now use rg-search-example-koreacentral and search-example-koreacentral-02.
+ The reproduction fence tells readers to prepare their own resources instead of claiming they
+ already exist. Free tier, region, dataset, analyzer outcomes and execution commands remain.
+ Current prose replacement begins: ''Azure Portal → search-example-koreacentral-02 → Indexes
+ → analyzer-compare-idx → Search explorer → 우측 상단 View → JSON 으로 전환하면 JSON query edit''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/korean-analyzer-comparison/index.md
+ category: prose
+ fingerprint: 07fb12f06c34db6a8b43d7af47db3b27b1612fa87fcf3c22fe8711c6233fe6b2
+ count: 1
+ code:
+ ab88d2080db1de056ee51cb84b7193aac8596fa11ef2b9c91de5c86207b85fa1:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/korean-analyzer-comparison/index.md: Safety: the validation/Portal/resource
+ table and cleanup command now use rg-search-example-koreacentral and search-example-koreacentral-02.
+ The reproduction fence tells readers to prepare their own resources instead of claiming they
+ already exist. Free tier, region, dataset, analyzer outcomes and execution commands remain.
+ Current code replacement begins: ''# 아래 리소스 이름은 예시입니다. 사용할 리소스 그룹과 Azure AI Search 서비스를 #
+ 먼저 준비한 뒤 실제 이름으로 바꾸십시오. # .env.example 을 .env 로 복사하고 SEARCH_ADMIN_KEY 채우기 # az sear''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/korean-analyzer-comparison/index.md
+ category: code
+ fingerprint: 7fe49a11c6fb5552698cfd4341b2b210918e8aa4b24263739d2a84b678a1e869
+ count: 1
+ 2021dfc593d5f03f0377b6e0fa5a3fc2b630131c42d4c9554ed26ee72b15918c:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/korean-analyzer-comparison/index.md: Safety: the validation/Portal/resource
+ table and cleanup command now use rg-search-example-koreacentral and search-example-koreacentral-02.
+ The reproduction fence tells readers to prepare their own resources instead of claiming they
+ already exist. Free tier, region, dataset, analyzer outcomes and execution commands remain.
+ Current code replacement begins: ''az group delete -n rg-search-example-koreacentral --yes
+ --no-wait''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/korean-analyzer-comparison/index.md
+ category: code
+ fingerprint: d858f3fb9fba92fb2b02dcfe8094df7ff46f2287d2cf98cbe16bb7415a772ac7
+ count: 1
+ tables:
+ 8f5f31717321c430bf0ba0b503369080e3d56e264ff8e5485f681a131bd1fa91:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/korean-analyzer-comparison/index.md: Safety: the validation/Portal/resource
+ table and cleanup command now use rg-search-example-koreacentral and search-example-koreacentral-02.
+ The reproduction fence tells readers to prepare their own resources instead of claiming they
+ already exist. Free tier, region, dataset, analyzer outcomes and execution commands remain.
+ Current tables replacement begins: ''검증 리소스 | RG rg-search-example-koreacentral / AI Search
+ search-example-koreacentral-02 (Free, koreacentral)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/korean-analyzer-comparison/index.md
+ category: tables
+ fingerprint: 0825641417d21dce7f0f353a928da2a4421150d83d38f52886262ac255ab3d13
+ count: 1
+ 6558295403a5c68b0f411a9f1628b300c862e6de77493f58d7d166ca093b0aef:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/korean-analyzer-comparison/index.md: Safety: the validation/Portal/resource
+ table and cleanup command now use rg-search-example-koreacentral and search-example-koreacentral-02.
+ The reproduction fence tells readers to prepare their own resources instead of claiming they
+ already exist. Free tier, region, dataset, analyzer outcomes and execution commands remain.
+ Current tables replacement begins: ''Resource Group | rg-search-example-koreacentral | koreacentral''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/korean-analyzer-comparison/index.md
+ category: tables
+ fingerprint: 3a5ab857a33cc8eecd997dad2766621c59ea229ebf3d442fcb2d890bae2f4071
+ count: 1
+ f8f04e0dfe11f80a9e8f12212b79c433fae1cae02e2c9ff6e3260c76fb09621a:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/korean-analyzer-comparison/index.md: Safety: the validation/Portal/resource
+ table and cleanup command now use rg-search-example-koreacentral and search-example-koreacentral-02.
+ The reproduction fence tells readers to prepare their own resources instead of claiming they
+ already exist. Free tier, region, dataset, analyzer outcomes and execution commands remain.
+ Current tables replacement begins: ''AI Search | search-example-koreacentral-02 | Free tier
+ (비용 0)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/korean-analyzer-comparison/index.md
+ category: tables
+ fingerprint: 64fe2b3d2f10bf43e295a6dc6956608c2dd63a9573ec516391103397cf07cca0
+ count: 1
+ - baseline_path: aisearch/custom_vectorization/01_custom_embedding_guide.md
+ pages_path: guides/azure-ai-search/custom-embedding-ingestion/index.md
+ reviewed_changes:
+ local_link_labels:
+ b1fdc5e64b1619b40e77c551df0df2e5a43c7aea3b6b54d76990c9037d6a70ba:
+ missing_count: 1
+ reason: Retained label 'tei-adapter' in docs/services/azure-ai-search/custom-vectorization/index.md,
+ pointing to docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/index.md
+ category: links
+ fingerprint: 9b0ace50c6b1c47e461d47ee6dbc75ae03f992f4343be4bcf2dbdbedb129d83a
+ count: 1
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter/Dockerfile
+ sha256: ef7b10e5dc4c9e63901448a1a86cb0e8697101d201400e69ff3efeb88532277e
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter/app.py
+ sha256: c731ce1adc3f64178c018fb46a72262bd4cbbe0435e3e54d220013b2900b5b10
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter/k8s/deployment.yaml
+ sha256: 843672887878b904c9be73126416a97f5f917d6dcd0dcaf0ea8bff3cbacc6496
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter/requirements.txt
+ sha256: 468be2c9de7dbe4976f66a16981d181fe746622ec5f7c6d1e08fd3c9d5a082df
+ link_change: replacement
+ - baseline_path: aisearch/custom_vectorization/02_custom_vectorization.md
+ pages_path: guides/azure-ai-search/custom-web-api-vectorization/index.md
+ reviewed_changes:
+ code:
+ f6df427f69dcd0be8f4e64827e5122f0f63325e86a686ba6a10a3ebd7fb9bf3b:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''az acr
+ build --registry acrexamplekrc01 \\ --image bge-m3-embedding:latest \\ --file embedding-api/Dockerfile
+ \\ embedding-api/''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 01ac7a7f18939e2a42f69003fd8ba131f8763480975620e6aa12d526cb60c896
+ count: 1
+ b9c12ed1dd63a966a1bd992a0658e489a3a18b7e81bd366b438574d0cf69411c:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''# Container
+ Apps 환경 생성 az containerapp env create --name cae-example-koreacentral-01 \\ --resource-group
+ rg-example-koreacentral-01 --locatio''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 9761607883e08ba00fea28983188fba8443f64ea0e43127e207c845b7e469ac1
+ count: 1
+ 49febf4034a25667d4f93acc9c709bf7a9f524ffe7245f598401a7fc23c07f0d:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''# FQDN
+ 확인 az containerapp show --name ca-embedding-example-01 \\ --resource-group rg-example-koreacentral-01
+ \\ --query properties.configurati''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 279d1f23345bdfa20811aadc5db9811d8482fe1e7c86edc50ea060c4002a3fd8
+ count: 1
+ 2b9f9401aa63df59fb41f841a26fffb0197d39d39209f50690452830245a6365:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: "# Ingress
+ LoadBalancer IP 확인 INGRESS_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller
+ \\ -o jsonpath=''{.status.loadBalancer.ing".'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 53e625b7a88017acad1f2e07cc56dd3b33506fbb73d55d49f694f37e773bd2ce
+ count: 1
+ fdf02e23afdeb9e94a114dc1ef232cfee33079c3229b26931d6fd815fdc87675:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: "# Option
+ A (Container Apps) curl -s https://ca-embedding-example-01.example.koreacentral.azurecontainerapps.io/health
+ # Option B (AKS + Let''".'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 17cfa9ba6eaf9e2f63e3adf7423a93ae0e5af06ec4509ff7004eada1b35cdad6
+ count: 1
+ 10245a8f6783fdcd19570715006a62fd315a59b6c0c318354764a251fa63453b:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''# AI Search
+ MI에 Storage Blob Data Reader 역할 부여 az role assignment create \\ --role "Storage Blob Data
+ Reader" \\ --assignee-object-id $(az sea''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 84b84eeda766bc9781801bb68de52dc486c23beb6eb633f58a7c00d3c8c8266b
+ count: 1
+ 64ce0dbb52cab5271fe47078ca1669de9db85c8a1db17efdc350aca4367c0bfc:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''# 데이터
+ 소스 생성 (Managed Identity 인증) curl -X POST "$SEARCH_URL/datasources?api-version=2024-07-01"
+ \\ -H "Content-Type: application/json" -H "ap''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 6492dab23a8b3d380755d7f033bff97314ad0b1feb7af4443c23c9b36c75082a
+ count: 1
+ 79d5f6459caea12c2e123d026c847442bf0a4d68bdf0a34f16c97a5c5552e1cc:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''# Option
+ A: Container Apps FQDN # EMBED_URL="https://ca-embedding-example-01.example.koreacentral.azurecontainerapps.io"
+ # Option B: AKS Ing''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: 4f2e40a08b7671ea1a97dc5acaf78572e37892ddbe38c4fac69911f78f8f1762
+ count: 1
+ 5de6394c79cd254fed3eacc0dfbd4a3d04417b639fd7368654717aaab969b5e9:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''az acr
+ build --registry acrexamplekrc01 \\ --image qwen3-embedding:latest \\ --build-arg EMBEDDING_MODEL=Qwen/Qwen3-Embedding-0.6B
+ \\ --file em''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: d86aaaae53af241b3c93ddb1bb6ebf575f5b1e8b7c14a2e131f11a9dd5aa04b9
+ count: 1
+ 07b00a8de2b7a108f5379f986b72b01c7b5343a4567b817116b8ee2deef968fd:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current code replacement begins: ''az acr
+ build --registry acrexamplekrc01 \\ --image tei-adapter:latest \\ --file tei-adapter/Dockerfile
+ \\ tei-adapter/''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: code
+ fingerprint: f3bf8807a657b989f844e86f8b38a29efed3de89e03b46dbb0ff0a530ecc698d
+ count: 1
+ tables:
+ 57c1d18b6d825b1e26c74f243a4a4d2c457a5c89620ea3a1216c611756e8fa87:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current tables replacement begins: ''AI Search
+ | search-example-koreacentral-01 (Standard) | 인덱스, 스킬셋, 인덱서, 벡터라이저''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: tables
+ fingerprint: 4c33ba995dbef22f0ff26a2c3aad57d94262dbd5f90ab536c51837683ad083b2
+ count: 1
+ f54f82d69e9c634f2418029141412f57d3ded42762a27e5933602792ef702ed4:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current tables replacement begins: ''HTTPS
+ 엔드포인트 (A) | Container Apps ca-embedding-example-01 | TLS 자동 적용, 설정 최소''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: tables
+ fingerprint: bb7969bc36fd9766e24ce264439c72e7ab673107afdb3b60f2f1e0a89767a705
+ count: 1
+ 548fae7f0359388390c89d11fbc7af46deee7b3c5fa287d911af83474708b2b2:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current tables replacement begins: ''컨테이너
+ 레지스트리 | acrexamplekrc01 (Basic) | Docker 이미지 저장소''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: tables
+ fingerprint: b2ee49f0f166651a83de73f773270430401a099e4a5f63567018e7408371f41e
+ count: 1
+ 8138e4867c0d1e56f4766e528db4c4e679db336c97c993fbb39f68557ec8633c:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md: Safety:
+ the hosting/resource rows and deployment fences consistently use search-example-koreacentral-01,
+ acrexamplekrc01, stexamplekrc01, rg-example-koreacentral-01 and example Container Apps names/FQDNs.
+ The historical public ingress address became INGRESS_IP; health requests interpolate it and
+ the index/vectorizer fence now rejects an unset INGRESS_IP. Models, image tags, ports, roles
+ and JSON skill/vectorizer payloads are retained. Current tables replacement begins: ''스토리지
+ | stexamplekrc01 | Blob 컨테이너 sample-docs (샘플 문서)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: tables
+ fingerprint: d026ba39766f47adfc44a8ac3345e6f216675f7dc1d01a997b6b69476cdbfb16
+ count: 1
+ - baseline_path: aisearch/custom_vectorization/03_gpu_vllm_rag_guide.md
+ pages_path: guides/azure-ai-search/gpu-vllm-rag/index.md
+ reviewed_changes:
+ prose:
+ 3bfc033e5ddee3fa15c3a5aadcf984f4220dd4694ababff7c32c2b7c9fa385b5:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md: Safety: the
+ index creation URL, GPU node-pool command, adapter image, actual-cluster note and Appendix
+ B environment now use example Search/AKS/ACR/resource-group names. The T4 SKU, Spot settings,
+ model configuration, sample sizes and measured results remain. Current prose replacement begins:
+ ''실제 구성: aks-example-koreacentral-01 클러스터, rg-example-koreacentral-01 리소스 그룹.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md
+ category: prose
+ fingerprint: fc39f364706524a9c1259c0a9f30e54eb7eff623c7cd057e9d0470960fafbc64
+ count: 1
+ a409e1b42b8f79b1509ac578b4fe0003d60279bc2b585c86a6f1d870b37348fc:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md: Safety: the
+ index creation URL, GPU node-pool command, adapter image, actual-cluster note and Appendix
+ B environment now use example Search/AKS/ACR/resource-group names. The T4 SKU, Spot settings,
+ model configuration, sample sizes and measured results remain. Current prose replacement begins:
+ ''테스트 환경: AKS (aks-example-koreacentral-01, koreacentral), GPU 노드풀 Standard_NC16as_T4_v3 Spot
+ × 3, AI Search Standard tier. 500개 Azure Docs .m''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md
+ category: prose
+ fingerprint: 1f70bc9f09959411bc3b2a0fd56ba3b541e02e48aa50ecae7d31379f515042a3
+ count: 1
+ code:
+ 9afb64263f717d27965ac4e39e77664066aeb4cfadd11cbb08aec6a5de2de39d:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md: Safety: the
+ index creation URL, GPU node-pool command, adapter image, actual-cluster note and Appendix
+ B environment now use example Search/AKS/ACR/resource-group names. The T4 SKU, Spot settings,
+ model configuration, sample sizes and measured results remain. Current code replacement begins:
+ ''SEARCH_URL="https://search-example-koreacentral-01.search.windows.net" KEY=""
+ curl -X PUT "$SEARCH_URL/indexes/prod-ch''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md
+ category: code
+ fingerprint: c8e541144c6d02da01168ede4088b27ea7d800d59b0588236a8186fa90dec8c7
+ count: 1
+ e5f1cb808ab09fbab50f085dbc7df5bef561320d76004392392b1f3824638ed9:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md: Safety: the
+ index creation URL, GPU node-pool command, adapter image, actual-cluster note and Appendix
+ B environment now use example Search/AKS/ACR/resource-group names. The T4 SKU, Spot settings,
+ model configuration, sample sizes and measured results remain. Current code replacement begins:
+ ''# GPU Spot 노드풀 (T4 × 1) az aks nodepool add \\ --resource-group rg-example-koreacentral-01
+ \\ --cluster-name aks-example-koreacentral-01 \\ --n''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md
+ category: code
+ fingerprint: 5c0242c702f2fc47ad79bdae195c63facd1e7c48d93488bd440f49da1958eaf8
+ count: 1
+ c73108ad4cc1e65904f20635d16ed3afd4aedba19d2a8d593702bd955dd8e54a:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md: Safety: the
+ index creation URL, GPU node-pool command, adapter image, actual-cluster note and Appendix
+ B environment now use example Search/AKS/ACR/resource-group names. The T4 SKU, Spot settings,
+ model configuration, sample sizes and measured results remain. Current code replacement begins:
+ ''apiVersion: apps/v1 kind: Deployment metadata: name: vllm-embedding labels: app: vllm-embedding
+ spec: replicas: 1 selector: matchLabels: app''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md
+ category: code
+ fingerprint: 44f7bac003382b7b2a0cfecbd238226211eb4bbdfa1cee9ea66559d5dfa0a5a1
+ count: 1
+ - baseline_path: aisearch/custom_vectorization/ref_a10_vs_t4_embedding_benchmark.md
+ pages_path: research/azure-ai-search/a10-vs-t4-embedding-benchmark/index.md
+ reviewed_changes:
+ local_link_labels:
+ 22986ffc7d5949e14cd6fe25b45adabbf8048d43de235516d27915319b0e9eda:
+ missing_count: 1
+ reason: Retained label 'bench/' in docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md,
+ pointing to docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md
+ category: links
+ fingerprint: e7c58296e9097c223f9eb6050d1f5889e656318e55e8074ee28045fdf9f5ef6d
+ count: 1
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/Dockerfile
+ sha256: d0112226dc8be7b0150158f65b4068e6306f6553f3a859e00484056f6a6c3fd3
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/bench_embed_gpu.py
+ sha256: cbdf418323deead0540d0f824b067e512292373ad4929f4d59d40aa14c777d66
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/bench_qps_sweep.py
+ sha256: db9e561cb63b95891dc1c55e0db55d86d4f3c536cbaedcaf9fadb0075acd23c3
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/bench_runner.py
+ sha256: 19ba637503e64d3bab5024cf5c4de6035956fa552729bb3ff44e23f4b45ff4df
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/k8s/configmap.yaml
+ sha256: e39cc542677a13ee563918244d574aa131d7737c42cc3f9ae3c2db049f449404
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/k8s/job-bench-concurrency.yaml
+ sha256: ddf98b58e1de53ccbec3e5609f0198c21c37bb4b509f0a8aa553203931aab94b
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/k8s/job-bench-indexer.yaml
+ sha256: baf4006544be251dcd104400fa4b14b62c725efdc8dfa6466701d6418e8dc357
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/k8s/job-bench-push.yaml
+ sha256: d9dbd9a28808094b9551087d90f1dfffa5864ccf5858655ee7fb2e703833b495
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/requirements.txt
+ sha256: 25498c398913549d3a8ed10b9c105067fdfe0a6f95709e15ad44b90cf752773f
+ link_change: replacement
+ c1168e03ae9ae43f12052ccdcab60029b049f217eea89f5193f9471bda029f51:
+ missing_count: 1
+ reason: Retained label 'bench_qps_sweep.py' in docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md,
+ pointing to docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/bench_qps_sweep.py.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md
+ category: links
+ fingerprint: c850fced94c8ff5aeabbbcee0e1e22ddb8ce8119a2f16def4345eb8a692baeea
+ count: 1
+ - kind: file
+ path: docs/services/azure-ai-search/custom-vectorization/samples/implementation/bench/bench_qps_sweep.py
+ sha256: db9e561cb63b95891dc1c55e0db55d86d4f3c536cbaedcaf9fadb0075acd23c3
+ link_change: replacement
+ - baseline_path: aisearch/custom_vectorization/ref_bge_m3_vs_qwen3_comparison.md
+ pages_path: research/azure-ai-search/bge-m3-vs-qwen3-embedding/index.md
+ reviewed_changes:
+ tables:
+ a680b3767ce1c680782fc16c086d62240a2b555db34aeccf79f3d8d12151170c:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md: Safety:
+ the test environment rows now use search-example-koreacentral-01 and ca-embedding-example-01/ca-qwen3-example-01.
+ Standard tier, region, index names, 1024 dimensions, HNSW settings and all quality measurements
+ are retained. Current tables replacement begins: ''AI Search | search-example-koreacentral-01
+ (Standard, koreacentral)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md
+ category: tables
+ fingerprint: 648697b85ef24e5749aa373ab2b8958bec0c3abb5410d3ff80d1c39104029dff
+ count: 1
+ 310a5ade796d7ca3f4e83217376141cb3f2a8db6365525105e9adf0805a44664:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md: Safety:
+ the test environment rows now use search-example-koreacentral-01 and ca-embedding-example-01/ca-qwen3-example-01.
+ Standard tier, region, index names, 1024 dimensions, HNSW settings and all quality measurements
+ are retained. Current tables replacement begins: ''HTTPS 엔드포인트 | ca-embedding-example-01 /
+ ca-qwen3-example-01 (Container Apps)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md
+ category: tables
+ fingerprint: d2ea71e617dac073cef53fa867cd5d3cd32c87754ee9a63ae7e13e7354897ab4
+ count: 1
+ - baseline_path: aisearch/custom_vectorization/ref_chunking_strategies_research.md
+ pages_path: research/azure-ai-search/rag-chunking-strategies/index.md
+ reviewed_changes:
+ prose:
+ ece93a9ccdfb8c70b20dac23e21e20abb7606051856d876ff784fdcd0e4d84fa:
+ missing_count: 1
+ reason: 'In docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md: The dated
+ related-document note retains 2026-05-22 and now names the Custom Web API 벡터화 가이드 at docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ instead of exposing its retired filename. Current prose replacement begins: ''작성일: 2026-05-22
+ | 관련 문서: Custom Web API 벡터화 가이드''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md
+ category: prose
+ fingerprint: f740e606b5dd81b7bccf148ae21fce1d9afd516bf6930893091316cafff639e6
+ count: 1
+ local_link_labels:
+ d5750f10270f9509bbb6a2003ad877365cc26a3151f031832a2f5fea0cfa4aa8:
+ missing_count: 1
+ reason: Replaced retired path label with 'Custom Web API 벡터화 가이드' in docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md,
+ pointing to docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md. The
+ current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md
+ category: links
+ fingerprint: 56cee62b75ddd0a0342108e896b9393b27f937b8a0cced3484e5e05a9b28c8bc
+ count: 1
+ - kind: structure
+ path: docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md
+ category: title
+ fingerprint: d2db99b98bfff9cde68bcb7933e337db05a9bcdc15635c51d0bfeb63ad756e1e
+ count: 1
+ link_change: replacement
+ - baseline_path: aisearch/eventual_consistency_reindex.md
+ pages_path: cases/azure-ai-search/eventual-consistency-reindex/index.md
+ reviewed_changes:
+ local_link_labels:
+ 7fabf46e6de24e286372bf664aaacd6b34e9cfb03c06588a01bf7d803d4addcf:
+ missing_count: 1
+ reason: Retained label 'repro.py' in docs/services/azure-ai-search/eventual-consistency-reindex/index.md,
+ pointing to docs/services/azure-ai-search/eventual-consistency-reindex/samples/reproduction/repro.py.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-ai-search/eventual-consistency-reindex/index.md
+ category: links
+ fingerprint: 8f9b2007f57c9817d967e5e8ee684069bf631121b3c1db26d50354a05a5fb1a2
+ count: 1
+ - kind: file
+ path: docs/services/azure-ai-search/eventual-consistency-reindex/samples/reproduction/repro.py
+ sha256: 2184fc28668b5484826a64aa8a3c4d70d11b449327ac207f0068bd17ab179db8
+ link_change: replacement
+ - baseline_path: aks/affinity.md
+ pages_path: guides/azure-kubernetes-service/pod-affinity-distribution/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/agent-pool.md
+ pages_path: guides/azure-kubernetes-service/pod-scheduling-agent-pools/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/argocd_image_updater.md
+ pages_path: guides/azure-kubernetes-service/argocd-image-updater-acr/index.md
+ reviewed_changes:
+ code:
+ d6b38fb9c0ed840ead39c29f04229843b078bc8a2b54ee798c093a2fb7dc623b:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md: Safety:
+ managed-identity example outputs now use and ;
+ the troubleshooting imageName example uses acrexamplekrc01.azurecr.io. AcrPull assignment
+ and the incorrect-tag/correct-no-tag comparison are unchanged. Current code replacement begins:
+ ''# 1. Managed Identity 생성 az identity create \\ --name id-image-updater-acr \\ --resource-group
+ \\ --location # 출력에서 client''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md
+ category: code
+ fingerprint: d88f43dc096cbb0b0879e33f91165b7039edce5dcca6e770e7158059ec8b803c
+ count: 1
+ 6933932059dfc66ed61ccef1aca0b7f81b88893b4ee82c962163db2eccb170fb:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md: Safety:
+ managed-identity example outputs now use and ;
+ the troubleshooting imageName example uses acrexamplekrc01.azurecr.io. AcrPull assignment
+ and the incorrect-tag/correct-no-tag comparison are unchanged. Current code replacement begins:
+ ''imageName: acrexamplekrc01.azurecr.io/test-app:1.0.0 # ❌ imageName: acrexamplekrc01.azurecr.io/test-app #
+ ✅''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md
+ category: code
+ fingerprint: 1f006678013d56bbe5a709c6b040e7b58cbd1b2ad1c7869082eed9e8ca9c954f
+ count: 1
+ - baseline_path: aks/authorization-troubleshooting.md
+ pages_path: guides/azure-kubernetes-service/authorization-troubleshooting/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/cni-overlay-nsg.md
+ pages_path: guides/azure-kubernetes-service/cni-overlay-nsg/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/file_io_throttling.md
+ pages_path: cases/azure-kubernetes-service/file-io-throttling/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/file_io_throttling2.md
+ pages_path: cases/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md
+ reviewed_changes:
+ prose:
+ ca84a9ed13a0e19a9634b801fbd6dda13453618a958b0c47bd195f8cb809ab9f:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md: The overview
+ still records the earlier NFS write-latency capacity fix and the later CPU/I/O bottleneck.
+ Its filename label was replaced with 파일 I/O 스로틀링 사례 at docs/services/azure-kubernetes-service/file-io-throttling/index.md.
+ Current prose replacement begins: ''AKS 환경에서 NetApp Files를 사용하는 Node.js 애플리케이션의 트래픽 증가 시 CPU
+ 급증과 File I/O 대기 현상을 분석한 사례입니다. 이전 용량 증설로 NFS write 지연 문제를 해결했으나(파일 I/O 스로틀링 사례 참조),''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md
+ category: prose
+ fingerprint: c87e1802dc9ec33f712247a3fc934e275ee0778dcb90f34760a0ab848c28d452
+ count: 1
+ local_link_labels:
+ 43ee97b242ed9670643e958e64845fe3f91e0f97472db308eeebb2c7efa42dd3:
+ missing_count: 1
+ reason: Replaced retired path label with '파일 I/O 스로틀링 사례' in docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md,
+ pointing to docs/services/azure-kubernetes-service/file-io-throttling/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md
+ category: links
+ fingerprint: a628138f90ce1391b9bc62428093bf41f905f3eee7a753bac817dd2f026e01b9
+ count: 1
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/file-io-throttling/index.md
+ category: title
+ fingerprint: e9930620dc993b0893956c55fdcd6b18d4f5eaf134f26cf22ea8089309403f49
+ count: 1
+ link_change: replacement
+ - baseline_path: aks/kaito_opensource_model.md
+ pages_path: guides/azure-kubernetes-service/kaito-open-source-model/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/local_dev_loop.md
+ pages_path: guides/azure-kubernetes-service/remote-cluster-local-development/index.md
+ reviewed_changes:
+ prose:
+ 6c5dd3ad75a6ee04e3c182dd61ce968c821fbbec7353c7acc439ae5beffefeaf:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current prose replacement begins:
+ ''예제 파일: local-development-loop/''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: prose
+ fingerprint: 9f3757040e16c6a5b9108dbd776c4e33aaea5902b91fe0455ef283d212bc0008
+ count: 1
+ d370a9e4b26a6c8ecb4d0c86183f5e236bb088596a49023f09a0a742ad6b9377:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current prose replacement begins:
+ ''- Azure CLI, kubectl, AKS 클러스터 (예시: aks-example-koreacentral-01 @ rg-example-koreacentral-01)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: prose
+ fingerprint: 3d46e79be66a08610c89a95885b16fcf91b1afc1064fb562611433ad02b05a90
+ count: 1
+ code:
+ 3fcb2b20c46c9b60307fb03c47964aa8d4cd4d5ce8fb59e269cee8f7c2a2a9e7:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''local-development-loop/ ├── k8s/echo-service.yaml # tp-demo 네임스페이스 + echo-server Deployment/Service
+ └── local/ ├── local-server.py # 트''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: 662b8622b9ff4a5f7b0c2dd893929963c5ed6d03ffdca2d5cdaaf2650f99ded0
+ count: 1
+ b54eb8ee828f85446be4829817a2bbfe800044daa18d071b396dee2a7bb2b6b0:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''az aks get-credentials -g rg-example-koreacentral-01 -n aks-example-koreacentral-01 kubectl
+ apply -f docs/services/azure-kubernetes-service/''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: aa62d538655c68319c3afde7ffe80d92f5086546032aeaf50648131096a41f94
+ count: 1
+ 7131334823c53fe79909967e4f42d5efa34a2708f6b6da14a3c1c59251ff5c20:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''# Traffic Manager 설치 (클러스터 측, 최초 1회) telepresence helm install # 연결 (macOS: 루트 데몬 때문에 sudo
+ 비밀번호 프롬프트가 뜸) telepresence connect # outbound: 로컬''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: 2ab41f133603fd51021db930a238b84a6e768e88fa9332cd1bb7d7f77a457120
+ count: 1
+ 0e93bf160d5d3cfe00cd225c9b7add6e27d7c37a1a42cbe5be1267c2482083f3:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''// docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop/local/mirrord.json
+ { "target": { "''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: faab6880107afc2d18236c291ca10e6d32a11036d3959beaebb9bb876859ffec
+ count: 1
+ 08bc7a5774277733446908b524f56ae61c877ac9c39daf00ce1e86c3483fff13:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''# 인바운드 steal + env/dns/fs 미러링 mirrord exec -f docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-developm''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: 6a8234e418843bc9f842460c126269653051a632a1f8be557021a3a705fc63da
+ count: 1
+ 132cad530fddd9189777ee3618042ab55e26b79199ced1e5fc140ff4b28c83bd:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''// .vscode/launch.json { "name": "mirrord demo: local-server (echo-server steal)", "type":
+ "debugpy", "request": "launch", "program": "${wor''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: 3c2f9df39bba37ac40d4f816823e4499da9ae8b9b3b172f3704a6d9c0d3ff02b
+ count: 1
+ 576beb4e09447093dd5ee0e25c559a64bc8e00812f2383bde770fbd4edb8174d:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md:
+ The tree, kubectl apply/delete, Python server, mirrord config/exec and debugpy workspaceFolder
+ paths now point into docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ Safety: AKS prerequisites/get-credentials/stop use example AKS and resource-group names. Intercept
+ ports, feature configuration and cleanup operations remain. Current code replacement begins:
+ ''# Telepresence telepresence leave echo-server # intercept 해제 (신버전: detach) telepresence
+ quit -s # 데몬 종료 telepr''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: code
+ fingerprint: d6ee41c25519bc91cd8afc311f7287a60102c76a8902948d61d41cd81e391838
+ count: 1
+ local_link_labels:
+ 12b7cd4f2ca28c859ddfcbca8c743e0c1c0344d036a7c9c770a4af743bf45f95:
+ missing_count: 1
+ reason: Replaced retired path label with 'local-development-loop/' in docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md,
+ pointing to docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md
+ category: links
+ fingerprint: 46d807c00926982c7b8672281bf38872483fcc6c9df95a8a7a064efa54c81f19
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop/README.md
+ sha256: 421b904e12e167829e12a5d1f1e5c224e360796db0696e4d345e6a0e894e0176
+ - kind: file
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop/k8s/echo-service.yaml
+ sha256: a31170ea1af3c2ccb64cf5c6ec34cad0fe4e29d6da0a5f18570917d204fe2596
+ - kind: file
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop/local/local-server.py
+ sha256: 160b1b250a2ab055d1c406befa89c880939ee3bd59cb76b39c2b572ebc48bcf9
+ - kind: file
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop/local/mirrord.json
+ sha256: 69468185d6c3ec7a44c7f1f010e2f25d71dbfc76482c491599f9dae225b60b29
+ - kind: file
+ path: docs/services/azure-kubernetes-service/remote-cluster-local-development/samples/local-development-loop/sample.yml
+ sha256: 35a24d4a46f99e284940dd9b6efd777d1f548e826be9f199c6795c42ddc7bfe8
+ link_change: replacement
+ - baseline_path: aks/memray_leak_profiling.md
+ pages_path: guides/azure-kubernetes-service/python-memory-leak-memray/index.md
+ reviewed_changes:
+ prose:
+ 4445d309dc075ebf3cc910ee5b24c785f8e34227649feb2b8eba5482efb8e1d3:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md: The ACR
+ build fence now enters docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling.
+ The interactive reports remain under its reports/leak-leaks.html and reports/leak-flamegraph.html;
+ the prose now explicitly instructs downloading and opening those files locally rather than
+ promising inline HTML rendering. Current prose replacement begins: ''- 원본 HTML 파일(내려받아 브라우저에서
+ 열기): reports/leak-leaks.html · reports/leak-flamegraph.html''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md
+ category: prose
+ fingerprint: 479770d6671f82490cd06fbac4a40702d63cc80cee2be10b0a71e1d217872145
+ count: 1
+ code:
+ e130e9f9113da3cc95d2e270adf0589443ae2f81e3cb38a05ae4c3d142697564:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md: The ACR
+ build fence now enters docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling.
+ The interactive reports remain under its reports/leak-leaks.html and reports/leak-flamegraph.html;
+ the prose now explicitly instructs downloading and opening those files locally rather than
+ promising inline HTML rendering. Current code replacement begins: ''cd docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling
+ az acr build --registry --image leak''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md
+ category: code
+ fingerprint: ded0aecec60d7c426645ca108063e2e23ff23463c84626576903c7df1fa30dd4
+ count: 1
+ local_link_labels:
+ 63298dbc04cec1489c6e9189312de05fb6810646969bde0c35a0092634fe7e7e:
+ missing_count: 1
+ reason: Retained label 'debug/Dockerfile' in docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md,
+ pointing to docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/debug/Dockerfile.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md
+ category: links
+ fingerprint: fee33260d2511b1b2d2e168c7168d47611abfed0c8910689283b88b2a2333c88
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/debug/Dockerfile
+ sha256: afd72d820f40468697e146cfba9793872d878ee11b5d0e271f6ff1d0ae248044
+ link_change: replacement
+ 88ce896b3271d19d7d900463556c60803f17ed606892a6fa76187d5b4f4c564b:
+ missing_count: 1
+ reason: Retained label 'reports/leak-leaks.html' in docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md,
+ pointing to docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-leaks.html.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md
+ category: links
+ fingerprint: e929e633981628fd6cb77bbe2aa1f14689ad0ab2569b98f79ae9fe74777e4cb1
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-leaks.html
+ sha256: 6ee1a470eaac5a37c53eb2ac8c13d294d1f0eacc058ba995c15f59a10cb3d89a
+ link_change: replacement
+ f7e6c7e8504b5d8b79baedd326e64c9dd09a3884626d55b4286a83071b6a0f64:
+ missing_count: 1
+ reason: Retained label 'reports/leak-flamegraph.html' in docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md,
+ pointing to docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-flamegraph.html.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md
+ category: links
+ fingerprint: 0002a43efaf77a16c5fdd98268e37cefe0cd78e826ca92ababa4437b869e03d5
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-flamegraph.html
+ sha256: 989ab827d87d5879ad38013bbc504a2280475b6c1aaadbe95b521b8e19dcbfc5
+ link_change: replacement
+ cce3bf06ad38a4d47f1debc2804da8da45849161ea38791f51ffb6227c6ae4ca:
+ missing_count: 1
+ reason: Retained label 'memray-leak-profiling/' in docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md,
+ pointing to docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md
+ category: links
+ fingerprint: b2db1648ea413fa4b1e8eb1eae03a9b1a09c5caa782c182874658d62cdc147ff
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/Dockerfile
+ sha256: a6f157d5d5d979095c9f0404c2f8eb7dcc4acf011092b56a9473c0d9e8df48cd
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/README.md
+ sha256: 375eba4d3966da160fa6bc09571749610c9f7522a70a3f409f803ce6f07b1782
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/app.py
+ sha256: 24719b9e16681f5648fcc0792497588309db6cca1c3fd6e049b6634b02e82d3b
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/debug/Dockerfile
+ sha256: afd72d820f40468697e146cfba9793872d878ee11b5d0e271f6ff1d0ae248044
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/k8s/leaky-agent.yaml
+ sha256: d4d0d5a4fc8befdb78a61f38798d7f8f84c58a3b5f1bb0ec144bdbfcb72bfe24
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/k8s/loadgen.yaml
+ sha256: 490a6ecfcaffeb4afbe9d57c71786e7da93df16adfb85fd82ab391e150b3b004
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/loadgen.py
+ sha256: 3eb97eae48b88529400a528ea6ea74e6bcd50de77472743fb1c37d2aee7f6d81
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-flamegraph.html
+ sha256: 989ab827d87d5879ad38013bbc504a2280475b6c1aaadbe95b521b8e19dcbfc5
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-leaks.html
+ sha256: 6ee1a470eaac5a37c53eb2ac8c13d294d1f0eacc058ba995c15f59a10cb3d89a
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/reports/leak-leaks.png
+ sha256: df4e54d5565006d2d54a57ee1d2403c3e67522e49bb64dbdba27bb5c2088d246
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/requirements.txt
+ sha256: ed58f33ba0120a0986bdabd19a6be0c75ece9f7187abae199fadc122177a4879
+ - kind: file
+ path: docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/sample.yml
+ sha256: 0d354f8901fdd8b0830902a135fc3ebbcda9d41eebf595530eee92845f103d85
+ link_change: replacement
+ - baseline_path: aks/pod_db_query_latency.md
+ pages_path: cases/azure-kubernetes-service/pod-database-query-latency/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/pyroscope_anf_s3_deployment.md
+ pages_path: guides/azure-kubernetes-service/pyroscope-anf-s3/index.md
+ reviewed_changes:
+ local_link_labels:
+ b5017936d6c2f031f9711c2bd2cff82f1db4c604507ef0de96f491addeca4221:
+ missing_count: 1
+ reason: Retained label 'pyroscope-chat-profiling/' in docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md,
+ pointing to docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md
+ category: links
+ fingerprint: 5191d2b491bb4f5544e0b06837f8e4f6a9a9a300737c0b7e629aa7b8406ae81c
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/Dockerfile
+ sha256: 3fe1aaf745c9dfeb91a65ad9ca9a9d6f33a5762c8ec2a85e0b582625286914e8
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/README.md
+ sha256: f8b278a0f661926f82b9b3d3477c8d88198192d2c45d28289703ddd304b43539
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/app.py
+ sha256: acaeb43adfc0dbe20c0a0ed22c4a5fd3d5dd7a5e80a3dee3ba552de83236217b
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/deploy.sh
+ sha256: 95c380cec713351f9b12d69a74a074b0ccd6d9750f3bbb7092461d8482717748
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/deployment.yaml
+ sha256: a185097a2eebb6d50dc1cd04cd43a1ba689002302e45e78b8c0df243d93fe27e
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/images/anf-create-bucket.png
+ sha256: 5511e6dad7189f545fce5dfabfab5ea91b7e9832a53808c8122a552b097e9932
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/images/anf-create-certificate.png
+ sha256: 14521e7c8659c497b6b355ed85e65d18f07cfd98798c1851469bcfe55bd8fb48
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/images/architecture.excalidraw
+ sha256: 027663d612ad23e7bf0f04bf96a1b57bcfe1bebbab1cb9cfd4493984a962d87e
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/images/architecture.png
+ sha256: c8110f8fe0dbc239df469cfff46dc089adf3077f1677514f1839a9f37e2a391a
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/images/pyroscope_ui.png
+ sha256: 2ecedcf3a40864840d8c4df89d67b840855f4bb07fb2a8b5a7561b0cd7291725
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/requirements.txt
+ sha256: b738d94ae2176c2cf8941b8a340096bd58d14e849ca580e7a7ee83e49864ead7
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/sample.yml
+ sha256: 76f7c0c54d7f44c2973f238d95a264365a90cdbf5afe53c6527b773cd82e67a4
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/test.py
+ sha256: 1b7ec68ad9cc9329b6241d5b00eb679cc43b70902e13f23b2f4eb301c20c51a2
+ link_change: replacement
+ d66ec5f5968445f509a55315423ed577736ca1d1d54c36b44c406ae0203af3f1:
+ missing_count: 1
+ reason: Retained label 'app.py' in docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md,
+ pointing to docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/app.py.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md
+ category: links
+ fingerprint: 36f7ac601fd6f39eade110008745493a617e9e01cfb7f57cf888a57183adbfb7
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/app.py
+ sha256: acaeb43adfc0dbe20c0a0ed22c4a5fd3d5dd7a5e80a3dee3ba552de83236217b
+ link_change: replacement
+ b974b045198c87e2e93578433fe9a3c2b3c6c76147ba84c204061456b504a306:
+ missing_count: 1
+ reason: Retained label 'deployment.yaml' in docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md,
+ pointing to docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/deployment.yaml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md
+ category: links
+ fingerprint: 9ac81652508bbe1827b6516b029fa7d3d7bbf7c9ec809f7a779b8adc6debae43
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/deployment.yaml
+ sha256: a185097a2eebb6d50dc1cd04cd43a1ba689002302e45e78b8c0df243d93fe27e
+ link_change: replacement
+ a0d74fb7f5bca6aea72cd6899b42ce77135fa30bd556e326b57fbcda776ddd71:
+ missing_count: 1
+ reason: Retained label 'deploy.sh' in docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md,
+ pointing to docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/deploy.sh.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md
+ category: links
+ fingerprint: 2ff59cf9791a4e30802bf76fb870aca744167a9f04af8c6c984524b4f77eb138
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/deploy.sh
+ sha256: 95c380cec713351f9b12d69a74a074b0ccd6d9750f3bbb7092461d8482717748
+ link_change: replacement
+ c2af7f80d42e50d1a7508f93267aa137e51fd3d85f2de2798fe712755a74fb88:
+ missing_count: 1
+ reason: Retained label 'test.py' in docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md,
+ pointing to docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/test.py.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md
+ category: links
+ fingerprint: 0cdbc47a3664256f9b6d771f88c3b967706591ba7d0d270b963cbefad18ad0f0
+ count: 1
+ - kind: file
+ path: docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/test.py
+ sha256: 1b7ec68ad9cc9329b6241d5b00eb679cc43b70902e13f23b2f4eb301c20c51a2
+ link_change: replacement
+ - baseline_path: aks/spot_gpu_kaito_llm.md
+ pages_path: guides/azure-kubernetes-service/spot-h100-kaito/index.md
+ reviewed_changes: {}
+ - baseline_path: aks/workload_identity_databricks_keyless.md
+ pages_path: guides/azure-kubernetes-service/workload-identity-databricks/index.md
+ reviewed_changes:
+ prose:
+ 5309a8209488f01ec9e373f8ba428a4ffe465b883a52f8325b6afbdb959ee275:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/workload-identity-databricks/index.md: The
+ intro, authentication explanation, optional four-layer architecture and references retain
+ the Pod-versus-gateway token-attachment distinction. They now link the official APIM authentication-managed-identity
+ policy. The referenced apim/databricks_keyless_managed_identity.md did not exist in the fixed
+ baseline Git tree; repairing that dangling reference is not deletion of an owned guide. Current
+ prose replacement begins: ''시나리오: AKS Pod → Databricks Foundation Model API (databricks-meta-llama-3-1-8b-instruct)
+ 어떤 PAT/시크릿/Databricks OAuth secret도 사용하지 않고, AKS Wor''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/workload-identity-databricks/index.md
+ category: prose
+ fingerprint: 61960f199bc73556fadf8ac0b3b6592938d9c1ccdd798a5593f94859635bd6f6
+ count: 1
+ 3fbb4c6449456a99caca004fd84f4168c84a0a3e427ec0f4fd26329558455e47:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/workload-identity-databricks/index.md: The
+ intro, authentication explanation, optional four-layer architecture and references retain
+ the Pod-versus-gateway token-attachment distinction. They now link the official APIM authentication-managed-identity
+ policy. The referenced apim/databricks_keyless_managed_identity.md did not exist in the fixed
+ baseline Git tree; repairing that dangling reference is not deletion of an owned guide. Current
+ prose replacement begins: ''인증 모델은 APIM authentication-managed-identity 정책과 동일합니다. APIM에서는
+ 게이트웨이가 토큰을 받아 붙여주지만, 여기서는 앱 코드가 같은 일을 두 줄로 합니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/workload-identity-databricks/index.md
+ category: prose
+ fingerprint: e531423c958685a650322e2013634bf61c5ebe6093979893aa53144e4b33f7b7
+ count: 1
+ 09a9f49ee2c61e7226b2b221eb01146c338d537cb35fc26d005733e080abe893:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/workload-identity-databricks/index.md: The
+ intro, authentication explanation, optional four-layer architecture and references retain
+ the Pod-versus-gateway token-attachment distinction. They now link the official APIM authentication-managed-identity
+ policy. The referenced apim/databricks_keyless_managed_identity.md did not exist in the fixed
+ baseline Git tree; repairing that dangling reference is not deletion of an owned guide. Current
+ prose replacement begins: ''- 필요하면 Pod 앞에 APIM(Internal VNet)을 추가해 Client → AppGW → APIM →
+ 백엔드의 4계층 패턴을 적용할 수 있습니다. 이 경우 인증 부착 책임을 APIM authentication-managed-identity ''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/workload-identity-databricks/index.md
+ category: prose
+ fingerprint: 02a34c9f830a85fe5b8579f1004564f5300f50fbb8c593b67226b91b26b8546e
+ count: 1
+ d502456a7aabecdb82a9f73864fc5352cf8d4d9188deafcae540ca1506f95c48:
+ missing_count: 1
+ reason: 'In docs/services/azure-kubernetes-service/workload-identity-databricks/index.md: The
+ intro, authentication explanation, optional four-layer architecture and references retain
+ the Pod-versus-gateway token-attachment distinction. They now link the official APIM authentication-managed-identity
+ policy. The referenced apim/databricks_keyless_managed_identity.md did not exist in the fixed
+ baseline Git tree; repairing that dangling reference is not deletion of an owned guide. Current
+ prose replacement begins: ''- 게이트웨이 측 keyless 참고: APIM authentication-managed-identity 정책''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/workload-identity-databricks/index.md
+ category: prose
+ fingerprint: 6923c4ffca0b935761c8494001642f0738154b6642aa614c72f2c21018110b07
+ count: 1
+ local_link_labels:
+ f349de5f189630a6caca82e70e49ac4e78f1cb93e6e83ec9aed097c3a577511b:
+ missing_count: 1
+ reason: In docs/services/azure-kubernetes-service/workload-identity-databricks/index.md, the
+ absent-in-baseline APIM guide reference is replaced by 'APIM authentication-managed-identity
+ 정책' at https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy;
+ the same gateway-versus-Pod token responsibility explanation remains.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-kubernetes-service/workload-identity-databricks/index.md
+ category: links
+ fingerprint: 86f50627741859b440339d249723d7025a84740b470e8123c9da64d9f09a01e7
+ count: 4
+ link_change: replacement
+ - baseline_path: appgw/sse_response_buffer_disable.md
+ pages_path: guides/azure-application-gateway/sse-response-buffering/index.md
+ reviewed_changes: {}
+ - baseline_path: appgw/waf_custom_rule_ip_whitelist.md
+ pages_path: guides/azure-application-gateway/waf-path-ip-allowlist/index.md
+ reviewed_changes: {}
+ - baseline_path: architect/request_in_second.md
+ pages_path: guides/azure-architecture/response-time-optimization/index.md
+ reviewed_changes: {}
+ - baseline_path: automation/portal-cli-limitations.md
+ pages_path: guides/azure-automation/portal-cli-limitations/index.md
+ reviewed_changes:
+ prose:
+ 8b131b3a5c426634400f2d55763466f20bbe442683fda9111d15a69b2fdbd061:
+ missing_count: 1
+ reason: 'In docs/services/azure-automation/portal-cli-limitations/index.md: Safety: the 2026-03-08
+ target note retains its date but uses and rg-example-koreacentral-01
+ rather than the operator''s Automation account and resource-group names. Current prose replacement
+ begins: ''작성일: 2026-03-08 대상: Automation Account (RG: rg-example-koreacentral-01)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-automation/portal-cli-limitations/index.md
+ category: prose
+ fingerprint: 87204d0fc37acf995f5644e2e625867373f5c9bb39763118987c3fa51f186f24
+ count: 1
+ - baseline_path: azureblob/mobile_resumable_upload_tus.md
+ pages_path: guides/azure-storage/mobile-resumable-upload-tus/index.md
+ reviewed_changes:
+ prose:
+ 41100ddc8ff1b9f5ec0ffc7c22adb27bd6619ef8371daa24c3cc7a0b1894b9b3:
+ missing_count: 1
+ reason: 'In docs/services/azure-storage/mobile-resumable-upload-tus/index.md: The architecture
+ source/sample labels and both build/test-file cd commands now point to docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application
+ (and its scripts subdirectory). Maven, jar, tus behavior and the 50 MB test-file command are
+ retained. Current prose replacement begins: ''편집 원본: spring-application/architecture.excalidraw''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/index.md
+ category: prose
+ fingerprint: a6aa91987c3f30969d2b3494e1b2b1d3f30a30fcde4b229885a03a38705a9a46
+ count: 1
+ 9b0d2f32c92f7cf4cd02e6a8dbb429fc4bcd9f2d3747c5db07e5aa7877a9f8cb:
+ missing_count: 1
+ reason: 'In docs/services/azure-storage/mobile-resumable-upload-tus/index.md: The architecture
+ source/sample labels and both build/test-file cd commands now point to docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application
+ (and its scripts subdirectory). Maven, jar, tus behavior and the 50 MB test-file command are
+ retained. Current prose replacement begins: ''샘플 코드 위치: spring-application/''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/index.md
+ category: prose
+ fingerprint: daa04b206085e46153c3f3898b9846607cd3b0c40013491c3f8c930587067040
+ count: 1
+ code:
+ 25753c58d20f9fba6bbcce5db5b44878f77b6b8a2feb80744cd858df341c8e4a:
+ missing_count: 1
+ reason: 'In docs/services/azure-storage/mobile-resumable-upload-tus/index.md: The architecture
+ source/sample labels and both build/test-file cd commands now point to docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application
+ (and its scripts subdirectory). Maven, jar, tus behavior and the 50 MB test-file command are
+ retained. Current code replacement begins: ''cd docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application
+ mvn -DskipTests package java -jar target/spring-resuma''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/index.md
+ category: code
+ fingerprint: 81b1659e83ccdb05fda938ed24f98746e4e13a23b49e95db8edfa7d438c53d46
+ count: 1
+ cf497682b5cd127d447220829e8848deb8492c10f1e8bf7e6635164dcd0e8bf0:
+ missing_count: 1
+ reason: 'In docs/services/azure-storage/mobile-resumable-upload-tus/index.md: The architecture
+ source/sample labels and both build/test-file cd commands now point to docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application
+ (and its scripts subdirectory). Maven, jar, tus behavior and the 50 MB test-file command are
+ retained. Current code replacement begins: ''cd docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/scripts
+ ./make-test-file.sh test-50mb.bin 50''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/index.md
+ category: code
+ fingerprint: 53009d6f818a645a09082c607cf9d95f6404f444d260cde8ceea6cd91a4d6747
+ count: 1
+ local_link_labels:
+ 5ed8f73e935ac3d1e2ccd8649c10e09062ba686fa92993b68390bf44f930bfce:
+ missing_count: 1
+ reason: Replaced retired path label with 'spring-application/architecture.excalidraw' in docs/services/azure-storage/mobile-resumable-upload-tus/index.md,
+ pointing to docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/architecture.excalidraw.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/index.md
+ category: links
+ fingerprint: de70232d0400c68a5b691ea4172d24e35b33a9df42b4e0e8661731e5c9cabd4b
+ count: 1
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/architecture.excalidraw
+ sha256: 44032168d2c735c4472cdb05d28b15de2d8d0577cf8c40b86ef645eb3db83ff2
+ link_change: replacement
+ 8029079ca642dbd6a81ba1d4c010793d347d31af7b12942eef74073216953c8f:
+ missing_count: 1
+ reason: Replaced retired path label with 'spring-application/' in docs/services/azure-storage/mobile-resumable-upload-tus/index.md,
+ pointing to docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/index.md
+ category: links
+ fingerprint: c56b8f108c3c3f95c4d9972de881ce0e8ee4ce889c676106951429bde3864131
+ count: 1
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/.gitignore
+ sha256: a46232e6737f10f230bc70c29caa8f510d8eaf14902cb92dccda2ab5969bf330
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/README.md
+ sha256: 05eca1accc833acd567328e6a22fbbb7cb39a050838bfd43d30c84538ed4af7f
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/architecture.excalidraw
+ sha256: 44032168d2c735c4472cdb05d28b15de2d8d0577cf8c40b86ef645eb3db83ff2
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/architecture.png
+ sha256: fac4f65b9a9bca9c22770b835be464d824e1d350496fbb8015ef7890bf70b344
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/pom.xml
+ sha256: 4e8e97b399de9cc0a761b8898a010d71c08155841481f71a050fc0bc7b4a821f
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/sample.yml
+ sha256: 0085dd88156710cacda0569a1dcccf7ec481f3404ae42510dafc1c4349f8b2a7
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/scripts/make-test-file.sh
+ sha256: dcc2519edf53a0a7b878daccfa2245d71b28456b337414837bb65da6c2837145
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/scripts/tus_client.py
+ sha256: 5c7ac76e32634d6b380c6a97cf1434470262cbc307149c87ac13ee56dfccab2e
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/scripts/verify-blob.sh
+ sha256: 2ad00d660c8608059a970e4a747fd148eae688ecd9a99c6b3794e733831c16ff
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/src/main/java/com/example/upload/UploadApplication.java
+ sha256: 811ea45d8cdc38ed7ccf195d61066a5170730bbfbc7a45d9fb1eeee4daf4c7bd
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/src/main/java/com/example/upload/config/BlobConfig.java
+ sha256: 8930ef51e329a44ff97e55ab08bdbb401c3e9de18aea001c7c9a612c81be4620
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/src/main/java/com/example/upload/tus/TusController.java
+ sha256: 3146d39536347bc7191565cc454c670e285d05cbf6f234d27bdf998ea24ee4d0
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/src/main/java/com/example/upload/tus/TusFilter.java
+ sha256: 54af1ddb6c6a352a9755bc239a0cb267fde28cab44678c73eaf4161fc7e933c9
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/src/main/java/com/example/upload/tus/TusUploadService.java
+ sha256: 69c96086d55e83bf5d014bde46917d52e67b658931f2940ea0acf6db334bf66d
+ - kind: file
+ path: docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/src/main/resources/application.yml
+ sha256: dd03e866d03037a3e0da5a4dd75768bc8ff669cfd80e4ec10b1d8a38867ea318
+ link_change: replacement
+ - baseline_path: cosmosdb/nodejs/client.md
+ pages_path: guides/azure-cosmos-db/nodejs-client-optimization/index.md
+ reviewed_changes: {}
+ - baseline_path: cosmosdb/nodejs/point_read.md
+ pages_path: guides/azure-cosmos-db/point-read-optimization/index.md
+ reviewed_changes: {}
+ - baseline_path: cosmosdb/nodejs/query_thread_with_low_core.md
+ pages_path: guides/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md
+ reviewed_changes:
+ code:
+ ffa3cac31015cbd346a0420d939ce942d2414671086d1447b0ababc6bbde0f4f:
+ missing_count: 1
+ reason: 'In docs/services/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md: Safety: the
+ CacheableLookup/http.get example uses cosmos-example-northeurope.documents.azure.com rather
+ than the production Cosmos account hostname. The caching installation, request and response
+ logging remain. Current code replacement begins: "const http = require(''http''); const CacheableLookup
+ = require(''cacheable-lookup''); const cacheable = new CacheableLookup(); cacheable.instal".'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md
+ category: code
+ fingerprint: 5d54b7280b631e6dee37418c44c982b8eaa6244f3036e594da7e1e0ab96a4ec0
+ count: 1
+ - baseline_path: develop/nodejs/fileio_issue_cpu_overhead.md
+ pages_path: guides/application-development/nodejs-file-io-cpu/index.md
+ reviewed_changes: {}
+ - baseline_path: hdinsight/kafka-catchup-sku-fetch-benchmark.md
+ pages_path: research/azure-hdinsight/kafka-catchup-sku-fetch-benchmark/index.md
+ reviewed_changes:
+ prose:
+ 6a7e1e6343d629bc301f15d232a9cdfe9a5be928406dbe6f1a3289a272bc40e6:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md:
+ Safety: the dated 2026-07-16 cluster note, teardown note and deletion fence use hdi-example-japaneast-01
+ and rg-example-japaneast-01. The related-document bullets now name the canonical monitoring
+ overview and prometheus-grafana child. Broker SKUs/counts, fetch-size results, timestamps
+ and teardown history are retained. Current prose replacement begins: ''- Date: 2026-07-16
+ | Region: japaneast | Cluster: hdi-example-japaneast-01 (HDInsight Kafka, broker 3)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: prose
+ fingerprint: 3ffb0ed3985e90026d6fb67f6ec9d56cda63c48a695eff6f5d221a2cb464264d
+ count: 1
+ 4068e7da34b27a234109d61cb752f3009a2cdfdc93541276f753943f6b85a9ac:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md:
+ Safety: the dated 2026-07-16 cluster note, teardown note and deletion fence use hdi-example-japaneast-01
+ and rg-example-japaneast-01. The related-document bullets now name the canonical monitoring
+ overview and prometheus-grafana child. Broker SKUs/counts, fetch-size results, timestamps
+ and teardown history are retained. Current prose replacement begins: ''- Cluster hdi-example-japaneast-01:
+ deleted (D4·D8·D16 각 테스트 후 재생성/삭제).''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: prose
+ fingerprint: b462a5530ca3e3c3cf593c942a163968decb47ee346bfebe69f70adfa8032722
+ count: 1
+ 83dd9a22cda4f857e76f175179141b2d19ede1a412dba47e14fee6dad86b4c2e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md:
+ Safety: the dated 2026-07-16 cluster note, teardown note and deletion fence use hdi-example-japaneast-01
+ and rg-example-japaneast-01. The related-document bullets now name the canonical monitoring
+ overview and prometheus-grafana child. Broker SKUs/counts, fetch-size results, timestamps
+ and teardown history are retained. Current prose replacement begins: ''- HDInsight Kafka 모니터링
+ 방식 비교 — Azure Monitor·Log Analytics·Ambari·진단 설정 개요.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: prose
+ fingerprint: d40a8efc81403209a12054ba7c1800aca8befd84e95ab7a94316ef30f1d4dad3
+ count: 1
+ 78f3c56debcb02da5e0b098c5b12319ed6ae8b47e0d9acbd48e46904bc7a73bf:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md:
+ Safety: the dated 2026-07-16 cluster note, teardown note and deletion fence use hdi-example-japaneast-01
+ and rg-example-japaneast-01. The related-document bullets now name the canonical monitoring
+ overview and prometheus-grafana child. Broker SKUs/counts, fetch-size results, timestamps
+ and teardown history are retained. Current prose replacement begins: ''- Prometheus + Grafana
+ 대시보드 구성 — broker JMX·kafka-exporter(partition별 lag) 대시보드 구성.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: prose
+ fingerprint: 51e59cbc416449dfe0f8df1ef8694e95164831e646b202c6bc7d563f28d604ca
+ count: 1
+ code:
+ e5a3880602f6c5d7fa163ef51ab92a8038f6fa8881fbcb752c05f17e5a977106:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md:
+ Safety: the dated 2026-07-16 cluster note, teardown note and deletion fence use hdi-example-japaneast-01
+ and rg-example-japaneast-01. The related-document bullets now name the canonical monitoring
+ overview and prometheus-grafana child. Broker SKUs/counts, fetch-size results, timestamps
+ and teardown history are retained. Current code replacement begins: ''az hdinsight delete
+ -g rg-example-japaneast-01 -n hdi-example-japaneast-01 --yes # 전체 정리: az group delete -n rg-example-japaneast-01
+ --yes''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: code
+ fingerprint: 5d3c3a32e4a048f553a253de78dd04da1c0a2d7db72aac079f528716d9610bdd
+ count: 1
+ local_link_labels:
+ 6b3c9691462574bae113e8fd71be07ec943e835a621e14bb9c459ae05d5c4df8:
+ missing_count: 1
+ reason: Replaced retired path label with 'HDInsight Kafka 모니터링 방식 비교' in docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: links
+ fingerprint: 8ddca1e54dfe1f211035522e92a5f2839935742a05216e81de1573fb6ab01207
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: title
+ fingerprint: 6b2f2e2b757771a85135aab52ed8f6f23cb7c1f61c01a4792c1b57fd5edd6ad0
+ count: 1
+ link_change: replacement
+ 1a367b94c351d88a8178eab696e123992e8ddec22279845c41be4c9c8d8ba89d:
+ missing_count: 1
+ reason: Replaced retired path label with 'Prometheus + Grafana 대시보드 구성' in docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md
+ category: links
+ fingerprint: 1e82b299fa84f13be524a392050e4c39ceb63a3c555ec16989c0103f4649f913
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: title
+ fingerprint: 8a3b50c0fb8014d577ca4609aeb5e8d1d7b0c2b6b3892893a5124296ca8f5b5b
+ count: 1
+ link_change: replacement
+ - baseline_path: loadtest/loadtest_locust_appgw_aks.md
+ pages_path: guides/azure-load-testing/locust-appgw-aks-private/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/01-memory-taxonomy.md
+ pages_path: research/microsoft-foundry/agent-memory-taxonomy/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/02-architecture-patterns.md
+ pages_path: research/microsoft-foundry/agent-memory-architecture-patterns/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/03-pipeline-and-retrieval.md
+ pages_path: research/microsoft-foundry/agent-memory-pipeline-retrieval/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/04-frameworks.md
+ pages_path: research/microsoft-foundry/agent-memory-frameworks/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/05-production-evaluation.md
+ pages_path: research/microsoft-foundry/agent-memory-production-evaluation/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/06-commerce-application.md
+ pages_path: research/microsoft-foundry/agent-memory-commerce/index.md
+ reviewed_changes: {}
+ - baseline_path: memory/agent-memory/README.md
+ pages_path: research/microsoft-foundry/agent-memory-overview/index.md
+ reviewed_changes:
+ headings:
+ 835522e41b3ff8bcf45c9484a90e83e402d787f26f4a75af836ed16205962cad:
+ missing_count: 1
+ reason: Restored in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "Historical reader map (pre-Pages)" with the complete six-row audience map; the canonical
+ 목적별 빠른 경로 section links to this historical evidence.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: headings
+ fingerprint: 835522e41b3ff8bcf45c9484a90e83e402d787f26f4a75af836ed16205962cad
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ a616717ab9bb843dd1e9b29a72135382fdf79033698f9646baced38daffb9241:
+ missing_count: 1
+ reason: Renamed to section "목적별 빠른 경로" in docs/services/microsoft-foundry/agent-memory/index.md;
+ the time-limited, design-first and implementation-first reading paths are unchanged.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: headings
+ fingerprint: 953992b9d73a7826c86badf503e96b07aca4be44c50e5887573f5004e4581e8c
+ count: 1
+ tables:
+ 6e38615f1cdfdd39f0aea0030a93f746e1c1578c803a4b72debcacc90d244c25:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 6e38615f1cdfdd39f0aea0030a93f746e1c1578c803a4b72debcacc90d244c25
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ 93c27befa80b335ea9d8d8d70ca85310dd3cb26f8007eaa75464a25f6c99454d:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 93c27befa80b335ea9d8d8d70ca85310dd3cb26f8007eaa75464a25f6c99454d
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ bb310d837851c9f8194c24e845aa8165eea897ab59daaf49b7932e491fb38c51:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: bb310d837851c9f8194c24e845aa8165eea897ab59daaf49b7932e491fb38c51
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ 2013c3ede52cd3f5bbc066a7c88e6c3ae08e0ab49008c0a7a0c39f95085f7708:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 2013c3ede52cd3f5bbc066a7c88e6c3ae08e0ab49008c0a7a0c39f95085f7708
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ 1210072435224aaf485a3771382f25c40414924bee56946ae7235373bc6a64fa:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 1210072435224aaf485a3771382f25c40414924bee56946ae7235373bc6a64fa
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ 2965b3b159e353b6284373789503745ad90499bcd8b641e6c6fc66b39214408f:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 2965b3b159e353b6284373789503745ad90499bcd8b641e6c6fc66b39214408f
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ b161a4f98ec2067c6e78b388a7631bb35b87ca64254f845d5246223793bd7257:
+ missing_count: 1
+ reason: Restored exact header/data row in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성" beneath the historical-map disclaimer, with canonical child links. Generated
+ topic navigation had omitted these detailed reading objectives and audience assignments.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: b161a4f98ec2067c6e78b388a7631bb35b87ca64254f845d5246223793bd7257
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ local_link_labels:
+ 6e0523469bc57a9e724a9b332a11bec54fd18dfaebc9b1d7609bef38f3421e6a:
+ missing_count: 1
+ reason: Restored label '01. 메모리 분류 체계' in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성", linking to docs/services/microsoft-foundry/agent-memory/taxonomy/index.md.
+ The original detailed audience map remains explicitly historical and is linked from the canonical
+ guide.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: links
+ fingerprint: 80be23731d6f5106ae82a486c59fa81af54a9754492ddf0d5de0c3602c10d988
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 93c27befa80b335ea9d8d8d70ca85310dd3cb26f8007eaa75464a25f6c99454d
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ link_change: replacement
+ 8d4cbf9cdc1e35d919f0900882f792fa23f92052eac2647f749a6d41c54e9045:
+ missing_count: 1
+ reason: Restored label '02. 아키텍처 패턴' in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성", linking to docs/services/microsoft-foundry/agent-memory/architecture-patterns/index.md.
+ The original detailed audience map remains explicitly historical and is linked from the canonical
+ guide.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: links
+ fingerprint: a30091cbd5bcfc7d0483cff3569a326722afe60740f3b0d44a24164543d62522
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: bb310d837851c9f8194c24e845aa8165eea897ab59daaf49b7932e491fb38c51
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ link_change: replacement
+ ac95bcf8bf57be7eb83236b143401db16a4a998d6b5e689ad646a8774e57f5c3:
+ missing_count: 1
+ reason: Restored label '04. 프레임워크 비교' in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성", linking to docs/services/microsoft-foundry/agent-memory/frameworks/index.md.
+ The original detailed audience map remains explicitly historical and is linked from the canonical
+ guide.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: links
+ fingerprint: 5a32bb677cf4631b3c9d6db4bc0aa2ad78db73af574523e83731eff823d975f0
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 1210072435224aaf485a3771382f25c40414924bee56946ae7235373bc6a64fa
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ link_change: replacement
+ 10d1e473e9ebe8c05b3c51bb0150eb66e3c7f8e4318b549c32577c2d208c4099:
+ missing_count: 1
+ reason: Restored label '05. 프로덕션과 평가' in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성", linking to docs/services/microsoft-foundry/agent-memory/production-evaluation/index.md.
+ The original detailed audience map remains explicitly historical and is linked from the canonical
+ guide.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: links
+ fingerprint: 8fb0a588984a83f0caec2b9edf9f597731d6e3ea793bfc90311158349a91417f
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 2965b3b159e353b6284373789503745ad90499bcd8b641e6c6fc66b39214408f
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ link_change: replacement
+ ee708ebb80e2486837892f2629a6ab654c815a44e5216e406b5d9894e1044571:
+ missing_count: 1
+ reason: Retained label 'images/01-memory-taxonomy.svg' in docs/services/microsoft-foundry/agent-memory/index.md,
+ pointing to docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/01-memory-taxonomy.svg.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: links
+ fingerprint: fa308a97bc47d4f5fdd91454ca04e978d5ddac8059a2f343e153e7cc2985e90b
+ count: 1
+ - kind: file
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/01-memory-taxonomy.svg
+ sha256: 05597176b2cddfae85b1db975a76d9a55b294256e08cbad2cd3041557494369c
+ link_change: replacement
+ 11a1f30b2750cc2cacdf6a469f4db322fba63e0a01c49f097bb0003a0b7777ee:
+ missing_count: 1
+ reason: Retained label 'images/02-architecture-patterns.svg' in docs/services/microsoft-foundry/agent-memory/index.md,
+ pointing to docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/02-architecture-patterns.svg.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: links
+ fingerprint: 90ce78c8160d909ce47656723738b91b67fb1de6e2bcb554c59c6bef2f9667a6
+ count: 1
+ - kind: file
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/02-architecture-patterns.svg
+ sha256: 3d956a3c082a022066c0a728ca71cfee06a5836b4000d7684e8675e26c2bce84
+ link_change: replacement
+ db83ec5f48f147e309ada3539b4cb341f1eac05606f8e063f97c4b3528e26a5c:
+ missing_count: 1
+ reason: Retained label 'images/03-memory-pipeline.svg' in docs/services/microsoft-foundry/agent-memory/index.md,
+ pointing to docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/03-memory-pipeline.svg.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: links
+ fingerprint: 27ddce3afc1f81877ec8b3bdfa019208701e99f45b5ee924deaa5cc22433c2c0
+ count: 1
+ - kind: file
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/03-memory-pipeline.svg
+ sha256: 1588cb6240cbeba2f41b9725783b48fb8f5a1b87f22f1cfd6bc2e4e54631fbb1
+ link_change: replacement
+ 9e5f8f51ef96fa84bc4552892a69cdfc89d8f94a5ef1a822ceabeddf02e35464:
+ missing_count: 1
+ reason: Retained label 'images/04-production-tiers.svg' in docs/services/microsoft-foundry/agent-memory/index.md,
+ pointing to docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/04-production-tiers.svg.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: links
+ fingerprint: ed33b4ee6186170ae96f4db8ceb5c4d532d929401cec15d45786ee8b2b500ad0
+ count: 1
+ - kind: file
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/04-production-tiers.svg
+ sha256: f1e42cc6e3987923e8c849f3228edad7d5cce8b7ccd80d5e091b39f1e3d3aafb
+ link_change: replacement
+ fe2b3e96ca22941642e539d494b3861481ebfc8abc5a54ec0a1b5705f21b25b9:
+ missing_count: 1
+ reason: Retained label 'images/05-commerce-blueprint.svg' in docs/services/microsoft-foundry/agent-memory/index.md,
+ pointing to docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/05-commerce-blueprint.svg.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: links
+ fingerprint: 6967936c08a78ed521c48b58fabe0e4428633a2a9609fcec2093aceb4438b829
+ count: 1
+ - kind: file
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/images/05-commerce-blueprint.svg
+ sha256: ad6cda6b2f207d1ac7d5f2ee19bb8e53ee5d292942c29967d14b271a0b372ab6
+ link_change: replacement
+ e545fbfd8a6db9a606f96c20118846de2fb6ce95bd45a6a6c9707adb282da640:
+ missing_count: 1
+ reason: Restored label '06. 커머스 적용 설계' in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성", linking to docs/services/microsoft-foundry/agent-memory/commerce/index.md.
+ The original detailed audience map remains explicitly historical and is linked from the canonical
+ guide.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: links
+ fingerprint: 5fc32c1aecb7e3275f4b65cec68318a8dd4b646971359583a75729b5588fe1d3
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: b161a4f98ec2067c6e78b388a7631bb35b87ca64254f845d5246223793bd7257
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ link_change: replacement
+ a63a178680757909d7b1cb6ad4e67e89420ea1882685f828f240218517493cb2:
+ missing_count: 1
+ reason: Restored label '03. 파이프라인과 검색' in docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ section "문서 구성", linking to docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/index.md.
+ The original detailed audience map remains explicitly historical and is linked from the canonical
+ guide.
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: links
+ fingerprint: 6edd451eb506f0b64755775b34f52e3f84b51ddc9d5d3082fbd5325022fa4ded
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ category: tables
+ fingerprint: 2013c3ede52cd3f5bbc066a7c88e6c3ae08e0ab49008c0a7a0c39f95085f7708
+ count: 1
+ - kind: structure
+ path: docs/services/microsoft-foundry/agent-memory/index.md
+ category: prose
+ fingerprint: 56058f5a83670fabf5c4bea061931702d3a582c44b1ad70c43f9008dd7956a01
+ count: 1
+ link_change: replacement
+ - baseline_path: memory/foundry-gpt-memory-v1.md
+ pages_path: guides/microsoft-foundry/gpt-memory-layer/index.md
+ reviewed_changes:
+ prose:
+ ec8de6ca2bb54061c8ece964f0c28859efdb393ce95b035616d56bb021e61ae3:
+ missing_count: 1
+ reason: 'In docs/services/microsoft-foundry/gpt-memory-layer/index.md: Safety: the opening verification-complete
+ assertion and dated closing assertion were replaced with an explicit semantic-verification-needed
+ notice. The historical 2026-07-22 update date and architecture content remain; the current
+ front matter is needs-review, so the old unsupported verification claim must not be restored.
+ Current prose replacement begins: ''이관된 원문입니다. 공식 Microsoft Learn 문서와의 의미 검증이 필요합니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/gpt-memory-layer/index.md
+ category: prose
+ fingerprint: 52fe1e2fda546c026381b09925e9e24639fc02328832674336e59f13b20dc033
+ count: 1
+ 78dd2dad9b02afaa7f3308466ce27391076bda54ee2f32dd0e0c28e911c5a0f1:
+ missing_count: 1
+ reason: 'In docs/services/microsoft-foundry/gpt-memory-layer/index.md: Safety: the opening verification-complete
+ assertion and dated closing assertion were replaced with an explicit semantic-verification-needed
+ notice. The historical 2026-07-22 update date and architecture content remain; the current
+ front matter is needs-review, so the old unsupported verification claim must not be restored.
+ Current prose replacement begins: ''마지막 업데이트: 2026년 7월 22일 — 공식 원문 의미 검증 대기''.'
+ evidence:
+ - kind: structure
+ path: docs/services/microsoft-foundry/gpt-memory-layer/index.md
+ category: prose
+ fingerprint: 9a723f48138487e2c68e1428681f27ab5875c5a431fa75f64f5209d20e17c156
+ count: 1
+ - baseline_path: monitor/azure-monitor-dynamic-thresholds-brief.md
+ pages_path: research/azure-monitor/dynamic-thresholds-brief/index.md
+ reviewed_changes: {}
+ - baseline_path: monitor/azure-sre-agent.md
+ pages_path: guides/azure-monitor/azure-sre-agent-overview/index.md
+ reviewed_changes:
+ local_link_labels:
+ c1ca913091c95d35a8482f0216524d86a34dd4070a62f9ca3ec7a89bef6abd81:
+ missing_count: 2
+ reason: Retained label '편집 가능한 SVG 보기' in docs/services/azure-monitor/azure-sre-agent/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/briefing/sre-agent-process.svg;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/briefing/s1-three-panel.svg.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/index.md
+ category: links
+ fingerprint: 4d2b5788682307ef4e79f9376844980ab02f473f10c0a6a52b00feebd0887843
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/index.md
+ category: links
+ fingerprint: dac9a181e89f1fc63ca186bd97c8756b502704db3e9889130c78547395bc2e73
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/briefing/sre-agent-process.svg
+ sha256: 51f68bd5daecfafaa47702f98c6753775fee7f2deb4e772f65b6d86f161f0095
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/briefing/s1-three-panel.svg
+ sha256: f8ab268f0999ddb6727b62ab999c05f6686d73f9f48d7db471cf9a18ea2a95f3
+ link_change: replacement
+ 6acfc255c5e8f3e2260323007f2f377c059f04a4cf27090d45f826a526d4d4f7:
+ missing_count: 1
+ reason: Retained label 'Issue 본문 보기' in docs/services/azure-monitor/azure-sre-agent/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/notifications/s1-github-issue.md.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/index.md
+ category: links
+ fingerprint: 4f915ee74e0c5d92bcfe00bc89efc4ee5f08d86d8235c9584c41d3895181adfa
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/notifications/s1-github-issue.md
+ sha256: 6f25ab7c79a3950ccfbb0ca34a8f69e6e10855c350f940ef6e1286b05b143f1e
+ link_change: replacement
+ 875bedfdbc15c549d09155e977dc244e9e99297dbf14c1ac75dee7f9ec774840:
+ missing_count: 1
+ reason: Retained label 'HTML 메일 보기' in docs/services/azure-monitor/azure-sre-agent/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/notifications/s1-incident-summary.html.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/index.md
+ category: links
+ fingerprint: e81041adc637938c05e5b77bc29187c0e2abd28184f177eeca62fce6c1ad35a5
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/notifications/s1-incident-summary.html
+ sha256: c025993ddafaacaaa12bc956e20a036ac1046ecd052a3de4aa5ab72549f66b90
+ link_change: replacement
+ 6d2ba3851d9f7930b9b842b58ab1c5a5124455736915e1a215b0f3aa3b10101d:
+ missing_count: 1
+ reason: Retained label 'RFC 5322 메일 파일 보기' in docs/services/azure-monitor/azure-sre-agent/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/notifications/s1-incident-summary.eml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/index.md
+ category: links
+ fingerprint: b5c7e0ca67c4a53034302976378b025cac46403983a023a96e427c196e1f54e6
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/notifications/s1-incident-summary.eml
+ sha256: 625a4cfad60d508c243ef361bc9bafb30391651c23dd9478ec0de98f34d2a2f6
+ link_change: replacement
+ - baseline_path: monitor/hdinsight-kafka-monitoring.md
+ pages_path: research/azure-monitor/hdinsight-kafka-monitoring-options/index.md
+ reviewed_changes:
+ prose:
+ 571ffe34f1b3d57af92b1ccf0de6e92a2589f0e24650bde126c8265d737dec0e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md: The three installation/dashboard
+ references now name Prometheus + Grafana 구성 가이드 at docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md.
+ Safety: the closing measured-environment note uses rg-example-japaneast-01 and hdi-example-japaneast-01,
+ retaining the scoped historical observation rather than making a new general product claim.
+ Current prose replacement begins: ''- 설치·대시보드 구성: Prometheus + Grafana 구성 가이드''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: prose
+ fingerprint: f219640494edde49577f87dd0634e045144c16f506a4a35d479c7755068a3f87
+ count: 1
+ 8867b2b135aa92de4daaf8b97edbfbca3de5f620a9d4afb56b1e01a8caade5e0:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md: The three installation/dashboard
+ references now name Prometheus + Grafana 구성 가이드 at docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md.
+ Safety: the closing measured-environment note uses rg-example-japaneast-01 and hdi-example-japaneast-01,
+ retaining the scoped historical observation rather than making a new general product claim.
+ Current prose replacement begins: ''HDInsight 기본 미제공 → 수동 설치. 둘 다 Prometheus 스크래핑 + Grafana
+ 시각화 조합. 설치·대시보드: Prometheus + Grafana 구성 가이드''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: prose
+ fingerprint: 6b0c69440e3985638be2b8a7660238ff8461cafabceb1d2d71ca8ba5bab0d8d0
+ count: 1
+ 7843f6e01e3ff68bb42fbe436f2c615c652654b170f65926fe856e0cd6e2779b:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md: The three installation/dashboard
+ references now name Prometheus + Grafana 구성 가이드 at docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md.
+ Safety: the closing measured-environment note uses rg-example-japaneast-01 and hdi-example-japaneast-01,
+ retaining the scoped historical observation rather than making a new general product claim.
+ Current prose replacement begins: ''권장 목표 상태: Monitor Integration으로 노드/클러스터 baseline 확보 +
+ 브로커/토픽 상세·JVM internals·Consumer Lag은 JMX/Kafka Exporter로 관측. → 설치·대시보드: Prometheus + ''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: prose
+ fingerprint: bf148487446161682e0bb6c28084af2b4035ba84be7f83ff237fcfa85f695cf2
+ count: 1
+ 4f6d715673a43b9318d459439569bad301fbf80e4a9736a4a160e68e4dfa2982:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md: The three installation/dashboard
+ references now name Prometheus + Grafana 구성 가이드 at docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md.
+ Safety: the closing measured-environment note uses rg-example-japaneast-01 and hdi-example-japaneast-01,
+ retaining the scoped historical observation rather than making a new general product claim.
+ Current prose replacement begins: ''이 문서는 rg-example-japaneast-01 배포(hdi-example-japaneast-01)
+ 기준 실측 기록. 리소스명·IP는 환경에 맞게 치환.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: prose
+ fingerprint: 83ecd5eaa15e1a5d922441e31a87dd7dc47e5c341bc6a8072002ebe204e3981d
+ count: 1
+ local_link_labels:
+ 79baf84e4c913a8549739c6081e3a9cf1b0d9c1acc04ba9480129bd96358e6d9:
+ missing_count: 3
+ reason: Replaced retired path label with 'Prometheus + Grafana 구성 가이드' in docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: links
+ fingerprint: 334cae235e554d53042ccca07b4651ce1cb8181940ea606511e22a4e17a780dc
+ count: 3
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: title
+ fingerprint: 8a3b50c0fb8014d577ca4609aeb5e8d1d7b0c2b6b3892893a5124296ca8f5b5b
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/hdinsight-kafka-prometheus-grafana.md
+ pages_path: guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md
+ reviewed_changes:
+ prose:
+ bd7b9c198dc08d1410385e42f06657cf6be04bff2f73972e56064ad96f5b3b64:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md:
+ The Why reference now names the canonical monitoring overview. The dashboard image instructions
+ distinguish bundle images from original sources at docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images.
+ Exporter rules, Prometheus configuration and Grafana dashboard source files remain in that
+ owning sample. Current prose replacement begins: ''전제: HDInsight Kafka 클러스터 + Prometheus +
+ Grafana 기구성. 도구별 커버 범위·선택 근거("Why") → HDInsight Kafka 모니터링 방식 비교.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: prose
+ fingerprint: 4e42c4c7755af764dbe9ce95dd416c3f5648a697fcb6d133a72714b41984aa5c
+ count: 1
+ ed4fb9a1ee16fecca8e6bcc2f346062915290e65a9f2f82f9512af21bcea8a24:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md:
+ The Why reference now names the canonical monitoring overview. The dashboard image instructions
+ distinguish bundle images from original sources at docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images.
+ Exporter rules, Prometheus configuration and Grafana dashboard source files remain in that
+ owning sample. Current prose replacement begins: ''렌더링 이미지는 이 문서 번들의 images/ 디렉터리에 아래 파일명으로
+ 두면 자동 표시됩니다. 원본 자료는 samples/monitoring-assets/images/에서 확인할 수 있습니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: prose
+ fingerprint: 4170b203da21be0358e01189a49fd4f678801580607a335931505606d0072672
+ count: 1
+ local_link_labels:
+ 9e702f459d67c67952e1ebd636ab6a69f895283b704f440bab1613f3f09c09a4:
+ missing_count: 1
+ reason: Replaced retired path label with 'HDInsight Kafka 모니터링 방식 비교' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 8ddca1e54dfe1f211035522e92a5f2839935742a05216e81de1573fb6ab01207
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md
+ category: title
+ fingerprint: 6b2f2e2b757771a85135aab52ed8f6f23cb7c1f61c01a4792c1b57fd5edd6ad0
+ count: 1
+ link_change: replacement
+ 4cbc12caed5b287e12ad58a40e0bffb7faca665597ab4ee1c45211467bb82ca2:
+ missing_count: 1
+ reason: Retained label 'assets/prometheus-grafana/' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 57849b2e19d78098e4c9471a851dbd89cec4e34534366bb8e3ffcc8ab09cafc8
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/docker-compose.yml
+ sha256: e6486b8cd25c41b7a5b38004e2d9a680c9599919158857499d61a215310dfeb3
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json
+ sha256: 15bda4477eb873e7e333b75265e75c9985c54bbb03324efe8c4ecc08f4f01d92
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json
+ sha256: 2863a6a527be917245bf1af98e0bdf3bd4620ded6a58ef7a3dba24279406ef0d
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml
+ sha256: 85c52f125f444030ffacf8f2be3854dc6c69e50f9e35327720aa2332e3071d0e
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml
+ sha256: 19d98cca02290926efd3e8b622d97213f3c19ed9b561898225d8e149ff48d9d6
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/jmx/b0.yml.example
+ sha256: 8d9a12fa3dcbc0661b1512125d8fcb63fe16d27e1839edc723f886e8f8f6ba53
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/jmx/kafka-rules.yml
+ sha256: 5e080b0afde650f3c8a619f5da3c1045cfa427bb6250125560158fa8f170370b
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/prometheus.yml
+ sha256: b32f4f5111347a0f0f897ecb795cdcff69ccb563ceee3080f90a71aeec52bebd
+ link_change: replacement
+ 8caa2a204eec50db21c591280d846fac7d80f202c6a829dd8b3843700d5f969f:
+ missing_count: 1
+ reason: Retained label 'jmx/kafka-rules.yml' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/jmx/kafka-rules.yml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 67085c6c9b35338b960ea8da39b354d53a888061b20c61cfdd8efd26f95e0b9e
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/jmx/kafka-rules.yml
+ sha256: 5e080b0afde650f3c8a619f5da3c1045cfa427bb6250125560158fa8f170370b
+ link_change: replacement
+ 656c7a584417e11b56b29c13f30fb5ca9620bb81643a4e4ba550f196afd4e658:
+ missing_count: 1
+ reason: Retained label 'jmx/b0.yml.example' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/jmx/b0.yml.example.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 68af65bffa2855679a0f4313a8fb30a4cb87938a08813988d8da562101462455
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/jmx/b0.yml.example
+ sha256: 8d9a12fa3dcbc0661b1512125d8fcb63fe16d27e1839edc723f886e8f8f6ba53
+ link_change: replacement
+ cc9c87015e90fe008b18f1d9853e54ba7d7b1eb21c6a9971039128ade73a1891:
+ missing_count: 1
+ reason: Retained label 'docker-compose.yml' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/docker-compose.yml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 934e672e98ea796373e4840f2c259a0a954e4539c88a560ebe86c50feafe2487
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/docker-compose.yml
+ sha256: e6486b8cd25c41b7a5b38004e2d9a680c9599919158857499d61a215310dfeb3
+ link_change: replacement
+ 50cb58547b9a5b500c46f975848fcb4e22aef66b2c517dc81ba944f5794afc09:
+ missing_count: 1
+ reason: Retained label 'prometheus.yml' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/prometheus.yml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: d6789f4efbf0446e96ab1b927e394061c31c502ebf9470c565fe78686b072bd4
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/prometheus.yml
+ sha256: b32f4f5111347a0f0f897ecb795cdcff69ccb563ceee3080f90a71aeec52bebd
+ link_change: replacement
+ d8b0873d32bd4f22164ceaffdf91a9df0788a4195931f31b3c8c4f8aa9180b86:
+ missing_count: 1
+ reason: Retained label 'grafana/provisioning/datasources/prometheus.yml' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 833581539edf8fe1f89df640c0843204d485c12b4ae66204b76e8d1db871bb98
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/provisioning/datasources/prometheus.yml
+ sha256: 19d98cca02290926efd3e8b622d97213f3c19ed9b561898225d8e149ff48d9d6
+ link_change: replacement
+ 2f9d7fc6e302cf2d64c80a3af3ad49d6363a4073da7278794d0212cdc69c2171:
+ missing_count: 1
+ reason: Retained label 'grafana/provisioning/dashboards/provider.yml' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 8ce82e2bd7413d3daf2b2a388097d3d745402730a4e9168abddefa8380f6dd91
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/provisioning/dashboards/provider.yml
+ sha256: 85c52f125f444030ffacf8f2be3854dc6c69e50f9e35327720aa2332e3071d0e
+ link_change: replacement
+ 4e30c0a736648cdbf3724c22290d2ce4d6e6467ffb8ec18015360f91aa22c5b2:
+ missing_count: 1
+ reason: Retained label 'kafka-exporter-adv.json' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: ca9d5e70332e1b46bb72e2bc531049e47b82882d9cc649af0b9da0428cf77515
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/dashboards/kafka-exporter-adv.json
+ sha256: 15bda4477eb873e7e333b75265e75c9985c54bbb03324efe8c4ecc08f4f01d92
+ link_change: replacement
+ e27b1fb145ebd9254aa8762cf881afc092d7a1388639715e48de4b6b0a492615:
+ missing_count: 1
+ reason: Retained label 'kafka-jmx-broker.json' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 29bedea51e83c8e0b273258ca32fe0ee63ed1b066055f20fe4a8dd9795556a21
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/prometheus-grafana/grafana/dashboards/kafka-jmx-broker.json
+ sha256: 2863a6a527be917245bf1af98e0bdf3bd4620ded6a58ef7a3dba24279406ef0d
+ link_change: replacement
+ 9e77ace5a4ee3cf33c393827cfda9dc6a2dfa57024e91cfda8a73e5919c8c10c:
+ missing_count: 1
+ reason: Replaced retired path label with 'samples/monitoring-assets/images/' in docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md,
+ pointing to docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md
+ category: links
+ fingerprint: 7ca66bd21fd07e0ea1b4cb450c8cc19a6b42df46b255f6a9101d49cb2fa5e5ec
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images/hdinsight_monitor_integration_1.png
+ sha256: 2024f2b0f661865f11f3f7d8c6478a3e7dd7609d8529b8382d833705c8e3d065
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images/kafka-exporter-adv.png
+ sha256: 7ff379a7efb9fa79864deb94d405138a07aae3197dfefcc070a3ba041db5a209
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images/kafka-exporter-overview.png
+ sha256: d75968193a69d62cd577cc1d55af6e75c1139cf9621b56fe08b48014016d4533
+ - kind: file
+ path: docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/images/kafka-jmx-broker.png
+ sha256: aa4b75569d6ed9b26dbb4475e3f3729b56b6e79cbae21e37182040a5e2325240
+ link_change: replacement
+ - baseline_path: monitor/otel-monitor-setup.md
+ pages_path: guides/azure-monitor/aks-private-opentelemetry/index.md
+ reviewed_changes:
+ prose:
+ c1a9ea48124d32711ff584f255753d47c4f12ffbe17ad6148c69e05e72afb1bd:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''1. 이름: ampls-example-krc-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: bbc574c89ca549a6062390468e8adfc409470634a1ff93ca6fad57ea22e3d910
+ count: 1
+ 5d1c0fcbfc31548179d5d0756ae3fb3974d997bf4df292a161ab641dd7c85e03:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''- MSProm-koreacentral-aks-example-koreacentral (RG: rg-example-koreacentral-01)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: c1f2483ebd9388bbaf330b59e957cb66a504b955e3339b66d3a44de3b823cced
+ count: 1
+ 8fbeff86074094523c052738ea0e6e18842511c0ea484c8d322d06c2f94f2feb:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''- azuremonitor-workspace-example-krc-01 (RG: MA_azuremonitor-workspace-example-krc-01_koreacentral_managed)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: c0451df8ad1cfd13db5ccb6fb5934f94b6f0ca7f2db132b69da63df74542e457
+ count: 1
+ ad04f0f2877e6e144327ec3be3e5105db452638ca45e94fdd3adc3d96c3615be:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''- 이름: pe-ampls-example-krc-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: 0fb25eca1ab807fbf74b0e385aa1d17fbc5a7fb3d633a84cf1de9bd88ff602c0
+ count: 1
+ ee3ef39d4d43df6bf7c946a70c01af30c9f3ed941567964fc003decfa83d93a1:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''1. Prometheus DCR 선택 (예: MSProm-koreacentral-aks-example-koreacentral-01)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: b155cc6748cd008ba20cbd36d902c1bddd67ac433c79b4d6096f66fc7e08e64a
+ count: 1
+ f998153bb254b703c5a1fb950d62e9d8315c1952cbb5d95e5a7f9846303212f4:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''기존 VNet(vnet-example-koreacentral-01)에 연결되어 있던 모든 Private DNS Zone 링크를 PRD VNet으로
+ 이전.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: 171f3f27014b606e8fe143a42696a97b44f7597e61e63dee231da1c17970805c
+ count: 1
+ d071edd197f1c6d03c88e8491ddb4280af4830a85448b1388e50296b6028bc31:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current prose replacement
+ begins: ''- Private Endpoint(pe-ampls-example-krc-01)는 기존 VNet의 pe-subnet에 그대로 위치''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: prose
+ fingerprint: 808b1302a9e4eeae104b8a710f78ea83a68fac2c0155c755f489722efc1909d0
+ count: 1
+ code:
+ 737daa04ea7fec43ce993922f36de08c0acfb5c59b90f06c95a6abd74b2fe37a:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current code replacement
+ begins: ''flowchart TB subgraph AKS["AKS Private Cluster"] subgraph APP_NS["Application Namespace"]
+ APP["Application Pod (OTLP SDK)"] end subgraph''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: code
+ fingerprint: 4ef1082f7cf220128cc308de31a66af20ff5da00c2409d7f9f039ae91b3590f3
+ count: 1
+ 4102e9632a8cf0e48ab0caeb6f23a0402430bbb15093989dc8154244e0158a3a:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current code replacement
+ begins: ''# AKS 파드 내부에서 nslookup msprom-koreacentral-aks-example-koreacentral-ruja.koreacentral-1.metrics.ingest.monitor.azure.com
+ # → 10.2.0.6 (Priva''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: code
+ fingerprint: a400fadc841f3d5ed9bafe714adab7fdb56b965987d3f22bf647120d8cbc6bbb
+ count: 1
+ 1e19db832dd173940587309e96ea10d33c9bab0961486660f2a096ec2a9025b4:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current code replacement
+ begins: ''az rest --method put \\ --url "/subscriptions//resourceGroups/rg-example-koreacentral-01/providers/Microsoft.ContainerServic''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: code
+ fingerprint: 855602bc880e0789d36297b99eb04a465691cff9b72e5131b9f575164fcae21e
+ count: 1
+ 1429b56f03ae454fc3a1269d43d36e7ec78701d174e706a4b5990006273e61e0:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current code replacement
+ begins: ''az rest --method get \\ --url "/subscriptions//resourceGroups/rg-example-koreacentral-01/providers/Microsoft.ContainerServic''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: code
+ fingerprint: 9ecf660a261d19a3d6b376a7104f0c354579fea4fe1a95c67129bc762889ebea
+ count: 1
+ c5ad99e6336f4dc7dfb1f317452800240fc429c588e2bb3f5e6b90d3e22e8b9b:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current code replacement
+ begins: ''flowchart TB VM["VM / AKS Pod (vnet-example-koreacentral-01)"] -->|"DNS Query"|
+ RESOLVER["systemd-resolved 127.0.0.53"] RESOLVER -->''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: code
+ fingerprint: 0f097ffb50dc9497206efc249fbb8b7de2275d1afe55d8364942dac2c4cfa607
+ count: 1
+ 4cc41bf5a9cd2b01eaefbd95bd337c47e7dbec1543ff325a21fdb7ab4eda69b3:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current code replacement
+ begins: ''# PRD VNet에 링크 추가 az network private-dns link vnet create \\ --name "link-to-prd-vnet"
+ \\ --zone-name "" \\ --resource-group ". Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''AKS 클러스터 | aks-example-koreacentral-01 (Private Cluster)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 2ca987a137329910594865b821e33e59f1f6bc0e60349d3dfabca7f89ada06a3
+ count: 1
+ bfd684c367e972b3460eb62a456f363c6bdbc785c002e59488aebda2284ba4a5:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''리소스 그룹 | rg-example-koreacentral-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 03128153a43f623a31a59f280f5cbb3c205d2056ee089aa5f7bf2072a8312bb6
+ count: 1
+ 94c21b10dbe667b9f47b937912e7d995de72f5526d5e6c2802e25ad697f8dcce:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''PRD 리소스 그룹 | rg-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 41e982c6bcd601d258af47ac038aafe14fca72e4063766b169953c4374da97bd
+ count: 1
+ 995158c790ba9fe3ee7f8a3232d66e14a647bffa46e7b8a5c6b7f20f1c746c72:
+ missing_count: 1
+ reason: 'Safety: the historical subscription GUID is replaced by the exact row 구독 |
+ in docs/services/azure-monitor/aks-private-opentelemetry/index.md section "환경 개요". The approval
+ binds that subscription row, not the unrelated Public IP row; the surrounding topology is
+ retained.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: d09ce819b129bc823f5842be992fb00a2f5737a81342aecd189468103d722a28
+ count: 1
+ baacdbdcd3a0b86ce62dc3ecdaecb62c91b4f07f4f4db8154a961e75134e8286:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''VNet (기존) | vnet-example-koreacentral-01 (10.0.0.0/10)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 481b907c8e8d5cf8e0b74ee4daae49deb7e69e0d968e197a2d9902be21feb385
+ count: 1
+ 60afb1c68779b99db7b65dff72dde0a4b3e66377f7579ac6e2a29b0d54a644ab:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''VNet (PRD) | vnet-example-koreacentral-prd-01 (10.100.0.0/16)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: dabac1b4ff5bc5220e08b45697a9c4c2009cb6ca26c7682c6fffbead8615d18a
+ count: 1
+ ed16ab3e60e9fb812d6e89188b1346f14bc8c49a551d0665ff6312d60e05d336:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''Azure Firewall | fw-example-koreacentral-prd-01 (Private IP: 10.100.0.4)''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 985dac84a65e64e9d1ba154b09f95b72ef2fb3215567e572f001f1c389225e46
+ count: 1
+ ddcefb2d91cc9d847a48657cdd22c75238df1e700e3957df9e7f7b0157e1ed7d:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''Azure Monitor Workspace | azuremonitor-workspace-example-krc-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 1887b1c0552d41df29a3c06b7d6107d7baca63687426cb48320fcf0d2e43b2b8
+ count: 1
+ 39224c38099b50425f28ad69fbe812a72684f6acb879bbbea236467626b5bc32:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''Managed Grafana | grafanaworkspaceexample''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: c06f1db22f748b70aa1bc356c385292d11b56dd52fdb84f5fa0e2a62b7d077e2
+ count: 1
+ d97e332b81cde3eba71a3802133c3cee677171ad8ad226385c783f33ff0b6f84:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''privatelink.monitor.azure.com | 메트릭 수집 엔드포인트 | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 17bfcdfb17759f755da6134996411bed3843dbad727f581531c06147bf6d6bf2
+ count: 1
+ b1bdcfb814366ed3a685131abb36a7a106f8723864d4d58a2e3d693cc5e69a4c:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''privatelink.ods.opinsights.azure.com | ODS 데이터 채널 | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 4bca0441b9eaad688d86cb48a00d560a5e4ae04d17e83f5d93219300ac10a58a
+ count: 1
+ fbf53873149396c6970488dc818e37574b77531268e0ddf1c868f95f451ebd1d:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''privatelink.oms.opinsights.azure.com | OMS 포털 | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 5fa00e6cd05ae23719f28e3a5214e55eda7ee5f2a52765aefc998266989dc380
+ count: 1
+ a023b598ecac63bcd02648fef1a4c89bed7d2a4f2de51f4426dae8607ce0c2f1:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''privatelink.agentsvc.azure-automation.net | 에이전트 서비스 | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 349000b7e85a3d2b38888924d5de1718e1de1a07158130be4dfd9863aabc1388
+ count: 1
+ 5ab80b1cde1ccc1cdc2d55d0034dda565fc8dcc1595d214c48cded114bbcb27a:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''privatelink.blob.core.windows.net | Blob 스토리지 | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 1da8e859896ce4aea549bc88d21118844eab3ab69b32cfbc410ff4d527b74281
+ count: 1
+ 4171198b18e12970a4d725b2ed7dfac43555ace5cc0aef041dd6dc9a725f9562:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''*.privatelink.koreacentral.azmk8s.io | AKS API Server | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 10422feb79fbc47d77c3bc8268b12d7aae01e0ce6354c2507cf70c6f26ac6b5d
+ count: 1
+ f59b5f0be1612339205a66b664559cf857a99375037daf859709b6efd7cd8d73:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''privatelink.redis.azure.net | Redis Cache | vnet-example-koreacentral-prd-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 5c19cc801bd492583b3ea899509ae32249f76ba8ffd376d2c530ddb1e514247d
+ count: 1
+ 933b1376898a2ead7b68157deb139af9a3f5101c60da5470896ae300bf07c27b:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''MSProm-koreacentral-aks-example-koreacentral-01 | rg-example-koreacentral-01 | MSProm-koreacentral-aks-example-koreacentral''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 76d2c2f89804d133299f27b69c7dbb43c3b582eebc58be63852d9bbbcd4bbba6
+ count: 1
+ e545d145796957437bcbea7610d0e6f25f691fa2c07cbc63225a5d9e14122059:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''azuremonitor-workspace-example-krc-01 | MA_..._managed | azuremonitor-workspace-example-krc-01''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: d7dc8fb9516159fc1a347e8f5c770216913d2aa4fcb2fd382809f7ed182a49cb
+ count: 1
+ 6fcf8f81ea35d9515ed12cf8a5c242e7fa802cc887e79d2aacc43416ba0db048:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''configurationAccessEndpoint | N/A | MSProm-koreacentral-aks-example-koreacentral
+ ✅''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 0bc918c9caee9e79a2db8af23a0554232018ffd121b175f3e7d4dfb69e00dde2
+ count: 1
+ 68b32234335d9a9ef7eadded9bfda2eb6768b72aa23e0a92979b92a546e65061:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''Resource Group | rg-example-koreacentral-prd-01 | Korea Central''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 14cbd2934a7c5ce85199362c79c6b8fc53a586b108d677530a0d36c847912f96
+ count: 1
+ 791178e78ff10c8a4e214cbdbf680ece83bcfc93724ac7da171bf5ab1211ccfb:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''VNet | vnet-example-koreacentral-prd-01 | 10.100.0.0/16''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 693e6a4d8337f736e6ff61a17d74a9c9df2d526a3e3ae457c1cbf7819fcdc8a9
+ count: 1
+ 06000515a008f2a2917bb02de0e4c0f686dbaeeefd02c3bbc5973fbc07c2987a:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''Azure Firewall | fw-example-koreacentral-prd-01 | Standard SKU, DNS Proxy 활성화''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: eac3ee1c7efff9f7a43924686095d9dd66de8c7c11c27bc8e827aa6fd03d7ddf
+ count: 1
+ e314e69e39f83a68b872625644fafa45e23c027f2feacc8c808f6c285a0b7798:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/aks-private-opentelemetry/index.md: Safety: the environment,
+ architecture, AMPLS/DCE/DCR, DNS-zone, Firewall and VNet-link examples now use the example
+ resource namespace and . Both az rest configurationAccessEndpoint requests
+ retain their methods, 2023-03-11 API version and JSON properties. DNS zones, address ranges,
+ metrics pipeline, taints and historical topology details remain. Current tables replacement
+ begins: ''Public IP | pip-fw-example-koreacentral-prd-01 | Standard SKU''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/aks-private-opentelemetry/index.md
+ category: tables
+ fingerprint: 755dae66bd3362048c7611a80bdc4edf450e66edd00ddd0968f2426685157a69
+ count: 1
+ - baseline_path: monitor/sre-agent-event-lab/README.md
+ pages_path: labs/azure-monitor/sre-agent-event-lab/index.md
+ reviewed_changes:
+ prose:
+ 1c3a39700c2a8d219e620296cdc608dda909d0077f62895183cd2f5313544f14:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current prose replacement begins: ''포털에서만
+ 할 수 있는 설정이 남아 있습니다. 저장소 연결, 지식 문서 업로드, Azure Monitor incident platform 연결, Review 모드 응답 계획,
+ 역할 할당을 에이전트 설정이 순서대로 안내합니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: prose
+ fingerprint: 235a018f4b0bc18cc2d583417464286dedcded28ee0a0921ffdeca1c6db51f73
+ count: 1
+ a2ad55106e8c2cb6f8391a41cadf665e3517675e3ff39d75ae5b57d1d1315db6:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current prose replacement begins: ''기본
+ 실습에는 Logic App bridge를 배포하지 않습니다. 제품 표준 경로는 Azure Monitor를 incident platform으로 연결하는 것이고, 예전
+ 실측에서 쓰던 Action Group + Logic App 인증 경로는 레거시 기''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: prose
+ fingerprint: 486ae0fd422b20a9c48989eec7fea0295edec45a787e8cd1f00b54fa3a60ae53
+ count: 1
+ 2ca86442294e2f99044c23fc5623c201686c9332f032e3d1253159630704d4e1:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current prose replacement begins: ''장애를
+ 주입하기 전에 정상 부하가 Application Insights까지 도달하는지 확인하고, 그 사실을 기록해야 S1이 열립니다. 텔레메트리가 없는 워크로드에 장애를
+ 넣으면 주입한 장애와 원래부터 안 보이던 상태를 구별할 수 없습니다. 명령은 에이''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: prose
+ fingerprint: 140621f40b6e6829edba8ef8b5c986ded469ef2666d4f36b1f98fafe3ebc885c
+ count: 1
+ f0a4ebba54d5beb20034578ede022134d9baa6047ad234c0223b2ccb6d014fa4:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current prose replacement begins: ''채점
+ 기준, 사람이 채워야 하는 판정, 종합 판정 해석은 결과 채점에 있습니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: prose
+ fingerprint: b9cd2d9d8b782900104f7a343e148055fd1519785df15e1c2cd00eeb6f34d372
+ count: 1
+ 33621ea5336f4b63bc4bffb679ec0aa3630e7dad73c975d29348530fdad07bf8:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current prose replacement begins: ''정적
+ 임계값 대신 Dynamic Threshold로 확장하는 설계는 Dynamic Thresholds 확장 설계에 정리해 두었습니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: prose
+ fingerprint: e488ae8fbc6bf58b7b5baf663fcf1d4cb0ff058fa3bcd547f69e0ad7af182896
+ count: 1
+ code:
+ 6faff368410a9e4c46cfeda44df7b9d874c51daf2930ac47df1578133c4775b1:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current code replacement begins: ''az
+ login --use-device-code azd auth login source ./docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/lab-env.sh''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: code
+ fingerprint: c4f3722e70e4fabe25384b17b0626591e6eb567ee527873ba09825e437d7dde3
+ count: 1
+ 7e2247eec243f924ffd1b30a2b412b3022389dfcbf3fba3b3cf5dc7d2f80e4c6:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current code replacement begins: ''cd
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: code
+ fingerprint: 590387a44563f4dc1b107eec2272f90da068822ad2ffab1149e9930b78f5ef94
+ count: 1
+ tables:
+ ffa178128112fd99f8d40cc82f4fd9fce361fbc8e3605f534aa3a0de7f44c89c:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current tables replacement begins:
+ ''S1 | HTTP 500 응답 | S1 — HTTP 500 장애''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: tables
+ fingerprint: 798148d0707575aec5bc0f94fd0be1044a7ad518330b79d6328788853882e405
+ count: 1
+ b46d11954a35fe0afd5b627882274a683e78a850a5e724008791321d3a8aeb16:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current tables replacement begins:
+ ''S2 | 주문 API 지연 | S2 — 응답 지연''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: tables
+ fingerprint: 02ad765a90d73afd95784d448fd37d8941bbb11af97b208d9b96d25f0051ea66
+ count: 1
+ 8b80f1ae4540feb17783ea900ad47f7e92cc74782a14e736d717219e45b327b6:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current tables replacement begins:
+ ''S3 | Blob 읽기 권한 제거 | S3 — Blob 권한 장애''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: tables
+ fingerprint: 36fce29b768c7dce85c51e59baf6aea4c72387f8c104e5dc6d0b14946601ab8d
+ count: 1
+ cc08d785e8dd9c540bb261bc5c1d481559a041087bc1d7a2f317b6dcb889d762:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current tables replacement begins:
+ ''Agent가 스레드를 만들지 않음 | 에이전트 설정의 incident platform·응답 계획 확인''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: tables
+ fingerprint: b947367c044bf54c899ce4e16af2af9d76d6c46b5a6811c573dba5e5e4426631
+ count: 1
+ 4c29553b4450e5b44b6e77e81286bd106bd9b811288a1cd6f78be94fc740bc5f:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/event-lab/index.md: The login/source
+ and cd fences now use docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Setup,
+ baseline approval, scenario, results, troubleshooting and dynamic-threshold references now
+ use named canonical children rather than retired guide filenames. The three S1/S2/S3 faults,
+ state gates, cost warning, scoring and cleanup remain. Current tables replacement begins:
+ ''채점이 INCOMPLETE | 결과 채점의 수동 판정 절''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: tables
+ fingerprint: 38c1088b8a5da1716452b34a67c41d302b8acc97370314d9524e5a1316925f07
+ count: 1
+ local_link_labels:
+ 22ebccef48adf3797ef14e253543c95913b641f5e0ac0482c5ab31694b8f74a0:
+ missing_count: 2
+ reason: Retained label '.devcontainer/README.md' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to .devcontainer/README.md. The current canonical source or owning-sample target
+ was checked; this is a source-link/reader-label migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: 140d77653154f7db2d17edcf9fa3ba03c675670cec80e3fd0ccc943d53877f2c
+ count: 2
+ - kind: file
+ path: .devcontainer/README.md
+ sha256: 438713fac44d3fe76c97a5878a27575ce9ac2c64576ddba56ba1ea955fcdc998
+ link_change: replacement
+ 44028050825bcfa642b14ff4262d4da0b9393687cf19137c36484aa09ff58e8b:
+ missing_count: 3
+ reason: Replaced retired path label with '에이전트 설정' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/setup/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: e221506ec2c901bfc94d0354c36eac6f1da24f6d2e7384f63fc1440b5fed4620
+ count: 3
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: title
+ fingerprint: 0c2e310e43b2b80c67123ce079d1d5a0112f23678958ccf29004b869ee1286b6
+ count: 1
+ link_change: replacement
+ 9e7735ab56a3c5447d3e3fced04c1b2fe3a2992cf3180f9ca2b688e7fee0ea64:
+ missing_count: 1
+ reason: Replaced retired path label with '실측 검증 결과' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/validation-results/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: 217e177f4b3f19ba43f9518a349b8038880a617115148956c72718bb43162a24
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: title
+ fingerprint: f76180846a8d104c7b8feac0a596c6d997866e148f63262b1c134162043ca7fc
+ count: 1
+ link_change: replacement
+ 721d882d49a3cd517405ccc227fe908e4a862088f67bfde974795095f05c02b1:
+ missing_count: 1
+ reason: Replaced retired path label with 'S1 — HTTP 500 장애' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: 69046480bd9e0ae6bcf3ea00e4df2413231512dd46f1779b69fa57dc6191b016
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: title
+ fingerprint: e32a47cbcbe792e95607534efe7d450a475dde792e18a3f8ac4abc5d0f935f99
+ count: 1
+ link_change: replacement
+ 5c8bc528ec7649959420de6f956d39d8fe06deebea6a99112373148f0d0e814c:
+ missing_count: 1
+ reason: Replaced retired path label with 'S2 — 응답 지연' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: a190c09ab3da96a63e07209b355514b025a8aaabcb968d836167b7bf0feac0f9
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: title
+ fingerprint: b4d59aad0c2d13c3c8f8744bd04fc4bf127cb223194fe0fbabed88257b1ef5e9
+ count: 1
+ link_change: replacement
+ 1898c96d3ba8c474502f1860ec20522ea2883d180b44f744e7b6812908fba571:
+ missing_count: 1
+ reason: Replaced retired path label with 'S3 — Blob 권한 장애' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: 4240ad1dacf8c2ea8c7e2a0ec074daba94e7b859b42e9b9fc260066c07aa5c5f
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: title
+ fingerprint: c90dcd7d518f37ad825833cff61808318928723b15dac70d17c8c4f88fef2234
+ count: 1
+ link_change: replacement
+ d0352a09849221675c8996d4f85514fa1067158f15ca0a74d3747d499c347b55:
+ missing_count: 2
+ reason: Replaced retired path label with '결과 채점' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/results/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: b7bfe677713910dd4521ea1a54ce1bce563e8a29c1f6f897ff06aba714d5b7ce
+ count: 2
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/results/index.md
+ category: title
+ fingerprint: 9adb1e924965dfde3be5ea5c982704e5b1caa168fa171ee2bb20b1eb003e167f
+ count: 1
+ link_change: replacement
+ b342b4cce7c7903c2a29215e5442ef0eb9d9e5d166fbb3a3a5a1a8a280f6d399:
+ missing_count: 1
+ reason: Replaced retired path label with 'Dynamic Thresholds 확장 설계' in docs/services/azure-monitor/azure-sre-agent/event-lab/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: links
+ fingerprint: d2d0eacfa039b39525fb2ee32201e738d52a8cd75398ded6dea212e71809211a
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md
+ category: title
+ fingerprint: 641fa5fe7671aee26dac81baabe5a609793a98d5c33d58fc2f7128dff7816012
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/sre-agent-event-lab/dynamic-thresholds.md
+ pages_path: guides/azure-monitor/sre-agent-dynamic-thresholds/index.md
+ reviewed_changes:
+ prose:
+ 014a10d01aa0e5fb54547492a3e2a67766e36db7efa6f0866894dba44914cdb5:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md: The legacy
+ Event path note still dates the Action Group/Logic App bridge to 2026-08-12 and says it is
+ not deployed by the current lab. Its reference is now 실측 검증 결과 at docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ rather than the retired filename. Current prose replacement begins: ''- Event path(레거시 bridge):
+ Dynamic alert → Action Group → Logic App managed identity → SRE HTTP Trigger → Review-mode
+ investigation. 2026-08-''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md
+ category: prose
+ fingerprint: 8e09f39b24cd36114d409043cb2d364cf4b63478a912a16c8820844a0fc04523
+ count: 1
+ local_link_labels:
+ 9e7735ab56a3c5447d3e3fced04c1b2fe3a2992cf3180f9ca2b688e7fee0ea64:
+ missing_count: 1
+ reason: Replaced retired path label with '실측 검증 결과' in docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/validation-results/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md
+ category: links
+ fingerprint: 217e177f4b3f19ba43f9518a349b8038880a617115148956c72718bb43162a24
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: title
+ fingerprint: f76180846a8d104c7b8feac0a596c6d997866e148f63262b1c134162043ca7fc
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/sre-agent-event-lab/guides/01-agent-setup.md
+ pages_path: labs/azure-monitor/sre-agent-event-lab-setup/index.md
+ reviewed_changes:
+ prose:
+ 24d42fb71e135de7ab48b81149ddb9e357fbe50028dd7fe668cb587d4725e108:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/setup/index.md: The environment fence
+ now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. The Knowledge files
+ row names the canonical incident-runbook and the next-step link names scenario-http-500. The
+ required roles, Review mode and baseline/setup state acknowledgements remain. Current prose
+ replacement begins: ''첫 장애를 주입합니다: S1 — HTTP 500 장애''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: prose
+ fingerprint: de582471c65d52b73f956bf0a806ac870c851e84bd339104532965bf6f40838d
+ count: 1
+ code:
+ b052995a546e9e1f9ae8a5ab4d9dfd8f2dcbfb5e46c85107831eee8f5cb45f8e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/setup/index.md: The environment fence
+ now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. The Knowledge files
+ row names the canonical incident-runbook and the next-step link names scenario-http-500. The
+ required roles, Review mode and baseline/setup state acknowledgements remain. Current code
+ replacement begins: ''cd docs/services/azure-monitor/azure-sre-agent/samples/event-lab source
+ ./scripts/lab-env.sh''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: code
+ fingerprint: 1e3c03a855ca5771d1335e37d91f68f1f22b05cfa8d892d324055bfedc6ef53e
+ count: 1
+ tables:
+ 3753279e936bb5b6e363f980fdbdec9b249ca829952b0bfff6a972580d0c6f1e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/setup/index.md: The environment fence
+ now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. The Knowledge files
+ row names the canonical incident-runbook and the next-step link names scenario-http-500. The
+ required roles, Review mode and baseline/setup state acknowledgements remain. Current tables
+ replacement begins: ''Knowledge files | 인시던트 대응 런북 | 조사 순서와 금지 사항을 팀 규칙으로 강제합니다''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: tables
+ fingerprint: 808c805c85cd82e00f67fa0af3f3d0261ed8e7dec10e0e07adb0b1424d6c4d52
+ count: 1
+ local_link_labels:
+ 673435c83fe369747f909ad329e995ff4aab440b39b30b0967fb7c937a670950:
+ missing_count: 1
+ reason: Replaced retired path label with '인시던트 대응 런북' in docs/services/azure-monitor/azure-sre-agent/setup/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/incident-runbook/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: links
+ fingerprint: 368b1667a951dd9c93cf58951c8f4cd6f3cb6518a2db65653ef19ad15911a989
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/incident-runbook/index.md
+ category: title
+ fingerprint: dfbc98a263caec85142c7a6384e01137129864422a2bfc277e6b70971632b60a
+ count: 1
+ link_change: replacement
+ 1c3dbf23991386b472cb4d1276a30837f5c72ce8a85a554953faff7c54e156d3:
+ missing_count: 1
+ reason: Replaced retired path label with 'S1 — HTTP 500 장애' in docs/services/azure-monitor/azure-sre-agent/setup/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: links
+ fingerprint: 69046480bd9e0ae6bcf3ea00e4df2413231512dd46f1779b69fa57dc6191b016
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: title
+ fingerprint: e32a47cbcbe792e95607534efe7d450a475dde792e18a3f8ac4abc5d0f935f99
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/sre-agent-event-lab/guides/02-scenario-s1.md
+ pages_path: labs/azure-monitor/sre-agent-scenario-http-500/index.md
+ reviewed_changes:
+ prose:
+ d0949831998e46a635c7028c64235af4300314bbe05fe974936b14e693d013d7:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Start-condition
+ and missing-thread advice point to setup, scoring points to results, and the next step points
+ to scenario-latency using reader-facing labels. HTTP 500 injection, state gates, capture statuses
+ and recovery commands remain. Current prose replacement begins: ''- 에이전트 설정을 마쳤고 evidence/state.json에
+ baseline_passed와 agent_setup_acknowledged가 기록되어 있습니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: prose
+ fingerprint: 98c383b512f361f6c4881866b09b6d13257fd447bbb42332a1e153c9d5bc8131
+ count: 1
+ 7a6cc00434bf3af476835bccced55cbbde8de49ad9c89040d54cbf91981667f1:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Start-condition
+ and missing-thread advice point to setup, scoring points to results, and the next step points
+ to scenario-latency using reader-facing labels. HTTP 500 injection, state gates, capture statuses
+ and recovery commands remain. Current prose replacement begins: ''성공은 conclusion 하나뿐입니다. 부분
+ 성공은 결론이 나왔지만 내용이 얕은 경우이며, 이때도 상태는 conclusion이고 점수는 결과 채점에서 갈립니다. 빈 성공 화면을 만들지 않고 누락 상태와 마지막
+ 확인 시각을 그대로 그림에 남깁니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: prose
+ fingerprint: 7e09809f09885a936d6fd0b3bac94c4e2928bf63fec7b2785b22e786d60f65ba
+ count: 1
+ 602ac0a5e1349c2f535a8ead51ad2cc1a399a7531214aca08c336408060609d1:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Start-condition
+ and missing-thread advice point to setup, scoring points to results, and the next step points
+ to scenario-latency using reader-facing labels. HTTP 500 injection, state gates, capture statuses
+ and recovery commands remain. Current prose replacement begins: ''thread-not-created가 나오면
+ 에이전트 설정의 incident platform과 응답 계획부터 다시 확인합니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: prose
+ fingerprint: 58a62a78b813deaafb3754bd6f6ae1e70f79108ef95c8d3c2eef85c592f49f1e
+ count: 1
+ aeb020ec3047ebb43cecf025f3d2e755d82055561281b915692982956fbe9079:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Start-condition
+ and missing-thread advice point to setup, scoring points to results, and the next step points
+ to scenario-latency using reader-facing labels. HTTP 500 injection, state gates, capture statuses
+ and recovery commands remain. Current prose replacement begins: ''지연 장애로 넘어갑니다: S2 — 응답 지연''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: prose
+ fingerprint: 7749d42bc6163170be716a5a0110293703a1359b8760c7490ba41bb477e0110c
+ count: 1
+ code:
+ b052995a546e9e1f9ae8a5ab4d9dfd8f2dcbfb5e46c85107831eee8f5cb45f8e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. Start-condition
+ and missing-thread advice point to setup, scoring points to results, and the next step points
+ to scenario-latency using reader-facing labels. HTTP 500 injection, state gates, capture statuses
+ and recovery commands remain. Current code replacement begins: ''cd docs/services/azure-monitor/azure-sre-agent/samples/event-lab
+ source ./scripts/lab-env.sh''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: code
+ fingerprint: 1e3c03a855ca5771d1335e37d91f68f1f22b05cfa8d892d324055bfedc6ef53e
+ count: 1
+ local_link_labels:
+ 220ab3d6bd9f5bcf6b432aee6496d3d33dfc04f52e3166900819d49d0a33660f:
+ missing_count: 2
+ reason: Replaced retired path label with '에이전트 설정' in docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/setup/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: links
+ fingerprint: e221506ec2c901bfc94d0354c36eac6f1da24f6d2e7384f63fc1440b5fed4620
+ count: 2
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/setup/index.md
+ category: title
+ fingerprint: 0c2e310e43b2b80c67123ce079d1d5a0112f23678958ccf29004b869ee1286b6
+ count: 1
+ link_change: replacement
+ 642002bebe57cfb2d5d3168236cb52f8f261354812320f7c408f833079661570:
+ missing_count: 1
+ reason: Replaced retired path label with '결과 채점' in docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/results/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: links
+ fingerprint: b7bfe677713910dd4521ea1a54ce1bce563e8a29c1f6f897ff06aba714d5b7ce
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/results/index.md
+ category: title
+ fingerprint: 9adb1e924965dfde3be5ea5c982704e5b1caa168fa171ee2bb20b1eb003e167f
+ count: 1
+ link_change: replacement
+ 2c64e239550534f5e89f6403e8338cac2756d397dfbbce90d29af73440d01992:
+ missing_count: 1
+ reason: Replaced retired path label with 'S2 — 응답 지연' in docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: links
+ fingerprint: a190c09ab3da96a63e07209b355514b025a8aaabcb968d836167b7bf0feac0f9
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: title
+ fingerprint: b4d59aad0c2d13c3c8f8744bd04fc4bf127cb223194fe0fbabed88257b1ef5e9
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/sre-agent-event-lab/guides/03-scenario-s2.md
+ pages_path: labs/azure-monitor/sre-agent-scenario-latency/index.md
+ reviewed_changes:
+ prose:
+ e4464baf5ec71e4bcbb043edc8a52d8e98d3fa260afffda990816f88c81a3ed7:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. The prerequisites
+ and next-step prose now name scenario-http-500 and scenario-blob-permission. S1 recovery requirements,
+ latency fault, conclusion capture gate and recovery commands remain. Current prose replacement
+ begins: ''- S1 — HTTP 500 장애의 S1이 복구되고 캡처가 conclusion으로 끝났습니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: prose
+ fingerprint: cafc0311504aa98114ba8439623c04f49aad3c6c07b3843ee307c5045b7386cb
+ count: 1
+ c0da5ea550501e006069b2ce322ed03f38ad31fbe447ba4559cbda587b4162a3:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. The prerequisites
+ and next-step prose now name scenario-http-500 and scenario-blob-permission. S1 recovery requirements,
+ latency fault, conclusion capture gate and recovery commands remain. Current prose replacement
+ begins: ''권한 장애로 넘어갑니다: S3 — Blob 권한 장애''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: prose
+ fingerprint: 57365ca466c0a0252a149c6ccbafc80d1476e3fafd81936c57397a3ba1920613
+ count: 1
+ code:
+ b052995a546e9e1f9ae8a5ab4d9dfd8f2dcbfb5e46c85107831eee8f5cb45f8e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md: The environment
+ fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab. The prerequisites
+ and next-step prose now name scenario-http-500 and scenario-blob-permission. S1 recovery requirements,
+ latency fault, conclusion capture gate and recovery commands remain. Current code replacement
+ begins: ''cd docs/services/azure-monitor/azure-sre-agent/samples/event-lab source ./scripts/lab-env.sh''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: code
+ fingerprint: 1e3c03a855ca5771d1335e37d91f68f1f22b05cfa8d892d324055bfedc6ef53e
+ count: 1
+ local_link_labels:
+ 1c3dbf23991386b472cb4d1276a30837f5c72ce8a85a554953faff7c54e156d3:
+ missing_count: 1
+ reason: Replaced retired path label with 'S1 — HTTP 500 장애' in docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: links
+ fingerprint: 69046480bd9e0ae6bcf3ea00e4df2413231512dd46f1779b69fa57dc6191b016
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md
+ category: title
+ fingerprint: e32a47cbcbe792e95607534efe7d450a475dde792e18a3f8ac4abc5d0f935f99
+ count: 1
+ link_change: replacement
+ 7d81587b0c8906bf8afea33f1abca69106523b68c0d0cd96c1371902e466f702:
+ missing_count: 1
+ reason: Replaced retired path label with 'S3 — Blob 권한 장애' in docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: links
+ fingerprint: 4240ad1dacf8c2ea8c7e2a0ec074daba94e7b859b42e9b9fc260066c07aa5c5f
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: title
+ fingerprint: c90dcd7d518f37ad825833cff61808318928723b15dac70d17c8c4f88fef2234
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/sre-agent-event-lab/guides/04-scenario-s3.md
+ pages_path: labs/azure-monitor/sre-agent-scenario-blob-permission/index.md
+ reviewed_changes:
+ prose:
+ 3980a12d12b3d02cd5d1d39a9dbb621bb87a8e5719bb8496cf084a074b1cbe22:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md: The
+ environment fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab.
+ Prerequisite and next-step prose now name scenario-latency and results. Blob-role removal,
+ S2 recovery/capture gates and least-privilege restoration evidence remain. Current prose replacement
+ begins: ''- S2 — 응답 지연의 S2가 복구되고 캡처가 conclusion으로 끝났습니다.''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: prose
+ fingerprint: 2a47996ce783158b2e5d88918c833a9ea7ae5c5c5011c72431ca561db225fe42
+ count: 1
+ 8ffa7b1113e65afce2b1049c2ae54853457120e34024b5e7358d7a96eb991c68:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md: The
+ environment fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab.
+ Prerequisite and next-step prose now name scenario-latency and results. Blob-role removal,
+ S2 recovery/capture gates and least-privilege restoration evidence remain. Current prose replacement
+ begins: ''수집한 근거를 채점합니다: 결과 채점''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: prose
+ fingerprint: 93e9b1a313e597e193690668b6101de5b6c226602528b82994b9100a74857a3f
+ count: 1
+ code:
+ b052995a546e9e1f9ae8a5ab4d9dfd8f2dcbfb5e46c85107831eee8f5cb45f8e:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md: The
+ environment fence now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab.
+ Prerequisite and next-step prose now name scenario-latency and results. Blob-role removal,
+ S2 recovery/capture gates and least-privilege restoration evidence remain. Current code replacement
+ begins: ''cd docs/services/azure-monitor/azure-sre-agent/samples/event-lab source ./scripts/lab-env.sh''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: code
+ fingerprint: 1e3c03a855ca5771d1335e37d91f68f1f22b05cfa8d892d324055bfedc6ef53e
+ count: 1
+ local_link_labels:
+ 2c64e239550534f5e89f6403e8338cac2756d397dfbbce90d29af73440d01992:
+ missing_count: 1
+ reason: Replaced retired path label with 'S2 — 응답 지연' in docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md. The current
+ canonical source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: links
+ fingerprint: a190c09ab3da96a63e07209b355514b025a8aaabcb968d836167b7bf0feac0f9
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md
+ category: title
+ fingerprint: b4d59aad0c2d13c3c8f8744bd04fc4bf127cb223194fe0fbabed88257b1ef5e9
+ count: 1
+ link_change: replacement
+ 642002bebe57cfb2d5d3168236cb52f8f261354812320f7c408f833079661570:
+ missing_count: 1
+ reason: Replaced retired path label with '결과 채점' in docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/results/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md
+ category: links
+ fingerprint: b7bfe677713910dd4521ea1a54ce1bce563e8a29c1f6f897ff06aba714d5b7ce
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/results/index.md
+ category: title
+ fingerprint: 9adb1e924965dfde3be5ea5c982704e5b1caa168fa171ee2bb20b1eb003e167f
+ count: 1
+ link_change: replacement
+ - baseline_path: monitor/sre-agent-event-lab/guides/05-results.md
+ pages_path: labs/azure-monitor/sre-agent-results/index.md
+ reviewed_changes:
+ code:
+ 67a6a1cd3fa2470fa487d0030975e2dcc8399fe8015dc21d9d02e112b6c00afe:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/results/index.md: The scoring fence
+ now enters docs/services/azure-monitor/azure-sre-agent/samples/event-lab before invoking the
+ unchanged app/.venv/bin/python scripts/score.py --evidence-root evidence command. Current
+ code replacement begins: ''cd docs/services/azure-monitor/azure-sre-agent/samples/event-lab
+ app/.venv/bin/python scripts/score.py --evidence-root evidence''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/results/index.md
+ category: code
+ fingerprint: c1f85dfd4ad2c382ac159ddfc21e9c89bd58c7266e2ac0e1f5036c7798d7d33d
+ count: 1
+ - baseline_path: monitor/sre-agent-event-lab/runbooks/incident-response.md
+ pages_path: guides/azure-monitor/sre-agent-incident-runbook/index.md
+ reviewed_changes: {}
+ - baseline_path: monitor/sre-agent-event-lab/validation-results.md
+ pages_path: research/azure-monitor/sre-agent-validation-results/index.md
+ reviewed_changes:
+ prose:
+ 8271d6dbca2ffc2138a6910a441d84bf389105cd37f8657345b5645f9a8e918b:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current prose
+ replacement begins: ''제품 개요와 실사용 패턴은 Azure SRE Agent 소개 자료를 먼저 참고한다. 이 문서는 S1/S2/S3 시나리오에서
+ 측정한 수치, timeline, evidence, 한계를 정리한다. 기록 시점 주의. 아래 결과는 azd 재구성 이전에 손으로 ''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: prose
+ fingerprint: 83f9d83eae1e08005fb765b63170c99021a5f243da5ee50d97bde624a04ca4c1
+ count: 1
+ 59eaa5e71ada1c5c2e7aa1c3f44779c0e9ced379a85d4255c9a6e91abdc1c509:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current prose
+ replacement begins: ''- Agent thread: ''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: prose
+ fingerprint: 05c215f003ac5cd5d823d6b788c1809b9c391d12c33f36a29450179c3ba95ed0
+ count: 1
+ 67c696e31c3eb32ecb73a7dff7650b346e0edbb471b2b6db308e7a5430673a3d:
+ missing_count: 1
+ reason: 'Safety: the historical S2 Agent thread identifier is replaced by
+ in the corresponding scenario capture/evidence list in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md.
+ The approval binds the S2 row specifically; the 2026-08-12 observations, outcome and limitations
+ remain.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: prose
+ fingerprint: 7482e4b1e9974321cc361a76dd65d6a1c3d34abae00f236414cce4c87b05b416
+ count: 1
+ a147e49be39803c15924b5b1afa506dfcfd9db543e37dfc5f984aebf685aa96a:
+ missing_count: 1
+ reason: 'Safety: the historical S3 Agent thread identifier is replaced by
+ in the corresponding scenario capture/evidence list in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md.
+ The approval binds the S3 row specifically; the 2026-08-12 observations, outcome and limitations
+ remain.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: prose
+ fingerprint: a9b7ce3b186784d8cc4eca9458e6483fe6b12c995e4f477799eb7c1c887ee7b4
+ count: 1
+ 8ad3fbef8612b932686e896eb20f5dbf33be2927c10d9f67dd72440f9359b068:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current prose
+ replacement begins: ''후속 보정: vnet workload의 OTEL_SERVICE_NAME을 sre-event-lab-example로 변경하고
+ alert/evidence query도 이 deployment-unique 값으로 제한했다. 변경 후 새 role name으로 ''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: prose
+ fingerprint: 29efed84fcf4a95539246f992684601b4f033c78c33d04e197a7690a052d8a1b
+ count: 1
+ tables:
+ 909ff96fff1da5b1cecb3b0a52c0e9d9d9a28c7ac806be0b82f2a20a78b52dce:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current tables
+ replacement begins: ''Log Analytics | law-sre-event-lab-example | ✅''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: tables
+ fingerprint: 9f5e7d67557d5f49d1a94ca626aa925ee15e4afdd056528061e61c99911782d6
+ count: 1
+ c019548adf1e89b3b9d0738c57fbb330eb8bc67f3e8616347a1ae7a28c3b0627:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current tables
+ replacement begins: ''Application Insights | appi-sre-event-lab-example | ✅''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: tables
+ fingerprint: 0741b213295f39071fca52fa3267d9a20bcc5c6d15c868d5c23bc8aab44c3be2
+ count: 1
+ 009b1f97083a84aea95449b9595f90dc667b4d8de5276bcba9a0bba4daf6c550:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current tables
+ replacement begins: ''Storage | stexample00000000, Blob private endpoint | ✅''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: tables
+ fingerprint: 816be4abf1532c9b0405d5813f84dada2c1b0e2475f09f24ead2a94a9f86922f
+ count: 1
+ d7fd4a1cb28109a4524750adffd0bebe2ece8457de6d9df13dd49556d4134d11:
+ missing_count: 1
+ reason: 'In docs/services/azure-monitor/azure-sre-agent/validation-results/index.md: Safety:
+ the 2026-08-12 historical results retain the manual-lab/legacy-bridge warning, measured outcomes
+ and recovery limitation while replacing resource suffixes and thread GUIDs with example names
+ and . The deployment-unique OTEL_SERVICE_NAME example is sre-event-lab-example.
+ Current execution instructions are explicitly linked to event-lab/index.md. Current tables
+ replacement begins: ''Agent | sre-devguidesample-example | ✅ Running''.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: tables
+ fingerprint: ea58fe7984e32f7043078917a77415cba0864104583cbd61ac45be43f933aa31
+ count: 1
+ images:
+ 98692385d5dc253eebe2615d8858b1f470fc703c18faa1171834eca38339fe49:
+ missing_count: 1
+ reason: Preserved byte-for-byte in docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/investigation.gif
+ (SHA-256 3b130e64f7566503dd1a2706ccbf46bd9d9eeef2bc555e2cb57dcc6a42f72025); sample.yml publishes
+ it as validation-results/images/s1-investigation.gif to distinguish the three same-basename
+ scenario captures. Alt text is unchanged.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: images
+ fingerprint: e35f8c6fd8ffa52bfd1e1c76253c16d19275044d523e36d14183aed020a2af46
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/investigation.gif
+ sha256: 3b130e64f7566503dd1a2706ccbf46bd9d9eeef2bc555e2cb57dcc6a42f72025
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml
+ sha256: 944645876dfc81e72177178e1a029745ef255b7bfa93b69449f714677289c550
+ 94f1c44330f916dca1ce2dde914e40fcb8959302e54ab2d3bd90937cf15090bf:
+ missing_count: 1
+ reason: Preserved byte-for-byte in docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/investigation.gif
+ (SHA-256 4ff453aa973f9be400790974afe506d7b6628c81cbc6f69eaef5e4a9e28f0510); sample.yml publishes
+ it as validation-results/images/s2-investigation.gif to distinguish the three same-basename
+ scenario captures. Alt text is unchanged.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: images
+ fingerprint: c128197fa974cf57d96aeb1d69eb4448f75c58e516eaf126a72a5cefb6adbe70
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/investigation.gif
+ sha256: 4ff453aa973f9be400790974afe506d7b6628c81cbc6f69eaef5e4a9e28f0510
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml
+ sha256: 944645876dfc81e72177178e1a029745ef255b7bfa93b69449f714677289c550
+ eb3e116af988c0bcb102a06972938519582b7f650db0897994ff117767ae0957:
+ missing_count: 1
+ reason: Preserved byte-for-byte in docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/investigation.gif
+ (SHA-256 5102eda3c2346968ea0deeae2e7cec434a864994f7ae869b377c12fc948a9eb5); sample.yml publishes
+ it as validation-results/images/s3-investigation.gif to distinguish the three same-basename
+ scenario captures. Alt text is unchanged.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: images
+ fingerprint: 9345e828b89a2c9fa75d25f348c9b336b77f38e6b2cc24d02464b980f2bff8df
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/investigation.gif
+ sha256: 5102eda3c2346968ea0deeae2e7cec434a864994f7ae869b377c12fc948a9eb5
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml
+ sha256: 944645876dfc81e72177178e1a029745ef255b7bfa93b69449f714677289c550
+ local_link_labels:
+ fabd7a655161bce48b915396b69df3bf2d77130e4bb0f5f9bbe4c626f890932a:
+ missing_count: 1
+ reason: Replaced retired path label with '현재 실습 문서' in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/event-lab/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 184b7e1a03820be5298039fa35a06ac1a3bdb14fd519d12333967a0c2c58f91e
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: title
+ fingerprint: d611d20ca77ec1bfca692ac1448fd34d1c59506dab94a52346a0b41750ee0364
+ count: 1
+ link_change: replacement
+ 176e60b305e09e5517119889b188d142890ad06562a56bee19e69327bc4f1709:
+ missing_count: 1
+ reason: Replaced retired path label with '현재 실습 문서' in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/event-lab/index.md. The current canonical
+ source or owning-sample target was checked; this is a source-link/reader-label migration,
+ not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 184b7e1a03820be5298039fa35a06ac1a3bdb14fd519d12333967a0c2c58f91e
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/event-lab/index.md
+ category: title
+ fingerprint: d611d20ca77ec1bfca692ac1448fd34d1c59506dab94a52346a0b41750ee0364
+ count: 1
+ link_change: replacement
+ 4e4283a27c18d5d6aeb2633f6000f9c32ebd13743bcadb62eec08c3451132d55:
+ missing_count: 3
+ reason: Retained label '결론 frame' in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/07-conclusion.png;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/07-conclusion.png;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/07-conclusion.png.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 9aadace542bd23072c923ce5adad7311445a57853cc8119752e356300283c934
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 29840f412ba4def4266f5acb120071df9e4eb6a950eb84c4d4da9c4db7d4571f
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: e5a726481d122c1b55f4fa96797bbb265ac246101a52963edd9561052019a6c5
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/07-conclusion.png
+ sha256: 11b0dd034a0bc72d841e347fd424b8a385340a22d8b04dc0601a0ea77a2d59e1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/07-conclusion.png
+ sha256: 8141589fcdc74501123bc7ce8d5de41df20167e1946ae7c30d15416ae1c17b7c
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/07-conclusion.png
+ sha256: 61dfda52ab8a632c6984a7c8b56a16f06a73fd8dc18decdfa14f977d8879a727
+ link_change: replacement
+ 22267b2e1c53a41a357f022e22cd0b554d3413718ed88f6db4c317745ed9ac17:
+ missing_count: 3
+ reason: Retained label '실제 event timeline' in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/timeline.md;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/timeline.md;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/timeline.md.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: bee3f25fb61cf92a29e36b6a74f0b2c1501add423c5f6b6e41a2ba57ac7cf7a2
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: af99b33303c4a676133075343097f73602fffded5692c00350c7d0344576ff9c
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 7af164444b6ac5d9758bbf2fe7ad576439c0c342b08926983443c8b4f3813d63
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/timeline.md
+ sha256: 23e41c5a1a3354a2a665b938e1ed5d289a4d3174c7d4aabb484419b965015d42
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/timeline.md
+ sha256: 2c994cfa4c36438d702e82017caab390bfd5534dcfaf471c02d62ba9936a1287
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/timeline.md
+ sha256: 59d7ea731503083635bcbd9196d02e33399214ed31807549e1878e467271229e
+ link_change: replacement
+ 3efcbd689fedb88f8a060668e0f285de17f2e95ad652571306c7deda71ede7b1:
+ missing_count: 3
+ reason: Retained label 'Mermaid sequence source' in docs/services/azure-monitor/azure-sre-agent/validation-results/index.md,
+ pointing to docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/timeline.mmd;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/timeline.mmd;
+ docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/timeline.mmd.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: fb89775bec29cae9dadc53e2eb7e367dd7cf2a8ab76fcb8cebe734e609eba9ab
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 22a586a0b9539178d411ab113d00c400c5978bf32d2795472118abc5944419f6
+ count: 1
+ - kind: structure
+ path: docs/services/azure-monitor/azure-sre-agent/validation-results/index.md
+ category: links
+ fingerprint: 2b9b47666b5e100645898fdd775cfc6611eb8df41d74e193cd502008bbf42bd0
+ count: 1
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/timeline.mmd
+ sha256: 5a5ded432982d0258fcd39b0a2bbd5fd9697e3ca8cde175b61c93474b7776cfd
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s2/timeline.mmd
+ sha256: cbf01cd4f59235aab08355a6c1a7103b973688a38b0896e8dd92cc56041cfa91
+ - kind: file
+ path: docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s3/timeline.mmd
+ sha256: 0e615f6083752517db6f6d59389419a853ba37891ea70cabc63d1d8d08c94aa1
+ link_change: replacement
+ - baseline_path: mysql/nodejs/db_upgrade_zero_downtime.md
+ pages_path: guides/azure-database-for-mysql/blue-green-upgrade/index.md
+ reviewed_changes: {}
+ - baseline_path: mysql/nodejs/read_write_routing.md
+ pages_path: guides/azure-database-for-mysql/nodejs-read-write-routing/index.md
+ reviewed_changes: {}
+ - baseline_path: ptu_lb/ptu-lb-test.md
+ pages_path: guides/azure-openai/adaptive-ptu-load-balancing/index.md
+ reviewed_changes:
+ prose:
+ fe5088d17216c7a31ebc8e6a922bc8fb7daaf1b8f7f863e6f428a52d1bfc4092:
+ missing_count: 1
+ reason: 'In docs/services/azure-openai/adaptive-ptu-load-balancing/index.md: The redundant 메인
+ 문서: ptu-lb-test.md self-link was removed from 파일 구성; the same guide is this canonical document.
+ The editable diagram, renderer, Python controller, dependencies and optional PowerShell implementation
+ remain in docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks. The bound
+ canonical title and 파일 구성 heading demonstrate the current document and navigation instead
+ of allowing an unbound self-link deletion.'
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: title
+ fingerprint: a4136a8c94dc8df9bab02145a638cdcdebeb62eeb6c21c4326de794c4fea16fb
+ count: 1
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: headings
+ fingerprint: 938c8cc107534cbde9d6d225d2ab9b537981d2c5ac282f37b9eae09fe1d1d522
+ count: 1
+ local_link_labels:
+ 782c9f35e32cc1974dd512db107e29fca6fdf72fa3b9a230282acd141f20784c:
+ missing_count: 1
+ reason: Removed only the redundant self-link to this same guide from 파일 구성 in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md.
+ The current canonical document title and file-navigation heading are retained and bound as
+ identity/topic evidence; no replacement hyperlink is claimed.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: title
+ fingerprint: a4136a8c94dc8df9bab02145a638cdcdebeb62eeb6c21c4326de794c4fea16fb
+ count: 1
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: headings
+ fingerprint: 938c8cc107534cbde9d6d225d2ab9b537981d2c5ac282f37b9eae09fe1d1d522
+ count: 1
+ link_change: redundant-self-link
+ d8340c1aaa9e95e871d626647970820cd4d84b2ebd67c0908548b3af1d912f75:
+ missing_count: 1
+ reason: Retained label 'ptu_architecture.excalidraw' in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md,
+ pointing to docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/ptu_architecture.excalidraw.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: links
+ fingerprint: 86c2acee570d15a23788475adbe416ff82772ef2d01afaf2df5770361d67a1ef
+ count: 1
+ - kind: file
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/ptu_architecture.excalidraw
+ sha256: bc33797780dfd0e8c7e48851dab8664bfd6e859450cd38b560363197a67daea4
+ link_change: replacement
+ 3780ddb85f0f2468a666925639ce03196d37651ae07198d5f07178660b4cb73a:
+ missing_count: 1
+ reason: Retained label 'ptu_architecture.png' in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md,
+ pointing to docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/ptu_architecture.png.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: links
+ fingerprint: 031821ab9eeaf49aa716655750cf7b94cd6d407ffa2a825e6f267701ed150948
+ count: 1
+ - kind: file
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/ptu_architecture.png
+ sha256: accf9178699bee9407e694335ce1e975a7a8954338ec9ff4db73bbda27822ac4
+ link_change: replacement
+ a44631d85720594c37c72f21f5d6086dd8e36ef4aa4df5d6ef8084402a49354b:
+ missing_count: 1
+ reason: Retained label 'render_excalidraw.py' in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md,
+ pointing to docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/render_excalidraw.py.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: links
+ fingerprint: ba4fc57980924c885e34c006daaf4b14938387edc5d6edf6ea04c2d4db917113
+ count: 1
+ - kind: file
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/render_excalidraw.py
+ sha256: ebfaf59455909722b8d447042ac84a03428c4c301f3200bfa8c0b4a8024391ae
+ link_change: replacement
+ 7ee017639b4fc20bb4aa51df646d0915fee435c7d2b1fb7422ac484d9d035c99:
+ missing_count: 1
+ reason: Retained label 'runbooks/adaptive_tpm_quota_controller.py' in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md,
+ pointing to docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/runbooks/adaptive_tpm_quota_controller.py.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: links
+ fingerprint: 1581464a84ac6e5e5728cbb9d252c39ce99f17c5fd05c8bf73e521afe3a8f182
+ count: 1
+ - kind: file
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/runbooks/adaptive_tpm_quota_controller.py
+ sha256: 391e561774bb963e90b88a78693c2aaf17708c84ea909244ad34a0a38e162f57
+ link_change: replacement
+ 59e441dc4125b74571fdfbbe9d389c5f81a03e3b76542f71a4b770af7cbab02d:
+ missing_count: 1
+ reason: Retained label 'runbooks/requirements.txt' in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md,
+ pointing to docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/runbooks/requirements.txt.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: links
+ fingerprint: 4610a13cd2acf7c3ab740360af1ac787c8f9b72e6857042f4e3b9c1949512a79
+ count: 1
+ - kind: file
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/runbooks/requirements.txt
+ sha256: 49656d26230e95250bb6c16864b4df682b929c08fa0858cd94873d87e69c7e40
+ link_change: replacement
+ 83d324c4a28e9157567e4fd24234bcd1f42e9974ac6778197bb506e0a84deda3:
+ missing_count: 1
+ reason: Retained label 'runbooks/optional_adaptive_tpm_quota_controller.ps1' in docs/services/azure-openai/adaptive-ptu-load-balancing/index.md,
+ pointing to docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/runbooks/optional_adaptive_tpm_quota_controller.ps1.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/index.md
+ category: links
+ fingerprint: d9255295f2005fb89e29e3f624911de988430a1f50a24cb86a41657569f2f1e7
+ count: 1
+ - kind: file
+ path: docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/runbooks/optional_adaptive_tpm_quota_controller.ps1
+ sha256: 3f8bb6d4736fd7b8672bc6c68d26cabbc18c67826460c711d17c94afbe162f71
+ link_change: replacement
+ - baseline_path: redis/nodejs/cluster_failover_recovery.md
+ pages_path: cases/azure-managed-redis/cluster-failover-recovery/index.md
+ reviewed_changes:
+ local_link_labels:
+ 1b08bfef216eff6b14e0ccca69a06c7914498e7563355de0e674d34ee3cc9b7b:
+ missing_count: 1
+ reason: Retained label 'cluster_example/' in docs/services/azure-managed-redis/cluster-failover-recovery/index.md,
+ pointing to docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example.
+ The current canonical source or owning-sample target was checked; this is a source-link/reader-label
+ migration, not removal of the referenced content.
+ evidence:
+ - kind: structure
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/index.md
+ category: links
+ fingerprint: 90d4edf130c94626505f5a433eff1c88becab816e468bc916c922b603c0f31a8
+ count: 1
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/README.md
+ sha256: 1b862bf4da527e51ebd984cfde204d60ff161193cd3d19e505f63c1da48a4ab2
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/healthCheck.js
+ sha256: 3b83cfc3ed87412a90c51f280cee951be88765b845e105335e4540673caab0fb
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/package-lock.json
+ sha256: db6deedfd1c7ce2be97ccf7c4dd7115c0982594779f511ce2f07e0c39b1417de
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/package.json
+ sha256: 323678bf6c88d9f0da9a0e9508da1ddc6ba571c33df9c03788ebf1c8d305df66
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/redisClient.js
+ sha256: af691af75f520b1ef8f5b01dd622125dfe75ea55be3ceddaf9090a6102de0a6c
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/routes.js
+ sha256: df1405faadeaa86335c29108e09a47e6ef06602e53b35c1edd4ece5ae21d22f5
+ - kind: file
+ path: docs/services/azure-managed-redis/cluster-failover-recovery/samples/nodejs-cluster/cluster_example/server.js
+ sha256: bcba13f99b4ec31285b491659e26baee3fec98256e9cf068d248446ebfbdae96
+ link_change: replacement
+dispositions:
+ .azure/deployment-plan.md:
+ status: excluded-local-state
+ current_paths: []
+ reason: Local Azure deployment planning state is intentionally not public.
+ .azure/validate-status.json:
+ status: excluded-local-state
+ current_paths: []
+ reason: Local Azure validation state is intentionally not public.
+ memory/README.md:
+ status: replaced-summary
+ current_paths:
+ - docs/services/microsoft-foundry/agent-memory/index.md
+ - docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/README.md
+ - docs/services/microsoft-foundry/gpt-memory-layer/samples/architecture/README.md
+ reason: The service-level index was split into the canonical topic overview and owning sample documentation.
+ ptu_lb/README.md:
+ status: replaced-summary
+ current_paths:
+ - docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/README.md
+ reason: The link-only README was replaced by the owning topic sample README.
+ monitor/sre-agent-event-lab/scripts/tests/test_lab_guides.py:
+ status: replaced-test
+ current_paths:
+ - docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_briefing_docs.py
+ - tests/docs/test_reader_navigation.py
+ reason: Guide-layout assertions were replaced by canonical topic, navigation, and briefing-document
+ contracts.
+ monitor/sre-agent-event-lab/scripts/tests/test_repo_readme.py:
+ status: replaced-test
+ current_paths:
+ - docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_repo_readme.py
+ reason: The repository discovery contract test moved with the sample and now targets canonical topic
+ discovery.
diff --git a/scripts/docs/pre_pages_site.py b/scripts/docs/pre_pages_site.py
new file mode 100644
index 00000000..87e30a98
--- /dev/null
+++ b/scripts/docs/pre_pages_site.py
@@ -0,0 +1,599 @@
+"""Fail-closed inspection of rendered pre-Pages destinations and local assets."""
+
+from __future__ import annotations
+
+from collections import Counter
+from dataclasses import dataclass
+from ipaddress import IPv4Address, IPv6Address
+import json
+import math
+from pathlib import Path, PurePosixPath
+import re
+from typing import Mapping
+from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
+
+from mkdocs.structure.files import File
+import yaml
+
+from scripts.docs.pre_pages import AuditFormatError, PrePagesInventory
+from scripts.docs.pre_pages_content import create_semantic_renderer
+from scripts.docs.pre_pages_html import (
+ AuditHTMLParser, implied_end_on_end, implied_end_on_start, open_p_in_scope, validate_table_text,
+)
+from scripts.docs.pre_pages_visibility import AuthoredContent
+from scripts.docs.topics import TopicCatalog
+
+
+@dataclass(frozen=True)
+class SiteInspection:
+ errors: tuple[str, ...]
+ details: Mapping[str, object]
+
+
+_VOID = frozenset((
+ "area", "base", "br", "col", "embed", "hr", "img", "input", "link",
+ "meta", "param", "source", "track", "wbr",
+))
+_INERT = frozenset(("script", "style", "template", "noscript", "iframe", "noembed", "noframes"))
+_HTML_WHITESPACE = " \t\n\f\r"
+_URL_C0_SPACE = "".join(chr(codepoint) for codepoint in range(0x21))
+_URL_REMOVED_CONTROLS = str.maketrans("", "", "\t\n\r")
+
+
+def _valid_srcset_descriptor(value: str) -> bool:
+ width = re.fullmatch(r"([0-9]+)w", value)
+ if width:
+ return any(digit != "0" for digit in width[1])
+ if re.fullmatch(r"-?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?x", value):
+ density = float(value[:-1])
+ return math.isfinite(density) and density >= 0
+ # HTML treats even the future-compatible h descriptor as a parse error.
+ return False
+
+
+def _srcset(value: str) -> list[str]:
+ """Read whole URL tokens; a comma without ASCII whitespace stays in the URL."""
+ urls = []
+ remaining = value.strip(_HTML_WHITESPACE)
+ if not remaining:
+ raise AuditFormatError("empty srcset")
+ while remaining:
+ if remaining.startswith(","):
+ raise AuditFormatError("empty srcset candidate")
+ match = re.match(r"([^ \t\n\f\r]+)(.*)", remaining, re.DOTALL)
+ assert match is not None
+ token, remaining = match.groups()
+ if token.endswith(","):
+ if token.endswith(",,"):
+ raise AuditFormatError("empty srcset candidate")
+ token = token[:-1]
+ else:
+ descriptors, _, remaining = remaining.partition(",")
+ descriptors = re.findall(r"[^ \t\n\f\r]+", descriptors)
+ if len(descriptors) > 1 or any(not _valid_srcset_descriptor(item) for item in descriptors):
+ raise AuditFormatError(f"malformed srcset descriptors: {descriptors}")
+ if not token:
+ raise AuditFormatError("empty srcset URL")
+ urls.append(token)
+ remaining = remaining.strip(_HTML_WHITESPACE)
+ return urls
+
+
+def _refresh_url(value: str) -> str | None:
+ space = r"[ \t\n\f\r]*"
+ match = re.fullmatch(
+ rf"""{space}0{space};{space}url{space}={space}(?:"([^"']+)"|'([^"']+)'|([^"']+?)){space}""",
+ value, re.IGNORECASE,
+ )
+ return next(group for group in match.groups() if group is not None) if match else None
+
+
+class _Page(AuditHTMLParser):
+ def __init__(self) -> None:
+ super().__init__(convert_charrefs=True)
+ self.stack: list[str] = []
+ self.seen: Counter[str] = Counter()
+ self.links: list[str] = []
+ self.targets: list[tuple[str, str]] = []
+ self.canonicals: list[str] = []
+ self.refreshes: list[str] = []
+ self.errors: list[str] = []
+ self.source_lines: list[str] = []
+ self.authored = AuthoredContent(material_article=True)
+
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ foreign = next((name for name in reversed(self.stack) if name in {"svg", "math", "foreignobject"}), None)
+ if foreign not in {"svg", "math"}:
+ implied = implied_end_on_start(self.stack, tag)
+ if implied is not None:
+ del self.stack[implied:]
+ attributes = dict(attrs)
+ if len(attributes) != len(attrs):
+ self.errors.append(f"duplicate attributes on <{tag}>")
+ parents = set(self.stack)
+ inert = bool(parents & _INERT)
+ if "aria-hidden" in attributes and attributes["aria-hidden"] not in {"true", "false"}:
+ self.errors.append(f"invalid aria-hidden attribute on <{tag}>")
+ if tag in {"html", "head", "body"}:
+ if (
+ tag == "html" and self.stack
+ or tag in {"head", "body"} and self.stack != ["html"]
+ or tag == "body" and self.seen["head"] != 1
+ ):
+ self.errors.append(f"misplaced <{tag}> element")
+ self.seen[tag] += 1
+ if tag == "base":
+ self.errors.append(" would change local URL resolution")
+ if not inert:
+ resource = None
+ if tag == "script" and "src" in attributes:
+ resource = ("script src", attributes["src"] or "")
+ elif tag == "iframe" and attributes.get("src"):
+ resource = ("iframe src", attributes["src"])
+ elif tag == "link":
+ relations = set((attributes.get("rel") or "").casefold().split())
+ destination = (attributes.get("as") or "").casefold()
+ if "stylesheet" in relations:
+ resource = ("link stylesheet href", attributes.get("href") or "")
+ elif "preload" in relations and destination in {"style", "script"}:
+ resource = ("link preload href", attributes.get("href") or "")
+ elif "modulepreload" in relations and destination in {"", "script", "worker", "sharedworker", "serviceworker"}:
+ resource = ("link modulepreload href", attributes.get("href") or "")
+ if resource is not None:
+ if resource[1]:
+ self.targets.append(resource)
+ else:
+ self.errors.append(f"empty {resource[0]} URL")
+ if tag in {"img", "source"}:
+ for attr in ("src", "srcset"):
+ if attr not in attributes:
+ continue
+ value = attributes[attr] or ""
+ try:
+ targets = _srcset(value) if attr == "srcset" else [value]
+ for target in targets:
+ if not target:
+ raise AuditFormatError(f"empty {tag} {attr}")
+ self.targets.append((f"{tag} {attr}", target))
+ except AuditFormatError as error:
+ self.errors.append(str(error))
+ if tag == "img" and not {"src", "srcset"} & attributes.keys():
+ self.errors.append(" has neither src nor srcset")
+ if tag == "a" and "href" in attributes:
+ if "a" in parents:
+ self.errors.append("nested anchor elements")
+ target = attributes["href"] or ""
+ self.targets.append(("a href", target))
+ if tag == "link" and "canonical" in (attributes.get("rel") or "").casefold().split():
+ if "head" not in parents:
+ self.errors.append("canonical link is outside ")
+ self.canonicals.append(attributes.get("href") or "")
+ if tag == "meta" and (attributes.get("http-equiv") or "").casefold() == "refresh":
+ if "head" not in parents:
+ self.errors.append("refresh metadata is outside ")
+ self.refreshes.append(attributes.get("content") or "")
+ if tag not in _VOID:
+ self.stack.append(tag)
+
+ def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
+ if tag not in _VOID | {"svg", "math"} and not set(self.stack) & {"svg", "math"}:
+ self.errors.append(f"self-closing non-void HTML element <{tag}/>")
+ self.handle_starttag(tag, attrs)
+ foreign = next((name for name in reversed(self.stack) if name in {"svg", "math", "foreignobject"}), None)
+ if foreign not in {"svg", "math"} and self.enter_raw_content(tag):
+ return
+ if tag not in _VOID:
+ self.handle_endtag(tag)
+
+ def handle_data(self, data: str) -> None:
+ if self.cdata_elem is None:
+ validate_table_text(self.stack, data)
+
+ def handle_endtag(self, tag: str) -> None:
+ if tag == "p" and not open_p_in_scope(self.stack):
+ return
+ implied = implied_end_on_end(self.stack, tag)
+ if implied is not None:
+ del self.stack[implied:]
+ if not self.stack or self.stack[-1] != tag:
+ self.errors.append(f"unmatched closing {tag}>")
+ else:
+ self.stack.pop()
+
+ def unknown_decl(self, data: str) -> None:
+ self.errors.append(f"unknown HTML declaration: {data}")
+
+ def handle_comment(self, data: str) -> None:
+ line, column = self.getpos()
+ if not self.source_lines[line - 1][column:].startswith("\n\n[Visible](visible.md)"
+ )
+ structure = extract_markdown_structure(source)
+ assert structure.links == tuple(renderer_oracle(source).links)
+
+
+def test_link_evidence_keeps_inline_code_label_text_not_placeholder_tokens() -> None:
+ structure = extract_markdown_structure("[`A_B`](../target.md)")
+ assert structure.links == ("A_B\n../target.md",)
+
+
+@pytest.mark.parametrize("text", [
+ 'Guide ',
+ "Guide ",
+ '\nGuide\n ',
+])
+def test_link_evidence_recognizes_html_anchors(text: str) -> None:
+ assert extract_markdown_structure(text).links == ("Guide\n../target.md?a=1&b=2#part",)
+
+
+def test_link_evidence_recognizes_explicit_autolinks_but_not_placeholder_tags() -> None:
+ structure = extract_markdown_structure(" ")
+ assert structure.links == ("https://example.com/a#b\nhttps://example.com/a#b",)
+
+
+def test_plain_label_text_is_not_link_evidence() -> None:
+ assert extract_markdown_structure("tei-adapter").links == ()
+
+
+@pytest.mark.parametrize("text", [
+ r"\[Adapter](https://example.com/adapter)",
+ "\\[Adapter][ref]\n\n[ref]: https://example.com/adapter",
+ r"\",
+ r'\Adapter ',
+])
+def test_escaped_link_markup_follows_the_pinned_renderer(text: str) -> None:
+ assert extract_markdown_structure(text).links == tuple(renderer_oracle(text).links)
+
+
+def test_even_backslashes_before_a_link_do_not_escape_its_markup() -> None:
+ assert extract_markdown_structure(r"\\[Adapter](target.md)").links == ("Adapter\ntarget.md",)
+
+
+def test_reference_links_and_images_use_definitions_not_definition_prose() -> None:
+ structure = extract_markdown_structure(
+ "# Title\n\n[Guide][g] and [More][] and [Short].\n\n![Diagram][img]\n\n"
+ "[g]: ../guide.md\n[More]: https://example.com\n[Short]: #details\n"
+ '[img]: ../images/plot.svg "Caption"\n'
+ )
+ assert structure.prose == ("Guide and More and Short.",)
+ assert structure.local_link_labels == ("Guide", "Short")
+ assert structure.images == ("Diagram\nplot.svg",)
+
+
+def test_comments_and_generated_wrappers_do_not_hide_their_markdown_content() -> None:
+ plain = "# 제목\n\n본문.\n\n## 다음\n\n내용."
+ wrapped = (
+ '\n\n'
+ '# 제목\n\n본문.\n\n\n
"
+ )
+ assert extract_markdown_structure(plain) == extract_markdown_structure(wrapped)
+
+
+def test_html_comments_ignore_fake_fences_but_not_inline_code_literals() -> None:
+ structure = extract_markdown_structure(
+ "Before `` after.\n\n\n\nKept."
+ )
+ assert structure.prose == ("Before after.", "Kept.")
+ assert structure.code == ()
+
+
+def test_whitespace_line_endings_list_and_heading_numbers_normalize() -> None:
+ first = "# 1. 제목\r\n\r\n단어 둘\r\n이어짐.\r\n\r\n2. 항목\r\n3) 다음\r\n\r\n## 2.3 절"
+ second = "# 9) 제목\n\n단어 둘 이어짐.\n\n1) 항목\n8. 다음\n\n## 10.4. 절"
+ assert extract_markdown_structure(first) == extract_markdown_structure(second)
+ assert extract_markdown_structure(first).prose == ("단어 둘 이어짐.", "1. 항목", "1. 다음")
+
+
+def test_nfkc_preserves_korean_text() -> None:
+ structure = extract_markdown_structure("# 1. 제목\n\nABC 123 한글")
+ assert structure.title == "제목"
+ assert structure.prose == ("ABC 123 한글",)
+
+
+def test_underscores_in_identifiers_are_not_emphasis_or_redaction() -> None:
+ structure = extract_markdown_structure("LAB_RESOURCE_GROUP and x__y__z with _emphasis_.")
+ assert structure.prose == ("LAB_RESOURCE_GROUP and x__y__z with emphasis.",)
+
+
+def test_inline_code_is_literal_not_a_link_or_html_wrapper() -> None:
+ structure = extract_markdown_structure("Run `[label](../real.md)` and `**literal** `.")
+ assert structure.prose == ("Run [label](../real.md) and **literal** .",)
+ assert not structure.local_link_labels
+
+
+def test_heading_years_are_not_removed_as_list_numbering() -> None:
+ assert extract_markdown_structure("## 2026 findings").headings == ("2026 findings",)
+
+
+def test_setext_headings_have_the_same_text_as_atx_headings() -> None:
+ assert extract_markdown_structure("제목\n===\n\n절\n---\n\n본문") == (
+ extract_markdown_structure("# 제목\n\n## 절\n\n본문")
+ )
+
+
+def test_code_language_is_fingerprinted() -> None:
+ result = compare("```bash\nrun\n```", "```text\nrun\n```")
+ assert result.missing == (StructureFinding("code", fingerprint("code", "bash\nrun"), "bash\nrun"),)
+
+
+def test_code_indentation_and_internal_spaces_are_semantic() -> None:
+ before = "```python\nif True:\n print('a b')\n```"
+ after = "```python\nif True:\n print('a b')\n```"
+ assert len(compare(before, after).missing) == 1
+ assert extract_markdown_structure(before).code == ("python\nif True:\n print('a b')",)
+
+
+def test_code_line_endings_trailing_spaces_and_fence_style_normalize() -> None:
+ assert compare("```sh\r\nrun \r\nnext\r\n```", "~~~~sh\nrun\nnext\n~~~~").status == "preserved"
+
+
+def test_fences_protect_comments_markdown_and_shorter_fences_inside_code() -> None:
+ structure = extract_markdown_structure(
+ "````md\n# literal\n\n[x](../a.md)\n```python\nprint(1)\n```\n````"
+ )
+ assert structure.code == ("md\n# literal\n\n[x](../a.md)\n```python\nprint(1)\n```",)
+ assert not structure.title and not structure.prose and not structure.local_link_labels
+
+
+def test_unterminated_fence_does_not_discard_historical_evidence() -> None:
+ assert extract_markdown_structure("```text\nlast observation").code == ("text\nlast observation",)
+
+
+@pytest.mark.parametrize("fence", ["```", "~~~"])
+@pytest.mark.parametrize("prefix", ["> ", "> > ", " >> "])
+def test_quoted_fences_keep_language_newlines_indentation_and_surrounding_prose(
+ fence: str, prefix: str,
+) -> None:
+ text = (
+ f"{prefix}Before.\n\n{prefix}{fence}python\n"
+ f"{prefix}if ready:\n{prefix} print('two spaces')\n"
+ f"{prefix}\n{prefix}> literal code prefix\n{prefix}{fence}\n\n{prefix}After."
+ )
+ structure = extract_markdown_structure(text)
+ assert structure.code == ("python\nif ready:\n print('two spaces')\n\n> literal code prefix",)
+ assert structure.prose == ("Before.", "After.")
+
+
+def test_quoted_fence_inside_indented_list_container() -> None:
+ structure = extract_markdown_structure(
+ "- Example:\n\n > ```bash\n > run first\n > run second\n > ```\n\nAfter."
+ )
+ assert structure.code == ("bash\nrun first\n run second",)
+ assert structure.prose == ("- Example:", "After.")
+
+
+def test_quoted_code_does_not_consume_prose_after_its_container_ends() -> None:
+ structure = extract_markdown_structure("> ```sh\n> run\n\nOutside paragraph.")
+ assert structure.code == ("sh\nrun",)
+ assert structure.prose == ("Outside paragraph.",)
+
+
+def test_splitting_the_real_argocd_redis_command_changes_its_code_fingerprint() -> None:
+ path = ROOT / "docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md"
+ text = path.read_text(encoding="utf-8")
+ command = "kubectl set image deployment/argocd-redis redis=redis:8.2.3-alpine -n argocd"
+ assert f"> {command}" in text
+ broken = text.replace(f"> {command}", "> kubectl set image\n> deployment/argocd-redis redis=redis:8.2.3-alpine -n argocd")
+ result = compare(text, broken)
+ assert StructureFinding("code", fingerprint("code", f"bash\n{command}"), f"bash\n{command}") in result.missing
+
+
+def test_table_separators_are_ignored_but_all_rows_remain() -> None:
+ before = "| Header | 값 |\n|:---|---:|\n| A | 10 |\n| B | 20 |\n"
+ after = "Header|값\n-----|-----\n A | 10\n B | 20\n"
+ assert extract_markdown_structure(before).tables == ("Header | 값", "A | 10", "B | 20")
+ assert compare(before, after).status == "preserved"
+ result = compare(before, "| Header | 값 |\n|---|---|\n| A | 10 |")
+ assert [finding.category for finding in result.missing] == ["tables"]
+
+
+def test_empty_edge_table_cells_cannot_disappear_without_a_finding() -> None:
+ before = "||B|\n|---|---|\n||value|"
+ after = "|B|\n|---|\n|value|"
+ assert compare(before, after).missing
+ assert extract_markdown_structure(before).tables == (" | B", " | value")
+
+
+def test_table_literal_pipes_are_not_column_delimiters() -> None:
+ before = "| A | B |\n|---|---|\n| `x | y` | z |"
+ structure = extract_markdown_structure(before)
+ assert structure.tables[-1] == r"x \| y | z"
+ after = "| A | B |\n|---|---|\n| x | y | z |"
+ assert compare(before, after).missing[0].category == "tables"
+
+
+def test_image_fingerprints_include_alt_and_basename_not_parent_path() -> None:
+ first = ""
+ same = ''
+ assert compare(first, same).status == "preserved"
+ assert extract_markdown_structure(first).images == ("한글 그림\nplot.svg",)
+ assert compare(first, "").missing[0].category == "images"
+ assert compare(first, "").missing[0].category == "images"
+
+
+def test_linked_and_html_images_are_extracted_without_markup_prose() -> None:
+ md = "[](https://example.com)"
+ html = ' '
+ assert extract_markdown_structure(md).images == extract_markdown_structure(html).images
+ assert not extract_markdown_structure(md).prose
+ assert not extract_markdown_structure(html).prose
+
+
+def test_unquoted_html_image_attributes_keep_the_alt_and_filename() -> None:
+ baseline = " "
+ assert extract_markdown_structure(baseline).images == ("diagram\nbefore.svg",)
+ assert compare(baseline, " ").missing
+
+
+@pytest.mark.parametrize(
+ ("category", "block", "value"),
+ [
+ ("title", "# Title", "Title"),
+ ("headings", "## Heading", "Heading"),
+ ("prose", "Historical observation.", "Historical observation."),
+ ("code", "```bash\nrun\n```", "bash\nrun"),
+ ("tables", "| Row | 20 |\n|---|---|", "Row | 20"),
+ ("images", "", "Evidence\nproof.png"),
+ ("local_link_labels", "[Sample](../sample.md)", "Sample"),
+ ],
+)
+def test_each_removed_structure_has_its_own_category_and_exact_fingerprint(
+ category: str, block: str, value: str,
+) -> None:
+ result = compare(block, "")
+ assert StructureFinding(category, fingerprint(category, value), value[:160]) in result.missing
+ assert result.status == "missing"
+
+
+def test_duplicate_historical_structures_require_matching_multiplicity() -> None:
+ result = compare("Repeated.\n\nRepeated.\n\nRepeated.", "Repeated.\n\nRepeated.")
+ assert len(result.missing) == 1
+ assert result.missing[0].fingerprint == fingerprint("prose", "Repeated.")
+
+
+def test_reviewed_exception_applies_only_to_the_exact_fingerprint(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ result = compare("Removed.\n\nOther.", "Replacement.", {"prose": {fingerprint("prose", "Removed."): approval()}}, repo_root=tmp_path)
+ assert [finding.excerpt for finding in result.reviewed] == ["Removed."]
+ assert [finding.excerpt for finding in result.missing] == ["Other."]
+
+
+def test_reviewed_status_requires_no_unreviewed_missing_structure(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ result = compare("Removed.", "Replacement.", {"prose": {fingerprint("prose", "Removed."): approval()}}, repo_root=tmp_path)
+ assert result.status == "reviewed"
+ assert not result.missing and len(result.reviewed) == 1
+
+
+def test_wrong_exception_category_is_stale_not_a_cross_category_approval() -> None:
+ with pytest.raises(AuditFormatError, match="stale"):
+ compare("## Heading", "", {"prose": {fingerprint("headings", "Heading"): approval()}})
+
+
+def test_stale_exception_is_an_inventory_error_even_after_a_block_is_restored() -> None:
+ with pytest.raises(AuditFormatError, match="stale"):
+ compare("Kept.", "Kept.", {"prose": {fingerprint("prose", "Kept."): approval()}})
+
+
+@pytest.mark.parametrize("reason", ["", " ", None, "updated", "not needed", "Preserved in sample README."])
+def test_exceptions_reject_empty_or_generic_reasons(reason: object) -> None:
+ with pytest.raises(AuditFormatError, match="reason"):
+ compare("Removed.", "", {"prose": {fingerprint("prose", "Removed."): approval(reason)}})
+
+
+def test_a_path_alone_does_not_make_a_generic_updated_reason_specific() -> None:
+ with pytest.raises(AuditFormatError, match="reason"):
+ compare(
+ "Removed.", "",
+ {"prose": {fingerprint("prose", "Removed."): approval("Updated docs/services/service/topic/index.md.")}},
+ )
+
+
+def test_specific_korean_preservation_reasons_are_valid(tmp_path: Path) -> None:
+ reason = (
+ "docs/services/service/topic/index.md의 이관 전 조사 명령 절에 "
+ "원문을 보존하고 현재 가이드에서 그 역사적 증거로 연결한다."
+ )
+ proof_file(tmp_path)
+ result = compare("Removed.", "Replacement.", {"prose": {fingerprint("prose", "Removed."): approval(reason)}}, repo_root=tmp_path)
+ assert result.status == "reviewed"
+
+
+@pytest.mark.parametrize("key", ["hash", "A" * 64, "g" * 64, "a" * 63, 1])
+def test_inventory_rejects_non_sha256_exception_keys(tmp_path: Path, key: object) -> None:
+ data = yaml.safe_load((ROOT / "scripts/docs/pre_pages_inventory.yml").read_text())
+ data["version"] = 3
+ data["documents"][0]["reviewed_changes"] = {"prose": {key: approval()}}
+ path = tmp_path / "inventory.yml"
+ path.write_text(yaml.safe_dump(data, allow_unicode=True), encoding="utf-8")
+ with pytest.raises(AuditFormatError, match="fingerprint"):
+ load_inventory(path)
+
+
+def test_inventory_rejects_unknown_exception_categories(tmp_path: Path) -> None:
+ data = yaml.safe_load((ROOT / "scripts/docs/pre_pages_inventory.yml").read_text())
+ data["version"] = 3
+ data["documents"][0]["reviewed_changes"] = {"anything": {"a" * 64: approval()}}
+ path = tmp_path / "inventory.yml"
+ path.write_text(yaml.safe_dump(data, allow_unicode=True), encoding="utf-8")
+ with pytest.raises(AuditFormatError, match="category"):
+ load_inventory(path)
+
+
+def test_similarity_cannot_approve_even_a_tiny_loss() -> None:
+ common = "A detailed retained observation. " * 400
+ result = compare(common + "\n\nLost.", common)
+ assert result.text_similarity > 0.99
+ assert result.status == "missing"
+ assert [finding.excerpt for finding in result.missing] == ["Lost."]
+
+
+def test_finding_excerpts_are_limited_to_160_normalized_characters() -> None:
+ text = "단어 " * 200
+ finding = compare(text, "").missing[0]
+ assert len(finding.excerpt) == 160
+ assert finding.fingerprint == fingerprint("prose", text.strip())
+
+
+def test_public_models_are_frozen() -> None:
+ structure = extract_markdown_structure("Text.")
+ result = compare("Text.", "")
+ for model, field in ((structure, "title"), (result, "status"), (result.missing[0], "category")):
+ with pytest.raises(FrozenInstanceError):
+ setattr(model, field, "changed")
+ assert isinstance(structure, MarkdownStructure)
+ assert result.baseline_path == BASELINE_PATH and result.current_path == CURRENT_PATH
+
+
+def test_json_review_is_deterministic_and_preserves_unicode_and_findings(tmp_path: Path) -> None:
+ results = (compare("한글.", ""), compare("Kept.", "Kept."))
+ output = tmp_path / "review.json"
+ write_content_review_json(output, results)
+ original = output.read_bytes()
+ write_content_review_json(output, results)
+ assert output.read_bytes() == original
+ data = json.loads(original)
+ assert data["summary"]["documents"] == 2
+ assert data["summary"]["missing"] == 1
+ assert data["documents"][0]["baseline_path"] == str(BASELINE_PATH)
+ assert data["documents"][0]["missing"][0]["excerpt"] == "한글."
+ assert "한글".encode() in original
+
+
+def test_real_repository_preserves_the_62_baseline_documents_with_current_growth() -> None:
+ inventory = load_inventory(ROOT / "scripts/docs/pre_pages_inventory.yml")
+ results = audit_document_content(ROOT, inventory)
+ assert len(results) == 62
+ assert all((ROOT / result.current_path).is_file() for result in results)
+ assert not [(str(result.baseline_path), len(result.missing)) for result in results if result.missing]
+
+
+def test_historical_reader_map_preserves_all_six_detailed_audience_rows_in_owning_sample() -> None:
+ inventory = load_inventory(ROOT / "scripts/docs/pre_pages_inventory.yml")
+ baseline = git_text(ROOT, inventory.baseline_commit, PurePosixPath("memory/agent-memory/README.md"))
+ original_map = baseline.split("## 문서 구성\n", 1)[1].split("\n---", 1)[0]
+ topic = ROOT / "docs/services/microsoft-foundry/agent-memory"
+ sample = topic / "samples/research-artifacts/README.md"
+ restored = extract_markdown_structure(sample.read_text(encoding="utf-8"))
+ expected = extract_markdown_structure(original_map)
+ assert len(expected.tables) == 7
+ assert not (Counter(expected.tables) - Counter(restored.tables))
+ assert "문서 구성" in restored.headings
+ assert "samples/research-artifacts/README.md#historical-reader-map-pre-pages" in (
+ (topic / "index.md").read_text(encoding="utf-8")
+ )
+ assert "not a current product-support statement" in sample.read_text(encoding="utf-8")
+
+
+def isolated_real_document_audit(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, baseline: str, current: str,
+):
+ inventory = load_inventory(ROOT / "scripts/docs/pre_pages_inventory.yml")
+ entry = next(doc for doc in inventory.documents if str(doc.baseline_path) == baseline)
+ current_path = tmp_path / current
+ topic = Path(*Path(current).parts[:4])
+ shutil.copytree(ROOT / topic, tmp_path / topic)
+ track_files(tmp_path, topic.as_posix())
+ monkeypatch.setattr(
+ pre_pages_content, "resolve_current_documents",
+ lambda repo, manifest: {entry.baseline_path: load_document(current_path, tmp_path / "docs")},
+ )
+ monkeypatch.setattr(pre_pages_content, "git_text", lambda repo, commit, path: git_text(ROOT, commit, path))
+ return replace(inventory, documents=(entry,)), current_path
+
+
+def test_deleting_current_nginx_fence_invalidates_its_real_baseline_approval(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "aifoundry/codex_closednetwork.md",
+ "docs/services/microsoft-foundry/codex-closed-network/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ text, removed = re.subn(r"```nginx\n.*?\n```", "", path.read_text(), flags=re.DOTALL)
+ assert removed == 1
+ path.write_text(text, encoding="utf-8")
+ with pytest.raises(AuditFormatError, match="evidence"):
+ audit_document_content(tmp_path, inventory)
+
+
+def test_agent_memory_second_missing_06_link_cannot_reuse_one_occurrence_approval(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "memory/agent-memory/README.md",
+ "docs/services/microsoft-foundry/agent-memory/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ text = path.read_text()
+ link = "[06. 커머스 적용 설계](commerce/index.md)"
+ assert text.count(link) == 1
+ path.write_text(text.replace(link, "06. 커머스 적용 설계"), encoding="utf-8")
+ with pytest.raises(AuditFormatError, match="missing_count"):
+ audit_document_content(tmp_path, inventory)
+
+
+def test_generic_safety_prefix_does_not_justify_a_deletion() -> None:
+ with pytest.raises(AuditFormatError, match="reason"):
+ compare("Removed.", "", {
+ "prose": {fingerprint("prose", "Removed."): approval("Safety: updated and no longer needed for this content.")},
+ })
+
+
+def test_structured_approvals_and_references_are_deeply_immutable() -> None:
+ raw = approval()
+ record = BaselineDocument(BASELINE_PATH, PurePosixPath("guides/service/topic/index.md"), {
+ "prose": {fingerprint("prose", "Removed."): raw},
+ })
+ change = record.reviewed_changes["prose"][fingerprint("prose", "Removed.")]
+ assert change.missing_count == 1 and change.reason == REASON
+ assert change.evidence[0].path == CURRENT_PATH
+ raw["evidence"][0]["count"] = 9
+ assert change.evidence[0].count == 1
+ with pytest.raises(FrozenInstanceError):
+ change.missing_count = 2
+ with pytest.raises(FrozenInstanceError):
+ change.evidence[0].count = 2
+
+
+def test_bare_reason_strings_are_no_longer_approvals() -> None:
+ with pytest.raises(AuditFormatError, match="approval"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): REASON}})
+
+
+@pytest.mark.parametrize("field", ["missing_count", "reason", "evidence"])
+def test_approval_requires_all_binding_fields(field: str) -> None:
+ change = approval()
+ del change[field]
+ with pytest.raises(AuditFormatError, match=field):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): change}})
+
+
+@pytest.mark.parametrize("count", [0, -1, True, 1.5, "1", None])
+def test_missing_count_must_be_a_positive_integer(count: object) -> None:
+ with pytest.raises(AuditFormatError, match="missing_count"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(missing_count=count)}})
+
+
+@pytest.mark.parametrize("references", [[], {}, "current.md", None])
+def test_approval_requires_a_nonempty_evidence_sequence(references: object) -> None:
+ change = approval()
+ change["evidence"] = references
+ with pytest.raises(AuditFormatError, match="evidence"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): change}})
+
+
+@pytest.mark.parametrize(
+ ("field", "value"),
+ [
+ ("kind", "unknown"), ("kind", None), ("category", "unknown"),
+ ("fingerprint", "sha"), ("fingerprint", "A" * 64),
+ ("count", 0), ("count", -1), ("count", True), ("count", 1.5), ("count", "1"),
+ ("path", "/outside.md"), ("path", "../outside.md"), ("path", "a/../outside.md"),
+ ("path", "C:/outside.md"), ("path", "a\\outside.md"), ("path", ""),
+ ],
+)
+def test_invalid_structural_reference_fields_are_rejected(field: str, value: object) -> None:
+ ref = structure_evidence()
+ ref[field] = value
+ with pytest.raises(AuditFormatError, match=field):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(evidence=[ref])}})
+
+
+@pytest.mark.parametrize("field", ["path", "category", "fingerprint", "count", "kind"])
+def test_structural_reference_requires_all_fields(field: str) -> None:
+ ref = structure_evidence()
+ del ref[field]
+ with pytest.raises(AuditFormatError, match=field):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(evidence=[ref])}})
+
+
+@pytest.mark.parametrize("kind", ["structure", "file"])
+def test_duplicate_references_are_rejected_even_if_counts_or_hashes_differ(kind: str) -> None:
+ if kind == "structure":
+ first, second = structure_evidence(), structure_evidence(count=2)
+ else:
+ first = {"kind": "file", "path": "docs/services/service/topic/evidence.bin", "sha256": "a" * 64}
+ second = {**first, "sha256": "b" * 64}
+ with pytest.raises(AuditFormatError, match="duplicate"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(evidence=[first, second])}})
+
+
+@pytest.mark.parametrize(
+ "reference",
+ [
+ {"kind": "file", "path": "docs/evidence.bin"},
+ {"kind": "file", "path": "docs/evidence.bin", "sha256": "A" * 64},
+ {"kind": "file", "path": "../evidence.bin", "sha256": "a" * 64},
+ {"kind": "file", "path": "docs/evidence.bin", "sha256": "a" * 64, "count": 1},
+ ],
+)
+def test_invalid_file_evidence_is_rejected(reference: dict) -> None:
+ with pytest.raises(AuditFormatError, match="evidence"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(evidence=[reference])}})
+
+
+@pytest.mark.parametrize(
+ "reason",
+ [
+ "Safety: updated and no longer needed for this content.",
+ "Safety: updated during migration and not needed anymore.",
+ "Review: this document was revised and approved after migration.",
+ "Reason: Safety: Updated and removed because this document is no longer needed.",
+ "Preservation: updated docs/services/service/topic/index.md.",
+ "안전: 이 내용은 더 이상 필요하지 않아서 업데이트하고 삭제했습니다.",
+ "검토: 해당 문서를 최신 내용으로 갱신하고 불필요한 내용을 삭제했습니다.",
+ "보존: docs/services/service/topic/index.md 문서의 내용을 업데이트했습니다.",
+ "안전: 기존 내용에 대한 검토를 거쳐서 불필요한 부분을 적절하게 삭제했습니다.",
+ "사유: 이전 문서의 이관 과정에서 일반적으로 더 이상 쓰이지 않는 항목을 정리했습니다.",
+ ],
+)
+def test_generic_only_reasons_cannot_hide_behind_english_or_korean_prefixes(reason: str) -> None:
+ with pytest.raises(AuditFormatError, match="reason"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(reason)}})
+
+
+@pytest.mark.parametrize(
+ "reason",
+ [
+ "The nginx reverse-proxy Host header uses the example domain while retaining the upstream port.",
+ "Nginx 프록시의 Host 헤더를 예시 도메인으로 치환하고 기존 upstream 포트와 전달 설정을 보존한다.",
+ ],
+)
+def test_concrete_reasons_must_name_an_exact_evidence_path(reason: str) -> None:
+ with pytest.raises(AuditFormatError, match="reason.*path"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(reason)}})
+ reason += f" Evidence: {CURRENT_PATH}."
+ parsed = validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(reason)}})
+ assert parsed["prose"][fingerprint("prose", "Removed.")].reason == reason
+
+
+def test_one_missing_occurrence_approval_cannot_clear_two(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(missing_count=1)}}
+ with pytest.raises(AuditFormatError, match="missing_count"):
+ compare("Removed.\n\nRemoved.", "Replacement.", reviewed, repo_root=tmp_path)
+
+
+def test_exact_missing_occurrence_count_is_accepted(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(missing_count=2)}}
+ result = compare("Removed.\n\nRemoved.", "Replacement.", reviewed, repo_root=tmp_path)
+ assert not result.missing and len(result.reviewed) == 2
+
+
+def test_approvals_cannot_skip_current_evidence_verification_by_omitting_repo_root() -> None:
+ with pytest.raises(AuditFormatError, match="repo_root"):
+ compare("Removed.", "Replacement.", {"prose": {fingerprint("prose", "Removed."): approval()}})
+
+
+@pytest.mark.parametrize("mutation", ["delete", "change", "duplicate"])
+def test_structure_evidence_is_reread_and_checked_on_every_comparison(
+ tmp_path: Path, mutation: str,
+) -> None:
+ file = proof_file(tmp_path)
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval()}}
+ assert compare("Removed.", "Replacement.", reviewed, repo_root=tmp_path).status == "reviewed"
+ if mutation == "delete":
+ file.unlink()
+ else:
+ file.write_text("Changed." if mutation == "change" else "Replacement.\n\nReplacement.")
+ with pytest.raises(AuditFormatError, match="evidence"):
+ compare("Removed.", "Replacement.", reviewed, repo_root=tmp_path)
+
+
+def test_evidence_occurrence_count_requires_the_exact_current_multiplicity(tmp_path: Path) -> None:
+ file = proof_file(tmp_path, "Replacement.\n\nReplacement.")
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(evidence=[structure_evidence(count=2)])}}
+ assert compare("Removed.", file.read_text(), reviewed, repo_root=tmp_path).status == "reviewed"
+ file.write_text("Replacement.")
+ with pytest.raises(AuditFormatError, match="count"):
+ compare("Removed.", file.read_text(), reviewed, repo_root=tmp_path)
+
+
+@pytest.mark.parametrize("mutation", ["change", "delete"])
+def test_binary_file_evidence_requires_the_exact_current_sha256(tmp_path: Path, mutation: str) -> None:
+ relative = "docs/services/service/topic/samples/evidence/payload.bin"
+ path = tmp_path / relative
+ path.parent.mkdir(parents=True)
+ original = b"\x00\xffhistorical capture"
+ path.write_bytes(original)
+ track_files(tmp_path, relative)
+ ref = {"kind": "file", "path": relative, "sha256": sha256(original).hexdigest()}
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(evidence=[ref])}}
+ assert compare("Removed.", "", reviewed, repo_root=tmp_path).status == "reviewed"
+ if mutation == "delete":
+ path.unlink()
+ else:
+ path.write_bytes(b"\x00changed")
+ with pytest.raises(AuditFormatError, match="evidence"):
+ compare("Removed.", "", reviewed, repo_root=tmp_path)
+
+
+@pytest.mark.parametrize("kind", ["structure", "file"])
+def test_evidence_cannot_escape_the_repository_through_a_symlink(tmp_path: Path, kind: str) -> None:
+ outside = tmp_path.parent / f"{tmp_path.name}-outside.md"
+ outside.write_text("Replacement.")
+ relative = "docs/services/service/topic/link.md"
+ path = tmp_path / relative
+ path.parent.mkdir(parents=True)
+ path.symlink_to(outside)
+ track_files(tmp_path, relative)
+ ref = structure_evidence(path=relative) if kind == "structure" else {
+ "kind": "file", "path": relative, "sha256": sha256(outside.read_bytes()).hexdigest(),
+ }
+ with pytest.raises(AuditFormatError, match="outside"):
+ compare("Removed.", "", {"prose": {fingerprint("prose", "Removed."): approval(evidence=[ref])}}, repo_root=tmp_path)
+
+
+def test_subscription_row_approval_binds_the_subscription_not_an_unrelated_public_ip_row() -> None:
+ inventory = load_inventory(ROOT / "scripts/docs/pre_pages_inventory.yml")
+ entry = next(d for d in inventory.documents if str(d.baseline_path) == "monitor/otel-monitor-setup.md")
+ baseline = extract_markdown_structure(git_text(ROOT, inventory.baseline_commit, entry.baseline_path))
+ row = next(row for row in baseline.tables if row.startswith("구독 |"))
+ change = entry.reviewed_changes["tables"][fingerprint("tables", row)]
+ assert any(
+ ref.kind == "structure" and ref.category == "tables"
+ and ref.fingerprint == fingerprint("tables", "구독 | ")
+ for ref in change.evidence
+ )
+
+
+def test_each_historical_sre_thread_approval_binds_its_corresponding_scenario() -> None:
+ inventory = load_inventory(ROOT / "scripts/docs/pre_pages_inventory.yml")
+ entry = next(d for d in inventory.documents if str(d.baseline_path) == "monitor/sre-agent-event-lab/validation-results.md")
+ baseline = extract_markdown_structure(git_text(ROOT, inventory.baseline_commit, entry.baseline_path))
+ threads = [value for value in baseline.prose if value.startswith("- Agent thread:")]
+ assert len(threads) == 3
+ for scenario, value in enumerate(threads, 1):
+ change = entry.reviewed_changes["prose"][fingerprint("prose", value)]
+ assert any(
+ ref.kind == "structure" and ref.category == "prose"
+ and ref.fingerprint == fingerprint("prose", f"- Agent thread: ")
+ for ref in change.evidence
+ )
+
+
+def test_removing_real_tei_adapter_link_markup_invalidates_its_approval(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "aisearch/custom_vectorization/01_custom_embedding_guide.md",
+ "docs/services/azure-ai-search/custom-vectorization/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ link = (
+ "[tei-adapter](https://github.com/hellices/devguidesample/tree/main/"
+ "docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter)"
+ )
+ text = path.read_text()
+ assert text.count(link) == 1
+ path.write_text(text.replace(link, "tei-adapter"))
+ assert (path.parent / "samples/implementation/tei-adapter/app.py").is_file()
+ with pytest.raises(AuditFormatError, match="evidence|links"):
+ audit_document_content(tmp_path, inventory)
+
+
+def test_ignored_baseline_snapshot_cannot_substitute_for_current_public_evidence(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ (tmp_path / ".gitignore").write_text(".superpowers/\n")
+ snapshot = tmp_path / ".superpowers/sdd/baseline.md"
+ snapshot.parent.mkdir(parents=True)
+ snapshot.write_text("Removed.")
+ reason = (
+ "The historical diagnostic commands are retained in .superpowers/sdd/baseline.md "
+ "as replacement evidence for the removed guide section."
+ )
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(
+ reason, evidence=[structure_evidence("Removed.", path=".superpowers/sdd/baseline.md")],
+ )}}
+ with pytest.raises(AuditFormatError, match="public|eligible|forbidden|tracked"):
+ compare("Removed.", "", reviewed, repo_root=tmp_path)
+
+
+def test_git_metadata_cannot_be_used_as_file_evidence(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ metadata = tmp_path / ".git/HEAD"
+ ref = {"kind": "file", "path": ".git/HEAD", "sha256": sha256(metadata.read_bytes()).hexdigest()}
+ reason = (
+ "The current .git/HEAD file is preserved as replacement evidence for the removed "
+ "deployment procedure and its execution context."
+ )
+ with pytest.raises(AuditFormatError, match="public|eligible|forbidden|tracked"):
+ compare("Removed.", "", {"prose": {fingerprint("prose", "Removed."): approval(reason, evidence=[ref])}}, repo_root=tmp_path)
+
+
+@pytest.mark.parametrize("include_path", [False, True])
+@pytest.mark.parametrize("reason", [
+ "Safety: this obsolete material is irrelevant and therefore unnecessary.",
+ "Nothing significant has changed; approved after review.",
+ "Replaced with a suitable equivalent for improved presentation.",
+])
+def test_rereview_generic_examples_are_rejected_even_with_an_exact_path(reason: str, include_path: bool) -> None:
+ if include_path:
+ reason += f" Evidence: {CURRENT_PATH}."
+ with pytest.raises(AuditFormatError, match="reason"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(reason)}})
+
+
+@pytest.mark.parametrize("category", ["prose", "local_link_labels", "title"])
+def test_replacement_link_approvals_require_destination_bound_links_evidence(category: str) -> None:
+ change = approval(evidence=[structure_evidence("Adapter", category)])
+ with pytest.raises(AuditFormatError, match="links"):
+ validate_reviewed_changes({"local_link_labels": {fingerprint("local_link_labels", "Adapter"): change}})
+
+
+def test_links_cannot_be_used_as_a_baseline_exception_category() -> None:
+ with pytest.raises(AuditFormatError, match="category"):
+ validate_reviewed_changes({"links": {fingerprint("links", "Adapter\nold.md"): approval()}})
+
+
+@pytest.mark.parametrize("mutation", ["plain-label", "changed-destination", "changed-fragment"])
+def test_exact_current_link_evidence_invalidates_on_markup_or_destination_changes(
+ tmp_path: Path, mutation: str,
+) -> None:
+ current = "[Adapter](https://example.com/adapter#setup)"
+ path = proof_file(tmp_path, current)
+ change = approval(evidence=[structure_evidence("Adapter\nhttps://example.com/adapter#setup", "links")])
+ reviewed = {"local_link_labels": {fingerprint("local_link_labels", "Adapter"): change}}
+ assert compare("[Adapter](adapter.md)", current, reviewed, repo_root=tmp_path).status == "reviewed"
+ changed = {
+ "plain-label": "Adapter",
+ "changed-destination": "[Adapter](https://example.com/other#setup)",
+ "changed-fragment": "[Adapter](https://example.com/adapter#other)",
+ }[mutation]
+ path.write_text(changed)
+ with pytest.raises(AuditFormatError, match="evidence.*links|links.*count"):
+ compare("[Adapter](adapter.md)", changed, reviewed, repo_root=tmp_path)
+
+
+def test_link_evidence_requires_exact_current_multiplicity(tmp_path: Path) -> None:
+ current = "[Adapter](https://example.com/adapter)"
+ path = proof_file(tmp_path, current + "\n\n" + current)
+ change = approval(missing_count=2, evidence=[
+ structure_evidence("Adapter\nhttps://example.com/adapter", "links", count=2),
+ ])
+ reviewed = {"local_link_labels": {fingerprint("local_link_labels", "Adapter"): change}}
+ baseline = "[Adapter](adapter.md)\n\n[Adapter](adapter.md)"
+ assert compare(baseline, path.read_text(), reviewed, repo_root=tmp_path).status == "reviewed"
+ path.write_text(current + "\n\nAdapter")
+ with pytest.raises(AuditFormatError, match="count"):
+ compare(baseline, path.read_text(), reviewed, repo_root=tmp_path)
+
+
+def test_duplicate_link_evidence_is_rejected_even_with_different_counts() -> None:
+ first = structure_evidence("Adapter\nhttps://example.com/adapter", "links")
+ change = approval(evidence=[first, {**first, "count": 2}])
+ with pytest.raises(AuditFormatError, match="duplicate"):
+ validate_reviewed_changes({"local_link_labels": {fingerprint("local_link_labels", "Adapter"): change}})
+
+
+@pytest.mark.parametrize("count", [0, -1, True, "1"])
+def test_link_evidence_counts_must_be_positive_integers(count: object) -> None:
+ change = approval(evidence=[structure_evidence("Adapter\nhttps://example.com/adapter", "links", count=count)])
+ with pytest.raises(AuditFormatError, match="count"):
+ validate_reviewed_changes({"local_link_labels": {fingerprint("local_link_labels", "Adapter"): change}})
+
+
+def self_link_approval() -> dict:
+ return approval(
+ f"The redundant self-link to this same guide is removed; its current title and Files navigation are retained in {CURRENT_PATH}.",
+ evidence=[
+ structure_evidence("Current guide", "title"),
+ structure_evidence("Files", "headings"),
+ ],
+ link_change="redundant-self-link",
+ )
+
+
+def test_intentionally_removed_self_link_is_bound_to_current_document_identity(tmp_path: Path) -> None:
+ current = "# Current guide\n\n## Files\n\nguide.md"
+ proof_file(tmp_path, current)
+ baseline = current.replace("\nguide.md", "\n[guide.md](guide.md)")
+ reviewed = {"local_link_labels": {fingerprint("local_link_labels", "guide.md"): self_link_approval()}}
+ assert compare(baseline, current, reviewed, repo_root=tmp_path).status == "reviewed"
+
+
+@pytest.mark.parametrize("destination", ["other.md", "guide.md#files", "guide.md?view=text"])
+def test_self_link_removal_exception_cannot_approve_another_target_or_section(
+ tmp_path: Path, destination: str,
+) -> None:
+ current = "# Current guide\n\n## Files\n\nguide.md"
+ proof_file(tmp_path, current)
+ baseline = current.replace("\nguide.md", f"\n[guide.md]({destination})")
+ with pytest.raises(AuditFormatError, match="self-link"):
+ compare(baseline, current, {"local_link_labels": {
+ fingerprint("local_link_labels", "guide.md"): self_link_approval(),
+ }}, repo_root=tmp_path)
+
+
+def test_self_link_removal_cannot_bind_only_an_unrelated_document(tmp_path: Path) -> None:
+ current = "# Current guide\n\n## Files\n\nguide.md"
+ proof_file(tmp_path, current)
+ other = "docs/services/service/other/index.md"
+ proof_file(tmp_path, current, other)
+ change = self_link_approval()
+ for reference in change["evidence"]:
+ reference["path"] = other
+ change["reason"] += f" Identity evidence: {other}."
+ with pytest.raises(AuditFormatError, match="self-link.*current|current.*identity"):
+ compare(current.replace("\nguide.md", "\n[guide.md](guide.md)"), current, {
+ "local_link_labels": {fingerprint("local_link_labels", "guide.md"): change},
+ }, repo_root=tmp_path)
+
+
+def test_self_link_removal_requires_both_title_and_topic_structure() -> None:
+ change = self_link_approval()
+ change["evidence"] = change["evidence"][:1]
+ with pytest.raises(AuditFormatError, match="self-link|identity"):
+ validate_reviewed_changes({"local_link_labels": {fingerprint("local_link_labels", "guide.md"): change}})
+
+
+def test_self_link_removal_cannot_pretend_an_unrelated_replacement_hyperlink_exists() -> None:
+ change = self_link_approval()
+ change["evidence"].append(structure_evidence("Other\nother.md", "links"))
+ with pytest.raises(AuditFormatError, match="self-link"):
+ validate_reviewed_changes({"local_link_labels": {fingerprint("local_link_labels", "guide.md"): change}})
+
+
+def test_self_link_removal_requires_a_specific_removal_reason() -> None:
+ change = self_link_approval()
+ change["reason"] = f"The Nginx Host header retains the example domain and upstream port in {CURRENT_PATH}."
+ with pytest.raises(AuditFormatError, match="self-link.*reason|reason.*self-link"):
+ validate_reviewed_changes({"local_link_labels": {fingerprint("local_link_labels", "guide.md"): change}})
+
+
+@pytest.mark.parametrize("category", ["prose", "code", "tables"])
+def test_link_change_exemptions_are_invalid_on_non_link_categories(category: str) -> None:
+ with pytest.raises(AuditFormatError, match="link_change"):
+ validate_reviewed_changes({category: {fingerprint(category, "Removed."): self_link_approval()}})
+
+
+@pytest.mark.parametrize("tracked", [False, True])
+def test_ignored_public_file_is_ineligible_even_if_force_tracked_at_head(tmp_path: Path, tracked: bool) -> None:
+ proof_file(tmp_path)
+ relative = "docs/services/service/topic/ignored.md"
+ path = tmp_path / relative
+ path.write_text("Replacement.")
+ (tmp_path / ".gitignore").write_text("**/ignored.md\n")
+ if tracked:
+ git(tmp_path, "add", "-f", "--", relative)
+ git(tmp_path, "-c", "commit.gpgsign=false", "commit", "-qm", "Track ignored fixture")
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(evidence=[structure_evidence(path=relative)])}}
+ with pytest.raises(AuditFormatError, match="ignored|HEAD"):
+ compare("Removed.", "", reviewed, repo_root=tmp_path)
+
+
+@pytest.mark.parametrize("staged", [False, True])
+def test_untracked_or_index_only_public_file_is_not_head_evidence(tmp_path: Path, staged: bool) -> None:
+ proof_file(tmp_path)
+ relative = "docs/services/service/topic/untracked.md"
+ (tmp_path / relative).write_text("Replacement.")
+ if staged:
+ git(tmp_path, "add", "--", relative)
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(evidence=[structure_evidence(path=relative)])}}
+ with pytest.raises(AuditFormatError, match="HEAD"):
+ compare("Removed.", "", reviewed, repo_root=tmp_path)
+
+
+@pytest.mark.parametrize("relative", [
+ "README.md", "scripts/snapshot.md", "baseline/guide.md",
+ ".devcontainer/README.md.backup", "docs/services/service/topic/.superpowers/snapshot.md",
+])
+def test_head_tracked_paths_outside_the_explicit_public_allowlist_are_ineligible(
+ tmp_path: Path, relative: str,
+) -> None:
+ proof_file(tmp_path, "Replacement.", relative)
+ with pytest.raises(AuditFormatError, match="eligible|public|forbidden"):
+ compare("Removed.", "", {"prose": {fingerprint("prose", "Removed."): approval(
+ evidence=[structure_evidence(path=relative)],
+ )}}, repo_root=tmp_path)
+
+
+def test_exact_tracked_devcontainer_readme_is_valid_and_working_tree_mutation_is_detected(tmp_path: Path) -> None:
+ relative = ".devcontainer/README.md"
+ file = proof_file(tmp_path, "Developer setup commands.", relative)
+ ref = {"kind": "file", "path": relative, "sha256": sha256(file.read_bytes()).hexdigest()}
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(evidence=[ref])}}
+ assert compare("Removed.", "", reviewed, repo_root=tmp_path).status == "reviewed"
+ file.write_text("Developer commands deleted.")
+ with pytest.raises(AuditFormatError, match="evidence.*SHA-256"):
+ compare("Removed.", "", reviewed, repo_root=tmp_path)
+
+
+def test_public_tracked_symlink_cannot_resolve_to_a_private_snapshot(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ snapshot = tmp_path / ".superpowers/sdd/baseline.md"
+ snapshot.parent.mkdir(parents=True)
+ snapshot.write_text("Replacement.")
+ relative = "docs/services/service/topic/alias.md"
+ (tmp_path / relative).symlink_to(snapshot)
+ track_files(tmp_path, relative)
+ with pytest.raises(AuditFormatError, match="public|eligible|forbidden|HEAD"):
+ compare("Removed.", "", {"prose": {fingerprint("prose", "Removed."): approval(
+ evidence=[structure_evidence(path=relative)],
+ )}}, repo_root=tmp_path)
+
+
+def test_public_symlink_to_public_head_tracked_source_preserves_mutation_checks(tmp_path: Path) -> None:
+ target = proof_file(tmp_path)
+ relative = "docs/services/service/topic/alias.md"
+ (tmp_path / relative).symlink_to(target)
+ track_files(tmp_path, relative)
+ reviewed = {"prose": {fingerprint("prose", "Removed."): approval(evidence=[structure_evidence(path=relative)])}}
+ assert compare("Removed.", "", reviewed, repo_root=tmp_path).status == "reviewed"
+ target.write_text("Changed.")
+ with pytest.raises(AuditFormatError, match="evidence"):
+ compare("Removed.", "", reviewed, repo_root=tmp_path)
+
+
+def test_worktree_git_pointer_is_not_public_file_evidence(tmp_path: Path) -> None:
+ proof_file(tmp_path)
+ linked = tmp_path / "linked"
+ git(tmp_path, "worktree", "add", "-q", "--detach", str(linked))
+ metadata = linked / ".git"
+ assert metadata.is_file()
+ ref = {"kind": "file", "path": ".git", "sha256": sha256(metadata.read_bytes()).hexdigest()}
+ with pytest.raises(AuditFormatError, match="public|eligible|forbidden"):
+ compare("Removed.", "", {"prose": {fingerprint("prose", "Removed."): approval(
+ "The exact .git worktree pointer is retained as evidence for the removed deployment procedure.",
+ evidence=[ref],
+ )}}, repo_root=linked)
+
+
+@pytest.mark.parametrize("mentioned", [
+ "docs/services/service/topic/index.md.backup",
+ "docs/services/service/topic/index.md/another",
+ "docs/services/service/topic/INDEX.md",
+])
+def test_reason_must_name_an_exact_reference_not_a_prefix_or_different_case(mentioned: str) -> None:
+ reason = f"The Nginx Host header retains its example domain and upstream port in {mentioned}."
+ with pytest.raises(AuditFormatError, match="reason.*path"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(reason)}})
+
+
+def test_devcontainer_path_itself_cannot_make_a_generic_reason_concrete() -> None:
+ reference = structure_evidence(path=".devcontainer/README.md")
+ reason = "Safety: updated and no longer needed for this content. Evidence: .devcontainer/README.md."
+ with pytest.raises(AuditFormatError, match="reason"):
+ validate_reviewed_changes({"prose": {fingerprint("prose", "Removed."): approval(reason, evidence=[reference])}})
+
+
+def test_real_tei_link_cannot_be_forged_inside_a_span_attribute(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "aisearch/custom_vectorization/01_custom_embedding_guide.md",
+ "docs/services/azure-ai-search/custom-vectorization/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ destination = (
+ "https://github.com/hellices/devguidesample/tree/main/docs/services/azure-ai-search/"
+ "custom-vectorization/samples/implementation/tei-adapter"
+ )
+ link = f"[tei-adapter]({destination})"
+ source = path.read_text()
+ assert source.count(link) == 1
+ path.write_text(source.replace(link, f'tei-adapter '))
+ with pytest.raises(AuditFormatError, match="evidence.*links|links.*count"):
+ audit_document_content(tmp_path, inventory)
+
+
+def test_real_s1_escaped_image_invalidates_its_current_image_approval(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "monitor/sre-agent-event-lab/validation-results.md",
+ "docs/services/azure-monitor/azure-sre-agent/validation-results/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ image = ""
+ source = path.read_text()
+ assert source.count(image) == 1
+ path.write_text(source.replace(image, "\\" + image))
+ with pytest.raises(AuditFormatError, match="evidence.*images|images.*count"):
+ audit_document_content(tmp_path, inventory)
+
+
+@pytest.mark.parametrize("attribute", [
+ 'data-link="[Fake](https://example.com/fake)"',
+ "data-link='[Fake](https://example.com/fake)'",
+ "data-link=[Fake](https://example.com/fake)",
+ 'data-image=""',
+ "data-image=",
+ 'data-auto=""',
+ 'data-reference="[Fake][ref]"',
+])
+@pytest.mark.parametrize("tag", ["span", "custom-widget"])
+def test_markdown_evidence_is_never_scanned_inside_html_attributes(tag: str, attribute: str) -> None:
+ text = f"<{tag} {attribute}>Visible{tag}>\n\n[ref]: https://example.com/fake"
+ structure = extract_markdown_structure(text)
+ assert structure.links == ()
+ assert structure.images == ()
+
+
+def test_attribute_reference_definitions_cannot_create_links_outside_the_tag() -> None:
+ structure = extract_markdown_structure(
+ 'Visible \n\n[ref]'
+ )
+ assert structure.links == ()
+
+
+def test_attribute_markdown_blocks_are_masked_before_structure_extraction() -> None:
+ structure = extract_markdown_structure(
+ 'Visible '
+ )
+ assert not structure.headings and not structure.code and not structure.tables
+
+
+@pytest.mark.parametrize("tag", [
+ "script", "style", "code", "pre", "textarea", "title", "xmp", "iframe",
+ "noembed", "noframes", "template",
+])
+def test_raw_or_code_html_containers_do_not_supply_markdown_or_nested_html_evidence(tag: str) -> None:
+ structure = extract_markdown_structure(
+ f'<{tag}>[Fake](https://example.com/fake) \n\n'
+ 'Fake \n\n'
+ f'```sh\nfake command\n```\n{tag}>\n\n[Real](real.md)'
+ )
+ assert structure.links == ("Real\nreal.md",)
+ assert structure.images == () and structure.code == ()
+
+
+def test_genuine_html_anchor_uses_rendered_nested_text_and_real_href_only() -> None:
+ structure = extract_markdown_structure(
+ ''
+ 'Read now '
+ ''
+ ' '
+ )
+ assert structure.links == ("Read now\nactual.md?x=1&y=2#part",)
+ assert structure.images == ()
+
+
+def test_html_anchor_text_is_not_reinterpreted_as_markdown() -> None:
+ structure = extract_markdown_structure(
+ '**literal** [Text](not-a-link.md) '
+ )
+ assert structure.links == ("literal[Text](not-a-link.md)\nactual.md",)
+ assert structure.images == ()
+
+
+def test_genuine_html_image_uses_actual_alt_and_src_not_attribute_markdown() -> None:
+ structure = extract_markdown_structure(
+ ' '
+ )
+ assert structure.links == ()
+ assert structure.images == ("**Literal** [label](not-link.md)\nactual.png",)
+
+
+def test_html_anchor_and_image_attributes_keep_first_duplicate_values() -> None:
+ structure = extract_markdown_structure(
+ 'Actual '
+ ' '
+ )
+ assert structure.links == ("Actual\nfirst.md",)
+ assert structure.images == ("First\nfirst.png",)
+
+
+def test_genuine_html_image_alt_text_labels_its_anchor() -> None:
+ source = (
+ ' '
+ )
+ structure = extract_markdown_structure(source)
+ expected = renderer_oracle(' ')
+ assert structure.links == tuple(expected.links)
+ assert structure.images == ("Actual\nactual.png",)
+
+
+@pytest.mark.parametrize("slashes", range(5))
+@pytest.mark.parametrize("style", ["inline", "reference", "collapsed", "shortcut"])
+def test_image_openers_obey_escape_parity_for_all_markdown_styles(slashes: int, style: str) -> None:
+ image = {
+ "inline": "",
+ "reference": "![Alt][image]",
+ "collapsed": "![Alt][]",
+ "shortcut": "![Alt]",
+ }[style]
+ definitions = "\n\n[image]: images/picture.png\n[Alt]: images/picture.png"
+ structure = extract_markdown_structure("\\" * slashes + image + definitions)
+ assert structure.images == (("Alt\npicture.png",) if slashes % 2 == 0 else ())
+
+
+@pytest.mark.parametrize("slashes", range(4))
+@pytest.mark.parametrize("image", ["", "![Alt][asset]"])
+def test_nested_linked_image_escape_parity(slashes: int, image: str) -> None:
+ text = "[" + "\\" * slashes + image + "](target.md)\n\n[asset]: images/picture.png"
+ structure = extract_markdown_structure(text)
+ assert structure.images == (("Alt\npicture.png",) if slashes % 2 == 0 else ())
+ assert structure.links == tuple(renderer_oracle(text).links)
+
+
+def test_escaped_image_bang_leaves_an_ordinary_link_not_an_image() -> None:
+ structure = extract_markdown_structure(r"\")
+ assert structure.images == ()
+ assert structure.links == ("Alt\nimages/picture.png",)
+
+
+@pytest.mark.parametrize("tag", ["noscript", "plaintext"])
+def test_additional_raw_html_containers_do_not_supply_evidence(tag: str) -> None:
+ structure = extract_markdown_structure(
+ f'<{tag}>[Fake](fake.md)  Fake {tag}>'
+ )
+ assert structure.links == () and structure.images == ()
+
+
+def test_html_tag_tokens_inside_markdown_image_alt_or_destination_do_not_emit_elements() -> None:
+ source = (
+ ' '
+ '[Actual]( )'
+ )
+ structure = extract_markdown_structure(source)
+ oracle = renderer_oracle(source)
+ assert structure.images == tuple(oracle.images)
+ assert structure.links == tuple(oracle.links)
+
+
+def test_html_anchor_attributes_cannot_forge_a_closing_tag_or_child_image() -> None:
+ structure = extract_markdown_structure(
+ ''
+ 'Actual '
+ )
+ assert structure.links == ("Actual\nactual.md",)
+ assert structure.images == ()
+
+
+def test_html_anchor_text_decodes_entities_once() -> None:
+ structure = extract_markdown_structure('<literal> ')
+ assert structure.links == ("<literal>\nactual.md",)
+
+
+def test_html_text_cannot_collide_with_inline_code_placeholder_tokens() -> None:
+ marker = "\ue0000\ue001"
+ structure = extract_markdown_structure(f'`Expected` {marker} ')
+ assert structure.links == (f"{marker}\nactual.md",)
+
+
+@pytest.mark.parametrize("tag", ["script", "style", "code", "pre", "textarea", "noscript"])
+def test_nonvoid_html_slash_does_not_expose_raw_content_to_markdown(tag: str) -> None:
+ structure = extract_markdown_structure(
+ f'<{tag}/>[Fake](fake.md) {tag}>\n\n[Real](real.md)'
+ )
+ assert structure.links == ("Real\nreal.md",)
+ assert structure.images == ()
+
+
+def test_self_closing_syntax_on_nonvoid_anchor_children_keeps_actual_rendered_text() -> None:
+ structure = extract_markdown_structure(
+ ' Actual[Hidden](hidden.md) text '
+ )
+ assert structure.links == ("Actual text\nactual.md",)
+ assert structure.images == ()
+
+
+@pytest.mark.parametrize("title", [
+ "", ' "valid quoted title"', " 'valid single-quoted title'", " (valid parenthesized title)",
+ " arbitrary unquoted trailing description",
+])
+@pytest.mark.parametrize("image", [False, True])
+def test_semantic_reference_definitions_match_the_pinned_renderer(title: str, image: bool) -> None:
+ marker = "!" if image else ""
+ source = f"{marker}[Capture][ref]\n\n[ref]: capture.gif{title}"
+ oracle = renderer_oracle(source)
+ structure = extract_markdown_structure(source)
+ assert structure.links == tuple(oracle.links)
+ assert structure.images == tuple(oracle.images)
+ expected_local = tuple(link.partition("\n")[0] for link in oracle.links if not urlsplit(link.partition("\n")[2]).scheme)
+ assert structure.local_link_labels == expected_local
+
+
+@pytest.mark.parametrize("source", [
+ '[A](target.md "quoted title")',
+ "[A](target.md 'single title')",
+ "[A](target.md (parenthesized title))",
+ "",
+ "[](details.md)",
+ '[Before  after](details.md)',
+ 'Before after ',
+ 'Plain ',
+ '[Fake](fake.md) ',
+ '',
+ '',
+ r"\",
+ "`[Fake](fake.md) `",
+ "`literal ```  tail`",
+])
+def test_semantic_elements_are_derived_from_renderer_output(source: str) -> None:
+ oracle = renderer_oracle(source)
+ structure = extract_markdown_structure(source)
+ assert structure.links == tuple(oracle.links)
+ assert structure.images == tuple(oracle.images)
+
+
+@pytest.mark.parametrize(("source", "expected"), [
+ ("`literal ```  tail`", "literal ```  tail"),
+ ("``literal ` one ``` two``", "literal ` one ``` two"),
+ ("```literal `` short ```` long```", "literal `` short ```` long"),
+ ("` line one\nline two `", "line one line two"),
+ ("`` padded ``", " padded "),
+ ("` `", " "),
+])
+def test_source_code_spans_use_exact_delimiter_runs_and_commonmark_whitespace(source: str, expected: str) -> None:
+ structure = extract_markdown_structure(source)
+ assert structure.prose == (expected,)
+ oracle = renderer_oracle(source)
+ assert structure.images == tuple(oracle.images)
+ assert structure.links == tuple(oracle.links)
+
+
+@pytest.mark.parametrize("source", [
+ "`unmatched ",
+ "``unmatched `short` ",
+ "````unmatched ```short``` ",
+ "````",
+])
+def test_unmatched_code_span_runs_follow_renderer_image_visibility(source: str) -> None:
+ structure = extract_markdown_structure(source)
+ oracle = renderer_oracle(source)
+ assert structure.images == tuple(oracle.images)
+ assert structure.links == tuple(oracle.links)
+
+
+def test_source_code_span_masks_pipes_until_an_exact_closer() -> None:
+ source = "`literal ```\n| A | B |\n tail`"
+ structure = extract_markdown_structure(source)
+ assert structure.title == "" and structure.headings == () and structure.tables == ()
+ assert structure.prose == ("literal ``` | A | B | tail",)
+
+
+def test_unmatched_inline_code_does_not_cross_real_heading_or_paragraph_boundaries() -> None:
+ source = "`unmatched\n## Heading\n\n tail`"
+ structure = extract_markdown_structure(source)
+ assert structure.headings == ("Heading",)
+ assert structure.images == tuple(renderer_oracle(source).images)
+
+
+def test_real_tei_malformed_reference_definition_cannot_replace_the_hyperlink(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "aisearch/custom_vectorization/01_custom_embedding_guide.md",
+ "docs/services/azure-ai-search/custom-vectorization/index.md",
+ )
+ destination = "https://github.com/hellices/devguidesample/tree/main/docs/services/azure-ai-search/custom-vectorization/samples/implementation/tei-adapter"
+ old = f"[tei-adapter]({destination})"
+ source = path.read_text()
+ assert source.count(old) == 1
+ changed = source.replace(old, "[tei-adapter][malformed-tei]") + f"\n\n[malformed-tei]: {destination} arbitrary unquoted description\n"
+ assert f"tei-adapter\n{destination}" not in renderer_oracle(changed).links
+ path.write_text(changed)
+ with pytest.raises(AuditFormatError, match="evidence.*links|links.*count"):
+ audit_document_content(tmp_path, inventory)
+
+
+@pytest.mark.parametrize("kind", ["image", "link", "code-span"])
+def test_real_s1_inline_parser_mutations_cannot_supply_rendered_evidence(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, kind: str,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "monitor/sre-agent-event-lab/validation-results.md",
+ "docs/services/azure-monitor/azure-sre-agent/validation-results/index.md",
+ )
+ source = path.read_text()
+ image = ""
+ if kind == "image":
+ changed = source.replace(image, "![S1 SRE Agent investigation][malformed-s1]") + "\n\n[malformed-s1]: images/s1-investigation.gif arbitrary unquoted description\n"
+ assert "S1 SRE Agent investigation\ns1-investigation.gif" not in renderer_oracle(changed).images
+ elif kind == "link":
+ destination = "https://github.com/hellices/devguidesample/blob/main/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/assets/captures/s1/07-conclusion.png"
+ link = f"[결론 frame]({destination})"
+ assert source.count(link) == 1
+ changed = source.replace(link, "[결론 frame][malformed-s1]") + f"\n\n[malformed-s1]: {destination} arbitrary unquoted description\n"
+ assert f"결론 frame\n{destination}" not in renderer_oracle(changed).links
+ else:
+ assert source.count(image) == 1
+ changed = source.replace(image, f"`literal ``` {image} tail`")
+ assert "S1 SRE Agent investigation\ns1-investigation.gif" not in renderer_oracle(changed).images
+ path.write_text(changed)
+ with pytest.raises(AuditFormatError, match="evidence"):
+ audit_document_content(tmp_path, inventory)
+
+
+def test_unrendered_memray_details_reference_is_not_a_historical_hyperlink_loss() -> None:
+ inventory = load_inventory(ROOT / "scripts/docs/pre_pages_inventory.yml")
+ entry = next(d for d in inventory.documents if str(d.baseline_path) == "aks/memray_leak_profiling.md")
+ baseline = git_text(ROOT, inventory.baseline_commit, entry.baseline_path)
+ current = (ROOT / "docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md").read_text()
+ for source in (baseline, current):
+ oracle = renderer_oracle(source)
+ assert not any(link.partition("\n")[0] == "Dockerfile" for link in oracle.links)
+ assert "Dockerfile" not in extract_markdown_structure(source).local_link_labels
+ assert "[Dockerfile](" in source
+ assert fingerprint("local_link_labels", "Dockerfile") not in entry.reviewed_changes["local_link_labels"]
+ payload = PurePosixPath("docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/Dockerfile")
+ assert any(ref.path == payload for group in entry.reviewed_changes.values() for change in group.values() for ref in change.evidence)
+
+
+def test_raw_html_attribute_autolinks_do_not_change_a_genuine_anchor() -> None:
+ source = 'Actual '
+ expected = renderer_oracle('Actual ')
+ structure = extract_markdown_structure(source)
+ assert structure.links == tuple(expected.links)
+
+
+def test_malformed_reference_definition_remains_source_prose() -> None:
+ source = "[Capture][ref]\n\n[ref]: capture.gif arbitrary unquoted description"
+ structure = extract_markdown_structure(source)
+ assert structure.prose == ("[Capture][ref]", "[ref]: capture.gif arbitrary unquoted description")
+ assert structure.links == tuple(renderer_oracle(source).links)
+ assert structure.images == tuple(renderer_oracle(source).images)
+
+
+def test_table_code_span_escaped_pipe_is_unescaped_once_after_literal_restoration() -> None:
+ source = "| Command | Result |\n|---|---|\n| `ps aux \\| grep MetricsExtension` | present |"
+ structure = extract_markdown_structure(source)
+ assert structure.tables[-1] == r"ps aux \| grep MetricsExtension | present"
+ assert structure.links == tuple(renderer_oracle(source).links)
+ assert structure.images == tuple(renderer_oracle(source).images)
+
+
+def test_real_s1_src_attribute_override_invalidates_the_existing_image_approval(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "monitor/sre-agent-event-lab/validation-results.md",
+ "docs/services/azure-monitor/azure-sre-agent/validation-results/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ image = ""
+ source = path.read_text()
+ assert source.count(image) == 1
+ changed = source.replace(image, image + '{: src="images/investigation.gif"}')
+ path.write_text(changed)
+ expected = "S1 SRE Agent investigation\ninvestigation.gif"
+ assert expected in renderer_oracle(changed).images
+ assert expected in extract_markdown_structure(changed).images
+ with pytest.raises(AuditFormatError, match="stale.*images|evidence.*images|images.*count"):
+ audit_document_content(tmp_path, inventory)
+
+
+@pytest.mark.parametrize(
+ ("source", "links", "images"),
+ [
+ ('{: src="images/investigation.gif"}', (), ("S1\ninvestigation.gif",)),
+ ("[Guide](original.md){href=override.md}", ("Guide\noverride.md",), ()),
+ ('[Guide](original.md){: href="override.md#part" .button}', ("Guide\noverride.md#part",), ()),
+ ('{alt="Final alt" .photo}', (), ("Final alt\noriginal.png",)),
+ ('{: src="images/final.png" alt="Final" .photo}', (), ("Final\nfinal.png",)),
+ ('{.photo width="400"}', (), ("Original\noriginal.png",)),
+ ('[Guide](original.md){.button #guide title="Visible title"}', ("Guide\noriginal.md",), ()),
+ ('[{alt="Final"}](guide.md){href=final.md}', ("Final\nfinal.md",), ("Final\noriginal.png",)),
+ ],
+)
+def test_repository_attribute_lists_define_final_element_semantics(source: str, links: tuple, images: tuple) -> None:
+ structure = extract_markdown_structure(source)
+ assert structure.links == links
+ assert structure.images == images
+ oracle = renderer_oracle(source)
+ assert structure.links == tuple(oracle.links)
+ assert structure.images == tuple(oracle.images)
+
+
+def test_raw_html_markdown_enabled_block_uses_nested_link_and_image_attributes() -> None:
+ source = (
+ '\n\n'
+ '[Guide](original.md){href=final.md}\n\n'
+ '{src="images/final.png" alt="Final" .photo}\n\n'
+ '
'
+ )
+ structure = extract_markdown_structure(source)
+ assert structure.links == ("Guide\nfinal.md",)
+ assert structure.images == ("Final\nfinal.png",)
+ oracle = renderer_oracle(source)
+ assert structure.links == tuple(oracle.links)
+ assert structure.images == tuple(oracle.images)
+
+
+def test_semantic_configuration_accounts_for_every_repository_extension() -> None:
+ configured = yaml.safe_load((ROOT / "mkdocs.yml").read_text())["markdown_extensions"]
+ site = {}
+ for entry in configured:
+ if isinstance(entry, str):
+ site[entry] = {}
+ else:
+ site.update(entry)
+ config = pre_pages_content.SEMANTIC_MARKDOWN_CONFIG
+ exclusions = pre_pages_content.SEMANTIC_MARKDOWN_EXCLUSIONS
+ assert {"attr_list", "md_in_html"} <= set(config["extensions"])
+ assert set(config["extensions"]) == set(site) - set(exclusions)
+ assert set(exclusions) == {"toc", "pymdownx.snippets"}
+ assert all(exclusions.values())
+ for extension in config["extensions"]:
+ for key, value in site[extension].items():
+ assert config["extension_configs"][extension][key] == value
+
+
+def test_safe_semantic_renderer_does_not_execute_snippet_inclusion(tmp_path: Path) -> None:
+ include = tmp_path / "not-an-audit-source.md"
+ include.write_text("[Included](should-not-be-read.md)\n")
+ source = f'--8<-- "{include.as_posix()}"'
+ assert "pymdownx.snippets" not in pre_pages_content.SEMANTIC_MARKDOWN_CONFIG["extensions"]
+ assert renderer_oracle(source).links == []
+ assert extract_markdown_structure(source).links == ()
+
+
+@pytest.mark.parametrize(("source", "category", "expected"), [
+ ("# Title", "title", ""),
+ ("## Heading", "headings", ()),
+ ("## Heading", "prose", ("## Heading",)),
+ ("```text\nLiteral\n```", "code", ()),
+ ("```text\nLiteral\n```", "prose", ("```text Literal ```",)),
+ ("~" * 3 + "\nLiteral\n" + "~" * 3, "code", ()),
+ ("[Guide](guide.md)", "links", ()),
+ ("", "images", ()),
+ ("", "links", ("Capture\ncapture.gif",)),
+ ("", "images", ()),
+ ("| A | B |\n|---|---|\n| C | D |", "tables", ()),
+ ("Title\n===", "title", ""),
+])
+def test_unicode_normalization_cannot_manufacture_markdown_syntax(source, category, expected) -> None:
+ structure = extract_markdown_structure(source)
+ assert getattr(structure, category) == expected
+ oracle = renderer_oracle(source)
+ assert structure.images == tuple(oracle.images)
+ assert structure.links == tuple(oracle.links)
+
+
+@pytest.mark.parametrize(("source", "category", "expected"), [
+ ("# Title ①", "title", "Title 1"),
+ ("## Heading ①", "headings", ("Heading 1",)),
+ ("**Text ①** `literal`", "prose", ("Text 1 literal",)),
+ ("```text\ncode ①\n```", "code", ("text\ncode 1",)),
+ ("| A | B |\n|---|---|\n| C|D | ① |", "tables", ("A | B", r"C\|D | 1")),
+ ("[Guide ①](guide.md)", "links", ("Guide 1\nguide.md",)),
+ ("", "images", ("Capture 1\ncapture.gif",)),
+ ("![Literal ![x]](capture.gif)", "images", ("Literal ![x]\ncapture.gif",)),
+ ("`[Guide](guide.md)`", "links", ("Guide\nguide.md",)),
+])
+def test_unicode_normalization_applies_only_to_extracted_text(source, category, expected) -> None:
+ assert getattr(extract_markdown_structure(source), category) == expected
+ structure = extract_markdown_structure(source)
+ oracle = renderer_oracle(source)
+ assert structure.images == tuple(oracle.images)
+ assert structure.links == tuple(oracle.links)
+
+
+def test_real_s1_fullwidth_exclamation_invalidates_the_existing_image_approval(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "monitor/sre-agent-event-lab/validation-results.md",
+ "docs/services/azure-monitor/azure-sre-agent/validation-results/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ image = ""
+ source = path.read_text()
+ assert source.count(image) == 1
+ changed = source.replace(image, "!" + image[1:])
+ assert "S1 SRE Agent investigation\ns1-investigation.gif" not in renderer_oracle(changed).images
+ path.write_text(changed)
+ with pytest.raises(AuditFormatError, match="evidence.*images|images.*count"):
+ audit_document_content(tmp_path, inventory)
+
+
+@pytest.mark.parametrize("attribute", [
+ "hidden", 'hidden="false"', 'aria-hidden="true"',
+ 'style="display:none"', 'style="visibility:hidden"',
+ 'style="DISPLAY: /* hidden */ none !important; display: block"',
+])
+@pytest.mark.parametrize("markdown_enabled", [False, True])
+def test_persistently_hidden_source_wrappers_reject_authored_markdown(attribute, markdown_enabled) -> None:
+ annotation = ' markdown="1"' if markdown_enabled else ""
+ source = f'\n\n\n\n# Title\n\nAuthored prose.\n\n
\n\n '
+ with pytest.raises(AuditFormatError, match="hidden.*authored|authored.*hidden"):
+ extract_markdown_structure(source)
+
+
+@pytest.mark.parametrize("content", [
+ "Authored prose.", "# Title", "```text\nAuthored code.\n```",
+ "| A | B |\n|---|---|\n| C | D |", "",
+])
+def test_source_hidden_state_applies_to_all_authored_structures(content) -> None:
+ with pytest.raises(AuditFormatError, match="hidden.*authored|authored.*hidden"):
+ extract_markdown_structure(f'\n\n{content}\n\n
')
+
+
+@pytest.mark.parametrize("wrapper", [
+ '{body}
',
+ 'Read more {body} ',
+ '{body}
',
+ '',
+ '{body}
',
+ '{body}
',
+])
+def test_source_browser_visible_prose_and_openable_details_remain_evidence(wrapper) -> None:
+ source = wrapper.format(body="\n\n# Title\n\nAuthored prose.\n\n")
+ structure = extract_markdown_structure(source)
+ assert structure.title == "Title"
+ assert any("Authored prose." in text for text in structure.prose)
+
+
+def test_source_hidden_examples_in_code_and_noscript_do_not_hide_visible_prose() -> None:
+ source = (
+ '# Title\n\nAuthored prose.\n\n'
+ '`Literal example
`\n\n'
+ 'No scripting fallback
'
+ )
+ assert extract_markdown_structure(source).title == "Title"
+
+
+@pytest.mark.parametrize("attribute", [
+ "hidden", 'aria-hidden="true"', 'style="display:none"', 'style="visibility:hidden"',
+])
+def test_real_foundry_local_hidden_source_body_cannot_pass_preservation(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, attribute: str,
+) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "aifoundry/foundry_local.md",
+ "docs/services/microsoft-foundry/foundry-local-air-gapped/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ source = path.read_text()
+ metadata, body = source.split("\n---\n", 1)
+ path.write_text(metadata + f'\n---\n\n\n' + body + "\n
\n")
+ with pytest.raises(AuditFormatError, match="foundry-local-air-gapped.*hidden|hidden.*foundry-local-air-gapped"):
+ audit_document_content(tmp_path, inventory)
+
+
+def test_hidden_svg_container_cannot_supply_source_authored_text() -> None:
+ with pytest.raises(AuditFormatError, match="hidden.*authored"):
+ extract_markdown_structure('# Title\n\nAuthored diagram label. ')
+
+
+@pytest.mark.parametrize(("source", "category", "expected"), [
+ ("| A | B |\n|---|---|\n| C\|D | 1 |", "tables", ("A | B", r"C\\\|D | 1")),
+ ("", "images", ("Capture\ncapture?variant.gif",)),
+ ("", "images", ("Capture\npart/capture.gif",)),
+ ('Guide ', "local_link_labels", ("Guide",)),
+])
+def test_unicode_cannot_manufacture_extracted_escape_or_url_syntax(source, category, expected) -> None:
+ structure = extract_markdown_structure(source)
+ assert getattr(structure, category) == expected
+ oracle = renderer_oracle(source)
+ assert structure.images == tuple(oracle.images)
+ assert structure.links == tuple(oracle.links)
+
+
+@pytest.mark.parametrize("container", ["details", "summary", "ancestor"])
+def test_closed_inert_details_cannot_supply_hidden_source_content(container) -> None:
+ source = (
+ 'Cannot open \n\n'
+ '# Title\n\nAuthored prose.\n\n '
+ )
+ source = f"{source}
" if container == "ancestor" else source.replace(f"<{container}", f"<{container} inert")
+ with pytest.raises(AuditFormatError, match="hidden.*authored"):
+ extract_markdown_structure(source)
+
+
+@pytest.mark.parametrize("container", ["details", "summary", "ancestor"])
+def test_open_inert_details_keep_visually_visible_source_content(container) -> None:
+ source = (
+ 'Already open \n\n'
+ '# Title\n\nAuthored prose.\n\n '
+ )
+ source = f"{source}
" if container == "ancestor" else source.replace(f"<{container}", f"<{container} inert")
+ structure = extract_markdown_structure(source)
+ assert structure.title == "Title"
+
+
+@pytest.mark.parametrize("case", CHROMIUM_VISIBILITY["css"], ids=lambda case: case["id"])
+def test_chromium_css_source_visibility_or_explicit_rejection(case) -> None:
+ source = f'\n\n# Title\n\nAuthored prose.\n\n '
+ if case["audit"] == "visible":
+ assert extract_markdown_structure(source).title == "Title"
+ else:
+ message = "inline CSS" if case["audit"] == "unsupported" else "hidden.*authored"
+ with pytest.raises(AuditFormatError, match=message):
+ extract_markdown_structure(source)
+
+
+@pytest.mark.parametrize("case", CHROMIUM_VISIBILITY["disclosures"], ids=lambda case: case["id"])
+def test_chromium_summary_descendants_determine_source_openability(case) -> None:
+ source = (
+ f''
+ f'<{case["tag"]} {case["child"]}>Open{case["tag"]}> \n\n'
+ '# Title\n\nAuthored prose.\n\n
'
+ )
+ if case["audit_open"]:
+ assert extract_markdown_structure(source).title == "Title"
+ else:
+ with pytest.raises(AuditFormatError, match="hidden.*authored"):
+ extract_markdown_structure(source)
+
+
+@pytest.mark.parametrize("style", [
+ "display:none;display:blo/**/ck", 'display:none;--note:";display:block;"',
+ r"d\69 splay:n\6f ne", "--display:none;display:var(--display)",
+])
+def test_real_foundry_local_css_rejection_has_source_path(tmp_path, monkeypatch, style) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "aifoundry/foundry_local.md",
+ "docs/services/microsoft-foundry/foundry-local-air-gapped/index.md",
+ )
+ metadata, body = path.read_text().split("\n---\n", 1)
+ path.write_text(metadata + f'\n---\n\n\n{body}\n
')
+ with pytest.raises(AuditFormatError, match="foundry-local-air-gapped"):
+ audit_document_content(tmp_path, inventory)
+
+
+@pytest.mark.parametrize("svg", [
+ 'Decorative label ',
+ 'Icon ',
+ 'Definition, not a painted label ',
+ 'Decorative group label ',
+ 'Decorative text label ',
+])
+def test_decorative_svg_does_not_supply_or_hide_authored_source_content(svg) -> None:
+ assert extract_markdown_structure("# Title\n\nAuthored prose.\n\n" + svg).title == "Title"
+
+
+def test_late_inert_summary_cannot_expose_earlier_source_body() -> None:
+ with pytest.raises(AuditFormatError, match="hidden.*authored"):
+ extract_markdown_structure(
+ '\n\n# Title\n\nAuthored prose.\n\n'
+ 'Cannot open '
+ )
+
+
+@pytest.mark.parametrize("style", CHROMIUM_VISIBILITY["unresolved_css"])
+def test_unresolved_inline_css_fails_closed_in_source(style) -> None:
+ with pytest.raises(AuditFormatError, match="inline CSS"):
+ extract_markdown_structure(
+ f'\n\n# Title\n\nAuthored prose.\n\n
'
+ )
+
+
+@pytest.mark.parametrize("attribute", ['visibility="hidden"', 'display="none"'])
+def test_authored_svg_presentation_hiding_is_not_visible_source(attribute) -> None:
+ with pytest.raises(AuditFormatError, match="hidden.*authored"):
+ extract_markdown_structure(f'Authored diagram label ')
+
+
+def test_inline_svg_visibility_overrides_the_presentation_attribute() -> None:
+ source = 'Authored diagram label '
+ assert extract_markdown_structure(source).prose
+
+
+@pytest.mark.parametrize("wrapper", [
+ '{content} ',
+ '{content} ',
+ '{content} ',
+ '{content} ',
+ '{content} ',
+ '{content} ',
+])
+def test_excluded_svg_cannot_supply_semantic_link_or_image_evidence(wrapper) -> None:
+ content = ' '
+ structure = extract_markdown_structure(wrapper.format(content=content))
+ assert structure.images == ()
+ assert structure.links == structure.local_link_labels == ()
+
+
+@pytest.mark.parametrize("image", [
+ "",
+ ' ',
+ ' ',
+])
+def test_visible_images_still_supply_exact_evidence_beside_excluded_svg(image) -> None:
+ hidden = (
+ ''
+ ' '
+ )
+ assert extract_markdown_structure(hidden + "\n\n" + image).images == ("Capture\ncapture.gif",)
+
+
+def test_excluded_svg_alt_text_cannot_forge_the_surrounding_link_label() -> None:
+ source = (
+ 'Visible'
+ ''
+ ' '
+ )
+ structure = extract_markdown_structure(source)
+ assert structure.links == ("Visible\nguide.md",)
+ assert structure.images == ()
+
+
+def test_semantic_link_can_use_a_visible_descendant_of_a_hidden_visibility_anchor() -> None:
+ source = 'Guide '
+ assert extract_markdown_structure(source).links == ("Guide\nguide.md",)
+
+
+def test_semantic_link_cannot_use_only_an_inert_descendant_as_evidence() -> None:
+ assert extract_markdown_structure('Guide ').links == ()
+
+
+def test_real_s1_hidden_svg_image_cannot_satisfy_its_preservation_approval(tmp_path, monkeypatch) -> None:
+ inventory, path = isolated_real_document_audit(
+ tmp_path, monkeypatch, "monitor/sre-agent-event-lab/validation-results.md",
+ "docs/services/azure-monitor/azure-sre-agent/validation-results/index.md",
+ )
+ assert not audit_document_content(tmp_path, inventory)[0].missing
+ source = path.read_text()
+ image = ""
+ assert source.count(image) == 1
+ hidden = (
+ ''
+ ' '
+ ' '
+ )
+ path.write_text(source.replace(image, hidden))
+ with pytest.raises(AuditFormatError, match="evidence.*images|images.*count"):
+ audit_document_content(tmp_path, inventory)
+
+
+@pytest.mark.parametrize("prefix", ["", "Not a direct summary
"])
+def test_closed_details_without_authored_summary_use_the_native_control(prefix):
+ assert extract_markdown_structure(
+ f'{prefix}\n\n# Title\n\nAuthored prose.\n\n '
+ ).title == "Title"
+
+
+def test_already_open_details_without_summary_keep_visible_source_evidence():
+ assert extract_markdown_structure(
+ '\n\n# Title\n\nAuthored prose.\n\n '
+ ).title == "Title"
+
+
+def test_source_late_first_summary_element_is_openable():
+ structure = extract_markdown_structure(
+ 'before
Late \n\n# Title\n\nAuthored prose.\n\n '
+ )
+ assert structure.title == "Title"
+
+
+@pytest.mark.parametrize("html", [
+ "
public",
+ "
public
",
+ "",
+ "public ",
+ "",
+ 'public ',
+])
+def test_source_optional_end_tags_do_not_extend_hidden_ancestors(html):
+ assert extract_markdown_structure(html).prose
+
+
+@pytest.mark.parametrize("source", [
+ "",
+ "",
+ "",
+])
+def test_source_table_foster_ambiguity_is_an_explicit_error(source):
+ with pytest.raises(AuditFormatError, match="foster"):
+ extract_markdown_structure(source)
+
+
+@pytest.mark.parametrize("tag", ["textarea", "xmp", "iframe", "noembed", "noframes", "plaintext"])
+def test_source_raw_container_trailing_markup_cannot_be_promoted(tag):
+ source = (
+ f'<{tag}/>Fake {tag}>'
+ 'After '
+ )
+ structure = extract_markdown_structure(source)
+ assert structure.images == ()
+ assert structure.links == (() if tag == "plaintext" else ("After\nafter.md",))
+
+
+DEFAULT_SUMMARY_ORACLE = json.loads((ROOT / "tests/docs/fixtures/pre_pages_default_summary_chromium.json").read_text())
+
+
+@pytest.mark.parametrize("case", DEFAULT_SUMMARY_ORACLE["cases"], ids=lambda case: case["id"])
+def test_native_summaryless_source_visibility_inherits_blockers(case):
+ source = (
+ f'\n\n'
+ '# Title\n\n[Visible link](guide.md)\n\n
'
+ )
+ if case["audit_open"]:
+ structure = extract_markdown_structure(source)
+ assert structure.title == "Title" and structure.links == ("Visible link\nguide.md",)
+ else:
+ with pytest.raises(AuditFormatError, match="hidden.*authored"):
+ extract_markdown_structure(source)
+
+
+def test_open_summaryless_inert_source_remains_visually_visible():
+ structure = extract_markdown_structure(
+ '\n\n# Title\n\nVisible prose.\n\n
'
+ )
+ assert structure.title == "Title"
diff --git a/tests/docs/test_pre_pages_html.py b/tests/docs/test_pre_pages_html.py
new file mode 100644
index 00000000..4355e004
--- /dev/null
+++ b/tests/docs/test_pre_pages_html.py
@@ -0,0 +1,115 @@
+import json
+from pathlib import Path
+
+import pytest
+
+from scripts.docs.pre_pages import AuditFormatError
+from scripts.docs.pre_pages_visibility import AuthoredContent
+from scripts.docs.pre_pages_visibility import Visibility
+
+
+ORACLE = json.loads((Path(__file__).parent / "fixtures/pre_pages_implied_end_chromium.json").read_text())
+FOURTH_ORACLE = json.loads((Path(__file__).parent / "fixtures/pre_pages_fourth_chromium.json").read_text())
+
+
+@pytest.mark.parametrize("case", ORACLE["cases"], ids=lambda case: case["id"])
+def test_implied_end_visibility_and_parent_match_chromium(case):
+ reader = AuthoredContent()
+ reader.feed('' + case["html"] + " ")
+ reader.close()
+
+ def find(parent):
+ for child in parent.children:
+ if not isinstance(child, str):
+ if child.attributes.get("id") == "public":
+ return parent, child
+ if found := find(child):
+ return found
+ return None
+
+ parent, node = find(reader.root)
+ assert parent.tag == case["parent"]
+ assert node.evidence_visible is case["visible"]
+
+
+def test_late_first_summary_element_remains_openable_per_chromium():
+ case = ORACLE["late_summary"]
+ assert case["first_element"] == "p" and case["after_programmatic_click"] and case["after_pointer_click"]
+ reader = AuthoredContent()
+ reader.feed(case["html"])
+ reader.close()
+ assert reader.hidden is None
+ assert reader.blocks[("p", "before")] == reader.blocks[("p", "after")] == 1
+
+
+@pytest.mark.parametrize("body", [
+ '
Ambiguous formatting adoption',
+ '
Ambiguous foster parenting
',
+])
+def test_unsupported_ambiguous_optional_end_recovery_fails_closed(body):
+ reader = AuthoredContent()
+ with pytest.raises(AuditFormatError, match="unsupported|ambiguous"):
+ reader.feed(body)
+ reader.close()
+
+
+@pytest.mark.parametrize("container", [
+ "", "",
+ "",
+])
+@pytest.mark.parametrize("body", ["text
", "text
", "text", " ", 'text '])
+def test_table_foster_parenting_flow_or_text_fails_closed(container, body):
+ reader = AuthoredContent()
+ with pytest.raises(AuditFormatError, match="foster"):
+ reader.feed(container.format(body=body))
+ reader.close()
+
+
+@pytest.mark.parametrize("container", [
+ "",
+ "",
+ "",
+])
+def test_table_cells_and_captions_allow_normal_flow_content(container):
+ reader = AuthoredContent()
+ reader.feed(container.format(body=""))
+ reader.close()
+ assert reader.blocks[("p", "public")] == 1
+
+
+@pytest.mark.parametrize("case", FOURTH_ORACLE["css"], ids=lambda case: case["id"])
+def test_css_inheritance_and_restoration_match_chromium(case):
+ parent = Visibility().descend({"style": "visibility:hidden!important"})
+ child = parent.descend({"style": case["style"]})
+ assert child.visibility_hidden is not case["visible"]
+
+
+@pytest.mark.parametrize("tag", FOURTH_ORACLE["raw"])
+@pytest.mark.parametrize("slash", ["", "/"])
+def test_raw_and_rcdata_markup_never_creates_parent_elements(tag, slash):
+ reader = AuthoredContent()
+ reader.feed(
+ f'Before<{tag}{slash}>Fake '
+ f'
Fake heading {tag}>After
'
+ )
+ reader.close()
+ assert reader.blocks[("h1", "Fake heading")] == 0
+ assert reader.blocks[("img", "")] == 0
+ assert reader.links == (["after.html"] if FOURTH_ORACLE["raw"][tag]["after_closing"] else [])
+
+
+@pytest.mark.parametrize("tag", ["iframe", "noembed", "noframes"])
+def test_iframe_and_fallback_text_are_not_parent_authored_content(tag):
+ reader = AuthoredContent()
+ reader.feed(f"Before<{tag}>Fallback markup and text{tag}>After
")
+ reader.close()
+ assert reader.blocks[("p", "BeforeAfter")] == 1
+ assert not any("Fallback" in text for _, text in reader.blocks)
+
+
+def test_textarea_rcdata_decodes_entities_without_parsing_the_result():
+ reader = AuthoredContent()
+ reader.feed('<img src="fake.png">& literalAfter
')
+ reader.close()
+ assert reader.blocks[("p", ' & literalAfter')] == 1
+ assert reader.blocks[("img", "")] == 0
diff --git a/tests/docs/test_pre_pages_site.py b/tests/docs/test_pre_pages_site.py
new file mode 100644
index 00000000..d6836955
--- /dev/null
+++ b/tests/docs/test_pre_pages_site.py
@@ -0,0 +1,1968 @@
+from __future__ import annotations
+
+from dataclasses import FrozenInstanceError
+from html import escape
+import json
+from pathlib import Path, PurePosixPath
+import posixpath
+import re
+import shutil
+import subprocess
+import sys
+
+from mkdocs.structure.files import File
+import pytest
+import yaml
+
+from scripts.docs.pre_pages import (
+ BaselineDocument, PrePagesInventory, audit_repository, load_inventory,
+)
+from scripts.docs.pre_pages_site import inspect_built_site
+from scripts.docs.topics import build_topic_catalog
+
+
+ROOT = Path(__file__).parents[2]
+CHROMIUM_VISIBILITY = json.loads((ROOT / "tests/docs/fixtures/pre_pages_chromium_visibility.json").read_text())
+CLI = ROOT / "scripts/docs/audit_pre_pages.py"
+ENTRY = "services/service/topic/index.md"
+CHILD = "services/service/topic/child/index.md"
+OLD = "guides/service/old/index.md"
+OLD_CHILD = "guides/service/old-child/index.md"
+ARTICLE = (
+ ''
+ 'Document Preserved prose.
'
+)
+FOUNDRY_HTML = "services/microsoft-foundry/foundry-local-air-gapped/index.html"
+
+
+def write(root: Path, path: str, text: str | bytes) -> Path:
+ target = root / path
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_bytes(text.encode() if isinstance(text, str) else text)
+ return target
+
+
+def html(body: str = "", head: str = "") -> str:
+ return f"{head}{ARTICLE}{body} "
+
+
+def source(path: str, old: str, order: int | None = None) -> str:
+ metadata = {
+ "title": "Document", "document_type": "guide", "services": ["service"],
+ "redirect_from": [old],
+ }
+ if order:
+ metadata["topic_order"] = order
+ return "---\n" + yaml.safe_dump(metadata) + "---\n\n# Document\n\nPreserved prose.\n"
+
+
+def search_entry(path: str, fragment: str = "") -> dict:
+ return {
+ "location": str(PurePosixPath(path).parent) + "/" + fragment,
+ "title": "Document", "text": "Preserved prose.",
+ }
+
+
+def redirect(target: str) -> str:
+ return html(
+ f'Moved document
',
+ f' '
+ f' ',
+ )
+
+
+@pytest.fixture
+def site_repo(tmp_path: Path):
+ write(tmp_path, "docs-taxonomy.yml", "services:\n service: Service\n")
+ write(tmp_path, "mkdocs.yml", "site_url: https://example.test/project/\n")
+ for path, old, order in ((ENTRY, OLD, None), (CHILD, OLD_CHILD, 1)):
+ write(tmp_path / "docs", path, source(path, old, order))
+ write(tmp_path / "site", path.replace(".md", ".html"), html())
+ for old, target in ((OLD, ENTRY), (OLD_CHILD, CHILD)):
+ write(
+ tmp_path / "site", old.replace(".md", ".html"),
+ redirect("../../../" + str(PurePosixPath(target).parent) + "/"),
+ )
+ write(tmp_path, "site/services/service/index.html", html('Topic '))
+ write(tmp_path, "site/explore/index.html", html(
+ 'Entry '
+ 'Child '
+ ))
+ write(tmp_path, "site/search/search_index.json", json.dumps({
+ "docs": [search_entry(ENTRY), search_entry(CHILD), search_entry(CHILD, "#heading")],
+ }))
+ sample = "docs/services/service/topic/samples/example/"
+ write(tmp_path, sample + "README.md", "# Sample\n")
+ write(tmp_path, sample + "asset.bin", b"\x00\xffsample\r\n")
+ write(tmp_path, sample + "sample.yml", yaml.safe_dump({
+ "title": "Sample", "description": "Sample fixture", "kind": "artifact",
+ "used_by": ["index"], "publish": [{"source": "asset.bin", "target": "downloads/asset.bin"}],
+ }))
+ write(tmp_path, "site/services/service/topic/downloads/asset.bin", b"\x00\xffsample\r\n")
+ inventory = PrePagesInventory(
+ "a" * 40, "b" * 40, 20,
+ tuple(BaselineDocument(PurePosixPath(f"old/{i}.md"), PurePosixPath(old), {})
+ for i, old in enumerate((OLD, OLD_CHILD))), {},
+ )
+ return tmp_path, inventory
+
+
+def inspect(site_repo):
+ root, inventory = site_repo
+ catalog = build_topic_catalog(root / "docs", {"services": {"service": "Service"}})
+ return inspect_built_site(root, root / "site", inventory, catalog)
+
+
+def findings(site_repo) -> str:
+ return "\n".join(inspect(site_repo).errors)
+
+
+def test_site_fixture_has_all_visibility_and_publish_guarantees(site_repo):
+ result = inspect(site_repo)
+ assert result.errors == ()
+ assert result.details["canonical_html"] == 2
+ assert result.details["redirects"] == 2
+ assert result.details["searchable_documents"] == 2
+ assert result.details["service_topics"] == 1
+ assert result.details["explore_documents"] == 2
+ assert result.details["published_assets"] == 1
+
+
+@pytest.mark.parametrize("path,label", [
+ ("docs/" + ENTRY, "source"),
+ ("site/" + ENTRY.replace(".md", ".html"), "canonical"),
+ ("site/" + OLD.replace(".md", ".html"), "redirect"),
+ ("site/services/service/index.html", "service"),
+ ("site/explore/index.html", "explore"),
+ ("site/search/search_index.json", "search"),
+])
+def test_missing_required_files_fail_closed(site_repo, path, label):
+ root, inventory = site_repo
+ catalog = build_topic_catalog(root / "docs", {"services": {"service": "Service"}})
+ (root / path).unlink()
+ result = inspect_built_site(root, root / "site", inventory, catalog)
+ assert path.removeprefix("site/").removeprefix("docs/") in "\n".join(result.errors)
+ assert label in "\n".join(result.errors).lower()
+
+
+@pytest.mark.parametrize("kind", ["canonical", "refresh", "fallback"])
+def test_each_redirect_channel_must_target_canonical(site_repo, kind):
+ root, _ = site_repo
+ target = "../../../services/service/topic/"
+ content = redirect(target)
+ if kind == "canonical":
+ content = content.replace(f'rel="canonical" href="{target}"', 'rel="canonical" href="/wrong/"')
+ elif kind == "refresh":
+ content = content.replace(f"0; url={target}", "0; url=/wrong/")
+ else:
+ content = content.replace(f'', ' ')
+ write(root / "site", OLD.replace(".md", ".html"), content)
+ assert kind in findings(site_repo)
+
+
+@pytest.mark.parametrize("head", [
+ ' ',
+ ' ',
+ ' ',
+])
+def test_duplicate_or_malformed_redirect_metadata_fails(site_repo, head):
+ root, _ = site_repo
+ path = root / "site" / OLD.replace(".md", ".html")
+ path.write_text(path.read_text().replace("", head + ""))
+ assert "redirect" in findings(site_repo)
+
+
+@pytest.mark.parametrize("suffix", ["?different=1", "#other-section"])
+def test_redirect_metadata_must_use_the_canonical_base_url(site_repo, suffix):
+ root, _ = site_repo
+ write(root / "site", OLD.replace(".md", ".html"),
+ redirect("../../../services/service/topic/" + suffix))
+ assert "redirect canonical" in findings(site_repo)
+ assert "redirect refresh" in findings(site_repo)
+
+
+@pytest.mark.parametrize("body", [
+ ' ',
+ 'Hidden ',
+ 'No script only ',
+])
+def test_invisible_fallback_anchors_do_not_prove_redirect_reachability(site_repo, body):
+ root, _ = site_repo
+ path = root / "site" / OLD.replace(".md", ".html")
+ original = path.read_text()
+ path.write_text(original[:original.index("") + 6] + body + "