diff --git a/README.ko.md b/README.ko.md
index c2ba49e..3f5ebea 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -56,9 +56,8 @@ bundle exec jekyll build # 결과물은 _site/ 에 생성
```
> **왜 `github-pages` gem이 아니라 순수 `jekyll`인가요?**
-> 이 사이트는 `_plugins/`에 직접 만든 Ruby 플러그인(읽기 시간, 이미지 lazy-load, 글 설명문 생성)을 씁니다.
-> `github-pages` gem은 보안 샌드박스 때문에 커스텀 플러그인을 막으므로,
-> 로컬과 CI 모두 Jekyll을 직접 실행합니다.
+> 이 사이트는 `_plugins/`에 직접 만든 Ruby 플러그인을 씁니다. `github-pages` gem은 보안
+> 샌드박스 때문에 커스텀 플러그인을 막으므로, 로컬과 CI 모두 Jekyll을 직접 실행합니다.
---
@@ -66,7 +65,7 @@ bundle exec jekyll build # 결과물은 _site/ 에 생성
```
_posts/ 글 — YYYY-MM-DD-slug.md (한국어 기본, 영어 번역본은 -en.md)
-_layouts/ 페이지 템플릿: default → post / page / archive
+_layouts/ 페이지 템플릿: default → post / page
_includes/ 재사용 조각: head, header, footer, nav_links,
page_divider, category-posts, language_switcher,
related_posts
@@ -76,7 +75,7 @@ _sass/ 스타일: _layout, _post, _tags, _syntax(Rouge 코드 테마
_plugins/ reading_time.rb (한·영 읽기 시간 계산)
lazy_images.rb (
에 loading="lazy" 추가)
post_description.rb (글의 page.description 채우기)
- related_posts.rb (글의 page.related 채우기)
+ related_posts.rb (page.related, 그리고 이전/다음 글 링크)
scrollable_tables.rb (넓은 표를 스크롤 컨테이너로 감싸기)
css/ main.scss(Sass 진입점) · search.css(검색 페이지 전용)
js/ main.js(테마 토글·코드 복사·목차·메뉴·이미지 확대 등)
@@ -138,20 +137,29 @@ RSS ``에 실리는 문장입니다. 생략하면 `_plugins/post_descri
카테고리는 **두 단계**입니다.
- `categories[0]` — **유형**: `Paper Reviews`, `Paper Summaries`, `Tech Guides`,
- `Insights` 중 하나. 글이 어느 섹션 페이지에 들어갈지를 정합니다. 현재 글이 있는 유형은
- `Paper Reviews`와 `Insights`뿐이라 내비에도 이 둘만 있습니다 — `paper-summaries.md`와
- `tech-guides.md`는 `main_nav` 없이 `sitemap: false` 상태이고, 첫 글이 들어오면 어떤 두
- 값을 되살리면 되는지가 각 파일 프런트매터에 적혀 있습니다.
+ `Insights` 중 하나. 글이 어느 내비 탭에 들어갈지를 정합니다. 아직 글이 없는 유형도
+ **탭을 유지하고** 빈 상태 문구를 띄웁니다 — 탭이 없으면 채우는 중인 섹션이 아니라
+ 없어진 섹션으로 읽히기 때문입니다.
- `categories[1]` — **주제**: `Language-Models`, `Multimodal-Learning`,
`Finetuning`, `Retrieval-Augmented-Generation`, `Agentic-AI` 등. 필요하면 자유롭게 추가합니다.
+Jekyll은 이 둘과 날짜를 합쳐 출력 경로를 만듭니다.
+
+```
+categories: ["Paper Reviews", "Language-Models"] + date: 2025-01-23
+ ↓
+_site/paper reviews/language-models/2025/01/23/.html
+```
+
+그래서 **이미 게시된 글의 카테고리나 날짜를 바꾸면 URL이 바뀝니다.** 외부에서 걸린 링크와
+검색 결과가 깨지므로, 한 번 정하면 그대로 둡니다.
+
### 태그: 세부 태그 위에 주제 태그를 하나
-태그는 자유 형식·하이픈 연결이고, 대부분 논문 하나의 기여를 서술합니다
-(`Fine-Grained-Expert-Segmentation`). 색인으로는 좋지만 글을 **잇지는** 못합니다 — 태그
-264개 중 220개가 단 한 편에만 붙어 있었고, `_plugins/related_posts.rb`는 **공유** 태그를
-요구하므로 11편이 관련 글을 하나도 얻지 못했습니다. 서로 남남이던 그래프 RAG 논문 네 편이
-거기 있었습니다.
+태그는 자유 형식·하이픈 연결입니다. 논문 하나의 기여를 그대로 옮긴 태그
+(`Fine-Grained-Expert-Segmentation`)는 그 논문에만 붙을 수 있습니다. 색인으로는 정확하지만
+글을 **잇지는** 못하고, `_plugins/related_posts.rb`가 **공유** 태그를 요구하므로 그렇게만
+달린 글은 "Related reading"이 아예 붙지 않은 채 배포됩니다.
그래서 글마다 아래 통제된 주제 태그를 최소 하나 답니다.
@@ -162,17 +170,11 @@ Retrieval-Augmented-Generation
```
기존 세부 태그는 **그대로 남깁니다.** 주제 태그가 담지 못하는 내용을 담고 있고, 지우면 이미
-살아 있는 `/tags/` 앵커가 움직입니다. 새 주제 태그를 만들기 전에 위 목록이나 이미 두 편
-이상이 쓰는 태그로 덮이는지 먼저 확인하세요. `Agentic-Architecture`·`Agentic-Patterns`·
-`Agentic-Infrastructure`가 한 주제의 세 이름이었던 것이 그 묶음에 링크가 없던 이유입니다.
-
-Jekyll은 이 둘과 날짜를 합쳐 출력 경로를 만듭니다.
+살아 있는 `/tags/` 앵커가 움직입니다. **주제 태그는 얹는 것이지 바꿔 다는 게 아닙니다.**
-```
-categories: ["Paper Reviews", "Language-Models"] + date: 2025-01-23
- ↓
-_site/paper reviews/language-models/2025/01/23/.html
-```
+새 주제 태그를 만들기 전에 위 목록에 이미 덮이는 게 없는지 확인하세요. 한 개념을 세 이름으로
+부르면(`Agentic-Architecture`·`Agentic-Patterns`·`Agentic-Infrastructure`) 글마다 아무도
+공유하지 않는 태그를 하나씩 들고 있게 되고, 그건 주제 태그가 없는 것과 같습니다.
### 수식은 반드시 `$$…$$`로
@@ -186,39 +188,39 @@ kramdown이 내용을 그대로 보존해 `\(…\)`로 내보내므로 안전합
### 푸시 전에 확인하세요
+CI가 돌리는 네 게이트를 같은 순서로 미리 돌립니다. `_site/`가 아니라 임시 경로로 빌드하는
+이유는 아래 주의에 있습니다.
+
```bash
-ruby test/run_all.rb # 플러그인 로직이 여전히 맞나?
-bundle exec jekyll build # 빌드가 깨끗한가?
-bundle exec htmlproofer ./_site --disable-external # 깨진 링크·이미지는 없나?
-script/validate-site.sh # 사이트맵·피드·메타데이터·헤딩
+ruby test/run_all.rb # 플러그인 로직이 여전히 맞나?
+bundle exec jekyll build --strict-front-matter \
+ --destination /tmp/site-verify # 빌드가 깨끗한가?
+bundle exec htmlproofer /tmp/site-verify --disable-external \
+ --allow-hash-href --no-enforce-https # 깨진 링크·이미지·앵커는 없나?
+script/validate-site.sh /tmp/site-verify # 사이트맵·피드·메타데이터·헤딩
```
-CI도 같은 세 검사를 돌립니다. 로컬에서 미리 잡으면 배포 실패를 막을 수 있습니다.
-
-`test/`는 `_plugins/`를 검사합니다 — 설명문 추출, 읽기 시간 계산, 이미지 lazy-load
-치환. `bundle exec`가 아니라 plain `ruby`인 이유는, 플러그인이 Jekyll·Liquid 등록을
-`defined?`로 감싸 로직만 단독 로드되게 해 두었고 minitest는 Ruby에 기본 포함이기
-때문입니다. **`_plugins/`를 건드리면 사이트의 모든 페이지가 바뀝니다.** 동작을 바꾸기
-전에 테스트 케이스를 먼저 추가하세요.
-
> **검사 결과가 말이 안 되면 먼저 떠 있는 `jekyll serve`를 찾으세요.** 파일을 감시하며
> `_site/`를 계속 덮어쓰고, `site.url`을 `http://localhost:4000`으로 바꾸며(사이트맵 URL이
> 전부 틀리게 보입니다), 시작할 때 읽은 `_config.yml`을 계속 들고 있어 그 뒤에 추가한
-> `exclude`가 적용되지 않습니다. 종료하거나, 다른 경로로 빌드해서 검사하세요.
+> `exclude`가 적용되지 않습니다. 다른 경로로 빌드하면 세 문제를 한꺼번에 비켜 갑니다.
>
> ```bash
> ps aux | grep '[j]ekyll serve'
-> bundle exec jekyll build --destination /tmp/site-verify
-> script/validate-site.sh /tmp/site-verify
> ```
+`test/`는 `_plugins/`의 순수 로직을 플러그인당 한 파일씩 단위 테스트합니다. `bundle exec`가
+아니라 plain `ruby`인 이유는, 각 플러그인이 Jekyll·Liquid 등록을 `defined?`로 감싸 로직만
+단독 로드되게 해 두었고 minitest는 Ruby에 기본 포함이기 때문입니다. **`_plugins/`를 건드리면
+사이트의 모든 페이지가 바뀝니다.** 동작을 바꾸기 전에 테스트 케이스를 먼저 추가하세요.
+
---
## 배포
`.github/workflows/jekyll.yml`은 `main` 푸시뿐 아니라 **`main`으로 향하는 PR에서도**
-돕니다. 그래서 아래 네 게이트가 잘못된 머지를 보고만 하지 않고 막습니다. 1~4단계는 양쪽
-모두에서, 5단계는 PR에서 건너뜁니다.
+돕니다. 그래서 아래 게이트가 잘못된 머지를 사후에 보고하는 데 그치지 않고 막습니다.
+1~4단계는 두 경우 모두, 5단계는 PR에서 건너뜁니다. 순서대로:
1. **`ruby test/run_all.rb`**로 `_plugins/` 단위 테스트를 돌리고
2. `JEKYLL_ENV=production`으로 사이트를 빌드하고
@@ -230,8 +232,8 @@ CI도 같은 세 검사를 돌립니다. 로컬에서 미리 잡으면 배포
중복되지 않는지, 저작 원본이 배포되지 않았는지를 검사한 뒤
5. GitHub Pages에 배포합니다.
-워크플로우가 실패한다면 대개 3·4단계입니다. Actions 로그에 어떤 링크·이미지·페이지가
-문제인지 그대로 나옵니다. 수동 배포 단계는 없습니다.
+실패하면 대개 3·4단계입니다. Actions 로그에 어떤 링크·이미지·페이지가 문제인지 그대로
+나옵니다. 수동 배포 단계는 없습니다.
> **⚠ `google*.html` / `naver*.html`을 `_config.yml`의 `exclude`에 넣지 마세요.**
> Search Console·네이버 소유권 인증 토큰이라 사이트 루트에 그대로 올라가야 합니다.
diff --git a/README.md b/README.md
index d6d98a4..58fdd43 100644
--- a/README.md
+++ b/README.md
@@ -58,9 +58,8 @@ bundle exec jekyll build
```
> **Why plain `jekyll` and not `github-pages`?** This site uses custom Ruby
-> plugins in `_plugins/` (read time, lazy images, post descriptions), which the
-> sandboxed `github-pages` gem disallows. So both local builds and CI run Jekyll
-> directly.
+> plugins in `_plugins/`, which the sandboxed `github-pages` gem disallows. So
+> both local builds and CI run Jekyll directly.
---
@@ -68,7 +67,7 @@ bundle exec jekyll build
```
_posts/ Posts — YYYY-MM-DD-slug.md (Korean; English twin is -en.md)
-_layouts/ Page templates: default → post / page / archive
+_layouts/ Page templates: default → post / page
_includes/ Reusable fragments: head, header, footer, nav_links,
page_divider, category-posts, language_switcher,
related_posts
@@ -78,7 +77,7 @@ _sass/ Styles: _layout, _post, _tags, _syntax (Rouge code theme),
_plugins/ reading_time.rb (KO/EN-aware read time)
lazy_images.rb (adds loading="lazy" to
)
post_description.rb (fills page.description for posts)
- related_posts.rb (fills page.related for posts)
+ related_posts.rb (page.related, and the prev/next links)
scrollable_tables.rb (wraps wide tables so they scroll)
css/ main.scss (Sass entry point) · search.css (search page only)
js/ main.js (theme toggle, code-copy, TOC, menu, image zoom…)
@@ -142,23 +141,31 @@ share one.
Categories are **two levels**:
- `categories[0]` — the **type**: `Paper Reviews`, `Paper Summaries`,
- `Tech Guides`, or `Insights`. This decides which section page the post appears
- on. Only `Paper Reviews` and `Insights` have posts today, so only those two are
- in the nav — `paper-summaries.md` and `tech-guides.md` sit at `sitemap: false`
- with no `main_nav`, and their front matter says which two keys to restore when
- the first post lands in either.
+ `Tech Guides`, or `Insights`. This decides which nav tab the post appears under.
+ A type with no posts yet keeps its tab and renders an empty-state line: a
+ missing tab reads as a section that was removed, not one still filling up.
- `categories[1]` — the **topic**: `Language-Models`, `Multimodal-Learning`,
`Finetuning`, `Retrieval-Augmented-Generation`, `Agentic-AI`, … (add new ones
freely).
+Jekyll combines the two with the date to build the output path:
+
+```
+categories: ["Paper Reviews", "Language-Models"] + date: 2025-01-23
+ ↓
+_site/paper reviews/language-models/2025/01/23/.html
+```
+
+So **changing the categories or date of a published post changes its URL**, which
+breaks inbound links and search results. Set them once and leave them.
+
### Tags: one topic tag on top of the specific ones
-Tags are free-form and hyphenated, and most describe one paper's contribution
-(`Fine-Grained-Expert-Segmentation`). Those are a good index but they connect
-nothing: 220 of 264 tags were used by exactly one post, and since
-`_plugins/related_posts.rb` requires a **shared** tag, 11 posts got no related
-reading at all — including four graph-RAG papers that share no tag with each
-other.
+Tags are free-form and hyphenated, and a tag phrased as one paper's contribution
+(`Fine-Grained-Expert-Segmentation`) can only ever apply to that paper. Those
+make a precise index and connect nothing, and `_plugins/related_posts.rb`
+requires a **shared** tag — so a post tagged only that way ships with no
+"Related reading" block at all.
So also give each post at least one tag from the controlled topic layer:
@@ -169,18 +176,12 @@ Retrieval-Augmented-Generation
```
Keep the specific tags — they say something the topic tag does not, and dropping
-them would move live `/tags/` anchors. Before inventing a topic tag, check
-whether one of the above (or an existing tag used by 2+ posts) already covers it:
-`Agentic-Architecture`, `Agentic-Patterns` and `Agentic-Infrastructure` were three
-names for one thing, which is why that cluster had no links.
-
-Jekyll combines them with the date to build the output path:
+them would move live `/tags/` anchors. **Add the topic tag, don't swap for it.**
-```
-categories: ["Paper Reviews", "Language-Models"] + date: 2025-01-23
- ↓
-_site/paper reviews/language-models/2025/01/23/.html
-```
+Before coining a new topic tag, check that nothing above already covers it. Three
+names for one idea (`Agentic-Architecture`, `Agentic-Patterns`,
+`Agentic-Infrastructure`) leave every post holding a tag no other post shares,
+which is the same as having no topic tag at all.
### Math: always use `$$…$$`
@@ -194,40 +195,42 @@ dollar signs like `$10M` are fine — they're not math.)
### Validate before pushing
+These are the four gates CI runs, in the same order. Build to a throwaway
+directory rather than `_site/`, for the reason in the note below:
+
```bash
-ruby test/run_all.rb # plugin logic still correct?
-bundle exec jekyll build # does it build clean?
-bundle exec htmlproofer ./_site --disable-external # any broken links/images?
-script/validate-site.sh # sitemap, feed, metadata, headings
+ruby test/run_all.rb # plugin logic still correct?
+bundle exec jekyll build --strict-front-matter \
+ --destination /tmp/site-verify # does it build clean?
+bundle exec htmlproofer /tmp/site-verify --disable-external \
+ --allow-hash-href --no-enforce-https # broken links, images, anchors?
+script/validate-site.sh /tmp/site-verify # sitemap, feed, metadata, headings
```
-CI runs the same three checks, so catching it locally saves a failed deploy.
-
-`test/` covers `_plugins/` — the description derivation, the read-time estimate,
-and the lazy-image rewrite. Plain `ruby`, not `bundle exec`: the plugins guard
-their Jekyll/Liquid registration behind `defined?` so their logic loads
-standalone, and minitest ships with Ruby. **Anything you change in `_plugins/`
-changes every page on the site**, so add a case there before changing behaviour.
-
-> **If the checks report something impossible, look for a running `jekyll serve`
+> **If a check reports something impossible, look for a running `jekyll serve`
> first.** It watches the tree and rewrites `_site/` behind you, it overrides
> `site.url` with `http://localhost:4000` (so every sitemap URL looks wrong), and
-> it keeps the `_config.yml` it started with — so `exclude` entries added since
-> then don't apply. Either stop it, or build somewhere else:
+> it holds the `_config.yml` it started with — so `exclude` entries added since
+> then don't apply. Building elsewhere sidesteps all three:
>
> ```bash
> ps aux | grep '[j]ekyll serve'
-> bundle exec jekyll build --destination /tmp/site-verify
-> script/validate-site.sh /tmp/site-verify
> ```
+`test/` unit-tests the pure logic in `_plugins/` — one file per plugin. Plain
+`ruby`, not `bundle exec`: each plugin guards its Jekyll/Liquid registration
+behind `defined?` so the logic loads standalone, and minitest ships with Ruby.
+**Anything you change in `_plugins/` changes every page on the site**, so add a
+case before changing behaviour.
+
---
## Deployment
`.github/workflows/jekyll.yml` runs on **pull requests to `main` as well as
-pushes to it**, so the four gates below block a bad merge instead of only
-reporting one. Steps 1–4 run on both; step 5 is skipped for pull requests. It:
+pushes to it**, so the gates below block a bad merge rather than merely reporting
+one after the fact. Steps 1–4 run on both events; step 5 is skipped for pull
+requests. In order, it:
1. runs **`ruby test/run_all.rb`** (the `_plugins/` unit tests),
2. builds the site with `JEKYLL_ENV=production`,
@@ -240,8 +243,8 @@ reporting one. Steps 1–4 run on both; step 5 is skipped for pull requests. It:
no authoring sources published — and
5. deploys to GitHub Pages.
-If the workflow fails, it's almost always step 3 or 4 — open the Actions log,
-which names the exact link, image, or page. No manual deploy step is needed.
+A failure is almost always step 3 or 4; the Actions log names the exact link,
+image, or page. There is no manual deploy step.
> **⚠ Don't add `google*.html` / `naver*.html` to `_config.yml`'s `exclude`.**
> They're Search Console / Naver ownership-verification tokens that must ship to
diff --git a/_posts/2026-04-12-agentcore-harness-engineering-analysis.md b/_posts/2026-04-12-agentcore-harness-engineering-analysis.md
index 36c6559..b65e36a 100644
--- a/_posts/2026-04-12-agentcore-harness-engineering-analysis.md
+++ b/_posts/2026-04-12-agentcore-harness-engineering-analysis.md
@@ -7,7 +7,7 @@ description: >-
Build·Deploy·Assess 세 층으로 열어 하네스 체크리스트의 어디를 채우고 어디를 비워 두었는지 짚습니다.
date: 2026-04-12 12:00:00
categories: ["Insights", "Agentic-AI"]
-tags: ["AgentCore", "AWS-Bedrock", "Harness-Engineering", "Agentic-Infrastructure", "Model-Context-Protocol", "Cedar-Policy", "Managed-RAG", "Agent-Registry", "Agentic-AI"]
+tags: ["AgentCore", "AWS-Bedrock", "Harness-Engineering", "Agentic-Infrastructure", "MCP", "Model-Context-Protocol", "Cedar-Policy", "Managed-RAG", "Agent-Registry", "Agentic-AI"]
cover: /assets/images/insights.jpg
use_math: false
---
diff --git a/docs/tech-doc.md b/docs/tech-doc.md
index 87212b4..0b6b66e 100644
--- a/docs/tech-doc.md
+++ b/docs/tech-doc.md
@@ -68,8 +68,8 @@ Pages가 그대로 서빙.**
> **실행을 거부**하게 만든다. 그래서 외부에서 불러오는 것은 버전을 고정하고 SRI를 붙인다.
> **왜 GitHub Pages 기본 빌드가 아니라 Jekyll을 직접 돌리나?** GitHub Pages의 내장
-> 빌드는 보안 샌드박스라 우리가 `_plugins/`에 만든 커스텀 플러그인(읽기시간·lazy-load·설명문)을
-> 막는다. 그래서 로컬과 CI 모두 순수 `jekyll`을 직접 실행한다.
+> 빌드는 보안 샌드박스라 우리가 `_plugins/`에 만든 커스텀 플러그인을 막는다. 그래서 로컬과
+> CI 모두 순수 `jekyll`을 직접 실행한다.
## 2. 빌드 파이프라인 — 마크다운 한 편이 HTML이 되기까지
@@ -107,9 +107,8 @@ Pages가 그대로 서빙.**
포스트는 프런트매터에 **2단계** 카테고리를 가진다: `categories: ["<유형>", "<주제>"]`.
- **0단계 (유형)** — `Paper Reviews`, `Paper Summaries`, `Tech Guides`, `Insights` 중 하나.
- 글이 어느 섹션 페이지에 들어갈지를 정한다. 넷 다 전용 페이지를 가지지만, 지금 글이 있는
- 유형은 `Paper Reviews`(28편)와 `Insights`(7편)뿐이고 내비에는 이 둘만 올라간다(아래
- "내비게이션은 코드가 아니라 데이터가 만든다" 참조).
+ 글이 어느 내비 탭에 들어갈지를 정한다. 넷 다 전용 페이지와 탭을 가지며, 글이 없는 유형은
+ 빈 상태 문구를 띄운다(아래 "내비게이션은 코드가 아니라 데이터가 만든다" 참조).
- **1단계 (주제)** — `Language-Models`, `Multimodal-Learning`, `Finetuning`,
`Retrieval-Augmented-Generation`, `Agentic-AI` 등. 세부 주제이며 필요하면 자유롭게 추가한다.
@@ -135,17 +134,17 @@ _site/paper reviews/language-models/2025/01/23/.html
| nav_order | 페이지 | 소스 |
|-----------|--------|------|
| 1 | About | `about.md` |
+| 2 | Paper Summaries | `paper-summaries.md` — `site.categories['Paper Summaries']` 필터 |
| 3 | Paper Reviews | `paper-reviews.md` — `site.categories['Paper Reviews']` 필터, 주제별 그룹화 |
+| 4 | Tech Guides | `tech-guides.md` — `site.categories['Tech Guides']` 필터 |
| 5 | Insights | `insights.md` — `site.categories['Insights']` 필터 |
| 6 | Search | `search.md` |
-`nav_order`의 2·4번이 빈 것은 실수가 아니다. `paper-summaries.md`(2)와
-`tech-guides.md`(4)는 **글이 0편이라 내비에서 내렸다** — 여섯 탭 중 둘이
-"this space will fill up soon"으로 이어지고 있었고, 같은 페이지가 사이트맵에도 올라가
-검색엔진에 빈 페이지를 내밀고 있었다. 두 파일은 `main_nav` 없이 `sitemap: false`로
-남아 있고(URL로는 계속 열린다), 첫 글이 그 유형으로 들어오면 프런트매터 주석이 지시하는
-대로 `main_nav: true`와 원래 `nav_order`를 되살리면 된다. 번호를 다시 매기지 않은 이유가
-이것이다.
+**빈 섹션도 탭을 유지한다.** 글이 없는 유형은 `_includes/category-posts.html`의 `empty`
+문구를 띄운다("Summaries are in the works…"). 편수가 0이라고 `main_nav`를 떼지 않는다 —
+탭이 사라지면 채우는 중인 섹션이 아니라 **없어진 섹션**으로 읽히고, 남은 탭이 로고 옆에
+좁게 뭉쳐 전체 폭 히어로와 어긋난다. 빈 상태 문구가 정직한 신호이고, 탭 자체가 "이 블로그는
+이 종류의 글도 쓴다"는 선언이다.
`categories.html`(`/categories/`)과 `tags.html`(`/tags/`)은 *모든* 카테고리/태그를 가로지르는
전체 색인 페이지다. 메인 내비에는 없고, 각 포스트 하단의 메타데이터에서 링크된다.
@@ -157,12 +156,13 @@ _site/paper reviews/language-models/2025/01/23/.html
> `site.categories['<유형>']`으로 먼저 거른 뒤** 주제별로 그룹화한다 — 절대 반대 순서로
> 하지 않는다.
-> **함정 — 앵커 점프 깨짐.** 메타데이터의 카테고리/태그 링크는 모두 `slugify` 필터로 ID를
-> 만든다. 과거에는 링크 쪽은 `downcase`(공백 유지), 제목(H2)의 id는 원본 케이스(`Paper
-> Reviews`)를 써서 서로 어긋났고, 클릭해도 해당 위치로 점프하지 못했다(html-proofer가
-> 128건 적발). 지금은 양쪽 다 `slugify`로 통일했다 — `paper-reviews.md`가 마지막까지
-> `id="{{cat}}"`로 원본 케이스를 내고 있었는데, 그 앵커를 가리키는 링크가 없어 html-proofer가
-> 잡지 못했다. 새 목록 페이지를 만들 땐 `| slugify`를 빼먹지 않는다.
+> **함정 — 앵커 점프 깨짐.** 카테고리·태그 앵커는 **링크 쪽과 헤딩 `id` 쪽이 같은 방식으로
+> 슬러그를 만들어야** 한다. 양쪽 다 `slugify` 필터를 쓴다. 한쪽만 `downcase`거나 원본
+> 케이스(`id="{{cat}}"`)를 내면 클릭해도 해당 위치로 점프하지 않는다. 새 목록 페이지를 만들 땐
+> `| slugify`를 빼먹지 않는다.
+>
+> 이 어긋남은 **아무 링크도 그 앵커를 가리키지 않으면 html-proofer가 잡지 못한다.** 목록
+> 페이지가 자기 헤딩으로 들어오는 링크를 갖고 있지 않은 경우가 그렇다.
### 태그: 세부 태그 + 통제된 주제 태그
@@ -174,15 +174,18 @@ _site/paper reviews/language-models/2025/01/23/.html
`Knowledge-Graph`, `Llama`, `Mixture-of-Experts`, `Multimodal-Models`,
`Reasoning-Models`, `Retrieval-Augmented-Generation`.
-세부 태그만 있으면 관련 글 기능이 굶는다. 태그 264개 중 220개(83%)가 단 한 편에만 붙어
-있었고, `related_posts.rb`가 공유 태그를 요구하므로 11편이 관련 글 0개였다 — 그중 넷은
-RAPTOR·GraphRAG·LightRAG·Zep, 즉 서로 붙어야 마땅한 그래프 RAG 논문들이었다. 한 주제를 세
-이름으로 부른 것(`Agentic-Architecture`/`Agentic-Patterns`/`Agentic-Infrastructure`)도 같은
-증상을 만들었다.
+**세부 태그만 달면 관련 글이 안 붙는다.** 논문 하나의 기여를 그대로 옮긴 태그는 그 논문에만
+붙을 수 있고, `related_posts.rb`는 공유 태그를 요구한다. 그래서 세부 태그만 달린 글은
+"Related reading" 없이 배포된다. 한 주제를 여러 이름으로 부르는 것도 같은 증상을 만든다
+(`Agentic-Architecture`/`Agentic-Patterns`/`Agentic-Infrastructure`가 그랬다).
-주제 태그를 한 층 얹어 35편 전부가 관련 글을 얻게 했다. 고친 곳이 `related_posts.rb`의
-규칙이 아니라 **글의 프런트매터**라는 점이 중요하다 — 규칙을 느슨하게 풀면 공통점 없는 글이
-서로 추천된다. 세부 태그는 지우지 않았다(살아 있는 `/tags/` 앵커가 움직인다).
+관련 글이 비어 보일 때 고칠 지점은 `related_posts.rb`의 규칙이 아니라 **글의 프런트매터**다.
+규칙을 느슨하게 풀어 카테고리만으로 연결하면, "둘 다 Paper Reviews / Language-Models"라는
+이유로 공통점 없는 글이 서로 추천된다. 가중치가 그걸 막는 구조는 플러그인 상단 주석
+(`TAG_WEIGHT`·`SUBCATEGORY_WEIGHT`·`CATEGORY_WEIGHT`)에 설명돼 있다.
+
+그리고 세부 태그는 지우지 않는다 — 살아 있는 `/tags/` 앵커가 움직인다. 주제 태그를
+**얹기만** 한다.
## 4. 수식 렌더링
@@ -204,10 +207,9 @@ kramdown은 단일 `$`를 수식으로 **취급하지 않는다**. `$x_i + y_j$`
MathJax에는 이미 망가진 입력이 전달된다(위 그림의 빨간 경로). 예를 들어 `$a*b*c$`가
`$abc$`가 되어버린다.
-과거의 임시 우회책은 모든 언더스코어를 `\_`로 손수 이스케이프하는 것이었다. 근본 해결은
-**`$$`를 쓰는 것** — 구간 안의 마크다운 처리를 통째로 끈다. 저장소는 이미 일괄
-마이그레이션되었다(git 히스토리 참조). 참고로 코드 블록 *안*의 단일 `$` 수식(예: DeepSeek-R1
-글의 `` 트레이스)은 verbatim(있는 그대로) 코드로 표시되므로 이 문제와 무관하다.
+언더스코어를 `\_`로 하나하나 이스케이프해도 증상은 사라지지만, 그건 우회책이다. 해결은
+**`$$`를 쓰는 것** — 구간 안의 마크다운 처리를 통째로 끈다. 참고로 코드 블록 *안*의 단일
+`$`는 verbatim(있는 그대로) 코드로 표시되므로 이 문제와 무관하다.
> **통화 표기 주의.** 본문에 쓰는 달러 기호(`$1.2B`, `$250M`)는 수식이 아니므로 단일 `$`로
> 둬야 한다. 그래서 MathJax 설정에서 `$`를 인라인 구분자에서 **일부러 뺐다**(`\(...\)`만
@@ -232,30 +234,31 @@ MathJax 설정은 `head.html`에 있고 `{% if page.use_math %}`로 감싸 **프
빌드 때 모든 포스트를 `{title, url, date, category, tags, snippet, content}` 형태로
뽑아낸다. 여기서 `snippet`은 결과 카드에 보여줄 40단어짜리 발췌, `content`는 매칭에 쓰는
**HTML을 제거한 전체 본문**이다.
-- **왜 발췌가 아니라 전체 본문을 색인하나** — `simple-jekyll-search`는 똑똑한 형태소 분석
- 없이 단순 부분문자열 매칭을 한다. 즉 색인에 없는 글자는 못 찾는다. 예전에 snippet(앞
- 40단어)만 색인했더니, "어텐션"·"트랜스포머"가 본문 중·후반에 18개 포스트나 있는데 발췌엔
- 안 들어가 검색 결과가 **0건**으로 나왔다. `content`로 전체를 색인해 한글 재현율을 회복했다.
- 대가는 실측 **3.2 MB, gzip 991 KB**다(글이 늘면 같이 는다). `/search/`에서만 내려받으므로
- 다른 페이지 속도엔 영향이 없다.
-
- > **알려진 한계 — 매치가 안 보일 수 있다.** 색인은 본문 전체인데 결과 카드에 찍히는 건
- > 앞 40단어짜리 `snippet`뿐이다. 그래서 5만 번째 글자에서 걸린 검색어는 스니펫에 없고
- > `` 강조도 안 걸려서, 맞는 결과가 엉뚱한 결과처럼 보인다. 고치려면 매치 위치
- > 주변을 잘라 보여 주는 스니펫이 필요하다(`simple-jekyll-search`의 템플릿 밖 작업).
+- **왜 발췌가 아니라 전체 본문을 색인하나** — `simple-jekyll-search`는 형태소 분석 없이
+ 단순 부분문자열 매칭을 한다. **색인에 없는 글자는 못 찾는다.** 발췌만 색인하면 본문
+ 중·후반에만 나오는 단어("어텐션", "트랜스포머" 같은)는 검색 결과가 0건이 된다. `content`로
+ 전체를 색인하는 건 한글 재현율을 위한 의도된 선택이다.
+ 대가는 파일 크기다 — 본문 전체가 들어가므로 **글 수에 비례해 선형으로 커진다**(현재 규모는
+ MB 단위, gzip 후 그 3분의 1 아래). `/search/`에서만 내려받으므로 다른 페이지 속도엔 영향이
+ 없다. 실제 값은 `curl -so /dev/null -w '%{size_download}' /search.json`로 확인한다.
+
+ > **비대칭에 주의.** 매칭은 `content`(전체)로 하고 표시는 `snippet`(앞 40단어)으로 한다.
+ > 그래서 본문 깊은 곳에서 걸린 검색어는 결과 카드에 보이지 않는다(§9).
- **`js/search.js`** 가 `simple-jekyll-search`(CDN 버전 **고정 + SRI**: `1.10.0`)를
`search.md`의 `#search-input` 입력칸에 연결한다. 매칭된 키워드를 ``로 강조하고
결과 개수를 라이브 상태줄에 표시한다. 강조 처리는 결과가 다 그려진 뒤 **디바운스**로 단
- 한 번만 실행한다(예전엔 키를 누를 때마다 `setTimeout`을 쌓아 서로 경합하며 깜빡였다).
+ 한 번만 실행한다 — 키 입력마다 `setTimeout`을 쌓으면 타이머가 서로 경합해 화면이 깜빡인다.
> **용어 — 디바운스(debounce).** 사용자가 빠르게 연속으로 일으키는 이벤트(타이핑 등)에서,
> 마지막 입력 뒤 잠깐 멈출 때까지 기다렸다가 **딱 한 번만** 함수를 실행하는 기법이다.
- `category`/`tags`도 색인에 들어가므로 제목·본문뿐 아니라 메타데이터로도 검색된다.
색인에는 **글(`site.posts`)만** 들어간다 — `search.json`이 `site.posts`를 순회하므로
- About·Search·index 같은 페이지는 애초에 후보가 아니다. (`_config.yml`에 `about.md`·
- `search.md`·`index.html`을 열거하던 `simple_jekyll_search.exclude` 블록이 있었는데,
- `simple-jekyll-search`는 브라우저에서 도는 JS라 `_config.yml`을 읽을 수 없어 아무것도 하지
- 않았다. 지워졌고, 왜 없는지가 `_config.yml`의 주석으로 남아 있다.)
+ About·Search·index 같은 페이지는 애초에 후보가 아니다.
+
+ > **주의 — `_config.yml`로는 검색을 제어할 수 없다.** `simple-jekyll-search`는 브라우저에서
+ > 도는 JS라 `_config.yml`을 읽지 못한다. 색인 대상을 바꾸려면 `search.json`의 Liquid를
+ > 고쳐야 한다. `_config.yml`에 `simple_jekyll_search:` 같은 블록을 만들어도 아무 일도
+ > 일어나지 않는다.
## 6. 스타일 (Sass)
@@ -270,9 +273,13 @@ Bourbon → base/ → Neat → _layout → _post → _tags → _syntax(Rouge 코
- **수정해도 되는 곳**: `_sass/_layout.scss`, `_sass/_post.scss`, `_sass/_tags.scss`,
`_sass/base/*`(특히 색·간격·브레이크포인트를 모아 둔 `_variables.scss`).
-- **수정하면 안 되는 곳**: `_sass/bourbon/**`, `_sass/neat/**`(외부에서 가져온 벤더
- 프레임워크 — 우리가 만든 게 아니다), `_sass/_syntax.scss`(자동 생성물 —
- `rougify style monokai.sublime`로 재생성한다).
+- **수정하면 안 되는 곳**: `_sass/bourbon/**`, `_sass/neat/**` — 외부에서 가져온 벤더
+ 프레임워크다.
+- **`_sass/_syntax.scss`는 재생성하지 말 것**: 원래 `rougify style monokai.sublime`의
+ 출력이지만 **그 뒤 손으로 고쳤다.** Monokai 기본 팔레트의 주석·키워드 색이 코드 배경에서
+ 명도 대비 4.5:1을 못 넘겨 두 색을 올려 놨고, 파일 맨 위 주석에 원래 값과 대비 수치가
+ 적혀 있다. `rougify`로 덮어쓰면 그 수정이 조용히 사라진다. 색을 바꿔야 하면 이 파일을
+ 직접 고치고 주석도 함께 갱신한다.
- **디자인 토큰을 쓸 것**: 값을 하드코딩하지 말고 미리 정의된 변수를 쓴다. 전환은
`$transition-*`, 그림자는 `$shadow-*`, 강조색/보조 텍스트는 `$action-color`/`$medium-gray`.
`0.3s ease`나 `#aaaaaa` 같은 리터럴을 직접 박지 않는다. (예: `$medium-gray`는
@@ -280,8 +287,8 @@ Bourbon → base/ → Neat → _layout → _post → _tags → _syntax(Rouge 코
읽히는 진한 파랑으로 헤더/푸터 배경용.)
- **절대 금지**: HTML 안의 인라인 `