feat(kebab-normalize): p9-fb-07 markdown title fallback chain #66

Merged
altair823 merged 2 commits from feat/p9-fb-07-title into main 2026-05-03 01:26:56 +00:00
Owner

변경 요약

kebab-normalize::derive_title 추가 — frontmatter title 이 비어있을 때 5 단계 fallback chain 으로 의미 있는 title 을 항상 보장. p9-fb-07 도그푸딩 피드백 (Inspect 패널이 title 비어 보이는 문제) 해소.

핵심 변경

  • kebab_normalize::derive_title(frontmatter_title, blocks, file_stem) -> String (new public)
    1. frontmatter title (trim 후 NFC)
    2. 첫 H1 텍스트
    3. 첫 H2 텍스트
    4. 첫 Paragraph (Quote / List / Code / Table / ImageRef 제외) 의 첫 80 자
    5. 파일 stem (확장자 제외)
    6. (sentinel) "untitled"
  • build_canonical_document 가 metadata lift 직후 helper 호출.
  • KEBAB_PARSE_MD_VERSION 상수 pulldown-cmark-0.xmd-frontmatter-v2 bump → §4.2 doc_id 갱신 → 기존 doc 은 다음 ingest 에서 idempotent upsert 로 자동 재처리.
  • kebab-store-sqlite snapshot fixture parser_version literal 도 동일 갱신.

정책 변경 (M7)

기존: metadata.user["title"] = ""CanonicalDocument.title = "" (lift 그대로).
신규: 빈 문자열은 fallback chain 통과 → file stem 까지 떨어짐. 빈 title 절대 없음 (spec p9-fb-07 line 37).

영향 받는 테스트 2 개 (title_empty_string_in_user_map_falls_back_to_default, title_non_string_in_user_map_silently_drops) 새 정책에 맞춰 이름 + 기댓값 갱신.

테스트

  • cargo test -p kebab-normalize — 24 통과 (신규 8 unit + 2 integration + 기존 14)
  • cargo test -p kebab-parse-md -p kebab-store-sqlite -p kebab-app -p kebab-core — 모두 통과
  • cargo clippy -p kebab-normalize -p kebab-app -p kebab-store-sqlite --all-targets -- -D warnings clean

문서

  • README: kebab ingest 행에 "title 자동 채움" 안내
  • HANDOFF: 2026-05-03 머지 후 발견 entry
  • spec tasks/p9/p9-fb-07-md-title-fallback.md: status plannedin_progress

사용자 안내

기존에 색인된 markdown 중 frontmatter title 비어있던 doc 은 다음 kebab ingest 실행 시 새 fallback 으로 title 이 채워진다 (parser_version bump 가 doc_id 갱신을 트리거 → idempotent re-process).

## 변경 요약 `kebab-normalize::derive_title` 추가 — frontmatter title 이 비어있을 때 5 단계 fallback chain 으로 의미 있는 title 을 항상 보장. p9-fb-07 도그푸딩 피드백 (Inspect 패널이 title 비어 보이는 문제) 해소. ## 핵심 변경 - **`kebab_normalize::derive_title(frontmatter_title, blocks, file_stem) -> String`** (new public) 1. frontmatter `title` (trim 후 NFC) 2. 첫 H1 텍스트 3. 첫 H2 텍스트 4. 첫 Paragraph (Quote / List / Code / Table / ImageRef 제외) 의 첫 80 자 5. 파일 stem (확장자 제외) 6. (sentinel) `"untitled"` - `build_canonical_document` 가 metadata lift 직후 helper 호출. - `KEBAB_PARSE_MD_VERSION` 상수 `pulldown-cmark-0.x` → `md-frontmatter-v2` bump → §4.2 doc_id 갱신 → 기존 doc 은 다음 ingest 에서 idempotent upsert 로 자동 재처리. - `kebab-store-sqlite` snapshot fixture parser_version literal 도 동일 갱신. ## 정책 변경 (M7) 기존: `metadata.user["title"] = ""` → `CanonicalDocument.title = ""` (lift 그대로). 신규: 빈 문자열은 fallback chain 통과 → file stem 까지 떨어짐. 빈 title 절대 없음 (spec p9-fb-07 line 37). 영향 받는 테스트 2 개 (`title_empty_string_in_user_map_falls_back_to_default`, `title_non_string_in_user_map_silently_drops`) 새 정책에 맞춰 이름 + 기댓값 갱신. ## 테스트 - `cargo test -p kebab-normalize` — 24 통과 (신규 8 unit + 2 integration + 기존 14) - `cargo test -p kebab-parse-md -p kebab-store-sqlite -p kebab-app -p kebab-core` — 모두 통과 - `cargo clippy -p kebab-normalize -p kebab-app -p kebab-store-sqlite --all-targets -- -D warnings` clean ## 문서 - README: `kebab ingest` 행에 "title 자동 채움" 안내 - HANDOFF: 2026-05-03 머지 후 발견 entry - spec `tasks/p9/p9-fb-07-md-title-fallback.md`: status `planned` → `in_progress` ## 사용자 안내 기존에 색인된 markdown 중 frontmatter title 비어있던 doc 은 다음 `kebab ingest` 실행 시 새 fallback 으로 title 이 채워진다 (parser_version bump 가 doc_id 갱신을 트리거 → idempotent re-process).
altair823 added 1 commit 2026-05-03 01:23:28 +00:00
`kebab-normalize::derive_title(frontmatter_title, blocks, file_stem)` 가
다음 단계로 비어있지 않은 첫 결과를 사용:

1. frontmatter `title` (trim 후)
2. 첫 H1 텍스트
3. 첫 H2 텍스트
4. 첫 Paragraph (Quote / List / Code / Table / ImageRef 제외) 의 첫 80 자
5. 파일 stem (확장자 제외)
6. (sentinel) `"untitled"` — 위 다섯 단계가 모두 blank 인 병적 케이스

선택된 문자열은 NFC 정규화. 빈 문자열은 절대 반환하지 않음.

`build_canonical_document` 가 metadata lift 직후 helper 호출. 기존 단순
lift 로직 (metadata.user["title"] → CanonicalDocument.title) 은 fallback
chain 의 1 단계 입력으로 자리 이동.

`KEBAB_PARSE_MD_VERSION` 상수를 `pulldown-cmark-0.x` → `md-frontmatter-v2`
로 bump. parser_version 변경 → §4.2 doc_id 입력 변화 → 기존 markdown
doc 의 `doc_id` 갱신, 다음 ingest 시 idempotent upsert 로 자동 재처리
(design §9 cascade). `kebab-store-sqlite` 의 snapshot fixture 도 같은
literal 로 갱신.

기존 M7 정책 ("metadata.user[\"title\"] = '' 가 빈 title 로 lift") 은
폐기. 빈 문자열 입력은 fallback chain 을 타고 file stem 까지 떨어진다.
spec p9-fb-07 line 37: "빈 문자열 반환 금지".

테스트 (kebab-normalize):
- 8 개 단위 테스트 (각 fallback 단계 + NFC + sentinel)
- `build_canonical_document` 통합 테스트 2 개 (H1 / file stem)
- 기존 M7 테스트 2 개를 새 정책에 맞춰 갱신

문서:
- README: `kebab ingest` 행에 "title 자동 채움" 안내 + 기존 doc 도
  다음 ingest 에서 갱신
- HANDOFF: 2026-05-03 머지 후 발견 entry
- spec status: `planned` → `in_progress`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-03 01:25:23 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — implementation 자체는 spec DoD 와 부합하고 fallback chain 5 단계 + sentinel + NFC 처리 모두 unit test 로 pin 됐습니다. parser_version cascade ( → ) 와 cross-crate snapshot 갱신도 깔끔합니다.\n\nactionable nit 3 건만 inline 으로 달았습니다 — 모두 미래 reader 친화적 cleanup (의도 명확화) 이고 동작 변경 없음.

회차 1 — implementation 자체는 spec DoD 와 부합하고 fallback chain 5 단계 + sentinel + NFC 처리 모두 unit test 로 pin 됐습니다. parser_version cascade ( → ) 와 cross-crate snapshot 갱신도 깔끔합니다.\n\nactionable nit 3 건만 inline 으로 달았습니다 — 모두 미래 reader 친화적 cleanup (의도 명확화) 이고 동작 변경 없음.
@@ -98,0 +102,4 @@
// stem. NFC-normalize the chosen string so the on-wire title is
// canonically equivalent to whatever the user stored, regardless
// of source NFD/NFC form.
let file_stem = workspace_path_stem(&asset.workspace_path.0);

doc comment 의 5. file_stem (filename minus extension, kebab-case preserved). 에서 "kebab-case preserved" 표현이 약간 오해 소지가 있습니다. 실제 동작은 "파일명을 그대로 반환 (확장자만 제거, 그 외 변환 없음)" 이고, kebab-case 가 입력에 있어야만 출력에도 남는 형태입니다 (snake_case 면 snake_case 유지, CamelCase 면 CamelCase 유지).

제안: 5. file_stem (filename minus extension — returned verbatim, no case transformation). 정도로 바꾸면 의도가 더 명확합니다.

doc comment 의 `5. file_stem (filename minus extension, kebab-case preserved).` 에서 "kebab-case preserved" 표현이 약간 오해 소지가 있습니다. 실제 동작은 "파일명을 그대로 반환 (확장자만 제거, 그 외 변환 없음)" 이고, kebab-case 가 입력에 있어야만 출력에도 남는 형태입니다 (snake_case 면 snake_case 유지, CamelCase 면 CamelCase 유지). 제안: `5. file_stem (filename minus extension — returned verbatim, no case transformation).` 정도로 바꾸면 의도가 더 명확합니다.

workspace_pathto_posix 가 NFC 정규화한 결과이므로 (asset.rs 주석 + design §6.6) 여기서 한 번 더 NFC 변환할 필요는 없습니다. 다음 두 옵션 중 하나로 의도를 명확히 하면 좋겠습니다:

  1. derive_title 안의 stem.nfc().collect() 호출을 제거 — 입력이 이미 NFC 라는 invariant 에 기댐.
  2. 그대로 두되 // defense-in-depth: workspace_path 가 이론상 NFC 지만 invariant 가 깨졌을 때 빈 title 보다는 일관된 결과를 낸다 주석 추가.

어느 쪽이든 "이게 왜 두 번 호출되는지" 가 미래 reader 한테 자명해지면 됩니다.

`workspace_path` 은 `to_posix` 가 NFC 정규화한 결과이므로 (asset.rs 주석 + design §6.6) 여기서 한 번 더 NFC 변환할 필요는 없습니다. 다음 두 옵션 중 하나로 의도를 명확히 하면 좋겠습니다: 1. `derive_title` 안의 `stem.nfc().collect()` 호출을 제거 — 입력이 이미 NFC 라는 invariant 에 기댐. 2. 그대로 두되 `// defense-in-depth: workspace_path 가 이론상 NFC 지만 invariant 가 깨졌을 때 빈 title 보다는 일관된 결과를 낸다` 주석 추가. 어느 쪽이든 "이게 왜 두 번 호출되는지" 가 미래 reader 한테 자명해지면 됩니다.
@@ -802,2 +891,3 @@
/// spec forbids empty `CanonicalDocument.title` (p9-fb-07 line 37).
#[test]
fn title_empty_string_in_user_map_falls_back_to_default() {
fn title_empty_string_in_user_map_falls_back_to_file_stem() {

테스트 docstring 의 (p9-fb-07 line 37) 참조는 spec 파일의 line number 가 변하면 함께 stale 해집니다. 차라리 인용문 자체를 박는 게 견고합니다:

/// spec p9-fb-07: "빈 문자열 반환 금지".

같은 변경을 title_non_string_in_user_map_falls_back_to_file_stem 의 docstring 에도 적용하면 일관됩니다.

테스트 docstring 의 `(p9-fb-07 line 37)` 참조는 spec 파일의 line number 가 변하면 함께 stale 해집니다. 차라리 인용문 자체를 박는 게 견고합니다: ``` /// spec p9-fb-07: "빈 문자열 반환 금지". ``` 같은 변경을 `title_non_string_in_user_map_falls_back_to_file_stem` 의 docstring 에도 적용하면 일관됩니다.
altair823 added 1 commit 2026-05-03 01:26:30 +00:00
- `derive_title` doc 의 step 5 표현 "kebab-case preserved" → "returned
  verbatim, no case transformation" (실제 동작과 일치)
- `file_stem` NFC 변환 제거 — workspace_path 가 to_posix 단계에서 이미
  NFC 정규화되므로 (§6.6) 이중 호출은 군더더기. 의도 명시 주석 추가.
- M7 revised 테스트 docstring 의 "p9-fb-07 line 37" 참조를 인용문
  ("빈 문자열 반환 금지") 으로 교체 — line number 변동에 안전.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-03 01:26:44 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 nit 3 건 모두 깔끔히 반영됐습니다.

  • step 5 표현 수정: "returned verbatim, no case transformation" 으로 의도 명확화
  • file_stem NFC 이중 호출 제거 + invariant 주석 추가 (workspace_path 가 to_posix 단계에서 이미 NFC)
  • M7 revised 테스트 docstring 의 line number 참조를 인용문으로 교체

추가 지적 사항 없음. 머지 OK.

회차 2 — 회차 1 nit 3 건 모두 깔끔히 반영됐습니다. - step 5 표현 수정: "returned verbatim, no case transformation" 으로 의도 명확화 - file_stem NFC 이중 호출 제거 + invariant 주석 추가 (workspace_path 가 to_posix 단계에서 이미 NFC) - M7 revised 테스트 docstring 의 line number 참조를 인용문으로 교체 추가 지적 사항 없음. 머지 OK.
altair823 merged commit eac08ed545 into main 2026-05-03 01:26:56 +00:00
altair823 deleted branch feat/p9-fb-07-title 2026-05-03 01:26:57 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#66