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"""])(?:"[^"]*"|'[^']*'|[^'">])*>""", + re.DOTALL, +) +_HTML_OPEN = re.compile(r"])") +_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"]*>", + 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"") + 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"", 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(" 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("""", 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 ") + 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' + "## 다음\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 = "![한글 그림](../old/plot.svg?raw=true)" + same = '![한글 그림](images/plot.svg "caption")' + assert compare(first, same).status == "preserved" + assert extract_markdown_structure(first).images == ("한글 그림\nplot.svg",) + assert compare(first, "![다른 그림](images/plot.svg)").missing[0].category == "images" + assert compare(first, "![한글 그림](images/other.svg)").missing[0].category == "images" + + +def test_linked_and_html_images_are_extracted_without_markup_prose() -> None: + md = "[![Chart](images/plot.svg)](https://example.com)" + html = 'Chart' + 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 = "diagram" + assert extract_markdown_structure(baseline).images == ("diagram\nbefore.svg",) + assert compare(baseline, "diagram").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](images/proof.png)", "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 = "![S1 SRE Agent investigation](images/s1-investigation.gif)" + 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="![Fake](images/fake.png)"', + "data-image=![Fake](images/fake.png)", + '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\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) ![Fake](images/fake.png)\n\n' + 'FakeFake\n\n' + f'```sh\nfake command\n```\n\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 ' + 'First' + ) + assert structure.links == ("Actual\nfirst.md",) + assert structure.images == ("First\nfirst.png",) + + +def test_genuine_html_image_alt_text_labels_its_anchor() -> None: + source = ( + 'Actual' + ) + structure = extract_markdown_structure(source) + expected = renderer_oracle('Actual') + 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": "![Alt](images/picture.png)", + "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](images/picture.png)", "![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"\![Alt](images/picture.png)") + 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](fake.png) Fake' + ) + assert structure.links == () and structure.images == () + + +def test_html_tag_tokens_inside_markdown_image_alt_or_destination_do_not_emit_elements() -> None: + source = ( + '![Outer Fake](outer.png) ' + '[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('&lt;literal&gt;') + 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) ![Fake](fake.png)\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 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))", + "", + "[![Capture](capture.gif)](details.md)", + '[Before ![Capture](capture.gif) after](details.md)', + 'Before Capture after', + 'Plain', + '[Fake](fake.md) ![Fake](fake.gif)', + '', + '', + r"\![Capture](capture.gif)", + "`[Fake](fake.md) ![Fake](fake.gif)`", + "`literal ``` ![Capture](capture.gif) 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 ``` ![Capture](capture.gif) tail`", "literal ``` ![Capture](capture.gif) 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 ![Capture](capture.gif)", + "``unmatched `short` ![Capture](capture.gif)", + "````unmatched ```short``` ![Capture](capture.gif)", + "````", +]) +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![Visible](capture.gif) 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 = "![S1 SRE Agent investigation](images/s1-investigation.gif)" + 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 = "![S1 SRE Agent investigation](images/s1-investigation.gif)" + 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"), + [ + ('![S1](images/s1-investigation.gif){: 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",), ()), + ('![Original](images/original.png){alt="Final alt" .photo}', (), ("Final alt\noriginal.png",)), + ('![Original](images/original.png){: src="images/final.png" alt="Final" .photo}', (), ("Final\nfinal.png",)), + ('![Original](images/original.png){.photo width="400"}', (), ("Original\noriginal.png",)), + ('[Guide](original.md){.button #guide title="Visible title"}', ("Guide\noriginal.md",), ()), + ('[![Original](images/original.png){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' + '![Original](images/original.png){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", ()), + ("![Capture](capture.gif)", "images", ()), + ("![Capture](capture.gif)", "links", ("Capture\ncapture.gif",)), + ("![Capture](capture.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",)), + ("![Capture ①](capture.gif)", "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 = "![S1 SRE Agent investigation](images/s1-investigation.gif)" + 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 |", "![Capture](capture.gif)", +]) +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'') + + +@pytest.mark.parametrize("wrapper", [ + '
{body}
', + '
Read more{body}
', + '
{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' + '``\n\n' + '' + ) + 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\n') + + +@pytest.mark.parametrize(("source", "category", "expected"), [ + ("| A | B |\n|---|---|\n| C\|D | 1 |", "tables", ("A | B", r"C\\\|D | 1")), + ("![Capture](images/capture?variant.gif)", "images", ("Capture\ncapture?variant.gif",)), + ("![Capture](images/part/capture.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\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", [ + '', + '', + 'Definition, not a painted 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}', + '', +]) +def test_excluded_svg_cannot_supply_semantic_link_or_image_evidence(wrapper) -> None: + content = 'Capture' + structure = extract_markdown_structure(wrapper.format(content=content)) + assert structure.images == () + assert structure.links == structure.local_link_labels == () + + +@pytest.mark.parametrize("image", [ + "![Capture](capture.gif)", + 'Capture', + 'Capture', +]) +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 = "![S1 SRE Agent investigation](images/s1-investigation.gif)" + 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
", + "
public
", + '', +]) +def test_source_optional_end_tags_do_not_extend_hidden_ancestors(html): + assert extract_markdown_structure(html).prose + + +@pytest.mark.parametrize("source", [ + "

text

", + "
text
", + "text
", +]) +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}/>FakeFake' + '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", [ + '