diff --git a/AGENTS.md b/AGENTS.md index 360152c7..183350df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,9 @@ 사용하고, 그 밖의 제품은 공식 프로젝트·제품명을 kebab-case로 사용한다. - 새 서비스, 기술 또는 태그가 필요할 때만 `docs-taxonomy.yml`을 함께 수정한다. +- 태그는 taxonomy의 `tag_groups`에 속한 통제 값 1–4개만 사용한다. + 목적 태그를 주제 태그보다 앞에 두고 중복·미사용 태그를 남기지 않는다. + Kubernetes 같은 기술은 `technologies`로 표현한다. - 개별 문서를 추가하기 위해 `mkdocs.yml`, `docs/.nav.yml`, README 또는 수동 문서 목록을 수정하지 않는다. @@ -35,15 +38,23 @@ ## Publishing automation -- 메뉴 트리, 서비스별·태그별·전체 글 목록, 검색 인덱스, collection 호환 +- 메뉴 트리, 서비스별 보기·글 찾기, 검색 인덱스, collection 호환 색인과 이전 URL redirect page는 빌드 시 자동 생성한다. -- 대메뉴는 `홈`, `서비스별 보기`, `태그별 보기`, `전체 글`, `기여하기`다. +- 대메뉴는 `홈`, `서비스별 보기`, `글 찾기`, `기여하기`다. 서비스 branch 아래에는 canonical topic entry와 순서가 있는 자식 문서만 배치한다. redirect page와 collection 호환 색인은 이 branch에 넣지 않는다. - 서비스 목록은 front matter의 `services` 전체를 반영한다. 사이드바에는 경로의 대표 서비스 아래 원본 topic을 한 번만 배치한다. -- 태그 링크는 정확히 일치하는 태그별 목록으로 연결한다. 전체 글에는 각 - canonical 문서를 한 번만 표시한다. +- 태그 링크는 `/explore/?tag=`로 연결한다. 글 찾기는 모든 주제를 + canonical topic 카드로 묶고 주제 수와 문서 수를 함께 표시한다. + 여러 태그·서비스·기술·제목/설명 검색어는 같은 문서에 AND로 적용한다. + topic 안에서 불일치 문서는 숨기고 일치 문서별 직접 링크를 제공한다. +- 글 찾기 제목·설명 검색의 URL 매개변수는 반복 가능한 `text`다. + Material 전체 검색이 소유한 `q`는 글 찾기 필터나 오류로 취급하지 않고, + URL 갱신·초기화 시 현재 주소에 있는 값을 보존한다. +- `/tags/`, `/articles/`는 `/explore/`로 redirect한다. 이전 태그 주소는 + taxonomy의 `legacy_tag_redirects`만으로 보존하며 문서별 legacy 태그를 + 추가하지 않는다. redirect page는 메뉴와 검색에서 제외한다. - 홈 대표 글은 topic entry의 `featured: true`로 지정한다. 생성기에 특정 문서·서비스·태그 경로나 제목 목록을 하드코딩하지 않는다. - sample 파일은 public-safety 검사 대상이지만 MkDocs 사이트와 검색 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c431edde..5514b230 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,17 @@ `document_type`은 `case`, `guide`, `lab`, `research` 중 문서의 수명주기에 맞게 선택하지만 물리 폴더에는 사용하지 않습니다. +`tags`에는 taxonomy의 목적·주제 통제 태그를 1–4개 지정하고 목적을 먼저 +배치합니다. 중복 태그나 기술을 대신하는 태그는 추가하지 않습니다. +글 찾기는 모든 주제를 묶어 보여 주며 선택한 태그·서비스·기술·제목/설명 +조건을 모두 충족하는 문서만 각 topic에 표시합니다. 주제 수와 문서 수를 +함께 표시하고 일치 문서로 바로 연결합니다. 이전 태그 주소는 문서가 아닌 +taxonomy의 `legacy_tag_redirects`에서 관리합니다. + +제목·설명 검색어는 반복 가능한 `text` query로 공유합니다. `q`는 Material +전체 검색 전용이므로 글 찾기가 읽거나 오류로 처리하지 않으며, 필터 변경과 +초기화에서도 보존합니다. + 실행 코드, 배포 매니페스트와 결과물은 `docs/services///samples//`에 `sample.yml`과 `README.md`를 포함해 둡니다. 최상위 collection 폴더와 `samples/`에는 새 diff --git a/README.md b/README.md index 0d4a3205..6e5e3ba8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,12 @@ Azure 기반 시스템의 설계·구현·운영 기록을 공유합니다. 실 **[Azure Engineering Notes에서 문서 보기](https://hellices.github.io/devguidesample/)** -사이트에서는 서비스별·태그별 보기로 글을 탐색할 수 있으며, 태그별 목록에는 front matter의 `tags`에 해당 태그가 정확히 지정된 글만 포함됩니다. 이와 별도로 전체 텍스트 검색에서 한국어·영어 제목과 본문, 제품명, 오류 메시지를 찾을 수 있습니다. +사이트에서는 **서비스별 보기** 또는 **글 찾기**로 탐색합니다. 글 찾기는 모든 주제를 카드로 묶고 서비스, 목적·주제 태그, 기술, 제목·설명으로 좁힙니다. 태그 하나를 선택하면 그 태그가 붙은 문서만, 여러 태그를 선택하면 모든 태그가 붙은 문서만 주제 안에 표시하며 각 문서로 바로 연결합니다. 서비스·기술·검색어도 같은 문서에 AND로 적용됩니다. 결과에는 주제 수와 문서 수를 함께 표시합니다. 별도의 전체 텍스트 검색에서는 한국어·영어 제목과 본문, 제품명, 오류 메시지를 찾을 수 있습니다. + +글 찾기의 제목·설명 검색어는 반복 가능한 `text`로 공유합니다 +(예: `/explore/?tag=design&text=Agent&text=Memory`). +`q`는 사이트 전체 검색 전용이며 글 찾기의 필터 변경·초기화로 지우지 않습니다. +전체 검색 창을 닫아도 글 찾기의 `text` 조건은 유지됩니다. | 모음 | 용도 | |---|---| @@ -15,7 +20,7 @@ Azure 기반 시스템의 설계·구현·운영 기록을 공유합니다. 실 | 실습 | 배포, 실행, 검증과 정리를 재현할 수 있는 시나리오입니다. | | 리서치 | 비교, 벤치마크와 아키텍처 조사를 기준 시점과 함께 기록합니다. | -전체 문서 목록은 파일로 중복 관리하지 않습니다. 올바른 폴더와 front matter를 사용하면 Pages 메뉴, 서비스 색인과 태그 색인에 자동으로 포함됩니다. +모든 주제 목록은 파일로 중복 관리하지 않습니다. 올바른 폴더와 front matter를 사용하면 Pages 메뉴, 서비스 색인과 글 찾기에 자동으로 포함됩니다. 기존 `/tags/`, `/articles/` 및 이전 태그 주소는 검색·메뉴에서 제외된 글 찾기 redirect로 유지합니다. 공개 문서의 canonical 원본은 `docs/services//[/]/index.md` topic package입니다. diff --git a/docs-taxonomy.yml b/docs-taxonomy.yml index c9a7e400..c3cc6abf 100644 --- a/docs-taxonomy.yml +++ b/docs-taxonomy.yml @@ -64,27 +64,50 @@ technologies: rest-api: REST API tags: + design: 설계 + build: 구현 + deploy: 배포 + diagnose: 진단 + optimize: 최적화 + operate: 운영 + secure: 보안 + evaluate: 비교·검증 + migrate: 마이그레이션 ai-agents: AI agents - architecture: Architecture - authentication: Authentication - authorization: Authorization - benchmarking: Benchmarking - deployment: Deployment - development: Development - diagnostics: Diagnostics - disaster-recovery: Disaster recovery - high-availability: High availability - kubernetes: Kubernetes - latency: Latency - migration: Migration - monitoring: Monitoring - networking: Networking - observability: Observability - performance: Performance - reliability: Reliability - security: Security - storage: Storage - troubleshooting: Troubleshooting + networking: 네트워킹 + identity: ID·권한 + storage: 스토리지 + +tag_groups: + goal: + label: 목적 + tags: [design, build, deploy, diagnose, optimize, operate, secure, evaluate, migrate] + subject: + label: 주제 + tags: [ai-agents, networking, identity, storage] + +legacy_tag_redirects: + ai-agents: {tag: [ai-agents]} + architecture: {tag: [design]} + authentication: {tag: [identity, secure]} + authorization: {tag: [identity, secure]} + benchmarking: {tag: [evaluate]} + deployment: {tag: [deploy]} + development: {tag: [build]} + diagnostics: {tag: [diagnose]} + troubleshooting: {tag: [diagnose]} + disaster-recovery: {tag: [operate]} + high-availability: {tag: [operate]} + monitoring: {tag: [operate]} + observability: {tag: [operate]} + reliability: {tag: [operate]} + kubernetes: {technology: [kubernetes]} + latency: {tag: [optimize]} + performance: {tag: [optimize]} + migration: {tag: [migrate]} + networking: {tag: [networking]} + security: {tag: [secure]} + storage: {tag: [storage]} verification_statuses: [verified, needs-review] required_source_host: learn.microsoft.com diff --git a/docs/.nav.yml b/docs/.nav.yml index b13498da..75d366ac 100644 --- a/docs/.nav.yml +++ b/docs/.nav.yml @@ -2,8 +2,7 @@ nav: - 홈: index.md - 서비스별 보기: services - - 태그별 보기: tags - - 전체 글: articles + - 글 찾기: explore - 기여하기: contributing - glob: "*" ignore_no_matches: true diff --git a/docs/assets/javascripts/explore.js b/docs/assets/javascripts/explore.js new file mode 100644 index 00000000..570c6897 --- /dev/null +++ b/docs/assets/javascripts/explore.js @@ -0,0 +1,140 @@ +(function () { + "use strict"; + + const fields = {tag: "tags", service: "services", technology: "technologies"}; + const emptyFilters = () => ({tag: [], service: [], technology: [], text: []}); + const normalize = value => value.normalize("NFKC").toLocaleLowerCase().trim(); + + function parseQuery(query, vocabulary) { + const filters = emptyFilters(); + const invalid = []; + for (const [key, value] of new URLSearchParams(query)) { + if (key === "q") continue; // Reserved for Material's global search. + if (key === "text") { + if (value.trim() && !filters.text.includes(value.trim())) filters.text.push(value.trim()); + } else if (Object.hasOwn(fields, key) && vocabulary[key].includes(value)) { + if (!filters[key].includes(value)) filters[key].push(value); + } else { + invalid.push(`${key}=${value}`); + } + } + return {filters, invalid}; + } + + function matchesMember(member, filters) { + return Object.entries(fields).every(([key, field]) => + filters[key].every(value => member[field].includes(value)) + ) && filters.text.every(value => normalize(member.search).includes(normalize(value))); + } + + function queryString(filters, currentQuery = "") { + const query = new URLSearchParams(); + for (const key of [...Object.keys(fields), "text"]) { + for (const value of filters[key]) query.append(key, value); + } + for (const value of new URLSearchParams(currentQuery).getAll("q")) query.append("q", value); + return query.size ? `?${query}` : ""; + } + + function mount(root, environment) { + if (root.dataset.exploreReady) return; + root.dataset.exploreReady = "true"; + const form = root.querySelector("[data-explore-form]"); + const search = root.querySelector('[name="text"]'); + const controls = Array.from(root.querySelectorAll('input[type="checkbox"]')); + const count = root.querySelector("[data-explore-count]"); + const summary = root.querySelector("[data-explore-summary]"); + const alert = root.querySelector("[data-explore-alert]"); + const empty = root.querySelector("[data-explore-empty]"); + const vocabulary = Object.fromEntries(Object.keys(fields).map(key => [ + key, controls.filter(control => control.name === key).map(control => control.value), + ])); + const topics = Array.from(root.querySelectorAll("[data-explore-topic]")).map(card => ({ + card, + count: card.querySelector("[data-explore-topic-count]"), + members: Array.from(card.querySelectorAll("[data-explore-member]")).map(row => ({ + row, + data: { + tags: JSON.parse(row.dataset.tags), + services: JSON.parse(row.dataset.services), + technologies: JSON.parse(row.dataset.technologies), + search: row.dataset.search, + }, + })), + })); + let filters = emptyFilters(); + + function update() { + let topicCount = 0; + let documentCount = 0; + for (const topic of topics) { + let matching = 0; + for (const member of topic.members) { + member.row.hidden = !matchesMember(member.data, filters); + if (!member.row.hidden) matching += 1; + } + topic.card.hidden = matching === 0; + topic.count.textContent = matching === topic.members.length + ? `${matching}개 문서` : `${matching} / ${topic.members.length}개 문서 일치`; + topicCount += matching > 0 ? 1 : 0; + documentCount += matching; + } + count.textContent = `${topicCount}개 주제 · ${documentCount}개 문서`; + const selected = controls.filter(control => control.checked).map(control => + control.labels[0].textContent.trim()); + selected.push(...filters.text.map(value => `검색: ${value}`)); + summary.textContent = selected.length ? `선택한 조건 (AND): ${selected.join(" · ")}` : "모든 주제 · 선택한 조건 없음"; + empty.hidden = documentCount !== 0; + const url = environment.location.pathname + queryString(filters, environment.location.search) + environment.location.hash; + environment.history.replaceState(null, "", url); + } + + function readQuery() { + const parsed = parseQuery(environment.location.search, vocabulary); + filters = parsed.filters; + for (const control of controls) control.checked = filters[control.name].includes(control.value); + // Repeated text values remain separate AND terms until the reader edits text. + search.value = filters.text.join(" "); + alert.hidden = parsed.invalid.length === 0; + alert.textContent = parsed.invalid.length + ? `알 수 없는 필터를 제외했습니다: ${parsed.invalid.join(", ")}` : ""; + update(); + } + + form.addEventListener("change", () => { + for (const key of Object.keys(fields)) { + filters[key] = controls.filter(control => control.name === key && control.checked).map(control => control.value); + } + update(); + }); + form.addEventListener("input", event => { + if (event.target !== search) return; + filters.text = search.value.trim() ? [search.value.trim()] : []; + update(); + }); + form.addEventListener("submit", event => event.preventDefault()); + form.addEventListener("reset", event => { + event.preventDefault(); + filters = emptyFilters(); + controls.forEach(control => { control.checked = false; }); + search.value = ""; + alert.hidden = true; + alert.textContent = ""; + update(); + }); + environment.addEventListener("popstate", readQuery); + readQuery(); + } + + if (typeof module !== "undefined" && module.exports) { + module.exports = {parseQuery, matchesMember, queryString, mount}; + } + if (typeof document !== "undefined") { + const start = () => { + const root = document.querySelector("[data-explore]"); + if (root) mount(root, window); + }; + if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); + else start(); + } +})(); diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css index 92176800..36dae0db 100644 --- a/docs/assets/stylesheets/extra.css +++ b/docs/assets/stylesheets/extra.css @@ -32,6 +32,163 @@ --md-code-fg-color: #24354c; } +.dg-explore [hidden] { + display: none !important; +} + +.dg-explore-filters { + padding: 1rem; + border: 1px solid var(--dg-border); + border-radius: 0.5rem; + background: var(--dg-surface); +} + +.dg-explore-filters fieldset { + min-width: 0; + margin: 0.8rem 0; + padding: 0.6rem; + border: 1px solid var(--dg-border); + border-radius: 0.35rem; +} + +.dg-explore-filters legend, +.dg-explore-search { + font-weight: 700; +} + +.dg-explore input[type="search"] { + display: block; + width: 100%; + min-height: 2.2rem; + padding: 0.4rem 0.6rem; + border: 1px solid var(--dg-border); + border-radius: 0.3rem; + color: var(--dg-text); + background: var(--dg-bg); +} + +.dg-filter-options { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.dg-filter-option { + display: inline-flex; + align-items: center; + gap: 0.35rem; + min-height: 2.2rem; + padding: 0.35rem 0.6rem; + border: 1px solid var(--dg-border); + border-radius: 0.3rem; + cursor: pointer; + transition: background-color 120ms ease; +} + +.dg-filter-option:has(input:checked) { + border-color: var(--dg-accent); + color: var(--dg-accent); + background: var(--dg-accent-soft); +} + +.dg-filter-option input { + flex-shrink: 0; + accent-color: var(--dg-accent); +} + +.dg-explore-reset { + min-height: 2.2rem; + padding: 0.4rem 0.8rem; + border: 1px solid var(--dg-accent); + border-radius: 0.3rem; + color: var(--dg-accent); + background: var(--dg-accent-soft); + cursor: pointer; +} + +.dg-explore input:focus-visible, +.dg-explore button:focus-visible, +.dg-filter-option:has(input:focus-visible) { + outline: 2px solid var(--dg-accent); + outline-offset: 3px; +} + +.dg-explore-status, +.dg-explore [role="alert"] { + margin-block: 1rem; + overflow-wrap: anywhere; +} + +.dg-explore [role="alert"] { + padding: 0.6rem; + border-inline-start: 3px solid var(--dg-accent); + background: var(--dg-accent-soft); +} + +.dg-explore-results { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; + gap: 1rem; +} + +.dg-explore-topic { + min-width: 0; + padding: 1rem; + border: 1px solid var(--dg-border); + border-radius: 0.5rem; + background: var(--dg-surface); + overflow-wrap: anywhere; +} + +.md-typeset .dg-explore-topic h2 { + margin-top: 0; +} + +.md-typeset .dg-explore-members { + margin: 0; + padding: 0; + list-style: none; +} + +.md-typeset .dg-explore-member { + margin: 0; + padding-block: 0.7rem; + border-top: 1px solid var(--dg-border); +} + +.md-typeset .dg-explore-member h3 { + margin-top: 0; +} + +@media (max-width: 760px) { + .dg-explore-results { + grid-template-columns: minmax(0, 1fr); + } + + .dg-explore-filters, + .dg-explore-topic { + padding: 0.7rem; + } +} + +@media (max-width: 480px) { + .dg-filter-options { + display: grid; + grid-template-columns: minmax(0, 1fr); + } + + .dg-explore-reset { + width: 100%; + } +} + +@media (prefers-reduced-motion: reduce) { + .dg-filter-option { + transition: none; + } +} + [data-md-color-scheme="slate"] { --dg-bg: #111720; --dg-surface: #18212d; @@ -693,7 +850,6 @@ body { .dg-collection--research .dg-doc-grid, .dg-service-grid, -.dg-browse-grid, .dg-tag-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } diff --git a/docs/contributing/case-template.md b/docs/contributing/case-template.md index b9281345..a927d9f7 100644 --- a/docs/contributing/case-template.md +++ b/docs/contributing/case-template.md @@ -14,7 +14,7 @@ description: 증상, 영향과 해결을 요약한 한 문장 document_type: case services: [azure-kubernetes-service] technologies: [kubernetes] -tags: [troubleshooting] +tags: [diagnose] status: resolved verification_status: verified sources_checked_at: 2026-09-12 diff --git a/docs/contributing/guide-template.md b/docs/contributing/guide-template.md index f952bf91..5968c775 100644 --- a/docs/contributing/guide-template.md +++ b/docs/contributing/guide-template.md @@ -14,7 +14,7 @@ description: 목표와 적용 범위를 요약한 한 문장 document_type: guide services: [azure-kubernetes-service] technologies: [kubernetes] -tags: [diagnostics] +tags: [diagnose] status: current verification_status: verified sources_checked_at: 2026-09-12 diff --git a/docs/contributing/index.md b/docs/contributing/index.md index da02c8e2..79b98475 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -84,6 +84,11 @@ kind: runnable used_by: - index - setup +publish: + - source: assets/architecture.svg + target: images/architecture.svg + - source: assets/architecture.svg + target: setup/images/architecture.svg ``` - `title`, `description`, `kind`는 비어 있지 않은 문자열입니다. @@ -95,9 +100,32 @@ used_by: 자식 문서 아래에 `samples/`를 두거나 topic의 `samples/` 바로 아래에 파일을 두지 않습니다. sample package 내부의 하위 `samples/` 폴더는 해당 package의 payload로 취급합니다. -- sample 파일은 공개 안전성 검사를 받지만 MkDocs 사이트와 검색 색인에는 +- sample 원본 폴더는 공개 안전성 검사를 받지만 MkDocs 사이트와 검색 색인에는 복사되지 않습니다. 문서에서는 생성되는 sample card 또는 GitHub 소스 링크로 연결합니다. +- 페이지와 sample에서 같은 이미지나 파일을 사용하는 경우 원본은 sample에만 + 보관하고 선택적 `publish` 목록에 명시합니다. `source`는 sample package, + `target`은 소유 topic 루트 기준 상대 경로입니다. 위 예의 페이지 링크는 + 각각 `images/architecture.svg`이며 실제 target 파일을 중복 저장하지 않습니다. +- 빌드는 선언한 파일만 원본 바이트 그대로 target에 게시합니다. 텍스트와 + Markdown 파일도 독립 문서나 검색 항목이 아닌 다운로드 자산으로 유지합니다. + `downloads/index.md` 같은 원본 자산은 해당 파일 경로로 직접 링크합니다. + `downloads/`처럼 문서 디렉터리로 줄여 쓰면 다운로드 자산을 가리키지 않습니다. + 공개 target의 경로 구성 요소는 어느 깊이에서도 `.`으로 시작할 수 없습니다. + [Pages artifact 패키징](https://github.com/actions/upload-pages-artifact/blob/v4/action.yml)이 + 숨김 파일과 숨김 디렉터리를 제외하므로, 이런 target은 빌드 전에 거부합니다. + sample 원본 payload 내부의 숨김 파일은 허용하며 원본 폴더는 계속 게시에서 제외합니다. + 페이지 본문과 링크 텍스트는 기존처럼 검색됩니다. `used_by`는 sample card의 + 표시 위치만 결정하며 `publish`와 독립적입니다. +- `publish`는 생략하거나 빈 목록으로 둘 수 있습니다. 각 항목의 `source`와 + `target`은 비어 있지 않은 문자열이어야 합니다. 절대 경로, `..`, 역슬래시, + 소유 경계를 벗어나는 심볼릭 링크와 존재하지 않는 원본은 허용하지 않습니다. + target은 `samples/` 아래, canonical 문서와 HTML 출력 경로, 이미 존재하는 파일이나 + 디렉터리를 가리킬 수 없습니다. 하나의 원본을 여러 target으로 게시할 수 + 있지만 각 target에는 하나의 선언만 허용합니다. + target 파일은 다른 target이나 canonical 출력 파일의 상위·하위 경로와도 + 충돌할 수 없습니다. 기존 파일을 target의 상위 디렉터리처럼 사용할 수 없으며, + 기존 디렉터리 아래에 새 파일을 게시하는 것은 허용합니다. 최상위 `samples/`에는 공개 콘텐츠를 추가하지 않습니다. @@ -125,11 +153,54 @@ redirect 경로는 `cases`, `guides`, `labs`, `research` 중 하나로 시작하 카드와 함께 해당 자식 문서로 바로 가는 링크도 제공합니다. `document_type`은 collection 호환 색인과 수명주기 요구사항을 선택하지만 -물리 경로를 결정하지 않습니다. `services/`, `tags/`, `articles/`와 -collection 호환 색인은 빌드가 생성합니다. 개별 문서를 추가하기 위해 +물리 경로를 결정하지 않습니다. `services/`, `explore/`와 collection 호환 +색인은 빌드가 생성합니다. `tags/`, `articles/`는 `explore/`로 이동하는 +검색 제외 redirect입니다. 개별 문서를 추가하기 위해 `mkdocs.yml`, `docs/.nav.yml`, README 또는 수동 문서 목록을 수정하지 않습니다. +## 글 찾기와 실용 태그 + +대메뉴는 **홈 · 서비스별 보기 · 글 찾기 · 기여하기**입니다. 글 찾기는 +모든 주제를 canonical topic 카드로 묶고 실제 catalog에서 계산한 +**주제 수 · 문서 수**를 함께 표시합니다. 여러 문서가 있는 topic도 +카드 하나이며 각 문서로 직접 연결합니다. + +- 서비스, 목적·주제별 태그, 기술은 front matter 값과 정확히 일치해야 합니다. +- 태그 하나를 선택하면 해당 태그가 붙은 문서만 표시합니다. 여러 태그, + 서비스, 기술은 **같은 문서에 AND**로 적용합니다. 서로 다른 자식의 + 태그를 합쳐서 일치한다고 판단하지 않습니다. +- 제목·설명 검색도 같은 문서에 적용합니다. 본문 검색은 사이트의 별도 + 전체 텍스트 검색을 사용합니다. +- 일치하지 않는 문서 행과 일치 문서가 없는 topic 카드는 숨깁니다. + JavaScript가 없으면 안내와 함께 모든 topic과 문서 링크를 표시합니다. +- 선택 조건은 반복 가능한 `tag`, `service`, `technology`, `text` query로 + 공유합니다. 예를 들어 `explore/?tag=design&text=Agent&text=Memory`는 + `design` 태그와 두 검색어가 모두 일치하는 문서만 표시합니다. +- `q`는 Material의 사이트 전체 검색 전용입니다. 글 찾기는 이를 자신의 + 검색어나 잘못된 필터로 취급하지 않으며 URL을 갱신할 때 현재 `q` 값을 + 보존합니다. 전체 검색 창을 닫아도 글 찾기의 `text` 조건은 지워지지 않습니다. +- 그 밖의 알 수 없는 매개변수나 필터 값은 경고 후 제외합니다. 초기화하면 + 글 찾기의 선택과 검색어, 해당 query를 지우되 전체 검색의 `q`는 유지합니다. +- 문서와 카드의 태그 링크는 `explore/?tag=`로 연결합니다. + +문서의 `tags`는 아래 통제 값에서 **1–4개** 선택합니다. 중복을 제거하고 +목적 태그를 주제 태그보다 먼저 배치합니다. 기술 분류와 중복되는 +`kubernetes`는 태그가 아니라 `technologies`로 지정합니다. + +| 그룹 | 값 | +|---|---| +| 목적 | `design` 설계 · `build` 구현 · `deploy` 배포 · `diagnose` 진단 · `optimize` 최적화 · `operate` 운영 · `secure` 보안 · `evaluate` 비교·검증 · `migrate` 마이그레이션 | +| 주제 | `ai-agents` AI agents · `networking` 네트워킹 · `identity` ID·권한 · `storage` 스토리지 | + +태그를 추가할 때는 `docs-taxonomy.yml`의 `tags`와 `tag_groups`를 함께 +수정하고 실제 문서에 적용합니다. 각 태그는 그룹 하나에 정확히 한 번 +등장해야 하며 미사용 태그는 허용하지 않습니다. `legacy_tag_redirects`는 +이전 `/tags//` 주소를 새 필터로 변환하는 taxonomy 소유 매핑입니다. +예를 들어 `authentication`은 `identity`와 `secure` 두 태그로, +`kubernetes`는 기술 필터로 이동합니다. 이 redirect는 검색·메뉴에서 +제외하며 문서마다 legacy 태그를 저장하지 않습니다. + ## 세 가지 작성 workflow ### 1. 독립 topic 추가 @@ -160,7 +231,8 @@ collection 호환 색인은 빌드가 생성합니다. 개별 문서를 추가 3. 코드·매니페스트·결과물을 같은 sample 폴더에 둡니다. 4. 실제 식별자와 비밀을 가상 값으로 치환합니다. 5. public-safety 검사가 sample을 검사하고 strict build의 `site/`에는 sample - 파일이 없는지 확인합니다. + 원본 폴더가 없는지 확인합니다. `publish`를 사용하면 선언한 target만 + 게시되며 원본과 바이트가 같은지도 확인합니다. ## 공통 front matter @@ -171,7 +243,7 @@ description: AKS 네트워크 문제를 진단하는 현재 절차 document_type: guide services: [azure-kubernetes-service] technologies: [kubernetes] -tags: [networking, troubleshooting] +tags: [diagnose, networking] status: current verification_status: verified sources_checked_at: 2026-09-12 diff --git a/docs/contributing/lab-template.md b/docs/contributing/lab-template.md index f1bd5304..e27435ef 100644 --- a/docs/contributing/lab-template.md +++ b/docs/contributing/lab-template.md @@ -14,7 +14,7 @@ description: 배포하고 확인할 동작을 요약한 한 문장 document_type: lab services: [azure-monitor] technologies: [bicep] -tags: [deployment, monitoring] +tags: [deploy, operate] status: verified verification_status: verified sources_checked_at: 2026-09-12 diff --git a/docs/contributing/research-template.md b/docs/contributing/research-template.md index ecd9285f..498fd3d9 100644 --- a/docs/contributing/research-template.md +++ b/docs/contributing/research-template.md @@ -14,7 +14,7 @@ description: 질문, 비교 대상과 결론을 요약한 한 문장 document_type: research services: [azure-ai-search] technologies: [rag] -tags: [benchmarking] +tags: [evaluate] status: current verification_status: verified sources_checked_at: 2026-09-12 diff --git a/docs/index.md b/docs/index.md index 62d9c899..918eeb14 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,8 +20,7 @@ hide:
[서비스별 보기](services/index.md){ .dg-button .dg-button--primary } -[태그별 보기](tags/index.md){ .dg-button .dg-button--secondary } -[전체 글](articles/index.md){ .dg-button .dg-button--secondary } +[글 찾기](explore/index.md){ .dg-button .dg-button--secondary }
@@ -46,7 +45,8 @@ hide: ## 글 찾아보기 -서비스별로 관련 글을 모아 보거나, 태그로 주제를 좁히거나, 전체 글을 한 번에 살펴보세요. +서비스별로 관련 글을 모아 보거나, 글 찾기에서 모든 주제를 살펴보세요. +여러 태그를 선택하면 모든 태그가 붙은 문서만 주제별로 표시합니다. diff --git a/docs/services/application-development/nodejs-file-io-cpu/index.md b/docs/services/application-development/nodejs-file-io-cpu/index.md index 25c276b7..122096d4 100644 --- a/docs/services/application-development/nodejs-file-io-cpu/index.md +++ b/docs/services/application-development/nodejs-file-io-cpu/index.md @@ -17,9 +17,9 @@ description: Node.js 서버의 파일 I/O로 인한 CPU 오버헤드를 분석 technologies: - nodejs tags: -- performance -- diagnostics -- development +- optimize +- diagnose +- build redirect_from: - guides/application-development/nodejs-file-io-cpu/index.md --- diff --git a/docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md b/docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md index 5c0f5cd2..f8b933a7 100644 --- a/docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md +++ b/docs/services/azure-ai-search/custom-vectorization/a10-vs-t4/index.md @@ -14,8 +14,8 @@ featured: true technologies: - python tags: -- benchmarking -- performance +- evaluate +- optimize published_at: 2026-07-23 topic_order: 5 redirect_from: diff --git a/docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md b/docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md index 7b2e2215..3a975a3e 100644 --- a/docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md +++ b/docs/services/azure-ai-search/custom-vectorization/bge-m3-vs-qwen3/index.md @@ -14,8 +14,8 @@ technologies: - python - rag tags: -- benchmarking -- performance +- evaluate +- optimize published_at: 2026-05-22 topic_order: 4 redirect_from: diff --git a/docs/services/azure-ai-search/custom-vectorization/custom-web-api/images/architecture.png b/docs/services/azure-ai-search/custom-vectorization/custom-web-api/images/architecture.png deleted file mode 100644 index 933ef85c..00000000 Binary files a/docs/services/azure-ai-search/custom-vectorization/custom-web-api/images/architecture.png and /dev/null differ diff --git a/docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md b/docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md index cec6a931..d3f8ceec 100644 --- a/docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md +++ b/docs/services/azure-ai-search/custom-vectorization/custom-web-api/index.md @@ -19,8 +19,8 @@ technologies: - rest-api - rag tags: -- deployment -- architecture +- build +- deploy topic_order: 2 redirect_from: - guides/azure-ai-search/custom-web-api-vectorization/index.md diff --git a/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/images/gpu_vllm_rag_architecture.png b/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/images/gpu_vllm_rag_architecture.png deleted file mode 100644 index e5465d5d..00000000 Binary files a/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/images/gpu_vllm_rag_architecture.png and /dev/null differ diff --git a/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md b/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md index 2776de2b..e78110b6 100644 --- a/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md +++ b/docs/services/azure-ai-search/custom-vectorization/gpu-vllm/index.md @@ -19,9 +19,9 @@ technologies: - kubernetes - rag tags: -- performance -- deployment -- architecture +- build +- deploy +- optimize topic_order: 3 redirect_from: - guides/azure-ai-search/gpu-vllm-rag/index.md diff --git a/docs/services/azure-ai-search/custom-vectorization/images/indexer_vs_pushapi.png b/docs/services/azure-ai-search/custom-vectorization/images/indexer_vs_pushapi.png deleted file mode 100644 index acfdae0b..00000000 Binary files a/docs/services/azure-ai-search/custom-vectorization/images/indexer_vs_pushapi.png and /dev/null differ diff --git a/docs/services/azure-ai-search/custom-vectorization/index.md b/docs/services/azure-ai-search/custom-vectorization/index.md index eb8354ee..7908beca 100644 --- a/docs/services/azure-ai-search/custom-vectorization/index.md +++ b/docs/services/azure-ai-search/custom-vectorization/index.md @@ -18,9 +18,9 @@ technologies: - python - rag tags: -- architecture -- performance -- deployment +- design +- build +- optimize redirect_from: - guides/azure-ai-search/custom-embedding-ingestion/index.md --- diff --git a/docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md b/docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md index 34618ca4..cec07403 100644 --- a/docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md +++ b/docs/services/azure-ai-search/custom-vectorization/rag-chunking/index.md @@ -13,8 +13,9 @@ description: RAG 검색을 위한 청킹 전략과 선택 기준을 비교합니 technologies: - rag tags: -- architecture -- performance +- design +- optimize +- evaluate published_at: 2026-05-22 topic_order: 1 redirect_from: diff --git a/docs/services/azure-ai-search/custom-vectorization/samples/implementation/sample.yml b/docs/services/azure-ai-search/custom-vectorization/samples/implementation/sample.yml index 77a6742d..e80c134d 100644 --- a/docs/services/azure-ai-search/custom-vectorization/samples/implementation/sample.yml +++ b/docs/services/azure-ai-search/custom-vectorization/samples/implementation/sample.yml @@ -8,3 +8,10 @@ used_by: - gpu-vllm - bge-m3-vs-qwen3 - a10-vs-t4 +publish: + - source: images/architecture.png + target: custom-web-api/images/architecture.png + - source: images/gpu_vllm_rag_architecture.png + target: gpu-vllm/images/gpu_vllm_rag_architecture.png + - source: images/indexer_vs_pushapi.png + target: images/indexer_vs_pushapi.png diff --git a/docs/services/azure-ai-search/eventual-consistency-reindex/index.md b/docs/services/azure-ai-search/eventual-consistency-reindex/index.md index 0f088ea0..67284199 100644 --- a/docs/services/azure-ai-search/eventual-consistency-reindex/index.md +++ b/docs/services/azure-ai-search/eventual-consistency-reindex/index.md @@ -13,8 +13,8 @@ description: Azure AI Search 버전 기반 재색인에서 관측한 eventual co technologies: - python tags: -- reliability -- troubleshooting +- operate +- diagnose occurred_at: 2026-04-17 redirect_from: - cases/azure-ai-search/eventual-consistency-reindex/index.md diff --git a/docs/services/azure-ai-search/korean-analyzer-comparison/images/portal_ko_microsoft.png b/docs/services/azure-ai-search/korean-analyzer-comparison/images/portal_ko_microsoft.png deleted file mode 100644 index cc5767c4..00000000 Binary files a/docs/services/azure-ai-search/korean-analyzer-comparison/images/portal_ko_microsoft.png and /dev/null differ diff --git a/docs/services/azure-ai-search/korean-analyzer-comparison/images/portal_standard_lucene.png b/docs/services/azure-ai-search/korean-analyzer-comparison/images/portal_standard_lucene.png deleted file mode 100644 index 23d208c9..00000000 Binary files a/docs/services/azure-ai-search/korean-analyzer-comparison/images/portal_standard_lucene.png and /dev/null differ diff --git a/docs/services/azure-ai-search/korean-analyzer-comparison/index.md b/docs/services/azure-ai-search/korean-analyzer-comparison/index.md index 11b6365f..5066e90a 100644 --- a/docs/services/azure-ai-search/korean-analyzer-comparison/index.md +++ b/docs/services/azure-ai-search/korean-analyzer-comparison/index.md @@ -17,8 +17,8 @@ description: Microsoft 한국어 분석기와 표준 Lucene 분석기의 동작 technologies: - python tags: -- diagnostics -- performance +- diagnose +- optimize redirect_from: - guides/azure-ai-search/korean-analyzer-comparison/index.md --- diff --git a/docs/services/azure-ai-search/korean-analyzer-comparison/samples/analyzer-comparison/sample.yml b/docs/services/azure-ai-search/korean-analyzer-comparison/samples/analyzer-comparison/sample.yml index 0bf62faf..cf5e932b 100644 --- a/docs/services/azure-ai-search/korean-analyzer-comparison/samples/analyzer-comparison/sample.yml +++ b/docs/services/azure-ai-search/korean-analyzer-comparison/samples/analyzer-comparison/sample.yml @@ -3,3 +3,8 @@ description: Runnable analyzer and synonym comparison scripts with sample data. kind: runnable used_by: - index +publish: + - source: images/portal_ko_microsoft.png + target: images/portal_ko_microsoft.png + - source: images/portal_standard_lucene.png + target: images/portal_standard_lucene.png diff --git a/docs/services/azure-application-gateway/sse-response-buffering/index.md b/docs/services/azure-application-gateway/sse-response-buffering/index.md index e83b8c29..9ef5e8ac 100644 --- a/docs/services/azure-application-gateway/sse-response-buffering/index.md +++ b/docs/services/azure-application-gateway/sse-response-buffering/index.md @@ -17,8 +17,8 @@ description: Server-Sent Events 통신을 위해 Application Gateway 응답 버 technologies: - rest-api tags: +- optimize - networking -- performance redirect_from: - guides/azure-application-gateway/sse-response-buffering/index.md --- diff --git a/docs/services/azure-application-gateway/waf-path-ip-allowlist/index.md b/docs/services/azure-application-gateway/waf-path-ip-allowlist/index.md index cb54a78f..766425df 100644 --- a/docs/services/azure-application-gateway/waf-path-ip-allowlist/index.md +++ b/docs/services/azure-application-gateway/waf-path-ip-allowlist/index.md @@ -17,7 +17,7 @@ description: Application Gateway WAF custom rule로 특정 경로의 IP 허용 technologies: - rest-api tags: -- security +- secure - networking redirect_from: - guides/azure-application-gateway/waf-path-ip-allowlist/index.md diff --git a/docs/services/azure-architecture/response-time-optimization/index.md b/docs/services/azure-architecture/response-time-optimization/index.md index f4c0296f..7a3583a3 100644 --- a/docs/services/azure-architecture/response-time-optimization/index.md +++ b/docs/services/azure-architecture/response-time-optimization/index.md @@ -17,9 +17,8 @@ description: 초 단위 응답 시간을 줄이기 위한 Azure 아키텍처 고 technologies: - rest-api tags: -- architecture -- latency -- performance +- design +- optimize redirect_from: - guides/azure-architecture/response-time-optimization/index.md --- diff --git a/docs/services/azure-automation/portal-cli-limitations/index.md b/docs/services/azure-automation/portal-cli-limitations/index.md index 471e7cc2..b2008159 100644 --- a/docs/services/azure-automation/portal-cli-limitations/index.md +++ b/docs/services/azure-automation/portal-cli-limitations/index.md @@ -18,8 +18,8 @@ technologies: - azure-cli - powershell tags: -- troubleshooting -- development +- diagnose +- build redirect_from: - guides/azure-automation/portal-cli-limitations/index.md --- diff --git a/docs/services/azure-cosmos-db/nodejs-client-optimization/index.md b/docs/services/azure-cosmos-db/nodejs-client-optimization/index.md index 376c3ba1..400716bf 100644 --- a/docs/services/azure-cosmos-db/nodejs-client-optimization/index.md +++ b/docs/services/azure-cosmos-db/nodejs-client-optimization/index.md @@ -18,8 +18,8 @@ technologies: - nodejs - cosmos-db tags: -- performance -- development +- optimize +- build redirect_from: - guides/azure-cosmos-db/nodejs-client-optimization/index.md --- diff --git a/docs/services/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md b/docs/services/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md index 940186eb..d303c266 100644 --- a/docs/services/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md +++ b/docs/services/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md @@ -18,9 +18,9 @@ technologies: - nodejs - cosmos-db tags: +- optimize +- diagnose - networking -- performance -- troubleshooting redirect_from: - guides/azure-cosmos-db/nodejs-dns-lookup-bottleneck/index.md --- diff --git a/docs/services/azure-cosmos-db/point-read-optimization/index.md b/docs/services/azure-cosmos-db/point-read-optimization/index.md index dd3af028..c1be6171 100644 --- a/docs/services/azure-cosmos-db/point-read-optimization/index.md +++ b/docs/services/azure-cosmos-db/point-read-optimization/index.md @@ -18,8 +18,7 @@ technologies: - nodejs - cosmos-db tags: -- performance -- latency +- optimize redirect_from: - guides/azure-cosmos-db/point-read-optimization/index.md --- 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 77d978a8..cc9c4b3d 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 @@ -19,9 +19,8 @@ technologies: - mysql - kubernetes tags: -- migration -- high-availability -- reliability +- migrate +- operate redirect_from: - guides/azure-database-for-mysql/blue-green-upgrade/index.md --- diff --git a/docs/services/azure-database-for-mysql/nodejs-read-write-routing/index.md b/docs/services/azure-database-for-mysql/nodejs-read-write-routing/index.md index b1d9ed94..53887c57 100644 --- a/docs/services/azure-database-for-mysql/nodejs-read-write-routing/index.md +++ b/docs/services/azure-database-for-mysql/nodejs-read-write-routing/index.md @@ -18,9 +18,9 @@ technologies: - nodejs - mysql tags: -- high-availability -- performance -- development +- operate +- optimize +- build redirect_from: - guides/azure-database-for-mysql/nodejs-read-write-routing/index.md --- diff --git a/docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md b/docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md index 3da21e6c..b9421897 100644 --- a/docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md +++ b/docs/services/azure-kubernetes-service/argocd-image-updater-acr/index.md @@ -18,8 +18,9 @@ technologies: - kubernetes - gitops tags: -- deployment -- authentication +- deploy +- secure +- identity redirect_from: - guides/azure-kubernetes-service/argocd-image-updater-acr/index.md --- diff --git a/docs/services/azure-kubernetes-service/authorization-troubleshooting/index.md b/docs/services/azure-kubernetes-service/authorization-troubleshooting/index.md index 3af92222..f5d71b30 100644 --- a/docs/services/azure-kubernetes-service/authorization-troubleshooting/index.md +++ b/docs/services/azure-kubernetes-service/authorization-troubleshooting/index.md @@ -18,8 +18,9 @@ technologies: - kubernetes - azure-cli tags: -- authorization -- troubleshooting +- secure +- diagnose +- identity redirect_from: - guides/azure-kubernetes-service/authorization-troubleshooting/index.md --- diff --git a/docs/services/azure-kubernetes-service/cni-overlay-nsg/index.md b/docs/services/azure-kubernetes-service/cni-overlay-nsg/index.md index a0f0bda1..28f2e519 100644 --- a/docs/services/azure-kubernetes-service/cni-overlay-nsg/index.md +++ b/docs/services/azure-kubernetes-service/cni-overlay-nsg/index.md @@ -17,8 +17,8 @@ description: AKS CNI Overlay를 사용할 때 NSG 규칙과 트래픽 경로의 technologies: - kubernetes tags: +- secure - networking -- security redirect_from: - guides/azure-kubernetes-service/cni-overlay-nsg/index.md --- diff --git a/docs/services/azure-kubernetes-service/file-io-throttling/index.md b/docs/services/azure-kubernetes-service/file-io-throttling/index.md index 73e06251..eb080dfc 100644 --- a/docs/services/azure-kubernetes-service/file-io-throttling/index.md +++ b/docs/services/azure-kubernetes-service/file-io-throttling/index.md @@ -13,9 +13,9 @@ description: AKS Pod에서 관측한 파일 I/O throttling의 증상과 분석 technologies: - kubernetes tags: +- optimize +- diagnose - storage -- performance -- troubleshooting occurred_at: 2025-11-05 redirect_from: - cases/azure-kubernetes-service/file-io-throttling/index.md diff --git a/docs/services/azure-kubernetes-service/kaito-open-source-model/index.md b/docs/services/azure-kubernetes-service/kaito-open-source-model/index.md index fc16444e..5ac4ce86 100644 --- a/docs/services/azure-kubernetes-service/kaito-open-source-model/index.md +++ b/docs/services/azure-kubernetes-service/kaito-open-source-model/index.md @@ -17,8 +17,8 @@ description: AKS에서 KAITO를 이용해 오픈소스 언어 모델을 배포 technologies: - kubernetes tags: +- deploy - ai-agents -- deployment redirect_from: - guides/azure-kubernetes-service/kaito-open-source-model/index.md --- diff --git a/docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md b/docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md index d98be166..7bd62b59 100644 --- a/docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md +++ b/docs/services/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md @@ -13,9 +13,9 @@ description: AKS와 Azure NetApp Files 환경의 CPU 급증 및 파일 I/O 대 technologies: - kubernetes tags: +- optimize +- diagnose - storage -- performance -- troubleshooting occurred_at: 2025-11-07 redirect_from: - cases/azure-kubernetes-service/netapp-files-cpu-io-wait/index.md diff --git a/docs/services/azure-kubernetes-service/pod-affinity-distribution/index.md b/docs/services/azure-kubernetes-service/pod-affinity-distribution/index.md index 7772c14e..3f07bc80 100644 --- a/docs/services/azure-kubernetes-service/pod-affinity-distribution/index.md +++ b/docs/services/azure-kubernetes-service/pod-affinity-distribution/index.md @@ -17,8 +17,7 @@ description: AKS에서 Pod affinity와 anti-affinity로 워크로드를 분산 technologies: - kubernetes tags: -- kubernetes -- reliability +- operate redirect_from: - guides/azure-kubernetes-service/pod-affinity-distribution/index.md --- diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/01_query_latency.png b/docs/services/azure-kubernetes-service/pod-database-query-latency/images/01_query_latency.png deleted file mode 100644 index 79dbc49e..00000000 Binary files a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/01_query_latency.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/02_pod_runtime.png b/docs/services/azure-kubernetes-service/pod-database-query-latency/images/02_pod_runtime.png deleted file mode 100644 index 2ff280d3..00000000 Binary files a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/02_pod_runtime.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/pod_db_query_latency.png b/docs/services/azure-kubernetes-service/pod-database-query-latency/images/pod_db_query_latency.png deleted file mode 100644 index 4b54a691..00000000 Binary files a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/pod_db_query_latency.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_01.png b/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_01.png deleted file mode 100644 index a0bb6906..00000000 Binary files a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_01.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_02.png b/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_02.png deleted file mode 100644 index aef1e668..00000000 Binary files a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_02.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_pod_runtime.png b/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_pod_runtime.png deleted file mode 100644 index c5b73fd6..00000000 Binary files a/docs/services/azure-kubernetes-service/pod-database-query-latency/images/result_pod_runtime.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/index.md b/docs/services/azure-kubernetes-service/pod-database-query-latency/index.md index 72138f86..ca34f7da 100644 --- a/docs/services/azure-kubernetes-service/pod-database-query-latency/index.md +++ b/docs/services/azure-kubernetes-service/pod-database-query-latency/index.md @@ -15,9 +15,9 @@ technologies: - kubernetes - nodejs tags: -- latency +- optimize +- diagnose - networking -- troubleshooting occurred_at: 2026-06-08 redirect_from: - cases/azure-kubernetes-service/pod-database-query-latency/index.md diff --git a/docs/services/azure-kubernetes-service/pod-database-query-latency/samples/observed-results/sample.yml b/docs/services/azure-kubernetes-service/pod-database-query-latency/samples/observed-results/sample.yml index 6267ad66..317eefa1 100644 --- a/docs/services/azure-kubernetes-service/pod-database-query-latency/samples/observed-results/sample.yml +++ b/docs/services/azure-kubernetes-service/pod-database-query-latency/samples/observed-results/sample.yml @@ -3,3 +3,16 @@ description: Captured diagrams and measurements from the investigated scenario. kind: artifact used_by: - index +publish: + - source: 01_query_latency.png + target: images/01_query_latency.png + - source: 02_pod_runtime.png + target: images/02_pod_runtime.png + - source: pod_db_query_latency.png + target: images/pod_db_query_latency.png + - source: result_01.png + target: images/result_01.png + - source: result_02.png + target: images/result_02.png + - source: result_pod_runtime.png + target: images/result_pod_runtime.png diff --git a/docs/services/azure-kubernetes-service/pod-scheduling-agent-pools/index.md b/docs/services/azure-kubernetes-service/pod-scheduling-agent-pools/index.md index 3ea0b7ef..85e3c8cb 100644 --- a/docs/services/azure-kubernetes-service/pod-scheduling-agent-pools/index.md +++ b/docs/services/azure-kubernetes-service/pod-scheduling-agent-pools/index.md @@ -17,8 +17,7 @@ description: AKS agent pool, label, taint와 affinity를 이용한 스케줄링 technologies: - kubernetes tags: -- kubernetes -- deployment +- deploy redirect_from: - guides/azure-kubernetes-service/pod-scheduling-agent-pools/index.md --- diff --git a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-bucket.png b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-bucket.png deleted file mode 100644 index 3e8b70b3..00000000 Binary files a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-bucket.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-certificate.png b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-certificate.png deleted file mode 100644 index c41c05c1..00000000 Binary files a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/anf-create-certificate.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/architecture.png b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/architecture.png deleted file mode 100644 index 80be1053..00000000 Binary files a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/architecture.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/pyroscope_ui.png b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/pyroscope_ui.png deleted file mode 100644 index 942a3565..00000000 Binary files a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/images/pyroscope_ui.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md index 61d0b13f..dc01970c 100644 --- a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md +++ b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/index.md @@ -18,8 +18,8 @@ technologies: - kubernetes - python tags: -- observability -- deployment +- operate +- deploy - storage redirect_from: - guides/azure-kubernetes-service/pyroscope-anf-s3/index.md diff --git a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/sample.yml b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/sample.yml index 2850287e..21310479 100644 --- a/docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/sample.yml +++ b/docs/services/azure-kubernetes-service/pyroscope-anf-s3/samples/pyroscope-chat-profiling/sample.yml @@ -3,3 +3,12 @@ description: Runnable profiling workload and deployment assets. kind: runnable used_by: - index +publish: + - source: images/anf-create-bucket.png + target: images/anf-create-bucket.png + - source: images/anf-create-certificate.png + target: images/anf-create-certificate.png + - source: images/architecture.png + target: images/architecture.png + - source: images/pyroscope_ui.png + target: images/pyroscope_ui.png diff --git a/docs/services/azure-kubernetes-service/python-memory-leak-memray/images/leak-leaks.png b/docs/services/azure-kubernetes-service/python-memory-leak-memray/images/leak-leaks.png deleted file mode 100644 index 01db9d17..00000000 Binary files a/docs/services/azure-kubernetes-service/python-memory-leak-memray/images/leak-leaks.png and /dev/null differ diff --git a/docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md b/docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md index 38808389..7a32fcb1 100644 --- a/docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md +++ b/docs/services/azure-kubernetes-service/python-memory-leak-memray/index.md @@ -18,9 +18,8 @@ technologies: - kubernetes - python tags: -- diagnostics -- performance -- troubleshooting +- diagnose +- optimize redirect_from: - guides/azure-kubernetes-service/python-memory-leak-memray/index.md --- diff --git a/docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/sample.yml b/docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/sample.yml index a2393eed..c5eca60f 100644 --- a/docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/sample.yml +++ b/docs/services/azure-kubernetes-service/python-memory-leak-memray/samples/memray-leak-profiling/sample.yml @@ -3,3 +3,6 @@ description: Runnable Python memory-leak workload with profiling reports. kind: runnable used_by: - index +publish: + - source: reports/leak-leaks.png + target: images/leak-leaks.png diff --git a/docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md b/docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md index da856ba1..8e9467d7 100644 --- a/docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md +++ b/docs/services/azure-kubernetes-service/remote-cluster-local-development/index.md @@ -18,7 +18,7 @@ technologies: - kubernetes - python tags: -- development +- build - networking redirect_from: - guides/azure-kubernetes-service/remote-cluster-local-development/index.md diff --git a/docs/services/azure-kubernetes-service/spot-h100-kaito/index.md b/docs/services/azure-kubernetes-service/spot-h100-kaito/index.md index aff29113..8dba9daa 100644 --- a/docs/services/azure-kubernetes-service/spot-h100-kaito/index.md +++ b/docs/services/azure-kubernetes-service/spot-h100-kaito/index.md @@ -18,9 +18,9 @@ technologies: - kubernetes - azure-cli tags: +- deploy +- optimize - ai-agents -- deployment -- performance redirect_from: - guides/azure-kubernetes-service/spot-h100-kaito/index.md --- diff --git a/docs/services/azure-kubernetes-service/workload-identity-databricks/index.md b/docs/services/azure-kubernetes-service/workload-identity-databricks/index.md index 304667b8..fb756142 100644 --- a/docs/services/azure-kubernetes-service/workload-identity-databricks/index.md +++ b/docs/services/azure-kubernetes-service/workload-identity-databricks/index.md @@ -20,8 +20,8 @@ technologies: - kubernetes - nodejs tags: -- authentication -- security +- secure +- identity redirect_from: - guides/azure-kubernetes-service/workload-identity-databricks/index.md --- diff --git a/docs/services/azure-load-testing/locust-appgw-aks-private/index.md b/docs/services/azure-load-testing/locust-appgw-aks-private/index.md index f1b560b7..8486bed5 100644 --- a/docs/services/azure-load-testing/locust-appgw-aks-private/index.md +++ b/docs/services/azure-load-testing/locust-appgw-aks-private/index.md @@ -18,9 +18,9 @@ technologies: - python - kubernetes tags: -- benchmarking +- evaluate +- optimize - networking -- performance redirect_from: - guides/azure-load-testing/locust-appgw-aks-private/index.md --- diff --git a/docs/services/azure-managed-redis/cluster-failover-recovery/index.md b/docs/services/azure-managed-redis/cluster-failover-recovery/index.md index 9d2d0105..44bbfaea 100644 --- a/docs/services/azure-managed-redis/cluster-failover-recovery/index.md +++ b/docs/services/azure-managed-redis/cluster-failover-recovery/index.md @@ -14,9 +14,8 @@ technologies: - nodejs - redis tags: -- disaster-recovery -- high-availability -- troubleshooting +- operate +- diagnose occurred_at: 2026-02-22 redirect_from: - cases/azure-managed-redis/cluster-failover-recovery/index.md diff --git a/docs/services/azure-monitor/aks-private-opentelemetry/index.md b/docs/services/azure-monitor/aks-private-opentelemetry/index.md index eab8b8a0..e59598b4 100644 --- a/docs/services/azure-monitor/aks-private-opentelemetry/index.md +++ b/docs/services/azure-monitor/aks-private-opentelemetry/index.md @@ -18,8 +18,7 @@ technologies: - kubernetes - open-telemetry tags: -- observability -- monitoring +- operate - networking redirect_from: - guides/azure-monitor/aks-private-opentelemetry/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md b/docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md index 315b99a5..7f8ae7b9 100644 --- a/docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/dynamic-thresholds/index.md @@ -17,9 +17,9 @@ description: Azure Monitor Dynamic Thresholds 경고를 SRE Agent 조사 흐름 technologies: - rest-api tags: -- monitoring +- operate +- design - ai-agents -- architecture topic_order: 9 redirect_from: - guides/azure-monitor/sre-agent-dynamic-thresholds/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/event-lab/index.md b/docs/services/azure-monitor/azure-sre-agent/event-lab/index.md index b9811e38..b2f7a2f5 100644 --- a/docs/services/azure-monitor/azure-sre-agent/event-lab/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/event-lab/index.md @@ -19,9 +19,9 @@ technologies: - bicep - python tags: +- diagnose +- evaluate - ai-agents -- monitoring -- troubleshooting topic_order: 1 redirect_from: - labs/azure-monitor/sre-agent-event-lab/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/images/agent-reasoning-flow.svg b/docs/services/azure-monitor/azure-sre-agent/images/agent-reasoning-flow.svg deleted file mode 100644 index 2b0b0b00..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/agent-reasoning-flow.svg +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - Your question - or incident alert - - - - - - - Understand - parse request - - - - - - - Gather context - parallel queries - - - - - - - Reason - analyze patterns - - - - - - - - - Execute - safe actions run immediately - - - - Ask for approval - destructive actions wait - - - - Respond - present findings - - - - iterate (up to 10×) - - - Your agent adjusts reasoning depth automatically — simple questions get quick answers, complex incidents get multistep analysis. - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/azure-sre-agent-networking-vnet.png b/docs/services/azure-monitor/azure-sre-agent/images/azure-sre-agent-networking-vnet.png deleted file mode 100644 index 172c118c..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/azure-sre-agent-networking-vnet.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/custom-skill-flow.svg b/docs/services/azure-monitor/azure-sre-agent/images/custom-skill-flow.svg deleted file mode 100644 index 136ef56d..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/custom-skill-flow.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - Agent - - - - - - - Custom Skill - - - - - - - Your API - - - - - - - Returns data - - - - - - - → - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/diagnose-azure-services.svg b/docs/services/azure-monitor/azure-sre-agent/images/diagnose-azure-services.svg deleted file mode 100644 index 31cad4ac..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/diagnose-azure-services.svg +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - How your agent diagnoses Azure services - - - - Service - issue - - - - - - - SRE Agent - queries all sources - correlates evidence - explains root cause - - - - - - Application Insights - KQL queries, traces, dependencies - - - - - Log Analytics - Workspace logs, custom events - - - - - Azure Monitor Metrics - CPU, memory, requests, analysis - - - - - Resource Graph & Activity - Topology, changes, deployment logs - - - - - Built-in Skills & Az CLI - Deep diagnostics, connectivity, - resource-specific checks, remediation - - - - - - - - - - - - Correlated - diagnosis - Evidence from all sources - - - No connectors needed — works through managed identity and Azure RBAC - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/github-issue.png b/docs/services/azure-monitor/azure-sre-agent/images/github-issue.png deleted file mode 100644 index dd105359..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/github-issue.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/incident-platform-flow.svg b/docs/services/azure-monitor/azure-sre-agent/images/incident-platform-flow.svg deleted file mode 100644 index d2960e1f..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/incident-platform-flow.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - Incident Platform - PagerDuty - Azure Monitor - - - - Alert - - - - Response - Plan - Filter + Mode - - - - Match - - - - Agent - Investigates - Automatically - - - - Findings - - - - Take Action - Mitigate issue - Update incident - - - - - - - Track - Metrics - - - Connect your incident platform → Agent responds automatically → Track effectiveness - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/incident-response-flow.svg b/docs/services/azure-monitor/azure-sre-agent/images/incident-response-flow.svg deleted file mode 100644 index 9d75e04f..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/incident-response-flow.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - From alert to resolution in minutes, not hours - - - - Alert Fires - PagerDuty - Azure Monitor - ServiceNow - - - - 30s - - - - Agent - Acknowledges - Notifies team - Starts investigation - - - - 1 min - - - - Investigates - Queries logs - Checks metrics - Correlates deployments - - - - 2 min - - - - Root Cause - Found - Proposes fix - or acts autonomously - - - - - - - Resolved - Ticket updated - Team notified - - - - - - - - - - 0:00 - 0:30 - 1:30 - 3:00 - 5:00 - - Minutes elapsed - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/knowledge-sources.svg b/docs/services/azure-monitor/azure-sre-agent/images/knowledge-sources.svg deleted file mode 100644 index ef2bb279..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/knowledge-sources.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - Knowledge Base Sources - - - - Upload Documents - Builder → Knowledge base - - 📄 Runbooks (.md, .txt) - 📄 Architecture guides - 📄 On-call playbooks - 📄 API documentation - 📄 Team procedures - - - - MCP Connectors - Settings → Connectors - - 🔗 Azure DevOps wikis - 🔗 GitHub repos and wikis - 🔗 Microsoft Learn docs - 🔗 Confluence pages - 🔗 Custom MCP servers - - - - - - - Agent's - Knowledge Base - Searched via - SearchMemory - + citations - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/managed-connectors-icon-grid.png b/docs/services/azure-monitor/azure-sre-agent/images/managed-connectors-icon-grid.png deleted file mode 100644 index b8750f79..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/managed-connectors-icon-grid.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/memory-auto-learning.svg b/docs/services/azure-monitor/azure-sre-agent/images/memory-auto-learning.svg deleted file mode 100644 index c8e1821f..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/memory-auto-learning.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - Your agent learns from every session - - - - Thread - Completes - Sync or Async - - - - 30 min - - - - Session - Evaluator - Auto-extracts learnings - - - - - - - Learns: - • Symptoms observed - • Steps that worked - • Root cause found - • Pitfalls to avoid - - - - - - - Indexed - Memory - For future use - - - Next time a similar incident occurs, your agent recalls what worked - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/memory-unified-search.svg b/docs/services/azure-monitor/azure-sre-agent/images/memory-unified-search.svg deleted file mode 100644 index c370461b..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/memory-unified-search.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - Sync Chat - Your questions - - - - Async - Auto-triggered - - - - - - - - SearchMemory - Searches all sources - simultaneously - - - - - - Past Sessions - Symptoms, Root Cause - Steps, Pitfalls - Auto-learned - - - - - User Memories - #remember facts - Explicit saves - - - - - Knowledge Base - Runbooks, Docs - Architecture guides - Uploaded + Connectors - - - - - - - - - Grounded - Response - With clickable citations - - - - Learnings saved back to memory (30 min after thread ends) - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/notification-paths.svg b/docs/services/azure-monitor/azure-sre-agent/images/notification-paths.svg deleted file mode 100644 index 45a7ed07..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/notification-paths.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - Agent - investigates - and recommends - - - - - - Response paths - - - - Execute fix - Scale, restart, rollback - - - - Create work item - ADO, Jira, GitHub - - - - Send notification - Teams, Outlook, Slack - - - - - - - - - Azure resources - Container Apps, VMs, AKS - - - Tracking systems - With context + evidence - - - Team channels - Formatted summary - - - - - - - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/operations-hub-overview-tab.png b/docs/services/azure-monitor/azure-sre-agent/images/operations-hub-overview-tab.png deleted file mode 100644 index f91fb636..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/operations-hub-overview-tab.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/permission-flow.svg b/docs/services/azure-monitor/azure-sre-agent/images/permission-flow.svg deleted file mode 100644 index f3ef5db3..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/permission-flow.svg +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - Agent needs to - perform action - - - - - - - Managed identity - has RBAC role? - - - - YES - - - Use managed - identity - - - - - - - NO - - - Request user - permissions (OBO) - - - - - - - Action - performed - - Restart service - Scale resources - Query logs - Modify config - Read metrics - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/portal-sub-agent-canvas-full.png b/docs/services/azure-monitor/azure-sre-agent/images/portal-sub-agent-canvas-full.png deleted file mode 100644 index 3f6ac2fd..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/portal-sub-agent-canvas-full.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/root-cause-analysis.svg b/docs/services/azure-monitor/azure-sre-agent/images/root-cause-analysis.svg deleted file mode 100644 index fe24fa57..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/root-cause-analysis.svg +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - Symptoms - errors, alerts - - - - - - Gather context - logs, metrics, deploys - - - - - - Form hypotheses - test & validate - - - - - - Root cause - with evidence - - - - - - Fix - resolve - - - - App Insights - - - Azure Monitor - - - Deployments - - - Source code - - - Memory - - - - - - - - - - Agent queries multiple data sources and correlates evidence to identify root cause - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/run-modes-comparison.svg b/docs/services/azure-monitor/azure-sre-agent/images/run-modes-comparison.svg deleted file mode 100644 index 680042a8..00000000 --- a/docs/services/azure-monitor/azure-sre-agent/images/run-modes-comparison.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - Review Mode - - - - Agent - - - - - Proposes - action - - - - - User - - - Autonomous Mode - - - - Agent - - - - - Executes - immediately - - - - - Done - - - - User approves or denies - before agent acts - - - Agent completes task - independently - diff --git a/docs/services/azure-monitor/azure-sre-agent/images/s1-agent-conclusion.png b/docs/services/azure-monitor/azure-sre-agent/images/s1-agent-conclusion.png deleted file mode 100644 index 3ebb00f2..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/s1-agent-conclusion.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/s1-email-preview.png b/docs/services/azure-monitor/azure-sre-agent/images/s1-email-preview.png deleted file mode 100644 index e58d1d1b..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/s1-email-preview.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/s1-three-panel.png b/docs/services/azure-monitor/azure-sre-agent/images/s1-three-panel.png deleted file mode 100644 index f963e1e7..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/s1-three-panel.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/images/sre-agent-process.png b/docs/services/azure-monitor/azure-sre-agent/images/sre-agent-process.png deleted file mode 100644 index 2d84f645..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/images/sre-agent-process.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/incident-runbook/index.md b/docs/services/azure-monitor/azure-sre-agent/incident-runbook/index.md index 054ba82e..71fe261c 100644 --- a/docs/services/azure-monitor/azure-sre-agent/incident-runbook/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/incident-runbook/index.md @@ -17,9 +17,9 @@ description: Azure SRE Agent 이벤트 실습의 장애 대응 runbook을 제공 technologies: - rest-api tags: -- troubleshooting -- monitoring -- reliability +- diagnose +- operate +- ai-agents topic_order: 8 redirect_from: - guides/azure-monitor/sre-agent-incident-runbook/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/index.md b/docs/services/azure-monitor/azure-sre-agent/index.md index f09671d4..9f10495a 100644 --- a/docs/services/azure-monitor/azure-sre-agent/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/index.md @@ -17,9 +17,9 @@ description: Azure SRE Agent의 기능, 구성 요소와 운영 흐름을 설명 technologies: - rest-api tags: +- operate +- diagnose - ai-agents -- monitoring -- troubleshooting redirect_from: - guides/azure-monitor/azure-sre-agent-overview/index.md --- diff --git a/docs/services/azure-monitor/azure-sre-agent/results/index.md b/docs/services/azure-monitor/azure-sre-agent/results/index.md index 79eb1a02..c5f55062 100644 --- a/docs/services/azure-monitor/azure-sre-agent/results/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/results/index.md @@ -18,9 +18,9 @@ description: 이벤트 기반 장애 분석 실습의 결과를 읽고 채점합 technologies: - python tags: +- evaluate +- diagnose - ai-agents -- monitoring -- diagnostics topic_order: 6 redirect_from: - labs/azure-monitor/sre-agent-results/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml index 5c2b8a93..bbc562d6 100644 --- a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml +++ b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/sample.yml @@ -12,3 +12,62 @@ used_by: - validation-results - incident-runbook - dynamic-thresholds +publish: + - source: assets/official/agent-reasoning-flow.svg + target: images/agent-reasoning-flow.svg + - source: assets/official/azure-sre-agent-networking-vnet.png + target: images/azure-sre-agent-networking-vnet.png + - source: assets/official/custom-skill-flow.svg + target: images/custom-skill-flow.svg + - source: assets/official/diagnose-azure-services.svg + target: images/diagnose-azure-services.svg + - source: assets/notifications/github-issue.png + target: images/github-issue.png + - source: assets/official/incident-platform-flow.svg + target: images/incident-platform-flow.svg + - source: assets/official/incident-response-flow.svg + target: images/incident-response-flow.svg + - source: assets/official/knowledge-sources.svg + target: images/knowledge-sources.svg + - source: assets/official/managed-connectors-icon-grid.png + target: images/managed-connectors-icon-grid.png + - source: assets/official/memory-auto-learning.svg + target: images/memory-auto-learning.svg + - source: assets/official/memory-unified-search.svg + target: images/memory-unified-search.svg + - source: assets/official/notification-paths.svg + target: images/notification-paths.svg + - source: assets/official/operations-hub-overview-tab.png + target: images/operations-hub-overview-tab.png + - source: assets/official/permission-flow.svg + target: images/permission-flow.svg + - source: assets/official/portal-sub-agent-canvas-full.png + target: images/portal-sub-agent-canvas-full.png + - source: assets/official/root-cause-analysis.svg + target: images/root-cause-analysis.svg + - source: assets/official/run-modes-comparison.svg + target: images/run-modes-comparison.svg + - source: assets/briefing/s1-agent-conclusion.png + target: images/s1-agent-conclusion.png + - source: assets/notifications/s1-email-preview.png + target: images/s1-email-preview.png + - source: assets/briefing/s1-three-panel.png + target: images/s1-three-panel.png + - source: assets/briefing/sre-agent-process.png + target: images/sre-agent-process.png + - source: assets/captures/s1-before-bridge/investigation.gif + target: validation-results/images/investigation.gif + - source: assets/captures/s1/investigation.gif + target: validation-results/images/s1-investigation.gif + - source: assets/captures/s2/investigation.gif + target: validation-results/images/s2-investigation.gif + - source: assets/captures/s3/investigation.gif + target: validation-results/images/s3-investigation.gif + - source: assets/official/portal-complete-setup-page.png + target: setup/images/portal-complete-setup-page.png + - source: assets/official/portal-incident-response-plans-list.png + target: setup/images/portal-incident-response-plans-list.png + - source: assets/official/portal-response-plan-autonomy-step.png + target: setup/images/portal-response-plan-autonomy-step.png + - source: assets/official/portal-setup-status-bar.png + target: setup/images/portal-setup-status-bar.png diff --git a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/published_layout.py b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/published_layout.py index 30793529..6bbd4b25 100644 --- a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/published_layout.py +++ b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/published_layout.py @@ -2,7 +2,8 @@ from __future__ import annotations -from pathlib import Path +from functools import cache +from pathlib import Path, PurePosixPath REPO_ROOT = Path(__file__).parents[8] @@ -39,3 +40,17 @@ def glob(self, pattern: str): VALIDATION_RESULTS = TOPIC_ROOT / "validation-results" / "index.md" BRIEFING = TOPIC_ROOT / "index.md" OFFICIAL_ASSETS = LAB_ROOT / "assets" / "official" + + +@cache +def resolve_published_file(path: Path) -> Path: + """Resolve a virtual page asset through the repository's validated catalog.""" + from scripts.docs.content import load_taxonomy + from scripts.docs.topics import build_topic_catalog + + if path.is_file(): + return path + catalog = build_topic_catalog(DOCS_ROOT, load_taxonomy(REPO_ROOT / "docs-taxonomy.yml")) + relative = PurePosixPath(path.resolve().relative_to(DOCS_ROOT).as_posix()) + asset = catalog.published_assets.get(relative) + return DOCS_ROOT / asset.source if asset is not None else path diff --git a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_briefing_docs.py b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_briefing_docs.py index 472774fd..7544ff93 100644 --- a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_briefing_docs.py +++ b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_briefing_docs.py @@ -2,7 +2,11 @@ import unicodedata from collections import Counter -from published_layout import BRIEFING, OFFICIAL_ASSETS as SOURCE_OFFICIAL_ASSETS +from published_layout import ( + BRIEFING, + OFFICIAL_ASSETS as SOURCE_OFFICIAL_ASSETS, + resolve_published_file, +) OFFICIAL_SVGS = { "incident-response-flow.svg", @@ -391,7 +395,7 @@ def test_briefing_uses_only_local_images_and_no_storyboards(): assert "storyboard" not in text.lower() assert ".gif" not in text.lower() for target in image_targets: - assert (BRIEFING.parent / target).resolve().exists(), target + assert resolve_published_file((BRIEFING.parent / target).resolve()).is_file(), target def test_every_official_asset_is_rendered_as_a_markdown_image(): diff --git a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_privacy.py b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_privacy.py index d56c94f6..2cbe3aa0 100644 --- a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_privacy.py +++ b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_privacy.py @@ -14,6 +14,7 @@ README, REPO_ROOT, VALIDATION_RESULTS, + resolve_published_file, ) @@ -74,7 +75,7 @@ def test_briefing_relative_links_resolve(): assert targets for target in targets: - assert (BRIEFING.parent / target).resolve().exists(), target + assert resolve_published_file((BRIEFING.parent / target).resolve()).is_file(), target def test_relocated_lab_docs_drop_internal_workflow_directives(): diff --git a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_published_layout.py b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_published_layout.py new file mode 100644 index 00000000..61ab7027 --- /dev/null +++ b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_published_layout.py @@ -0,0 +1,16 @@ +import published_layout + + +def test_published_layout_resolves_only_declared_virtual_assets(): + assert hasattr(published_layout, "resolve_published_file") + resolve = published_layout.resolve_published_file + target = published_layout.TOPIC_ROOT / "images/incident-response-flow.svg" + source = published_layout.OFFICIAL_ASSETS / "incident-response-flow.svg" + + assert not target.exists() + assert resolve(target) == source + assert source.is_file() + assert resolve(published_layout.BRIEFING) == published_layout.BRIEFING + missing = published_layout.TOPIC_ROOT / "images/not-declared.svg" + assert resolve(missing) == missing + assert not resolve(missing).exists() diff --git a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_repo_readme.py b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_repo_readme.py index 7579ec44..3eb58abc 100644 --- a/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_repo_readme.py +++ b/docs/services/azure-monitor/azure-sre-agent/samples/event-lab/scripts/tests/test_repo_readme.py @@ -11,7 +11,8 @@ def test_root_readme_routes_readers_to_the_searchable_pages_site(): text = ROOT_README.read_text(encoding="utf-8") assert "https://hellices.github.io/devguidesample/" in text - assert "전체 문서 목록은 파일로 중복 관리하지 않습니다" in text + assert "모든 주제 목록은 파일로 중복 관리하지 않습니다" in text + assert "글 찾기" in text def test_public_lab_page_routes_runnable_commands_to_the_sample_tree(): diff --git a/docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md b/docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md index 83362ef5..e9b2c3cb 100644 --- a/docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/scenario-blob-permission/index.md @@ -18,9 +18,10 @@ description: Blob 권한을 제거하고 SRE Agent의 권한 문제 진단을 technologies: - python tags: +- secure +- operate +- identity - ai-agents -- authorization -- monitoring topic_order: 5 redirect_from: - labs/azure-monitor/sre-agent-scenario-blob-permission/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md b/docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md index 8db25a96..c00a21f7 100644 --- a/docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/scenario-http-500/index.md @@ -18,9 +18,9 @@ description: HTTP 500 장애를 주입하고 SRE Agent의 탐지와 조사를 technologies: - python tags: +- diagnose +- operate - ai-agents -- troubleshooting -- monitoring topic_order: 3 redirect_from: - labs/azure-monitor/sre-agent-scenario-http-500/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md b/docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md index 50925b25..482a7588 100644 --- a/docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/scenario-latency/index.md @@ -18,9 +18,9 @@ description: 응답 지연을 주입하고 SRE Agent의 원인 분석을 확인 technologies: - python tags: +- optimize +- operate - ai-agents -- latency -- monitoring topic_order: 4 redirect_from: - labs/azure-monitor/sre-agent-scenario-latency/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-complete-setup-page.png b/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-complete-setup-page.png deleted file mode 100644 index 75ceb9af..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-complete-setup-page.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-incident-response-plans-list.png b/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-incident-response-plans-list.png deleted file mode 100644 index 398a7ee0..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-incident-response-plans-list.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-response-plan-autonomy-step.png b/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-response-plan-autonomy-step.png deleted file mode 100644 index b3ecd6c2..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-response-plan-autonomy-step.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-setup-status-bar.png b/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-setup-status-bar.png deleted file mode 100644 index 26304e90..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/setup/images/portal-setup-status-bar.png and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/setup/index.md b/docs/services/azure-monitor/azure-sre-agent/setup/index.md index 7fde52e0..5e28da0b 100644 --- a/docs/services/azure-monitor/azure-sre-agent/setup/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/setup/index.md @@ -18,9 +18,9 @@ description: 이벤트 기반 장애 분석 실습에 필요한 Azure SRE Agent technologies: - bicep tags: +- deploy +- operate - ai-agents -- deployment -- monitoring topic_order: 2 redirect_from: - labs/azure-monitor/sre-agent-event-lab-setup/index.md diff --git a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/investigation.gif b/docs/services/azure-monitor/azure-sre-agent/validation-results/images/investigation.gif deleted file mode 100644 index 524391c2..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/investigation.gif and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s1-investigation.gif b/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s1-investigation.gif deleted file mode 100644 index 4316475d..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s1-investigation.gif and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s2-investigation.gif b/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s2-investigation.gif deleted file mode 100644 index d9df97af..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s2-investigation.gif and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s3-investigation.gif b/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s3-investigation.gif deleted file mode 100644 index ba331b7e..00000000 Binary files a/docs/services/azure-monitor/azure-sre-agent/validation-results/images/s3-investigation.gif and /dev/null differ diff --git a/docs/services/azure-monitor/azure-sre-agent/validation-results/index.md b/docs/services/azure-monitor/azure-sre-agent/validation-results/index.md index dd79e2d8..a056c179 100644 --- a/docs/services/azure-monitor/azure-sre-agent/validation-results/index.md +++ b/docs/services/azure-monitor/azure-sre-agent/validation-results/index.md @@ -13,9 +13,9 @@ description: 세 가지 장애 시나리오에서 관측한 Azure SRE Agent 동 technologies: - python tags: +- evaluate +- operate - ai-agents -- monitoring -- diagnostics published_at: 2026-08-15 topic_order: 7 redirect_from: diff --git a/docs/services/azure-monitor/dynamic-thresholds-brief/index.md b/docs/services/azure-monitor/dynamic-thresholds-brief/index.md index 6fb39ee6..de95ddd9 100644 --- a/docs/services/azure-monitor/dynamic-thresholds-brief/index.md +++ b/docs/services/azure-monitor/dynamic-thresholds-brief/index.md @@ -13,8 +13,7 @@ description: Dynamic Thresholds 기능 변화와 도입 시점을 분석합니 technologies: - rest-api tags: -- monitoring -- reliability +- operate published_at: 2026-08-17 redirect_from: - research/azure-monitor/dynamic-thresholds-brief/index.md diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md b/docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md index 4c86b0f1..bc5e8bb8 100644 --- a/docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md +++ b/docs/services/azure-monitor/hdinsight-kafka-monitoring/catch-up-benchmark/index.md @@ -14,8 +14,8 @@ description: HDInsight Kafka broker SKU와 consumer fetch size에 따른 catch-u technologies: - kafka tags: -- benchmarking -- performance +- evaluate +- optimize published_at: 2026-07-31 topic_order: 2 redirect_from: diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/images/hdinsight_monitor_integration_1.png b/docs/services/azure-monitor/hdinsight-kafka-monitoring/images/hdinsight_monitor_integration_1.png deleted file mode 100644 index adf48331..00000000 Binary files a/docs/services/azure-monitor/hdinsight-kafka-monitoring/images/hdinsight_monitor_integration_1.png and /dev/null differ diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md b/docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md index d9433ee8..3b757a0e 100644 --- a/docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md +++ b/docs/services/azure-monitor/hdinsight-kafka-monitoring/index.md @@ -14,8 +14,8 @@ technologies: - kafka - prometheus tags: -- monitoring -- observability +- evaluate +- operate published_at: 2026-07-10 redirect_from: - research/azure-monitor/hdinsight-kafka-monitoring-options/index.md diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-exporter-adv.png b/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-exporter-adv.png deleted file mode 100644 index 270ae9a6..00000000 Binary files a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-exporter-adv.png and /dev/null differ diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-exporter-overview.png b/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-exporter-overview.png deleted file mode 100644 index cee3fb21..00000000 Binary files a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-exporter-overview.png and /dev/null differ diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-jmx-broker.png b/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-jmx-broker.png deleted file mode 100644 index 71c821a1..00000000 Binary files a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/images/kafka-jmx-broker.png and /dev/null differ diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md b/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md index 7d27f1c3..f48d2e59 100644 --- a/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md +++ b/docs/services/azure-monitor/hdinsight-kafka-monitoring/prometheus-grafana/index.md @@ -19,9 +19,8 @@ technologies: - prometheus - grafana tags: -- monitoring -- observability -- deployment +- deploy +- operate topic_order: 1 redirect_from: - guides/azure-monitor/hdinsight-kafka-prometheus-grafana/index.md diff --git a/docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/sample.yml b/docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/sample.yml index 1a8aefc1..7659592e 100644 --- a/docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/sample.yml +++ b/docs/services/azure-monitor/hdinsight-kafka-monitoring/samples/monitoring-assets/sample.yml @@ -5,3 +5,12 @@ used_by: - index - prometheus-grafana - catch-up-benchmark +publish: + - source: images/hdinsight_monitor_integration_1.png + target: images/hdinsight_monitor_integration_1.png + - source: images/kafka-exporter-adv.png + target: prometheus-grafana/images/kafka-exporter-adv.png + - source: images/kafka-exporter-overview.png + target: prometheus-grafana/images/kafka-exporter-overview.png + - source: images/kafka-jmx-broker.png + target: prometheus-grafana/images/kafka-jmx-broker.png diff --git a/docs/services/azure-openai/adaptive-ptu-load-balancing/images/ptu_architecture.png b/docs/services/azure-openai/adaptive-ptu-load-balancing/images/ptu_architecture.png deleted file mode 100644 index c79ced68..00000000 Binary files a/docs/services/azure-openai/adaptive-ptu-load-balancing/images/ptu_architecture.png and /dev/null differ diff --git a/docs/services/azure-openai/adaptive-ptu-load-balancing/index.md b/docs/services/azure-openai/adaptive-ptu-load-balancing/index.md index 824e3359..6c2d5c7a 100644 --- a/docs/services/azure-openai/adaptive-ptu-load-balancing/index.md +++ b/docs/services/azure-openai/adaptive-ptu-load-balancing/index.md @@ -18,8 +18,8 @@ technologies: - python - powershell tags: -- performance -- reliability +- optimize +- operate - ai-agents redirect_from: - guides/azure-openai/adaptive-ptu-load-balancing/index.md diff --git a/docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/sample.yml b/docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/sample.yml index 8609b52f..a3d7cb06 100644 --- a/docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/sample.yml +++ b/docs/services/azure-openai/adaptive-ptu-load-balancing/samples/runbooks/sample.yml @@ -3,3 +3,6 @@ description: Runnable quota controllers and architecture assets for PTU balancin kind: runnable used_by: - index +publish: + - source: ptu_architecture.png + target: images/ptu_architecture.png diff --git a/docs/services/azure-storage/mobile-resumable-upload-tus/images/architecture.png b/docs/services/azure-storage/mobile-resumable-upload-tus/images/architecture.png deleted file mode 100644 index d5625069..00000000 Binary files a/docs/services/azure-storage/mobile-resumable-upload-tus/images/architecture.png and /dev/null differ diff --git a/docs/services/azure-storage/mobile-resumable-upload-tus/index.md b/docs/services/azure-storage/mobile-resumable-upload-tus/index.md index 3a5e079a..dd006a1a 100644 --- a/docs/services/azure-storage/mobile-resumable-upload-tus/index.md +++ b/docs/services/azure-storage/mobile-resumable-upload-tus/index.md @@ -18,9 +18,9 @@ technologies: - java - rest-api tags: +- operate +- build - storage -- reliability -- development redirect_from: - guides/azure-storage/mobile-resumable-upload-tus/index.md --- diff --git a/docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/sample.yml b/docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/sample.yml index 49a3dcae..3b326bc0 100644 --- a/docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/sample.yml +++ b/docs/services/azure-storage/mobile-resumable-upload-tus/samples/spring-application/sample.yml @@ -3,3 +3,6 @@ description: Runnable Spring and tus implementation with verification scripts. kind: runnable used_by: - index +publish: + - source: architecture.png + target: images/architecture.png diff --git a/docs/services/microsoft-foundry/agent-framework-2026/index.md b/docs/services/microsoft-foundry/agent-framework-2026/index.md index 2efabf17..b26683bc 100644 --- a/docs/services/microsoft-foundry/agent-framework-2026/index.md +++ b/docs/services/microsoft-foundry/agent-framework-2026/index.md @@ -14,8 +14,8 @@ technologies: - python - go tags: +- design - ai-agents -- architecture published_at: 2026-08-17 redirect_from: - research/microsoft-foundry/agent-framework-2026/index.md diff --git a/docs/services/microsoft-foundry/agent-memory/architecture-patterns/images/02-architecture-patterns.svg b/docs/services/microsoft-foundry/agent-memory/architecture-patterns/images/02-architecture-patterns.svg deleted file mode 100644 index 6a941af4..00000000 --- a/docs/services/microsoft-foundry/agent-memory/architecture-patterns/images/02-architecture-patterns.svg +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - 메모리 시스템 아키텍처 5가지 패턴 - 왼쪽에서 오른쪽으로 갈수록 복잡도가 커진다. 패턴은 중첩된다 — Full Cognitive 안에 Graph-Augmented가, 그 안에 Dual-Store가 들어 있다. - 권장: Dual-Store에서 시작해 필요할 때 확장한다. - - - - - - 1. Single-Store - 복잡도 낮음 · 저장소 1개 - - - Vector Store - 단일 컬렉션 - 모든 기억을 한 곳에 임베딩 - - 검색 - 시맨틱 top-k 만 - 강점 - 50줄이면 구현. 라우팅 불필요 - 약점 - 커질수록 검색 품질 급락 - 시간 개념·압축 없음 - 적합 - 프로토타입 · 데모 - - - - - - 2. Dual-Store ★ - 복잡도 중간 · 버퍼 + 영구저장 - - - Short-Term Buffer - 최근 k턴 / 러닝 요약 - - flush - - Long-Term Store - 추출 사실 / 엔티티 / 요약 - 주기적으로 버퍼를 증류해 이관 - - 검색 - 버퍼 스캔 + 시맨틱 검색 - 강점 - 최근맥락·누적지식 명확히 분리 - 약점 - flush 설계(시점·추출·중복)가 - 전적으로 개발자 책임 - 적합 - 개인비서 · 고객지원 · B2C 시작점 - - - - - - 3. Tiered - 복잡도 높음 · 3계층 캐시 모델 - - - HOT · Azure Managed Redis - <1ms · 현재 세션 · 고정 사실 - - WARM · pgvector / AI Search - 5~50ms · 최근 세션 · 활성 엔티티 - - COLD · Azure Blob (Cool/Archive) - 100ms+ · 전체 이력 - 접근 빈도로 승격(promote) / 강등(demote) - - 검색 - HOT → WARM → COLD 폴스루 - 강점 - 토큰·지연·비용을 동시에 통제 - 약점 - 승격/강등 정책 튜닝 난이도 최상 - 중요 기억이 COLD에 갇힐 위험 - 적합 - 대규모 · 엄격한 지연 요구 - - - - - - 4. Graph-Augmented - 복잡도 중상 · 벡터 + 지식그래프 - - - Vector - 의미 유사도 - - Graph - 관계 순회 - - - - Result Merger - 중복제거 · 재랭킹 - 턴마다 엔티티 추출기가 양쪽을 갱신 - - 검색 - 시맨틱 + 그래프 순회 병렬 - 강점 - 멀티홉 추론. 시간 정보를 엣지에 - 약점 - 엔티티 추출 오류·환각. 스키마 - 설계 필요. 병합 지연 추가 - 적합 - 엔티티 풍부한 도메인 (커머스 카탈로그) - - - - - - 5. Full Cognitive - 복잡도 최상 · 전문화된 다중 저장소 - - - Working Memory (토큰 예산) - - Memory Router - - Episodic - 무슨 일이 - - Semantic - 무엇이 참 - - Procedural - 어떻게 - - Background Jobs - Consolidation · Compaction · Decay · Reflection - 라우터 정확도가 시스템 전체 품질을 결정 - - 검색 - 라우터가 지목한 저장소만 조회 - 강점 - 저장소별 독립 확장. 최고 표현력 - 약점 - 구현·운영 부담 최대. 오버엔지니어링 - 위험이 가장 큰 패턴 - 적합 - 수개월 축적되는 장수 에이전트 - - 실사례 매핑 — Single: LangChain VectorStoreRetrieverMemory · Dual: Mem0 · Tiered: MemGPT 계층, Anthropic Claude Code 계층 · Graph: Zep, Graphiti, GraphRAG · Cognitive: Letta/MemGPT, SOAR - diff --git a/docs/services/microsoft-foundry/agent-memory/architecture-patterns/index.md b/docs/services/microsoft-foundry/agent-memory/architecture-patterns/index.md index d9420977..e7c92ba0 100644 --- a/docs/services/microsoft-foundry/agent-memory/architecture-patterns/index.md +++ b/docs/services/microsoft-foundry/agent-memory/architecture-patterns/index.md @@ -13,8 +13,8 @@ description: Agent memory를 저장하고 연결하는 주요 아키텍처 패 technologies: - rag tags: +- design - ai-agents -- architecture published_at: 2026-08-24 topic_order: 2 redirect_from: diff --git a/docs/services/microsoft-foundry/agent-memory/commerce/images/05-commerce-blueprint.svg b/docs/services/microsoft-foundry/agent-memory/commerce/images/05-commerce-blueprint.svg deleted file mode 100644 index d970895a..00000000 --- a/docs/services/microsoft-foundry/agent-memory/commerce/images/05-commerce-blueprint.svg +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - - - - - - - - - - B2C 커머스 에이전트 — 실시간 추천 · 개인 맞춤 구매 유도를 위한 메모리 설계 - 자체 에이전트 프레임워크 위에 얹는 구조. 핵심은 읽기(실시간)와 쓰기(비동기)의 완전한 분리, 그리고 decay 속도가 다른 기억을 섞지 않는 것. - - - 신호 소스 — 대화만이 메모리 입력이 아니다 - - 대화 턴 - 발화 · 질문 · 불만 - - - 탐색 행동 - 조회 · 검색어 · 체류시간 - - - 장바구니 · 위시리스트 - 담기 · 빼기 · 이탈 - - - 거래 이력 - 구매 · 반품 · 교환 사유 - - - CS · 리뷰 - 불만 · 만족 · 사이즈 후기 - - - - ① 실시간 경로 — 사용자를 기다리게 하는 유일한 구간 - LLM 호출은 마지막 1회만. 앞단은 전부 조회/스코어링으로 끝낸다. - - - 1. 컨텍스트 조립 - HOT 캐시에서 유저 - 고정 프로필 pin 로드 - + 현재 세션 의도 - - 목표 < 5ms - - - 2. 후보 생성 - 벡터 + BM25 병렬 - → RRF 융합 - 관련 기억 + 상품 top-K - - 20~50ms - - - 3. 랭킹 · 하드 필터 - temporal decay 가중 곱 - 제약 위반 상품 제거 - + MMR 다양성 - - < 10ms - - - 4. 메시지 생성 - LLM 호출 1회 - 근거 기억 ID를 함께 - 반환 (설명가능성) - - 지연 대부분 차지 - - - - - - - 조회 - - - 실시간 경로에서 금지할 것 - Cross-Encoder 리랭크(+50~200ms) · HyDE(LLM 1회 추가) · Memory Router LLM 분류(200~500ms) · 사실 추출 · Consolidation - → 정확도가 정말 필요하면 "추천 카드 프리페치"처럼 사용자 대기 밖에서 미리 계산해 둔다. - - - - 메모리 저장소 — decay 속도로 분리한다 - - - A. 고정 프로필 (Semantic) — No decay · Pinned - 사이즈 · 알레르기/성분 회피 · 브랜드 선호 · 예산대 · 배송지 유형 - HOT - - - B. 세션 의도 (Working) — half-life 수십 분 - 지금 찾는 카테고리 · 비교 중인 상품 · 방금 거절한 추천 · 예산 발언 - HOT - - - C. 행동 에피소드 (Episodic) — half-life 수주 - 조회/담기/구매/반품 + 타임스탬프 · "지난달에 뭘 샀지" 질의 대응 - WARM - - - D. 상품 · 관계 그래프 (Entity Graph) - 유저 ↔ 상품 ↔ 브랜드 ↔ 카테고리 ↔ 대체재 · 멀티홉 추천 근거 - - - - ② 비동기 학습 경로 — 여기서 메모리 품질이 결정된다 - 응답 반환 직후 큐에 적재. 사용자 지연에 전혀 포함되지 않으므로 비싼 LLM 작업을 여기 몰아넣는다. - - - 1. 이벤트 큐 - 대화 턴 + 행동 로그를 - 동일 스키마로 정규화 - 유실 허용 X · 재처리 가능하게 - - - 2. 동의 · PII 게이트 - 마케팅 활용 동의 여부 확인 - 민감정보 마스킹 - 동의 없으면 여기서 폐기 - - - 3. 사실 추출 - 저비용 모델로 "저장 가치" - 1차 판정 → 통과분만 정밀 추출 - 지연·비용 최적화의 정석 - - - 4. 중복 · 모순 해소 + 라우팅 - "235 신는다" vs "240으로 바꿨다" - 최신 우선 + 구버전 아카이브 - A/B/C/D 저장소로 분배 - - - 5. 유지보수 배치 - Consolidation · Decay · 승격/강등 - 유저당 예산 초과분 eviction - 야간/유휴 시간대 실행 - - - - - - - - 저장 (비동기 쓰기) - - - B2C 전용 가드레일 — 기술 문제가 아니라 신뢰 문제다 - - - 과잉 개인화 방지 - 기억을 드러내는 강도를 단계화한다. - "지난번 그 브랜드" O / "화요일 밤 11시에 - 보시던 상품" X - - - 오기억 방지 · 확인 루프 - 추론된 사실은 confidence를 낮게 두고, - 구매 유도에 쓰기 전 가볍게 확인한다. - 틀린 기억 1회가 신뢰를 크게 깎는다. - - - 사용자 통제권 - "이거 기억해 / 잊어" 명령 지원, - 기억 목록 조회·삭제 UI, 삭제 시 - 임베딩·그래프·캐시까지 전파. - - - 메시지 빈도 · 피로도 - "거절한 추천"도 기억해 재노출을 막는다. - 푸시/유도 메시지는 유저별 쿨다운을 - 메모리에 기록해 강제한다. - diff --git a/docs/services/microsoft-foundry/agent-memory/commerce/index.md b/docs/services/microsoft-foundry/agent-memory/commerce/index.md index a0eb1727..41feb12e 100644 --- a/docs/services/microsoft-foundry/agent-memory/commerce/index.md +++ b/docs/services/microsoft-foundry/agent-memory/commerce/index.md @@ -13,8 +13,9 @@ description: 실시간 추천과 개인화 구매 경험에 agent memory를 적 technologies: - rag tags: +- design +- build - ai-agents -- architecture published_at: 2026-08-24 topic_order: 6 redirect_from: diff --git a/docs/services/microsoft-foundry/agent-memory/frameworks/index.md b/docs/services/microsoft-foundry/agent-memory/frameworks/index.md index 4968cfd2..b06ddca5 100644 --- a/docs/services/microsoft-foundry/agent-memory/frameworks/index.md +++ b/docs/services/microsoft-foundry/agent-memory/frameworks/index.md @@ -13,8 +13,8 @@ description: Agent memory 구현을 위한 프레임워크와 관리형 플랫 technologies: - rag tags: +- evaluate - ai-agents -- architecture published_at: 2026-08-24 topic_order: 4 redirect_from: diff --git a/docs/services/microsoft-foundry/agent-memory/index.md b/docs/services/microsoft-foundry/agent-memory/index.md index 4d059a12..44a8276e 100644 --- a/docs/services/microsoft-foundry/agent-memory/index.md +++ b/docs/services/microsoft-foundry/agent-memory/index.md @@ -13,8 +13,8 @@ description: AI agent memory의 분류, 아키텍처, 검색, 프레임워크와 technologies: - rag tags: +- design - ai-agents -- architecture published_at: 2026-08-24 redirect_from: - research/microsoft-foundry/agent-memory-overview/index.md diff --git a/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/images/03-memory-pipeline.svg b/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/images/03-memory-pipeline.svg deleted file mode 100644 index 68a51a5d..00000000 --- a/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/images/03-memory-pipeline.svg +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - 메모리 파이프라인 — 쓰기 경로와 읽기 경로는 분리한다 - 실시간 응답을 지키는 핵심 원칙: LLM을 쓰는 무거운 작업(추출·통합)은 전부 비동기 쓰기 경로로 밀어낸다. - - - - ① 쓰기 경로 (비동기 · 응답 경로를 막지 않는다) - 턴 종료 후 / 배치 / 유휴 시간에 실행 — 사용자 대기시간에 포함되지 않음 - - - - 대화 턴 - user + assistant - 메시지 원문 - - - PII 스캔 · 마스킹 - 저장 전에 반드시 - 정규식만으론 부족 - - - 사실 추출 (LLM) - 선호 · 제약 · 의도 - 저비용 모델로 1차 게이트 - - - 중복 · 모순 판정 - 코사인 > 0.85 → 중복 - 0.50~0.85 → 모순 검사 - - - Memory Router - 유형 분류 후 분배 - 오분류 시 조용히 유실 - - - 저장소 기록 - Episodic / Semantic - Procedural / Entity Graph - - - 유지보수 잡 - Consolidation · Compaction · Decay - 주기/임계값 트리거 - - - - - - - - - - 유지보수 잡의 3종 세트 - • Consolidation(14) — 유사 기억 클러스터링 → 병합 → 모순 해소(최신 우선) → 중요도 재계산. 클러스터당 LLM 호출이 들어가므로 비용이 저장소 크기에 비례한다. - • Compaction(15) — 다단계 요약으로 토큰/용량 절감. 정확도가 아니라 "크기"가 목적일 때 쓴다. - • Forgetting & Decay(19) — S(t) = S₀·e^(−λt). 접근 시 강화(reinforcement), 임계값 미만은 soft/hard delete. 반드시 pin 예외 목록을 함께 설계한다. - - - - ② 읽기 경로 (동기 · 사용자 지연 예산 안에서 끝나야 한다) - 단계를 더할수록 정확도는 오르고 지연도 오른다 — 실시간 채팅에서는 뒷단계를 과감히 생략한다 - - - - 사용자 쿼리 - 현재 발화 + - 세션 컨텍스트 - - - HyDE 확장 - 가상 답변 생성 후 - 그걸로 검색 (선택) - + LLM 호출 1회 - - - 벡터 검색 - - BM25 키워드 - 병렬 실행 · 20~50ms - - - RRF 융합 - 두 랭킹을 하나로 - Reciprocal Rank Fusion - 거의 무료 - - - Cross-Encoder - (쿼리,문서) 쌍 재점수 - 정밀도 최대 향상 - +50~200ms - - - MMR 다양성 - 유사 결과 5개 반복 - 방지 (lambda 튜닝) - 거의 무료 - - - 시간 가중 - recency decay 곱 - stable은 decay 면제 - 거의 무료 - - - 컨텍스트 조립 - 섹션별 토큰 예산 - 초과분 evict - 거의 무료 - - - - - - - - - - - - 지연 예산 판단 기준 - • 벡터 단독 20~50ms → 전체 파이프라인 200~500ms. 하이브리드 검색만으로도 리콜이 통상 15~30% 개선된다. - • 항목 50개 미만 저장소에서는 풀 파이프라인 이득이 거의 없다. 파라미터(RRF k, MMR lambda, 후보 수)가 늘수록 오설정 위험이 커진다. - - 핵심 설계 결론 - 쓰기 경로에서 품질을 만들고(추출 정확도·중복 제거·모순 해소), 읽기 경로에서는 저장된 품질을 싸게 꺼내 쓴다. 읽기 경로에 LLM 호출을 넣는 순간 실시간성이 무너진다. - Mem0가 채택한 2단계(extraction → update) 구조가 정확히 이 원칙이며, "knowledge agent"(대화를 관찰하는 별도 에이전트) 패턴도 같은 분리를 말한다. - diff --git a/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/index.md b/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/index.md index 69af8c25..e1b5f40c 100644 --- a/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/index.md +++ b/docs/services/microsoft-foundry/agent-memory/pipeline-retrieval/index.md @@ -14,8 +14,9 @@ featured: true technologies: - rag tags: +- build +- optimize - ai-agents -- architecture published_at: 2026-08-24 topic_order: 3 redirect_from: diff --git a/docs/services/microsoft-foundry/agent-memory/production-evaluation/images/04-production-tiers.svg b/docs/services/microsoft-foundry/agent-memory/production-evaluation/images/04-production-tiers.svg deleted file mode 100644 index 6313c796..00000000 --- a/docs/services/microsoft-foundry/agent-memory/production-evaluation/images/04-production-tiers.svg +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - 프로덕션 메모리 아키텍처 — 계층 저장 + 가드레일 - 메모리 기법이 아니라 "그 위에 얹는 운영 계층"이다. B2C 규모에서는 선택이 아니라 전제 조건. - - - - Ingestion — 저장 전 처리 - - - 신규 메시지 - 대화 턴 / 행동 이벤트 - - - PII 스캐너 · 레닥터 - 이메일·전화·주민번호 등 - 정규식은 이름·주소를 놓친다 → 전용 서비스 필요 - - - Fact Extractor (LLM) - 구조화 사실 + 엔티티 관계 - 저비용 모델로 가치 판단 후 승격 - - - Embedding Generator - 배치 처리 · 임베딩 캐시로 - 중복 생성 비용 제거 - - - Cost Tracker (게이트) - 일/월 상한 대비 실지출 추적 - 초과 시 추출 강도 하향 - - - - - - - - - Tiered Storage — 접근 빈도로 승격/강등 - - - HOT - Azure Managed Redis - TTL: 수분~수시간 - 지연 < 1ms - - - WARM - Azure DB for PostgreSQL (pgvector) / AI Search - TTL: 수일~수개월 - 지연 5~50ms - - - COLD - Azure Blob Storage (Cool/Archive) - TTL: 무기한 - 지연 100ms+ - - - 강등 - - 승격 - - 강등 - - 승격 - - 승격(promote): 접근 횟수 임계 초과 · 에이전트가 명시적으로 pin · 최근 N턴 내 참조 - 강등(demote): M턴 동안 미접근 · relevance score 하락 · 세션 종료 · decay score < 컷오프 - - - - Relationship Layer - - Entity Graph - 노드 = 유저 · 상품 · 카테고리 · 브랜드 - 엣지 = 조회 · 구매 · 반품 · 선호 · 대체 - 멀티홉 질의: "이 사람이 좋아한 브랜드의 신상" - 엣지에 타임스탬프를 실으면 시간 추론이 자연스러워진다 - (Graphiti / Zep이 채택한 방식) - - - - Guardrails — 모든 티어를 가로지른다 - - - Budget Enforcer - 유저당 최대 토큰 / 최대 기억 개수 상한 - 초과 시 중요도 가중 eviction - 중요도 점수가 틀리면 가치 있는 기억이 날아간다 - - - Privacy Manager - GDPR 삭제권 — 원문·추출사실·임베딩· - 그래프 노드/엣지·캐시까지 전부 삭제 - 저장소가 여러 개일수록 누락 위험 ↑ · 감사 로그 필수 - - - Observability - 검색 지연 · 캐시 적중률 · 저장소 크기 · - 에러율 · 추출 품질을 상시 계측 - OpenTelemetry / 구조화 로깅 / 분산 추적 - - 이 계층을 언제 넣나 - 활성 유저 100명 초과 · 민감정보 취급 · 가용성 보장 필요 — 이 중 하나라도 해당하면 필요하다. 반대로 그 전에 넣으면 오버엔지니어링이다. - 주의: 기법 30은 단일 노드 패턴까지만 다룬다. 수평 샤딩·분산 캐시·리전 간 복제는 별도 인프라 설계가 필요하다. - diff --git a/docs/services/microsoft-foundry/agent-memory/production-evaluation/index.md b/docs/services/microsoft-foundry/agent-memory/production-evaluation/index.md index 3fbb6513..965509f6 100644 --- a/docs/services/microsoft-foundry/agent-memory/production-evaluation/index.md +++ b/docs/services/microsoft-foundry/agent-memory/production-evaluation/index.md @@ -13,8 +13,9 @@ description: Agent memory를 프로덕션에서 운영하고 품질을 평가하 technologies: - rag tags: +- operate +- evaluate - ai-agents -- reliability published_at: 2026-08-24 topic_order: 5 redirect_from: diff --git a/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/sample.yml b/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/sample.yml index 61fc3485..2b89b8ac 100644 --- a/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/sample.yml +++ b/docs/services/microsoft-foundry/agent-memory/samples/research-artifacts/sample.yml @@ -9,3 +9,14 @@ used_by: - frameworks - production-evaluation - commerce +publish: + - source: images/02-architecture-patterns.svg + target: architecture-patterns/images/02-architecture-patterns.svg + - source: images/05-commerce-blueprint.svg + target: commerce/images/05-commerce-blueprint.svg + - source: images/03-memory-pipeline.svg + target: pipeline-retrieval/images/03-memory-pipeline.svg + - source: images/04-production-tiers.svg + target: production-evaluation/images/04-production-tiers.svg + - source: images/01-memory-taxonomy.svg + target: taxonomy/images/01-memory-taxonomy.svg diff --git a/docs/services/microsoft-foundry/agent-memory/taxonomy/images/01-memory-taxonomy.svg b/docs/services/microsoft-foundry/agent-memory/taxonomy/images/01-memory-taxonomy.svg deleted file mode 100644 index e14c46bc..00000000 --- a/docs/services/microsoft-foundry/agent-memory/taxonomy/images/01-memory-taxonomy.svg +++ /dev/null @@ -1,94 +0,0 @@ - - - - Agent Memory 분류 체계 — 6개 패밀리 / 30개 기법 - 각 패밀리는 서로 다른 문제를 해결한다. 하나의 기법이 다른 기법을 대체하지 않는다. - - - - - - - ① 단기 기억 (Short-term) - 01–05 - 한 대화(세션) 안에서 컨텍스트 윈도우를 넘기지 - 않으면서 최근 맥락을 유지한다. - 01 Conversation Buffer — 전체 원문 보관 - 02 Sliding Window — 최근 k턴만 유지 - 03 Summary — 과거를 요약으로 치환 - 04 Summary Buffer — 요약 + 최근 원문 - 05 Token Buffer — 토큰 예산 강제 트리밍 - - - - - - ② 장기 기억 (Long-term) - 06–11 - 세션·사용자·시간을 넘어 지식을 보존한다. - 개인화의 실질적 토대가 되는 계층. - 06 Vector Store — 의미 기반 유사도 검색 - 07 Entity — 인물·상품·프로젝트 단위 추적 - 08 Knowledge Graph — 관계 그래프 순회 - 09 Episodic — "언제 무슨 일이" 사건 기록 - 10 Semantic · 11 Procedural — 사실 / 절차 - - - - - - ③ 인지 아키텍처 (Cognitive) - 12–19 - 인간 기억을 모사한 운영 계층. 저장이 아니라 - "기억을 관리하는 방법"을 다룬다. - 12 Working · 13 Hierarchical — 주의/티어 - 14 Consolidation — 병합·중복제거·충돌해소 - 15 Compaction — 압축으로 토큰 절감 - 16 Self-Reflection · 17 Routing - 18 Temporal · 19 Forgetting & Decay - - - - - - - ④ 검색 · 멀티에이전트 - 20–23 - "무엇을 언제 꺼낼 것인가". 저장보다 검색이 - 체감 품질을 좌우한다. - 20 Retrieval Patterns — 하이브리드+리랭크 - 21 Cross-Session — 유저별 상태 복원 - 22 Multi-Agent Shared — 공유 저장소·권한 - 23 Memory as Tools — 저장/검색/삭제를 - 에이전트 툴로 노출 - - - - - - ⑤ 프레임워크 · 플랫폼 - 24–27 - 직접 구현 대신 도입 가능한 완제품 계층. - 속도를 얻고 통제권을 내준다. - 24 Graphiti — 시간 인식 지식 그래프 - 25 Mem0 — 관리형 추출·저장·검색 레이어 - 26 Letta(MemGPT) — 자기수정 3계층 메모리 - 27 Zep — 대화 분류 + 시간 그래프 - (MS 자료 추가: Cognee — 듀얼 스토어 시맨틱 메모리) - - - - - - ⑥ 평가 · 프로덕션 - 28–30 - "동작한다"를 숫자로 바꾸고 규모에서 버티게 - 만드는 운영 계층. B2C에서는 필수. - 28 Evaluation — Precision/Recall/MRR, - staleness · 모순 탐지 · LLM-as-Judge - 29 Benchmarks — LoCoMo / LongMemEval - 30 Production — 캐싱·TTL·샤딩·GDPR· - 관측성·비용 관리 - - 단기 → 장기 → 인지 → 검색 → (프레임워크) → 프로덕션 순으로 쌓아 올린다. 하위 패밀리 없이 상위만 도입하면 대부분 실패한다. - diff --git a/docs/services/microsoft-foundry/agent-memory/taxonomy/index.md b/docs/services/microsoft-foundry/agent-memory/taxonomy/index.md index a4167e42..00a12181 100644 --- a/docs/services/microsoft-foundry/agent-memory/taxonomy/index.md +++ b/docs/services/microsoft-foundry/agent-memory/taxonomy/index.md @@ -13,8 +13,8 @@ description: AI agent가 기억할 정보의 유형과 수명주기를 분류합 technologies: - rag tags: +- design - ai-agents -- architecture published_at: 2026-08-24 topic_order: 1 redirect_from: diff --git a/docs/services/microsoft-foundry/codex-closed-network/index.md b/docs/services/microsoft-foundry/codex-closed-network/index.md index 8c7863e8..26e9b7e5 100644 --- a/docs/services/microsoft-foundry/codex-closed-network/index.md +++ b/docs/services/microsoft-foundry/codex-closed-network/index.md @@ -17,8 +17,8 @@ description: 폐쇄망에서 Codex와 Microsoft Foundry를 연동할 때 필요 technologies: - rest-api tags: +- secure - networking -- security - ai-agents redirect_from: - guides/microsoft-foundry/codex-closed-network/index.md diff --git a/docs/services/microsoft-foundry/foundry-local-air-gapped/index.md b/docs/services/microsoft-foundry/foundry-local-air-gapped/index.md index 778ca72a..6fe9417c 100644 --- a/docs/services/microsoft-foundry/foundry-local-air-gapped/index.md +++ b/docs/services/microsoft-foundry/foundry-local-air-gapped/index.md @@ -17,8 +17,8 @@ description: Foundry Local SDK와 CLI의 제약 및 에어갭 환경 구성을 technologies: - azure-cli tags: -- deployment -- security +- deploy +- secure - ai-agents redirect_from: - guides/microsoft-foundry/foundry-local-air-gapped/index.md diff --git a/docs/services/microsoft-foundry/gpt-memory-layer/images/hybrid-architecture.png b/docs/services/microsoft-foundry/gpt-memory-layer/images/hybrid-architecture.png deleted file mode 100644 index 6f1a577e..00000000 Binary files a/docs/services/microsoft-foundry/gpt-memory-layer/images/hybrid-architecture.png and /dev/null differ diff --git a/docs/services/microsoft-foundry/gpt-memory-layer/index.md b/docs/services/microsoft-foundry/gpt-memory-layer/index.md index 281dffc6..409f7d33 100644 --- a/docs/services/microsoft-foundry/gpt-memory-layer/index.md +++ b/docs/services/microsoft-foundry/gpt-memory-layer/index.md @@ -17,8 +17,8 @@ description: Microsoft Foundry 기반 GPT 애플리케이션의 메모리 레이 technologies: - rag tags: +- design - ai-agents -- architecture redirect_from: - guides/microsoft-foundry/gpt-memory-layer/index.md --- diff --git a/docs/services/microsoft-foundry/gpt-memory-layer/samples/architecture/sample.yml b/docs/services/microsoft-foundry/gpt-memory-layer/samples/architecture/sample.yml index 0eece244..17d1a3a6 100644 --- a/docs/services/microsoft-foundry/gpt-memory-layer/samples/architecture/sample.yml +++ b/docs/services/microsoft-foundry/gpt-memory-layer/samples/architecture/sample.yml @@ -3,3 +3,6 @@ description: Editable and rendered hybrid memory architecture diagrams. kind: artifact used_by: - index +publish: + - source: hybrid-architecture.png + target: images/hybrid-architecture.png diff --git a/mkdocs.yml b/mkdocs.yml index 22157952..73df1196 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -80,4 +80,7 @@ markdown_extensions: extra_css: - assets/stylesheets/extra.css +extra_javascript: + - assets/javascripts/explore.js + copyright: Copyright © Azure Engineering Notes contributors diff --git a/scripts/docs/content.py b/scripts/docs/content.py index 82fc02ce..16cfa758 100644 --- a/scripts/docs/content.py +++ b/scripts/docs/content.py @@ -57,6 +57,59 @@ def load_taxonomy(path: Path | str) -> dict[str, Any]: return data +def validate_taxonomy(taxonomy: Mapping[str, Any]) -> list[str]: + """Require a disjoint, complete tag grouping and valid legacy filter targets.""" + errors: list[str] = [] + tags = taxonomy.get("tags", {}) + groups = taxonomy.get("tag_groups") + if not isinstance(groups, Mapping) or not groups: + return ["tag_groups must be a non-empty mapping"] + seen: set[str] = set() + for slug, group in groups.items(): + if not isinstance(group, Mapping): + errors.append(f"tag_groups.{slug} must be a mapping") + continue + if not isinstance(group.get("label"), str) or not group["label"].strip(): + errors.append(f"tag_groups.{slug}.label must be a non-empty string") + values = group.get("tags") + if not isinstance(values, list) or not values or not all(isinstance(v, str) for v in values): + errors.append(f"tag_groups.{slug}.tags must be a non-empty list of strings") + continue + for tag in values: + if tag not in tags: + errors.append(f"tag_groups.{slug}: unknown tag: {tag}") + if tag in seen: + errors.append(f"tag_groups.{slug}: duplicate tag: {tag}") + seen.add(tag) + for tag in sorted(set(tags) - seen): + errors.append(f"tag_groups: missing tag: {tag}") + + redirects = taxonomy.get("legacy_tag_redirects", {}) + if not isinstance(redirects, Mapping): + errors.append("legacy_tag_redirects must be a mapping") + return errors + fields = {"tag": "tags", "service": "services", "technology": "technologies"} + for slug, filters in redirects.items(): + if not isinstance(slug, str) or not KEBAB_CASE.fullmatch(slug) or slug == "index": + errors.append(f"legacy_tag_redirects: invalid slug: {slug}") + if not isinstance(filters, Mapping) or not filters: + errors.append(f"legacy_tag_redirects.{slug} must be a non-empty mapping") + continue + for field, values in filters.items(): + if field not in fields: + errors.append(f"legacy_tag_redirects.{slug}: unknown filter: {field}") + continue + if not isinstance(values, list) or not values or not all(isinstance(v, str) for v in values): + errors.append(f"legacy_tag_redirects.{slug}.{field} must be a non-empty list of strings") + continue + if len(values) != len(set(values)): + errors.append(f"legacy_tag_redirects.{slug}.{field}: duplicate values") + for value in values: + if value not in taxonomy.get(fields[field], {}): + errors.append(f"legacy_tag_redirects.{slug}: unknown {field}: {value}") + return errors + + def _infer_docs_dir(path: Path) -> Path: for parent in (path.parent, *path.parents): if parent.name == "docs": @@ -283,6 +336,25 @@ def validate_document( _validate_string_list(metadata, "services", services, "service", errors) _validate_string_list(metadata, "technologies", technologies, "technology", errors) _validate_string_list(metadata, "tags", tags, "tag", errors) + tag_values = metadata.get("tags") + if isinstance(tag_values, list): + if not 1 <= len(tag_values) <= 4: + errors.append("tags must contain 1-4 tags") + if all(isinstance(tag, str) for tag in tag_values): + if len(tag_values) != len(set(tag_values)): + errors.append("tags must not contain duplicates") + groups = taxonomy.get("tag_groups", {}) + if isinstance(groups, Mapping): + ranks = { + tag: rank + for rank, name in enumerate(("goal", "subject")) + for group in [groups.get(name)] + if isinstance(group, Mapping) and isinstance(group.get("tags"), list) + for tag in group["tags"] if isinstance(tag, str) + } + order = [ranks[tag] for tag in tag_values if tag in ranks] + if order != sorted(order): + errors.append("tags must keep goal tags before subject tags") if len(parts) >= 2 and isinstance(metadata.get("services"), list): service_folder = parts[1] diff --git a/scripts/docs/explore.py b/scripts/docs/explore.py new file mode 100644 index 00000000..313227aa --- /dev/null +++ b/scripts/docs/explore.py @@ -0,0 +1,150 @@ +"""Pre-rendered topic discovery and taxonomy-owned compatibility redirects.""" + +from __future__ import annotations + +from html import escape +import json +from pathlib import PurePosixPath +from typing import Any, Mapping, Sequence +from urllib.parse import urlencode + +import yaml + +from scripts.docs.content import Document +from scripts.docs.topics import TopicCatalog + + +def topic_groups( + documents: Sequence[Document], catalog: TopicCatalog | None +) -> list[tuple[Document, list[Document]]]: + groups: dict[PurePosixPath, tuple[Document, list[Document]]] = {} + for document in documents: + topic = catalog.by_document.get(document.relative_path) if catalog else None + entry = topic.entry if topic else document + _, members = groups.setdefault(entry.relative_path, (entry, [])) + if document not in members: + members.append(document) + if catalog: + for entry, members in groups.values(): + members.sort(key=lambda doc: catalog.position_by_document.get(doc.relative_path, 0)) + return sorted(groups.values(), key=lambda group: str(group[0].metadata.get("title", "")).casefold()) + + +def count_label(documents: Sequence[Document], catalog: TopicCatalog | None) -> str: + return f"{len(topic_groups(documents, catalog))}개 주제 · {len(documents)}개 문서" + + +def _options(field: str, values: Sequence[str], vocabulary: Mapping[str, Any]) -> str: + return '
\n' + "\n".join( + f'' + for value in values + ) + "\n
\n" + + +def build_explore_page( + documents: Sequence[Document], taxonomy: Mapping[str, Any], catalog: TopicCatalog | None +) -> str: + description = "서비스·목적·주제·기술과 제목·설명으로 모든 주제를 찾습니다. 선택한 조건을 모두 갖춘 문서만 주제 안에 표시합니다." + metadata = yaml.safe_dump( + {"title": "글 찾기", "description": description, "hide": ["toc"]}, + allow_unicode=True, sort_keys=False, + ) + pieces = [ + f"---\n{metadata}---\n\n# 글 찾기\n", + '
', + f'

{description}

', + "", + '
', + '', + '', + '

여러 조건은 AND로 적용합니다. 한 문서가 선택한 모든 조건을 충족해야 합니다.

', + ] + for field, plural, label in (("service", "services", "서비스"), ("tag", "tags", "태그"), ("technology", "technologies", "기술")): + vocabulary = taxonomy.get(plural, {}) + pieces.append(f"
{label}") + if field == "tag" and taxonomy.get("tag_groups"): + for group in taxonomy["tag_groups"].values(): + pieces.append(f'
{escape(group["label"])}') + pieces.append(_options(field, group["tags"], vocabulary)) + pieces.append("
") + else: + pieces.append(_options(field, list(vocabulary), vocabulary)) + pieces.append("
") + pieces.extend([ + '', + "
", + '', + '
', + f'

{count_label(documents, catalog)}

', + '

모든 주제 · 선택한 조건 없음

', + "
", + '', + '
', + ]) + for entry, members in topic_groups(documents, catalog): + topic_id = entry.relative_path.parent.as_posix() + pieces.extend([ + f'
', + f'

{escape(str(entry.metadata.get("title", "")))}

', + f'

{len(members)}개 문서

', + '
    ', + ]) + for member in members: + metadata = member.metadata + attributes = { + "data-explore-member": member.relative_path.as_posix(), + **{ + f"data-{field}": json.dumps(metadata.get(field, []), ensure_ascii=False) + for field in ("services", "tags", "technologies") + }, + "data-search": f'{metadata.get("title", "")}\n{metadata.get("description", "")}', + } + attrs = " ".join(f'{key}="{escape(value, quote=True)}"' for key, value in attributes.items()) + target = "../" + member.relative_path.parent.as_posix() + "/" + services = " · ".join( + str(taxonomy.get("services", {}).get(slug, slug)) + for slug in metadata.get("services", []) + ) + pieces.extend([ + f'
  • ', + f'

    {escape(str(metadata.get("title", "")))}

    ', + f'

    {escape(str(metadata.get("description", "")))}

    ', + f'

    {escape(services)}

    ', + '
    ', + ]) + for tag in metadata.get("tags", []): + href = "./?" + urlencode({"tag": tag}) + label = str(taxonomy.get("tags", {}).get(tag, tag)) + pieces.append(f'{escape(label)}') + pieces.extend(["
    ", "
  • "]) + pieces.extend(["
", "
"]) + pieces.extend(["
", "
\n"]) + return "\n".join(pieces) + + +def filter_redirect_targets(taxonomy: Mapping[str, Any]) -> dict[PurePosixPath, str]: + targets = { + PurePosixPath("tags/index.md"): "../explore/", + PurePosixPath("articles/index.md"): "../explore/", + } + for slug, filters in taxonomy.get("legacy_tag_redirects", {}).items(): + targets[PurePosixPath(f"tags/{slug}.md")] = "../../explore/?" + urlencode(filters, doseq=True) + return targets + + +def build_filter_redirect_pages(taxonomy: Mapping[str, Any]) -> dict[PurePosixPath, str]: + front_matter = yaml.safe_dump( + {"title": "글 찾기로 이동", "search": {"exclude": True}, "hide": ["navigation", "toc"]}, + allow_unicode=True, sort_keys=False, + ) + return { + path: ( + f"---\n{front_matter}---\n\n" + f'\n' + f'\n\n' + "# 글 찾기로 이동\n\n" + f'

글 찾기에서 모든 주제 살펴보기

\n' + ) + for path, target in filter_redirect_targets(taxonomy).items() + } diff --git a/scripts/docs/generate_indexes.py b/scripts/docs/generate_indexes.py index 36268de7..c8e30c82 100644 --- a/scripts/docs/generate_indexes.py +++ b/scripts/docs/generate_indexes.py @@ -9,6 +9,7 @@ import posixpath import sys from typing import Any, Iterable, Mapping, Sequence +from urllib.parse import urlencode import yaml @@ -16,7 +17,8 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) -from scripts.docs.content import Document, load_taxonomy +from scripts.docs.content import Document, DocumentFormatError, load_taxonomy +from scripts.docs.explore import build_explore_page, build_filter_redirect_pages, count_label, topic_groups from scripts.docs.topics import ( LEGACY_COLLECTIONS, Topic, @@ -113,7 +115,8 @@ def build_tag_links( tags = taxonomy.get("tags", {}) links = [] for tag in dict.fromkeys(tag_values): - target = posixpath.relpath(f"tags/{tag}.md", start=index_path.parent.as_posix()) + target = posixpath.relpath("explore/index.md", start=index_path.parent.as_posix()) + target += "?" + urlencode({"tag": tag}) links.append(f"[{_md_label(_label_for(tags, tag))}]({target}){{ .dg-tag }}") if not links: return "" @@ -429,7 +432,7 @@ def _build_collection_page( body = _front_matter(title, description) body += f'
\n\n' body += _page_heading(eyebrow_prefix, len(matching), title, description) - body += "\n" + body += f'

{count_label(matching, catalog)}

\n\n' if not matching: body += _empty_state("아직 등록된 문서가 없습니다.") @@ -473,7 +476,9 @@ def _build_service_page( body = _front_matter(label, description) body += '
\n\n' body += _page_heading(_SERVICES_EYEBROW, len(matching), label, description) - body += "\n" + body += f'

{count_label(matching, catalog)}

\n\n' + explore_link = posixpath.relpath("explore/index.md", start=page_path.parent.as_posix()) + body += f'[글 찾기에서 조건 더하기]({explore_link}?{urlencode({"service": slug})}){{ .dg-text-link }}\n\n' body += _document_grid(page_path, matching, taxonomy, catalog=catalog) body += "
\n" @@ -483,7 +488,8 @@ def _build_service_page( def _build_services_overview_page( by_service: Mapping[str, list[Document]], taxonomy: Mapping[str, Any], - total_documents: int, + documents: list[Document], + catalog: TopicCatalog | None, ) -> tuple[PurePosixPath, str]: services_path = PurePosixPath("services/index.md") services = taxonomy.get("services", {}) @@ -492,8 +498,8 @@ def _build_services_overview_page( body = _front_matter(title, description) body += '
\n\n' - body += _page_heading(_SERVICES_EYEBROW, total_documents, title, description) - body += "\n" + body += _page_heading(_SERVICES_EYEBROW, len(documents), title, description) + body += f'

{count_label(documents, catalog)}

\n\n' if not services: available_slugs = sorted(by_service) @@ -520,7 +526,7 @@ def _build_services_overview_page( matching = by_service.get(slug, []) body += '
\n\n' body += f"## [{_md_label(label)}]({slug}/index.md)\n\n" - body += f'

{_escape(f"{len(matching)}개 문서")}

\n' + body += f'

{count_label(matching, catalog)}

\n' body += "\n
\n\n" body += "
\n\n" @@ -528,70 +534,6 @@ def _build_services_overview_page( return services_path, body -def _build_tag_pages( - documents: list[Document], - taxonomy: Mapping[str, Any], - *, - catalog: TopicCatalog | None = None, -) -> dict[PurePosixPath, str]: - by_tag: dict[str, list[Document]] = defaultdict(list) - for document in documents: - for tag in _metadata_values(document, "tags"): - by_tag[tag].append(document) - - tags = taxonomy.get("tags", {}) - ordered_tags = sorted(by_tag, key=lambda tag: (_label_for(tags, tag).casefold(), tag)) - title = "태그별 보기" - description = "태그를 선택하면 같은 주제를 다룬 글을 볼 수 있습니다." - overview = _front_matter(title, description) - overview += '
\n\n' - overview += _page_heading("TAGS", len(documents), title, description) - overview += "\n" - pages: dict[PurePosixPath, str] = {} - if not ordered_tags: - overview += _empty_state("아직 사용 중인 태그가 없습니다.") - else: - overview += '
\n\n' - for tag in ordered_tags: - label = _label_for(tags, tag) - matching = by_tag[tag] - overview += '
\n\n' - overview += f"## [{_md_label(label)}]({tag}.md) {{ #tag:{tag} }}\n\n" - overview += f"

{len(matching)}개 문서

\n\n
\n\n" - - page_path = PurePosixPath(f"tags/{tag}.md") - tag_description = f"{label} 태그가 붙은 글을 제목순으로 모았습니다." - body = _front_matter(label, tag_description) - body += '
\n\n' - body += _page_heading("TAG", len(matching), label, tag_description) - body += '\n[모든 태그](index.md){ .dg-text-link }\n\n' - body += _document_grid(page_path, matching, taxonomy, catalog=catalog) - body += "
\n" - pages[page_path] = body - overview += "
\n" - overview += "
\n" - pages[PurePosixPath("tags/index.md")] = overview - return pages - - -def _build_articles_page( - documents: list[Document], - taxonomy: Mapping[str, Any], - *, - catalog: TopicCatalog | None = None, -) -> tuple[PurePosixPath, str]: - page_path = PurePosixPath("articles/index.md") - title = "전체 글" - description = "설계·구현·운영 기록을 제목순으로 살펴보세요. 서비스와 태그로도 찾아볼 수 있습니다." - body = _front_matter(title, description) - body += '
\n\n' - body += _page_heading("ARTICLES", len(documents), title, description) - body += '\n[서비스별 보기](../services/index.md){ .dg-text-link } · [태그별 보기](../tags/index.md){ .dg-text-link }\n\n' - body += _document_grid(page_path, documents, taxonomy, catalog=catalog) - body += "
\n" - return page_path, body - - def build_index_pages( documents: Iterable[Document], taxonomy: Mapping[str, Any], @@ -656,11 +598,10 @@ def build_index_pages( ) pages[page_path] = page_body - services_path, services_body = _build_services_overview_page(by_service, taxonomy, len(docs)) + services_path, services_body = _build_services_overview_page(by_service, taxonomy, docs, catalog) pages[services_path] = services_body - pages.update(_build_tag_pages(docs, taxonomy, catalog=catalog)) - articles_path, articles_body = _build_articles_page(docs, taxonomy, catalog=catalog) - pages[articles_path] = articles_body + pages[PurePosixPath("explore/index.md")] = build_explore_page(docs, taxonomy, catalog) + pages.update(build_filter_redirect_pages(taxonomy)) return pages @@ -719,7 +660,7 @@ def _feature_card( return _doc_card(index_path, document, taxonomy, extra_classes="dg-feature-card") -def _build_home_stats(documents: list[Document]) -> str: +def _build_home_stats(documents: list[Document], catalog: TopicCatalog | None) -> str: total = len(documents) used_services: set[str] = set() for document in documents: @@ -729,6 +670,7 @@ def _build_home_stats(documents: list[Document]) -> str: used_tags = {tag for document in documents for tag in _metadata_values(document, "tags")} stats = [ + (len(topic_groups(documents, catalog)), "모든 주제"), (total, "전체 문서"), (len(used_services), "다루는 서비스"), (len(used_tags), "다루는 태그"), @@ -763,13 +705,11 @@ def _build_home_featured( return body -def _build_home_browse(documents: list[Document]) -> str: +def _build_home_browse(documents: list[Document], catalog: TopicCatalog | None) -> str: used_services = {service for document in documents for service in _metadata_values(document, "services")} - used_tags = {tag for document in documents for tag in _metadata_values(document, "tags")} destinations = ( ("services", "SERVICES", "서비스별 보기", "사용 중인 서비스의 설계·구현·운영 기록을 함께 읽습니다.", f"{len(used_services)}개 서비스"), - ("tags", "TAGS", "태그별 보기", "관심 있는 주제를 선택해 같은 태그가 붙은 글을 모아 봅니다.", f"{len(used_tags)}개 태그"), - ("articles", "ARTICLES", "전체 글", "모든 기록을 제목순으로 살펴봅니다.", f"{len(documents)}개 문서"), + ("explore", "EXPLORE", "글 찾기", "모든 주제에서 서비스·목적·주제·기술과 제목·설명으로 좁혀 봅니다. 선택한 조건을 모두 갖춘 문서로 바로 이동합니다.", count_label(documents, catalog)), ) body = '
\n\n' for path, eyebrow, title, description, count in destinations: @@ -832,14 +772,14 @@ def build_home_page( ) rendered = template - rendered = rendered.replace("", _build_home_stats(docs), 1) + rendered = rendered.replace("", _build_home_stats(docs, catalog), 1) rendered = rendered.replace( "", _build_home_featured(docs, taxonomy, catalog=catalog), 1, ) rendered = rendered.replace( - "", _build_home_browse(docs), 1 + "", _build_home_browse(docs, catalog), 1 ) return rendered @@ -854,6 +794,16 @@ def write_generated_pages(repo_root: Path | None = None) -> None: catalog = build_topic_catalog(root / "docs", taxonomy, documents=public_documents) documents = list(catalog.documents) + for asset in catalog.published_assets.values(): + try: + content = (root / "docs" / asset.source).read_bytes() + with mkdocs_gen_files.open(asset.target.as_posix(), "wb") as generated: + generated.write(content) + except OSError as error: + raise DocumentFormatError( + f"cannot publish asset {asset.source} to {asset.target}: {error}" + ) from error + for path, content in build_index_pages(documents, taxonomy, catalog=catalog).items(): with mkdocs_gen_files.open(path.as_posix(), "w") as generated: generated.write(content) diff --git a/scripts/docs/hooks.py b/scripts/docs/hooks.py index 5bd0ba53..353a32f7 100644 --- a/scripts/docs/hooks.py +++ b/scripts/docs/hooks.py @@ -10,7 +10,7 @@ import sys from typing import Any, Mapping -from mkdocs.structure.files import InclusionLevel +from mkdocs.structure.files import File, InclusionLevel from mkdocs.structure import StructureItem from mkdocs.structure.nav import Navigation, Section from mkdocs.structure.pages import Page @@ -22,6 +22,7 @@ from scripts.docs.content import load_taxonomy from scripts.docs.generate_indexes import build_tag_links +from scripts.docs.explore import filter_redirect_targets from scripts.docs.topics import TopicCatalog, build_topic_catalog, iter_topic_documents @@ -38,16 +39,30 @@ def _navigation_pages( return pages +class _PublishedAssetFile(File): + def is_documentation_page(self) -> bool: + return False + + def on_files(files: Any, config: Mapping[str, Any]) -> Any: - """Keep internal sample READMEs out of the published site and search index.""" + """Exclude sample sources and keep published Markdown payloads as raw assets.""" try: config.pop("_topic_catalog", None) # type: ignore[union-attr] except Exception: pass - for file in files: + catalog = _topic_catalog_from_config(config) + for file in list(files): src_uri = getattr(file, "src_uri", "") if isinstance(src_uri, str) and "/samples/" in src_uri: file.inclusion = InclusionLevel.EXCLUDED + elif catalog is not None and PurePosixPath(src_uri) in catalog.published_assets: + if file.is_documentation_page(): + published = _PublishedAssetFile( + src_uri, file.src_dir, file.dest_dir, file.use_directory_urls, + dest_uri=src_uri, inclusion=file.inclusion, + ) + files.remove(file) + files.append(published) return files @@ -251,6 +266,11 @@ def on_nav(nav: Navigation, config: Mapping[str, Any], files: Any) -> Navigation direct_pages = [child for child in children if isinstance(child, Page)] if any(page.file.src_uri in collection_indexes for page in direct_pages): continue + if any( + page.file.src_uri.startswith(("tags/", "articles/")) + for page in direct_pages + ): + continue if isinstance(item, Section) and any( page.file.src_uri == "services/index.md" for page in direct_pages ): @@ -368,11 +388,15 @@ def on_post_page(output: str, page: Any, config: Mapping[str, Any]) -> str: return output redirect_path = PurePosixPath(src_uri) + taxonomy = load_taxonomy(Path(config["docs_dir"]).parent / "docs-taxonomy.yml") + destination = filter_redirect_targets(taxonomy).get(redirect_path) canonical_path = catalog.redirects.get(redirect_path) - if canonical_path is None: + if destination is None and canonical_path is not None: + destination = _redirect_target(redirect_path, canonical_path) + if destination is None: return output - target = escape(_redirect_target(redirect_path, canonical_path), quote=True) + target = escape(destination, quote=True) output = re.sub( r'', f'', diff --git a/scripts/docs/topics.py b/scripts/docs/topics.py index 86a89988..e0bdeef6 100644 --- a/scripts/docs/topics.py +++ b/scripts/docs/topics.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from pathlib import Path, PurePosixPath +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Any, Iterable, Mapping import yaml @@ -14,6 +14,14 @@ LEGACY_COLLECTIONS = frozenset(("cases", "guides", "labs", "research")) +@dataclass(frozen=True) +class PublishedAsset: + """Validated docs-relative source and virtual destination paths.""" + + source: PurePosixPath + target: PurePosixPath + + @dataclass(frozen=True) class SampleAsset: slug: str @@ -22,6 +30,7 @@ class SampleAsset: kind: str relative_path: PurePosixPath used_by: tuple[PurePosixPath, ...] + publish: tuple[PublishedAsset, ...] = () @dataclass(frozen=True) @@ -41,6 +50,7 @@ class TopicCatalog: position_by_document: dict[PurePosixPath, int] samples_by_document: dict[PurePosixPath, tuple[SampleAsset, ...]] redirects: dict[PurePosixPath, PurePosixPath] + published_assets: dict[PurePosixPath, PublishedAsset] def _is_canonical_document_path(relative_path: PurePosixPath) -> bool: @@ -142,6 +152,86 @@ def _normalize_redirects(document: Document) -> tuple[list[PurePosixPath], list[ return paths, errors +def _paths_overlap(left: PurePosixPath, right: PurePosixPath) -> bool: + return left.is_relative_to(right) or right.is_relative_to(left) + + +def _published_assets( + manifest: Mapping[str, Any], + sample_dir: Path, + docs_root: Path, + document_paths: Iterable[PurePosixPath], +) -> tuple[tuple[PublishedAsset, ...], list[str]]: + relative = PurePosixPath(sample_dir.relative_to(docs_root).as_posix()) + publish = manifest.get("publish", []) + if not isinstance(publish, list): + return (), [f"{relative}: publish must be a list"] + + assets: list[PublishedAsset] = [] + errors: list[str] = [] + canonical_paths = set(document_paths) + canonical_paths.update(path.with_suffix(".html") for path in tuple(canonical_paths)) + for mapping in publish: + if not isinstance(mapping, Mapping): + errors.append(f"{relative}: publish entries must be mappings") + continue + paths: dict[str, PurePosixPath] = {} + for field in ("source", "target"): + raw = mapping.get(field) + if not isinstance(raw, str) or not raw.strip(): + errors.append(f"{relative}: publish {field} must be a non-empty string") + continue + path = PurePosixPath(raw) + if path.is_absolute() or PureWindowsPath(raw).drive or "\\" in raw or ".." in path.parts: + errors.append( + f"{relative}: publish {field} must be a relative path without '..' or backslashes: {raw}" + ) + continue + if field == "target" and any(component.startswith(".") for component in raw.split("/")): + errors.append( + f"{relative}: publish target must not contain hidden path components: {raw}" + ) + continue + paths[field] = path + if len(paths) != 2: + continue + + source = sample_dir / paths["source"] + topic_root = sample_dir.parent.parent + target = topic_root / paths["target"] + escaped = False + for field, path, boundary in ( + ("source", source, sample_dir), + ("target", target, topic_root), + ): + if not path.resolve().is_relative_to(boundary.resolve()): + errors.append(f"{relative}: publish {field} must stay within {boundary.name}: {paths[field]}") + escaped = True + if escaped: + continue + if not source.is_file(): + errors.append(f"{relative}: publish source must be an existing file: {paths['source']}") + continue + target_relative = relative.parent.parent / paths["target"] + if "samples" in paths["target"].parts: + errors.append(f"{relative}: publish target must not be under samples: {paths['target']}") + continue + if any(_paths_overlap(target_relative, path) for path in canonical_paths): + errors.append(f"{relative}: publish target collides with a canonical document: {target_relative}") + continue + if target.exists() or target.is_symlink(): + errors.append(f"{relative}: publish target already exists: {target_relative}") + continue + if any( + not parent.is_dir() and (parent.exists() or parent.is_symlink()) + for parent in target.parents + ): + errors.append(f"{relative}: publish target conflicts with an existing ancestor file: {target_relative}") + continue + assets.append(PublishedAsset(source=relative / paths["source"], target=target_relative)) + return tuple(assets), errors + + def _build_sample_asset( sample_dir: Path, docs_root: Path, @@ -192,6 +282,10 @@ def _build_sample_asset( else: linked_documents.append(document_path) + publish, publish_errors = _published_assets( + manifest, sample_dir, docs_root, slug_to_document.values() + ) + errors.extend(publish_errors) if errors: return None, errors @@ -203,6 +297,7 @@ def _build_sample_asset( kind=values["kind"], relative_path=relative, used_by=tuple(linked_documents), + publish=publish, ), [], ) @@ -248,6 +343,7 @@ def build_topic_catalog( by_document: dict[PurePosixPath, Topic] = {} position_by_document: dict[PurePosixPath, int] = {} samples_by_document: dict[PurePosixPath, tuple[SampleAsset, ...]] = {} + published_assets: dict[PurePosixPath, PublishedAsset] = {} for key in sorted(grouped): service, slug = key @@ -312,6 +408,13 @@ def build_topic_catalog( errors.extend(sample_errors) if sample is not None: samples.append(sample) + for asset in sample.publish: + if any(_paths_overlap(asset.target, target) for target in published_assets): + errors.append( + f"{sample.relative_path}: publish target is already owned or overlaps another target: {asset.target}" + ) + else: + published_assets[asset.target] = asset if errors: continue @@ -365,4 +468,5 @@ def build_topic_catalog( position_by_document=position_by_document, samples_by_document=samples_by_document, redirects=redirects, + published_assets=published_assets, ) diff --git a/scripts/docs/validate_links.py b/scripts/docs/validate_links.py index 7ff5631f..d917403e 100644 --- a/scripts/docs/validate_links.py +++ b/scripts/docs/validate_links.py @@ -22,7 +22,7 @@ iter_public_document_paths, load_taxonomy, ) -from scripts.docs.topics import iter_topic_documents +from scripts.docs.topics import build_topic_catalog @dataclass(frozen=True) @@ -50,7 +50,9 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None self.links.append(_RenderedLink(target, alt_text)) -def _resolve_target(page: Path, docs_dir: Path, raw_target: str) -> Path | None: +def _resolve_target( + page: Path, docs_dir: Path, raw_target: str, *, directory_shorthand: bool = True +) -> Path | None: target = raw_target.removeprefix("<").removesuffix(">") if target.startswith("#"): return None @@ -62,6 +64,8 @@ def _resolve_target(page: Path, docs_dir: Path, raw_target: str) -> Path | None: return None resolved = (docs_dir / path_text.lstrip("/")) if path_text.startswith("/") else (page.parent / path_text) resolved = resolved.resolve() + if not directory_shorthand: + return None if path_text.endswith("/") else resolved if resolved.is_dir() or path_text.endswith("/"): return resolved / "index.md" if not resolved.suffix: @@ -82,11 +86,12 @@ def validate_repository(repo_root: Path | str) -> ValidationResult: errors: list[str] = [] count = 0 try: - documents = list(iter_topic_documents(docs_dir, taxonomy)) + catalog = build_topic_catalog(docs_dir, taxonomy) except DocumentFormatError as error: count = sum(1 for _ in iter_public_document_paths(docs_dir, taxonomy)) return ValidationResult(count, [str(error)]) - for document in documents: + published_targets = {(docs_dir / target).resolve() for target in catalog.published_assets} + for document in catalog.documents: count += 1 page = document.path relative = page.relative_to(root).as_posix() @@ -99,7 +104,11 @@ def validate_repository(repo_root: Path | str) -> ValidationResult: errors.append(f"{relative}: image alt text is required") target = _resolve_target(page, docs_dir, link.target) if target is not None and not target.exists(): - errors.append(f"{relative}: target does not exist: {link.target}") + asset_target = _resolve_target( + page, docs_dir, link.target, directory_shorthand=False + ) + if asset_target not in published_targets: + errors.append(f"{relative}: target does not exist: {link.target}") return ValidationResult(count, errors) diff --git a/scripts/docs/validate_metadata.py b/scripts/docs/validate_metadata.py index b3ce8062..0116b986 100644 --- a/scripts/docs/validate_metadata.py +++ b/scripts/docs/validate_metadata.py @@ -18,6 +18,7 @@ load_document, load_taxonomy, validate_document, + validate_taxonomy, ) from scripts.docs.topics import build_topic_catalog, iter_topic_documents @@ -53,7 +54,9 @@ def validate_repository(repo_root: Path | str, today: date | None = None) -> Val root = Path(repo_root) docs_dir = root / "docs" taxonomy = load_taxonomy(root / "docs-taxonomy.yml") - errors: list[str] = [] + errors: list[str] = [ + f"docs-taxonomy.yml: {error}" for error in validate_taxonomy(taxonomy) + ] authoring_paths = list(_authoring_markdown_paths(docs_dir, taxonomy)) try: canonical_documents = list(iter_topic_documents(docs_dir, taxonomy)) @@ -84,6 +87,14 @@ def validate_repository(repo_root: Path | str, today: date | None = None) -> Val build_topic_catalog(docs_dir, taxonomy, documents=documents) except DocumentFormatError as error: errors.extend(_topic_catalog_errors(error)) + used_tags = { + tag for document in documents + if isinstance(document.metadata.get("tags"), list) + for tag in document.metadata.get("tags", []) + if isinstance(tag, str) + } + for tag in sorted(set(taxonomy.get("tags", {})) - used_tags): + errors.append(f"docs-taxonomy.yml: unused tag: {tag}") return ValidationResult(count, errors) diff --git a/scripts/docs/validate_search_index.py b/scripts/docs/validate_search_index.py index b692b4f8..cf9115a2 100644 --- a/scripts/docs/validate_search_index.py +++ b/scripts/docs/validate_search_index.py @@ -154,9 +154,20 @@ def validate_repository(repo_root: Path | str) -> SearchIndexResult: locations = { str(entry.get("location", "")) for entry in entries if isinstance(entry, dict) } + if "explore/" not in locations: + errors.append("search index explore page is missing") + explore_text = _normalized(" ".join( + _visible_text(str(entry.get("text", ""))) + for entry in entries if isinstance(entry, dict) + and (entry.get("location") == "explore/" or str(entry.get("location", "")).startswith("explore/#")) + )) for tag in sorted(used_tags): - if f"tags/{tag}/" not in locations: - errors.append(f"search index tag is missing: {tag}") + label = str(taxonomy.get("tags", {}).get(tag, tag)) + if _normalized(label) not in explore_text: + errors.append(f"search index explore tag is missing: {tag}") + for location in sorted(locations): + if location.startswith(("tags/", "articles/")): + errors.append(f"search index contains a discovery redirect: {location}") return SearchIndexResult(len(documents), len(used_tags), errors) diff --git a/tests/docs/explore-fixture.cjs b/tests/docs/explore-fixture.cjs new file mode 100644 index 00000000..c992cc7c --- /dev/null +++ b/tests/docs/explore-fixture.cjs @@ -0,0 +1,48 @@ +const vocabulary = {tag: ["design", "build", "operate", "identity"], service: ["one", "two"], technology: ["python", "kubernetes"]}; + +function element(extra = {}) { + return { + hidden: false, textContent: "", value: "", checked: false, dataset: {}, + listeners: {}, addEventListener(type, callback) { this.listeners[type] = callback; }, + ...extra, + }; +} + +function fixture(query = "", catalog) { + const controls = (catalog?.controls || Object.entries(vocabulary).flatMap(([name, values]) => + values.map(value => ({name, value, label: value})) + )).map(({name, value, label}) => element({name, value, labels: [{textContent: label}]})); + const topics = catalog?.topics || [ + [ + {tags: '["design"]', services: '["one"]', technologies: '["python"]', search: "Agent entry"}, + {tags: '["design","build"]', services: '["one"]', technologies: '["python"]', search: "Agent Memory"}, + ], + [{tags: '["build"]', services: '["two"]', technologies: '["kubernetes"]', search: "Other"}], + ]; + const rows = []; + const cards = topics.map(topic => { + const members = topic.map(dataset => element({dataset})); + rows.push(...members); + const count = element(); + return element({querySelectorAll: () => members, querySelector: () => count}); + }); + const nodes = Object.fromEntries(["form", "count", "summary", "alert", "empty"].map(key => [`[data-explore-${key}]`, element()])); + const search = element({name: "text"}); + const root = element({ + querySelector: selector => selector === '[name="text"]' ? search : nodes[selector], + querySelectorAll: selector => selector === 'input[type="checkbox"]' ? controls : cards, + }); + const urls = []; + const listeners = {}; + const env = { + location: new URL(`https://example.test/project/explore/${query}`), + history: {replaceState: (_, __, url) => { + urls.push(url); + env.location = new URL(url, env.location.href); + }}, + addEventListener(type, callback) { listeners[type] = callback; }, + }; + return {root, env, urls, listeners, nodes, controls, rows, cards, search, form: nodes["[data-explore-form]"]}; +} + +module.exports = {fixture, vocabulary}; diff --git a/tests/docs/explore-material.test.cjs b/tests/docs/explore-material.test.cjs new file mode 100644 index 00000000..225056e1 --- /dev/null +++ b/tests/docs/explore-material.test.cjs @@ -0,0 +1,98 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const vm = require("node:vm"); +const explore = require("../../docs/assets/javascripts/explore.js"); +const {fixture} = require("./explore-fixture.cjs"); + +// Execute the installed theme's real deep-link/focus/cleanup block, not a copy +// of its q behavior. A changed upstream block fails explicitly for re-review. +const assets = path.join(__dirname, "../../site/assets/javascripts"); +const mapPath = process.env.MATERIAL_SOURCE_MAP || path.join(assets, + fs.readdirSync(assets).find(name => /^bundle.*\.js\.map$/.test(name))); +const map = JSON.parse(fs.readFileSync(mapPath, "utf8")); +const sourceIndex = map.sources.findIndex(source => source.endsWith("/components/search/query/index.ts")); +assert.ok(sourceIndex >= 0, "Material search query source is available"); +const source = map.sourcesContent[sourceIndex]; +const block = source.split("/* Support search deep linking */")[1]?.split("/* Intercept focus and input events */")[0]; +assert.ok(block?.includes('url.searchParams.delete("q")'), "Material cleanup behavior must be inspected if changed"); +const materialBootstrap = block.replace('searchParams.get("q")!', 'searchParams.get("q")'); +const catalog = process.env.EXPLORE_FIXTURE ? JSON.parse(fs.readFileSync(process.env.EXPLORE_FIXTURE, "utf8")) : undefined; +const expected = catalog?.expectedCount || "1개 주제 · 1개 문서"; + +function material(f) { + let onClose = () => {}; + const state = {open: false, focusCount: 0, value: ""}; + vm.runInNewContext(materialBootstrap, { + el: {set value(value) { state.value = value; }, focus() { state.focusCount += 1; }}, + getLocation: () => new URL(f.env.location.href), + setToggle: (name, active) => { assert.equal(name, "search"); state.open = active; }, + watchToggle: name => { + assert.equal(name, "search"); + return {pipe: predicate => ({subscribe: callback => { + onClose = () => { if (predicate(false)) callback(); }; + }})}; + }, + first: predicate => predicate, + history: f.env.history, + }); + return {state, close() { state.open = false; onClose(); }}; +} + +test("Material q cleanup positive control opens/focuses search and removes every q", () => { + const f = fixture("?q=Global&q=Second&text=Agent", catalog); + const search = material(f); + assert.equal(search.state.open, true); + assert.equal(search.state.focusCount, 1); + assert.equal(search.state.value, "Global"); + search.close(); + assert.equal(f.env.location.searchParams.has("q"), false); + assert.deepEqual(f.env.location.searchParams.getAll("text"), ["Agent"]); +}); + +for (const materialFirst of [true, false]) { + test(`direct text deep link never opens Material search; reload reproduces count (Material first: ${materialFirst})`, () => { + const f = fixture("?tag=design&text=Agent&text=Memory", catalog); + let search; + if (materialFirst) search = material(f); + explore.mount(f.root, f.env); + if (!materialFirst) search = material(f); + assert.equal(search.state.open, false); + assert.equal(search.state.focusCount, 0); + assert.equal(f.nodes["[data-explore-alert]"].hidden, true); + assert.equal(f.nodes["[data-explore-count]"].textContent, expected); + assert.deepEqual(f.env.location.searchParams.getAll("text"), ["Agent", "Memory"]); + search.close(); + const reload = fixture(f.env.location.search, catalog); + const reloadedSearch = material(reload); + explore.mount(reload.root, reload.env); + assert.equal(reloadedSearch.state.focusCount, 0); + assert.equal(reload.nodes["[data-explore-count]"].textContent, expected); + assert.deepEqual(reload.rows.map(row => row.hidden), f.rows.map(row => row.hidden)); + }); +} + +test("Material dismissal deletes q but preserves Explore text, result count, and reload", () => { + const f = fixture("?tag=design&text=Agent&text=Memory&q=Global&unknown=bad", catalog); + const search = material(f); + explore.mount(f.root, f.env); + assert.equal(search.state.open, true); + assert.equal(f.env.location.searchParams.get("q"), "Global"); + assert.equal(f.nodes["[data-explore-count]"].textContent, expected); + assert.match(f.nodes["[data-explore-alert]"].textContent, /unknown=bad/); + assert.doesNotMatch(f.nodes["[data-explore-alert]"].textContent, /q=Global/); + f.form.listeners.change(); + assert.equal(f.env.location.searchParams.get("q"), "Global"); + search.close(); + assert.equal(f.env.location.searchParams.has("q"), false); + assert.deepEqual(f.env.location.searchParams.getAll("text"), ["Agent", "Memory"]); + assert.equal(f.nodes["[data-explore-count]"].textContent, expected); + f.form.listeners.change(); + assert.equal(f.env.location.searchParams.has("q"), false); + const reload = fixture(f.env.location.search, catalog); + material(reload); + explore.mount(reload.root, reload.env); + assert.equal(reload.nodes["[data-explore-count]"].textContent, expected); + assert.deepEqual(reload.rows.map(row => row.hidden), f.rows.map(row => row.hidden)); +}); diff --git a/tests/docs/explore.test.cjs b/tests/docs/explore.test.cjs new file mode 100644 index 00000000..eda66c71 --- /dev/null +++ b/tests/docs/explore.test.cjs @@ -0,0 +1,92 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const script = path.join(__dirname, "../../docs/assets/javascripts/explore.js"); +const available = fs.existsSync(script); +const explore = available ? require(script) : {}; +const {fixture, vocabulary} = require("./explore-fixture.cjs"); +const member = {tags: ["design", "build"], services: ["one"], technologies: ["python"], search: "Agent Memory\nFast retrieval"}; + +test("explore script is present", () => assert.ok(available, "dependency-free explore script is required")); +test("repeated validated parameters are deduplicated, unknowns are reported", {skip: !available}, () => { + const {filters, invalid} = explore.parseQuery("?tag=design&tag=design&tag=build&tag=old&service=one&technology=python&technology=rust&text=Agent&text=Memory&bad=x", vocabulary); + assert.deepEqual(filters, {tag: ["design", "build"], service: ["one"], technology: ["python"], text: ["Agent", "Memory"]}); + assert.deepEqual(invalid, ["tag=old", "technology=rust", "bad=x"]); +}); +test("all facets and text apply to the same member, never a topic union", {skip: !available}, () => { + const base = {tag: ["design", "build"], service: ["one"], technology: ["python"], text: ["agent", "RETRIEVAL"]}; + assert.equal(explore.matchesMember(member, base), true); + for (const [key, value] of [["tag", "operate"], ["service", "two"], ["technology", "kubernetes"], ["text", "absent"]]) { + assert.equal(explore.matchesMember(member, {...base, [key]: [...base[key], value]}), false); + } + assert.equal(explore.matchesMember({...member, tags: ["design-advanced"]}, {...base, tag: ["design"]}), false); + assert.equal(explore.matchesMember({...member, tags: ["design"]}, base), false); + assert.equal(explore.matchesMember({...member, tags: ["build"]}, base), false); +}); +test("URL roundtrip preserves repeated filters and text, excludes invalid values", {skip: !available}, () => { + const {filters} = explore.parseQuery("?tag=design&tag=old&tag=build&text=a%26b&text=%EA%B2%80%EC%83%89", vocabulary); + assert.deepEqual(explore.parseQuery(explore.queryString(filters), vocabulary).filters, filters); + assert.equal(explore.queryString({tag: [], service: [], technology: [], text: []}), ""); +}); +test("mount hides nonmatching members, keeps their topic, counts and resets", {skip: !available}, () => { + const f = fixture("?tag=design&tag=build"); + explore.mount(f.root, f.env); + assert.deepEqual(f.rows.map(row => row.hidden), [true, false, true]); + assert.deepEqual(f.cards.map(card => card.hidden), [false, true]); + assert.equal(f.nodes["[data-explore-count]"].textContent, "1개 주제 · 1개 문서"); + assert.match(f.nodes["[data-explore-summary]"].textContent, /design.*build/); + f.search.value = "missing"; + f.form.listeners.input({target: f.search}); + assert.equal(f.nodes["[data-explore-empty]"].hidden, false); + assert.equal(f.nodes["[data-explore-count]"].textContent, "0개 주제 · 0개 문서"); + f.form.listeners.reset({preventDefault() {}}); + assert.deepEqual(f.rows.map(row => row.hidden), [false, false, false]); + assert.equal(f.search.value, ""); + assert.ok(f.controls.every(control => !control.checked)); + assert.equal(f.urls.at(-1), "/project/explore/"); + assert.equal(f.nodes["[data-explore-count]"].textContent, "2개 주제 · 3개 문서"); +}); +test("invalid query values alert visibly and are removed from URL", {skip: !available}, () => { + const f = fixture("?tag=old&service=one"); + explore.mount(f.root, f.env); + assert.equal(f.nodes["[data-explore-alert]"].hidden, false); + assert.match(f.nodes["[data-explore-alert]"].textContent, /tag=old/); + assert.deepEqual(f.rows.map(row => row.hidden), [false, false, true]); + assert.equal(f.urls.at(-1), "/project/explore/?service=one"); +}); + +test("Material q is external: not a text filter, not invalid, preserved on rewrites", () => { + const query = "?q=Global&q=Search&text=Agent&tag=design&unknown=x"; + const {filters, invalid} = explore.parseQuery(query, vocabulary); + assert.deepEqual(filters, {tag: ["design"], service: [], technology: [], text: ["Agent"]}); + assert.deepEqual(invalid, ["unknown=x"]); + const serialized = new URLSearchParams(explore.queryString(filters, query)); + assert.deepEqual(serialized.getAll("q"), ["Global", "Search"]); + assert.deepEqual(serialized.getAll("text"), ["Agent"]); + assert.equal(serialized.has("unknown"), false); +}); + +test("Explore edit, popstate, and reset preserve current external q without reviving deleted q", () => { + const f = fixture("?q=Global&text=Agent&text=Memory&tag=design"); + explore.mount(f.root, f.env); + assert.equal(f.nodes["[data-explore-alert]"].hidden, true); + assert.equal(f.search.value, "Agent Memory"); + assert.equal(f.nodes["[data-explore-count]"].textContent, "1개 주제 · 1개 문서"); + f.search.value = "entry"; + f.form.listeners.input({target: f.search}); + assert.equal(f.env.location.searchParams.get("q"), "Global"); + assert.deepEqual(f.env.location.searchParams.getAll("text"), ["entry"]); + assert.match(f.nodes["[data-explore-summary]"].textContent, /검색: entry/); + f.env.location = new URL("https://example.test/project/explore/?tag=design&text=Agent&text=Memory&q=Next#results"); + f.listeners.popstate(); + assert.equal(f.search.value, "Agent Memory"); + assert.equal(f.nodes["[data-explore-count]"].textContent, "1개 주제 · 1개 문서"); + f.form.listeners.reset({preventDefault() {}}); + assert.equal(f.env.location.search, "?q=Next"); + assert.equal(f.env.location.hash, "#results"); + assert.equal(f.search.value, ""); + f.env.location.searchParams.delete("q"); + f.form.listeners.change(); + assert.equal(f.env.location.search, ""); +}); diff --git a/tests/docs/test_faceted_discovery.py b/tests/docs/test_faceted_discovery.py new file mode 100644 index 00000000..17a2f50a --- /dev/null +++ b/tests/docs/test_faceted_discovery.py @@ -0,0 +1,303 @@ +from collections import Counter +from copy import deepcopy +from html.parser import HTMLParser +import json +import os +import re +from pathlib import Path, PurePosixPath +import shutil +import subprocess +from types import SimpleNamespace +from urllib.parse import parse_qs, urljoin, urlsplit + +from markdown import Markdown +import pytest +import yaml +import material + +from scripts.docs import content +from scripts.docs.generate_indexes import build_home_page, build_index_pages +from scripts.docs.hooks import on_page_markdown, on_post_page +from scripts.docs.topics import build_topic_catalog + + +ROOT = Path(__file__).parents[2] +GOALS = dict(zip( + "design build deploy diagnose optimize operate secure evaluate migrate".split(), + "설계 구현 배포 진단 최적화 운영 보안 비교·검증 마이그레이션".split(), +)) +SUBJECTS = {"ai-agents": "AI agents", "networking": "네트워킹", "identity": "ID·권한", "storage": "스토리지"} +LEGACY = { + "ai-agents": {"tag": ["ai-agents"]}, "architecture": {"tag": ["design"]}, + "authentication": {"tag": ["identity", "secure"]}, "authorization": {"tag": ["identity", "secure"]}, + "benchmarking": {"tag": ["evaluate"]}, "deployment": {"tag": ["deploy"]}, + "development": {"tag": ["build"]}, "diagnostics": {"tag": ["diagnose"]}, + "troubleshooting": {"tag": ["diagnose"]}, "disaster-recovery": {"tag": ["operate"]}, + "high-availability": {"tag": ["operate"]}, "monitoring": {"tag": ["operate"]}, + "observability": {"tag": ["operate"]}, "reliability": {"tag": ["operate"]}, + "kubernetes": {"technology": ["kubernetes"]}, "latency": {"tag": ["optimize"]}, + "performance": {"tag": ["optimize"]}, "migration": {"tag": ["migrate"]}, + "networking": {"tag": ["networking"]}, "security": {"tag": ["secure"]}, + "storage": {"tag": ["storage"]}, +} +REFINEMENTS = { + "microsoft-foundry/agent-memory": { + "index": "design ai-agents", "taxonomy": "design ai-agents", + "architecture-patterns": "design ai-agents", + "pipeline-retrieval": "build optimize ai-agents", "frameworks": "evaluate ai-agents", + "production-evaluation": "operate evaluate ai-agents", "commerce": "design build ai-agents", + }, + "azure-monitor/azure-sre-agent": { + "index": "operate diagnose ai-agents", "event-lab": "diagnose evaluate ai-agents", + "setup": "deploy operate ai-agents", "scenario-http-500": "diagnose operate ai-agents", + "scenario-latency": "optimize operate ai-agents", + "scenario-blob-permission": "secure operate identity ai-agents", + "results": "evaluate diagnose ai-agents", "validation-results": "evaluate operate ai-agents", + "incident-runbook": "diagnose operate ai-agents", "dynamic-thresholds": "operate design ai-agents", + }, + "azure-ai-search/custom-vectorization": { + "index": "design build optimize", "rag-chunking": "design optimize evaluate", + "custom-web-api": "build deploy", "gpu-vllm": "build deploy optimize", + "bge-m3-vs-qwen3": "evaluate optimize", "a10-vs-t4": "evaluate optimize", + }, + "azure-monitor/hdinsight-kafka-monitoring": { + "index": "evaluate operate", "prometheus-grafana": "deploy operate", + "catch-up-benchmark": "evaluate optimize", + }, +} + + +@pytest.fixture +def taxonomy(): + return content.load_taxonomy(ROOT / "docs-taxonomy.yml") + + +@pytest.fixture +def catalog(taxonomy): + return build_topic_catalog(ROOT / "docs", taxonomy) + + +class Elements(HTMLParser): + def __init__(self, markdown): + super().__init__() + self.elements = [] + body = markdown.split("---", 2)[2] if markdown.startswith("---") else markdown + self.rendered = Markdown(extensions=["md_in_html", "attr_list"]).convert(body) + self.feed(self.rendered) + + def handle_starttag(self, tag, attrs): + self.elements.append((tag, dict(attrs))) + + def select(self, attribute): + return [attrs for _, attrs in self.elements if attribute in attrs] + + +def test_repository_practical_tags_and_refinements(taxonomy, catalog): + assert taxonomy["tags"] == GOALS | SUBJECTS + assert taxonomy.get("tag_groups") == { + "goal": {"label": "목적", "tags": list(GOALS)}, + "subject": {"label": "주제", "tags": list(SUBJECTS)}, + } + assert taxonomy.get("legacy_tag_redirects") == LEGACY + counts = Counter(tag for doc in catalog.documents for tag in doc.metadata["tags"]) + assert set(counts) == set(taxonomy["tags"]) + for doc in catalog.documents: + tags = doc.metadata["tags"] + assert 1 <= len(tags) <= 4 + assert len(tags) == len(set(tags)) + assert tags == [t for t in tags if t in GOALS] + [t for t in tags if t in SUBJECTS] + assert "legacy_tags" not in doc.metadata + parts = doc.relative_path.parts + topic = "/".join(parts[1:3]) + child = parts[3] if len(parts) == 5 else "index" + if topic in REFINEMENTS: + assert tags == REFINEMENTS[topic][child].split() + + +@pytest.mark.parametrize("mutation,fragment", [ + (lambda t: t.pop("tag_groups"), "tag_groups"), + (lambda t: t["tag_groups"]["goal"]["tags"].remove("build"), "missing"), + (lambda t: t["tag_groups"]["goal"]["tags"].append("unknown"), "unknown"), + (lambda t: t["tag_groups"]["goal"]["tags"].append("build"), "duplicate"), + (lambda t: t["tag_groups"]["subject"]["tags"].append("build"), "duplicate"), + (lambda t: t["tag_groups"]["goal"].update(tags="build"), "list"), + (lambda t: t["legacy_tag_redirects"].update(bad={"tag": ["old"]}), "unknown"), +]) +def test_taxonomy_validator_rejects_invalid_groups(mutation, fragment): + taxonomy = { + "tags": GOALS | SUBJECTS, "technologies": {"kubernetes": "Kubernetes"}, + "tag_groups": {"goal": {"label": "목적", "tags": list(GOALS)}, "subject": {"label": "주제", "tags": list(SUBJECTS)}}, + "legacy_tag_redirects": deepcopy(LEGACY), + } + mutation(taxonomy) + assert hasattr(content, "validate_taxonomy"), "taxonomy validation is required" + assert any(fragment in e for e in content.validate_taxonomy(taxonomy)) + + +@pytest.mark.parametrize("tags", [[], ["design"] * 2, list(GOALS)[:5], ["identity", "secure"]]) +def test_document_tag_contract_rejects_invalid_tags(tags, taxonomy, catalog): + doc = catalog.documents[0] + errors = content.validate_document(doc.with_metadata({**doc.metadata, "tags": tags}), taxonomy) + assert any("tags" in error for error in errors), errors + + +def test_goal_before_subject_does_not_depend_on_yaml_mapping_order(taxonomy, catalog): + taxonomy["tag_groups"] = dict(reversed(list(taxonomy["tag_groups"].items()))) + doc = catalog.documents[0] + valid = doc.with_metadata({**doc.metadata, "tags": ["secure", "identity"]}) + assert content.validate_document(valid, taxonomy) == [] + + +def test_two_home_browse_destinations_do_not_leave_a_third_grid_column(): + css = (ROOT / "docs/assets/stylesheets/extra.css").read_text() + for selectors, body in re.findall(r"([^{}]+)\{([^{}]+)\}", css): + if ".dg-browse-grid" in [selector.strip() for selector in selectors.split(",")]: + assert "repeat(3," not in body + + +def test_explore_has_exact_member_data_and_one_card_per_topic(taxonomy, catalog): + pages = build_index_pages(catalog.documents, taxonomy, catalog=catalog) + assert PurePosixPath("explore/index.md") in pages + page = pages[PurePosixPath("explore/index.md")] + assert yaml.safe_load(page.split("---", 2)[1])["title"] == "글 찾기" + assert "40개 주제 · 62개 문서" in page + parser = Elements(page) + assert len(parser.select("data-explore-topic")) == len(catalog.topics) == 40 + members = parser.select("data-explore-member") + assert len(members) == len(catalog.documents) == 62 + by_path = {row["data-explore-member"]: row for row in members} + for doc in catalog.documents: + row = by_path[doc.relative_path.as_posix()] + for field in ("services", "tags", "technologies"): + assert json.loads(row[f"data-{field}"]) == doc.metadata[field] + assert row["data-search"] == f'{doc.metadata["title"]}\n{doc.metadata["description"]}' + assert "hidden" not in row + href = "../" + doc.relative_path.parent.as_posix() + "/" + assert any(a.get("href") == href for _, a in parser.elements) + legends = [tag for tag, _ in parser.elements if tag == "legend"] + assert len(legends) == 5 # service, tags with two groups, technology + assert "