docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹 페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요 type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec 의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring 다이어그램 + 진입 가이드 보유. ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로 교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입 링크 추가. 이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md 의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md, per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
5.7 KiB
Markdown
108 lines
5.7 KiB
Markdown
# Source
|
|
|
|
> 워크스페이스를 walk 하고 gitignore-style 필터로 거른 뒤 BLAKE3 checksum 으로 컨텐츠 어드레스된 `RawAsset` 목록을 만든다. ingest pipeline 의 첫 단계.
|
|
|
|
## 구성 crate
|
|
|
|
| Crate | 역할 |
|
|
|-------|------|
|
|
| `kebab-source-fs` | 로컬 파일시스템 `SourceConnector` 단일 구현. `kebab-config` + `kebab-core` 만 의존. |
|
|
|
|
## 구조
|
|
|
|
```mermaid
|
|
classDiagram
|
|
class FsSourceConnector {
|
|
+new(config) Result~Self~
|
|
+scan(scope) Result~Vec~RawAsset~~
|
|
-default_root: PathBuf
|
|
-default_exclude: Vec~String~
|
|
-copy_threshold_bytes: u64
|
|
}
|
|
class WalkerModule {
|
|
build_overrides(root, excludes, kbignore) Override
|
|
read_kbignore(root) Vec~String~
|
|
walk_files(root, overrides) Vec~PathBuf~
|
|
}
|
|
class HashModule {
|
|
hash_file(path) (byte_len, hex)
|
|
}
|
|
class MediaModule {
|
|
media_type_for(path) MediaType
|
|
}
|
|
class SourceConnector {
|
|
<<trait kebab-core>>
|
|
scan(scope) Vec~RawAsset~
|
|
}
|
|
SourceConnector <|.. FsSourceConnector
|
|
FsSourceConnector --> WalkerModule
|
|
FsSourceConnector --> HashModule
|
|
FsSourceConnector --> MediaModule
|
|
```
|
|
|
|
## Data flow
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Cfg["Config<br/>(workspace.root, exclude, copy_threshold_mb)"]
|
|
Scope["SourceScope<br/>(per-call lens)"]
|
|
Walk["walk_files<br/>walkdir + gitignore overrides<br/>+ symlink cycle guard"]
|
|
KBI[".kebabignore<br/>(매 scan 재읽음)"]
|
|
Default["DEFAULT_EXCLUDES<br/>.DS_Store / ._*"]
|
|
Hash["blake3 file hash<br/>(byte_len + hex)"]
|
|
Media["media_type_for<br/>(extension → MediaType)"]
|
|
Posix["to_posix<br/>NFC + leading ./ strip<br/># 거부"]
|
|
Asset["RawAsset<br/>{asset_id, workspace_path,<br/>media_type, byte_len,<br/>checksum, stored}"]
|
|
Sort["sort by workspace_path"]
|
|
Cfg --> Walk
|
|
Scope --> Walk
|
|
KBI --> Walk
|
|
Default --> Walk
|
|
Walk --> Hash --> Asset
|
|
Walk --> Media --> Asset
|
|
Walk --> Posix --> Asset
|
|
Asset --> Sort --> Out["Vec~RawAsset~"]
|
|
```
|
|
|
|
## 주요 type / trait / 함수
|
|
|
|
- `FsSourceConnector::new(&Config) -> Result<Self>` — `Config::resolve_workspace_root()` 호출 (p9-fb-05 path policy 통일), `copy_threshold_mb * 1 MiB` 미리 곱해 둠.
|
|
- `FsSourceConnector::scan(&SourceScope) -> Result<Vec<RawAsset>>` — kebab-core `SourceConnector` trait 구현. 결정성 보장 sort by `workspace_path`.
|
|
- `walker::build_overrides(root, config_exclude, kbignore_patterns) -> Override` — `ignore` crate 의 gitignore engine 위에서 union 빌드. 모든 패턴이 `!` prefix (positive override = include 의미라 negate 필요).
|
|
- `walker::read_kbignore(root) -> Vec<String>` — `<root>/.kebabignore` 매 scan 재읽음 (long-running process 가 file edit 즉시 반영).
|
|
- `walker::walk_files(root, overrides) -> Vec<PathBuf>` — `walkdir::WalkDir::follow_links(true)` + 별도 `visited: HashSet<canonical PathBuf>` 으로 symlink cycle 방어.
|
|
- `hash::hash_file(path) -> Result<(byte_len, hex)>` — streaming BLAKE3, 큰 파일도 메모리 안 쌓음.
|
|
- `media::media_type_for(path) -> MediaType` — extension 기반 단순 매핑 (`.md` → `Markdown`, `.pdf` → `Pdf`, `.png/.jpg/...` → `Image(_)` 등).
|
|
|
|
## 외부 의존
|
|
|
|
- crate dep: `kebab-core` (`RawAsset`, `Checksum`, `SourceConnector`, `id_for_asset`, `to_posix`), `kebab-config` (`Config`).
|
|
- 외부 lib: `walkdir` (디렉토리 walk + symlink follow), `ignore` (gitignore override engine, walker 는 안 씀), `blake3` (file hash), `time` (`OffsetDateTime::now_utc` for `discovered_at`).
|
|
- 외부 서비스: 없음.
|
|
|
|
## 핵심 결정
|
|
|
|
- **`walkdir` 사용 (not `ignore::WalkBuilder`)**.
|
|
**왜**: `ignore::WalkBuilder` 가 gitignore + cycle detection 을 한 번에 묶지만, sibling-subtree symlink (`a -> ../b`) 의 cycle 을 ancestor-only check 가 놓치는 케이스가 있음. `walkdir` + 자체 `visited` set 으로 canonical-path 비교를 명시 제어.
|
|
|
|
- **DEFAULT_EXCLUDES = `.DS_Store` + `._*`**.
|
|
**왜**: macOS Finder 메타파일 + AppleDouble resource fork. 모든 사용자 `.kebabignore` 에 들어갈 noise — 한 번에 baked-in. 사용자가 끄려면 별 메커니즘 필요 (현재 미제공, 필요 시 P+).
|
|
|
|
- **`AssetStorage::{Copied, Reference}` = intent signal, not actual copy**.
|
|
**왜**: scan 단계는 byte_len 만 보고 의도만 표시. 실제 디스크 copy 는 P1-6 의 asset writer 책임. `copy_threshold_mb = 100` (default) 미만은 `Copied`, 이상은 `Reference + sha`. 큰 파일 중복 저장 회피.
|
|
|
|
- **`SourceScope::include` 무시 (router 책임)**.
|
|
**왜**: §6.2 의 `WorkspaceCfg.include` 는 extractor router 가 적용. SourceConnector 가 또 필터링하면 markdown/PDF 가 router 에 도달 전 이중 필터. Connector 는 모든 non-excluded 파일 emit.
|
|
|
|
- **Filename `#` 거부 = warn + skip (not abort)**.
|
|
**왜**: `to_posix` 가 `#` 포함 path 를 Err 반환 (Citation URI fragment separator 와 충돌). 단일 파일이 10000 파일 ingest 를 죽이면 안 됨 — `tracing::warn` 로 알리고 다음 파일.
|
|
|
|
- **scan 결과 sort by `workspace_path`**.
|
|
**왜**: 결정성. 두 번 scan 의 `Vec<RawAsset>` 가 `discovered_at` 빼고 동일해야 idempotent ingest 가능. test `scan_idempotent_modulo_timestamp` 가 회귀 핀.
|
|
|
|
## 관련 spec / HOTFIXES
|
|
|
|
- frozen 설계 §3.3 (`RawAsset`), §6.2 (workspace + .kebabignore), §6.6 (POSIX 정규화), §7.1 (`SourceScope`), §7.2 (`SourceConnector`): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
|
|
- task spec: [`tasks/p1/p1-1-source-fs.md`](../../../tasks/p1/p1-1-source-fs.md)
|
|
- p9-fb-05 (`expand_tilde` shim 제거 + `Config::resolve_workspace_root` 일원화): [`tasks/p9/p9-fb-05-config-path-policy.md`](../../../tasks/p9/p9-fb-05-config-path-policy.md)
|