feat(p1-4): kb-normalize + kb-core Inline schema hotfix #9

Merged
altair823 merged 8 commits from feat/p1-4-normalize into main 2026-04-30 16:23:18 +00:00
Owner

요약

P1-4 task spec 구현. 신규 crate kb-normalizeRawAsset (P1-1) + Metadata (P1-2) + Vec<ParsedBlock> (P1-3) → kb_core::CanonicalDocument 결정적 ID 부여. 동시에 P1-3 spec reviewer가 P1-4로 defer한 kb_core::Inline serde schema §9 hotfix 동반.

기준: tasks/p1/p1-4-normalize.md, docs/superpowers/specs/2026-04-27-kb-final-form-design.md (§3.4 / §3.7b / §4.2-3 / §3.6 / §8 / §9).

구현

kb-core::Inline schema 핫픽스 (§9 migration)

  • Inline::Text(String) / Code(String) / Strong(Vec<…>) / Emph(Vec<…>) 4개 newtype variant → struct variant 형태로 변환 (Text { text: String } 등). tag = \"kind\" 내부 태그가 newtype variant + non-struct payload 조합을 지원하지 않는 serde 한계로 4/5 variant runtime 실패.
  • inline_serde_round_trip (kb-core unit test) — 5 variant 모두 round-trip 검증.
  • kb-parse-md/src/blocks.rs 모든 호출자 mechanical sweep.
  • kb-parse-md/tests/blocks_snapshots.rsBlockView/PayloadView projection 우회 코드 제거, 직접 Vec<ParsedBlock> 직렬화로 전환. baseline nested-headings.blocks.snapshot.json 재생성.
  • BlockTuple (id_for_block recipe) 에는 Inline 미포함 — ID stability 영향 없음 (검증).

crates/kb-normalize

  • Cargo.toml — 워크스페이스 멤버, deps 정확히 사용분만 (kb-core, kb-parse-types, anyhow, serde, serde_json, time, tracing, unicode-normalization). kb-parse-md[dev-dependencies] 만 (§8 경계 유지)
  • src/lib.rs — public surface 정확히 build_canonical_document + pub use kb_core::{id_for_doc, id_for_block}
  • tests/normalize_snapshot.rscode-and-table.md 픽스처 → CanonicalDocument JSON 결정적 round-trip
  • fixtures/markdown/code-and-table.canonical.snapshot.json — baseline

동작 contract 핵심

  • doc_id = id_for_doc(workspace_path, asset_id, parser_version) per §4.2
  • block_id per §4.3 ordinal rule — (heading_path, kind) 별 0-based, document order
  • heading_path strings NFC-normalize 후 hash + wire form 양쪽 모두 NFC 형태 사용 (NFD/NFC 동일성 보장)
  • metadata.user[\"title\"] / [\"lang\"]CanonicalDocument.title / .lang 로 lift, user map 에서 제거
  • Provenance: Discovered (asset.discovered_at) → Parsed → Normalized + Warning per upstream warning. Parsed/Normalized 는 now_utc() 1회 공유
  • AudioRef block 은 P8 audio extractor 도입 전까지 skip + Warning emit (AssetId(\"\") placeholder 유효성 위반 방지)
  • warning_agent 라우팅: upstream ExtractFailedkb-parse-md (panic-recovery 출처), lift-stage warnings → kb-normalize (별도 lift_warnings 벡터)

검증

  • cargo check / build (RUSTFLAGS=-D warnings) / clippy --all-targets -D warnings clean
  • cargo test -p kb-normalize14 unit + 1 integration pass
  • cargo test --workspace147 passing 전체 그린
  • cargo tree -p kb-normalize --depth 1 → 허용 deps 만 (parser-impl 없음)
  • 결정적 ID property: 1000 iter < 1초, NFC=NFD 동치 확인 (path + heading_path 양쪽)

Review trail

  • spec compliance: PASS-WITH-NICE-TO-FIX → must-fix 0
  • code quality round 1: APPROVED-WITH-MINOR — Important 3 (ExtractFailed 귀속 / AudioRef 우회 / heading_path NFC 누락) + Minor 5 (unused deps / 결정적 테스트 빈 입력 / title-lift end-to-end / title-lift edge / shared now_utc 단언)
  • 반영 2 commit (e0df429, 557275c)
  • code quality round 2 (re-review): APPROVED — must-fix-now 0, 새 regression 없음

⚠️ Bisect 주의

606ce1c (kb-core 핫픽스) → cfccb36 (kb-parse-md 호출자 갱신) 사이 중간 커밋은 컴파일 안 됨. 워크스페이스 내부 schema migration 의 본질적 한계 — 한 crate 단독 atomic 불가. git bisect 시 두 커밋을 같이 건너뛰거나 squash 권장.

후속 자연 해소 (비차단)

  • kb-core serde_json_canonicalizer v0.3 가 NFC normalize 안 함 — 현재 caller 측에서 NFC 보장. 추후 id_from 자체에 NFC 단계 추가 시 caller 의존성 줄일 수 있음.
  • list-item 들이 부모 List 의 block_id 공유 — §4.2 recipe 확장 시 per-item ID 가능 (rustdoc 명시).
  • ExtractFailed 가 lift-stage 에서도 발생 가능 — 향후 WarningWarningSource 필드 추가가 구조적 fix.
  • 미혼합 upstream + lift warnings 통합 ordering test (nice-to-fix), title_empty_string_in_user_map_falls_back_to_default 테스트명 약간 오해 소지 (cosmetic).

다음

머지 후 feat/p1-5-chunk 분기 → P1-5 (kb-chunk) 진행.

## 요약 P1-4 task spec 구현. 신규 crate `kb-normalize` — `RawAsset` (P1-1) + `Metadata` (P1-2) + `Vec<ParsedBlock>` (P1-3) → `kb_core::CanonicalDocument` 결정적 ID 부여. 동시에 P1-3 spec reviewer가 P1-4로 defer한 **`kb_core::Inline` serde schema §9 hotfix** 동반. 기준: tasks/p1/p1-4-normalize.md, docs/superpowers/specs/2026-04-27-kb-final-form-design.md (§3.4 / §3.7b / §4.2-3 / §3.6 / §8 / §9). ## 구현 ### `kb-core::Inline` schema 핫픽스 (§9 migration) - `Inline::Text(String)` / `Code(String)` / `Strong(Vec<…>)` / `Emph(Vec<…>)` 4개 newtype variant → struct variant 형태로 변환 (`Text { text: String }` 등). `tag = \"kind\"` 내부 태그가 newtype variant + non-struct payload 조합을 지원하지 않는 serde 한계로 4/5 variant runtime 실패. - `inline_serde_round_trip` (kb-core unit test) — 5 variant 모두 round-trip 검증. - `kb-parse-md/src/blocks.rs` 모든 호출자 mechanical sweep. - `kb-parse-md/tests/blocks_snapshots.rs` — `BlockView`/`PayloadView` projection 우회 코드 제거, 직접 `Vec<ParsedBlock>` 직렬화로 전환. baseline `nested-headings.blocks.snapshot.json` 재생성. - `BlockTuple` (id_for_block recipe) 에는 `Inline` 미포함 — ID stability 영향 없음 (검증). ### `crates/kb-normalize` - `Cargo.toml` — 워크스페이스 멤버, deps 정확히 사용분만 (kb-core, kb-parse-types, anyhow, serde, serde_json, time, tracing, unicode-normalization). `kb-parse-md` 는 `[dev-dependencies]` 만 (§8 경계 유지) - `src/lib.rs` — public surface 정확히 `build_canonical_document` + `pub use kb_core::{id_for_doc, id_for_block}` - `tests/normalize_snapshot.rs` — `code-and-table.md` 픽스처 → `CanonicalDocument` JSON 결정적 round-trip - `fixtures/markdown/code-and-table.canonical.snapshot.json` — baseline ### 동작 contract 핵심 - `doc_id = id_for_doc(workspace_path, asset_id, parser_version)` per §4.2 - `block_id` per §4.3 ordinal rule — `(heading_path, kind)` 별 0-based, document order - `heading_path` strings NFC-normalize 후 hash + wire form 양쪽 모두 NFC 형태 사용 (NFD/NFC 동일성 보장) - `metadata.user[\"title\"]` / `[\"lang\"]` → `CanonicalDocument.title` / `.lang` 로 lift, user map 에서 제거 - Provenance: Discovered (asset.discovered_at) → Parsed → Normalized + Warning per upstream warning. Parsed/Normalized 는 `now_utc()` 1회 공유 - AudioRef block 은 P8 audio extractor 도입 전까지 skip + Warning emit (`AssetId(\"\")` placeholder 유효성 위반 방지) - `warning_agent` 라우팅: upstream `ExtractFailed` → `kb-parse-md` (panic-recovery 출처), lift-stage warnings → `kb-normalize` (별도 `lift_warnings` 벡터) ## 검증 - `cargo check / build (RUSTFLAGS=-D warnings) / clippy --all-targets -D warnings` clean - `cargo test -p kb-normalize` → **14 unit + 1 integration** pass - `cargo test --workspace` → **147 passing** 전체 그린 - `cargo tree -p kb-normalize --depth 1` → 허용 deps 만 (parser-impl 없음) - 결정적 ID property: 1000 iter < 1초, NFC=NFD 동치 확인 (path + heading_path 양쪽) ## Review trail - spec compliance: PASS-WITH-NICE-TO-FIX → must-fix 0 - code quality round 1: APPROVED-WITH-MINOR — Important 3 (ExtractFailed 귀속 / AudioRef 우회 / heading_path NFC 누락) + Minor 5 (unused deps / 결정적 테스트 빈 입력 / title-lift end-to-end / title-lift edge / shared now_utc 단언) - 반영 2 commit (`e0df429`, `557275c`) - code quality round 2 (re-review): **APPROVED** — must-fix-now 0, 새 regression 없음 ## ⚠️ Bisect 주의 `606ce1c` (kb-core 핫픽스) → `cfccb36` (kb-parse-md 호출자 갱신) 사이 중간 커밋은 컴파일 안 됨. 워크스페이스 내부 schema migration 의 본질적 한계 — 한 crate 단독 atomic 불가. `git bisect` 시 두 커밋을 같이 건너뛰거나 squash 권장. ## 후속 자연 해소 (비차단) - kb-core `serde_json_canonicalizer` v0.3 가 NFC normalize 안 함 — 현재 caller 측에서 NFC 보장. 추후 `id_from` 자체에 NFC 단계 추가 시 caller 의존성 줄일 수 있음. - list-item 들이 부모 List 의 `block_id` 공유 — §4.2 recipe 확장 시 per-item ID 가능 (rustdoc 명시). - `ExtractFailed` 가 lift-stage 에서도 발생 가능 — 향후 `Warning` 에 `WarningSource` 필드 추가가 구조적 fix. - 미혼합 upstream + lift warnings 통합 ordering test (nice-to-fix), `title_empty_string_in_user_map_falls_back_to_default` 테스트명 약간 오해 소지 (cosmetic). ## 다음 머지 후 `feat/p1-5-chunk` 분기 → P1-5 (kb-chunk) 진행.
altair823 added 8 commits 2026-04-30 15:46:43 +00:00
`#[serde(tag = "kind")]` rejects newtype variants whose payload is not a
struct, so 4 of 5 `Inline` variants (`Text(String)`, `Code(String)`,
`Strong(Vec<…>)`, `Emph(Vec<…>)`) failed to serialize at runtime — only
`Link { text, href }` worked. Convert every variant to struct form so the
internally-tagged shape is well-formed and round-trips through JSON.

Add `inline_serde_round_trip` covering all five variants. Per design §9,
this is a wire-schema migration; no `docs/wire-schema/v1/*.json` change
required since `Inline` is not directly referenced there. Callers in
kb-parse-md follow in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical sweep over `Inline::Text(_)` / `Code(_)` / `Strong(_)` / `Emph(_)`
construction and match sites under the new struct-variant shape introduced
in the previous commit. `Inline::Link { text, href }` is unchanged.

The snapshot test in `tests/blocks_snapshots.rs` previously projected
`ParsedBlock` into a `BlockView`/`PayloadView` shim because the old
`Inline` could not serialize. With the schema fix in place we now
serialize `ParsedBlock` directly through serde — the shim and its
`flatten_inline` helper are removed. Inlines surface as structured
objects (`{"kind":"text","text":"…"}` etc.). Regenerated
`nested-headings.blocks.snapshot.json` to reflect the new shape via
the existing `--ignored` emitter; `code-and-table.blocks.snapshot.json`
has no inlines and is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the workspace member, `Cargo.toml` with the §8-allowed dep set
(kb-core, kb-parse-types, kb-config, serde, serde_json_canonicalizer,
blake3, unicode-normalization, time, anyhow, tracing) and a stubbed
`build_canonical_document` that pins the public signature plus
`doc_id` derivation. `kb-parse-md` is permitted only as a *dev*-dep so
the integration snapshot test (added later in this series) can drive
a fixture through the real parser without violating the production
boundary — `cargo tree -p kb-normalize --depth 1 --edges normal`
confirms no parser implementation appears in the regular dep tree.

`id_for_doc` and `id_for_block` are re-exported from kb-core (which
holds the canonical recipe per §4.2); kb-normalize is the canonical
*entry point* per design §8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement the §4.3 ordinal rule and §3.4 block lift. Each `ParsedBlock`
maps to a `kb_core::Block` variant carrying a `CommonBlock` whose
`block_id = id_for_block(doc_id, payload_kind, heading_path, ordinal,
source_span)`. Ordinals are scoped to `(heading_path, payload_kind)`,
0-based, in document order — three paragraphs under one H1 get 0/1/2,
a code block under the same H1 starts fresh at 0, a paragraph under a
different H1 also starts at 0.

`payload_kind` is the lowercase-no-spaces convention from §4.2:
"heading", "paragraph", "list", "code", "table", "quote", "imageref",
"audioref".

`ListBlock.items` re-uses the parent list's `CommonBlock` per §3.4 (no
per-item BlockId is allocated). `AudioRefBlock` placeholder fields
(`asset_id`, `duration_ms`) are filled in by P8 — for now we synthesize
the minimal record so the document is well-typed.

Tests pin the four §4.4 ID properties (1000-iteration determinism, NFC
≡ NFD Korean path, `./a/b.md` ≡ `a/b.md`, ordinal grouping). Provenance
and title/lang lift land in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Build a `Provenance` with one event per pipeline stage (`Discovered`
sourced from `RawAsset.discovered_at`, then `Parsed` and `Normalized`
stamped with one shared `now_utc()` reading), plus one `Warning` event
per upstream warning. Sharing `now` between Parsed and Normalized
bounds intra-call timestamp jitter — event ordering is preserved by
`Vec` position regardless. Warning agents are routed back to the
upstream component (`kb-parse-md` for parse warnings,
`kb-normalize` for `ExtractFailed`).

Lift `metadata.user["title"]` and `metadata.user["lang"]` (where P1-2
stashes them since the `Metadata` struct itself does not carry those
fields) into `CanonicalDocument.title` / `CanonicalDocument.lang`.
Both keys are removed from the user map after lifting so the wire
form does not duplicate the data; missing keys default to empty
string / empty `Lang`. Other user-map keys survive.

Tests pin the event ordering, the warning routing, and the lift
behavior (including non-duplication in `metadata.user`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the integration snapshot test pinning the full `CanonicalDocument`
JSON for `fixtures/markdown/code-and-table.md` (run through the real
`kb-parse-md::parse_frontmatter` + `parse_blocks`, dev-dep only).
Non-deterministic `provenance.events[*].at` for the Parsed and
Normalized events is stripped before comparison; the Discovered
event's `at` is pinned by constructing the test `RawAsset` with a
fixed `discovered_at`. Run with `UPDATE_SNAPSHOTS=1` to regenerate.

Add the 1000-iteration determinism property: same inputs ⇒ byte-
identical JSON (modulo the same stripped timestamps), in under one
second of wall-clock time. A regression in canonical JSON, BLAKE3
hashing, ordinal counting, or any other deterministic field would
surface here immediately.

The integration test depends on `kb-parse-md` only as a dev-dep, so
`cargo tree -p kb-normalize --depth 1 --edges normal` confirms no
parser implementation appears in the production dep tree per design
§8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
I1: warning_agent maps ExtractFailed → "kb-parse-md" (the panic-recovery
emitter in kb-parse-md/src/blocks.rs). Lift-stage warnings from
build_canonical_document are tracked separately and attributed to
"kb-normalize", so the I1 mapping change does not lie about
kb-normalize-originated drops.

I2: ParsedPayload::AudioRef no longer synthesizes Block::AudioRef with
an invalid empty AssetId (would violate AssetId::from_str's 32-hex
invariant). Block is dropped, Warning surfaces in Provenance with src
mention, attributed to kb-normalize (lift-stage decision). TODO(P8)
comment marks this as a placeholder until the audio extractor lands.

I3: NFC-normalize each heading_path string in lift_block before feeding
into id_for_block AND into CommonBlock.heading_path. pulldown-cmark does
not NFC heading text and serde_json_canonicalizer v0.3 does not either,
so canonically-equivalent NFD/NFC inputs would produce different
block_ids without this normalization. Mirrors the existing doc_id NFC
handling via to_posix.

Minors:
- M4: trim Cargo.toml — drop kb-config, serde_json_canonicalizer,
  blake3 (unused); keep tracing (now wired) + unicode-normalization
  (now used by I3).
- M5: determinism_1000_iterations_under_1s now uses the same 5-block
  fixture as block_ordinals_scoped_per_heading_and_kind (extracted into
  fixture_blocks_five helper) so the determinism property is exercised
  on a real lift_block path, not just an empty Vec. Still < 1s.
- M6: snapshot integration test now passes BodyHints { first_h1:
  Some("Code And Table"), .. } and asserts doc.title == "Code And Table"
  end-to-end. Baseline JSON updated.
- M7: title/lang edge-case unit tests pin policy: empty string lifts to
  empty string; non-stringy values silently drop. Rustdoc updated.
- M10: provenance_contains_stage_events_in_order asserts events[1].at
  == events[2].at to pin the shared-now_utc invariant.

New tests (unit, kb-normalize):
- provenance_with_extract_failed_warning_attributes_to_kb_parse_md (I1)
- audio_ref_block_skipped_with_warning (I2)
- nfc_nfd_korean_heading_path_same_block_id (I3)
- title_empty_string_in_user_map_falls_back_to_default (M7)
- title_non_string_in_user_map_silently_drops (M7)
- lang_invalid_shape_silently_drops (M7)

kb-normalize unit tests: 9 → 14. Integration snapshot: 1 (unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M8: kb-parse-md frontmatter doc-comment claimed filename fallback was
P1-4's job; P1-4 spec did not include it. Reconcile: defer to a later
phase (P1-7 / kb-app integration) where the workspace_path filename is
known to the caller. Updated comment in build_metadata().

M9: kb-parse-md tests use the #[ignore] regenerator pattern, while
kb-normalize's integration test uses an UPDATE_SNAPSHOTS=1 env-var.
Migrating kb-parse-md is out of scope; one-line note added to
blocks_snapshots.rs mod doc-comment to flag the intentional split.

M11, M12: doc-only comments in lift_block (already added in the
previous commit) — list-item shared block_id rationale and the
intentional camel-case Debug-format for WarningKind in Provenance
notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 reviewed 2026-04-30 15:47:06 +00:00
claude-reviewer-01 left a comment
Member

리뷰: 모든 항목 해소 — APPROVE 권고 (gate 정책상 COMMENT 등록)

내부 review loop 3회 완료. 마지막 리뷰어 verdict: APPROVED, must-fix-now 0건.

Review trail

Round Verdict 반영
Spec compliance PASS-WITH-NICE-TO-FIX (must-fix 0, 3 nice-to-fix)
Quality round 1 APPROVED-WITH-MINOR (Important 3 + Minor 5) e0df429, 557275c
Quality round 2 APPROVED (must-fix-now 0)

핵심 검증

항목 상태
kb_core::Inline schema (§9 migration) struct variants, 5/5 round-trip pass, BlockTuple 에 Inline 미포함 → ID stability 보존
doc_id / block_id per §4.2-3 ordinal (heading_path, kind) scoped 0-based, 5개 block fixture 로 검증
heading_path NFC 핫픽스 NFC=NFD 동치 보장 (id_for_block + CommonBlock wire form 양쪽), nfc_nfd_korean_heading_path_same_block_id
AudioRef placeholder 제거 skip + lift_warning emit, AssetId("") 유효성 위반 차단, TODO(P8) 명시
Provenance ordering + agent 귀속 Discovered → Parsed → Normalized → upstream warnings → lift warnings, 각각 정확한 agent
결정적 ID 1000 iter < 1초 non-empty 5-block fixture 로 검증
title/lang lift + user map cleanup end-to-end snapshot + 3개 edge case test

Gate 통과

  • cargo check / build (RUSTFLAGS=-D warnings) / clippy --all-targets -D warnings clean
  • cargo test --workspace147 passing (was 142, +5)
  • cargo tree -p kb-normalize --depth 1 parser-impl 부재

⚠️ Bisect 주의

606ce1ccfccb36 사이는 컴파일 안 됨 (cross-crate schema migration). git bisect 시 두 커밋 같이 skip 또는 squash 권장.

후속 자연 해소 (비차단)

  • kb-core id_from NFC 자체 적용 (현재 caller 측 보장)
  • list-item per-item BlockId (§4.2 recipe 확장 필요)
  • Warning::WarningSource 필드 (multi-stage 동일 kind 구조적 분리)
  • mixed upstream + lift warning ordering test (nice-to-fix)

결론

papered over 없음. self-review gate 정책상 본 코멘트는 COMMENT — author 측 수동 APPROVE + merge 부탁.

## 리뷰: 모든 항목 해소 — APPROVE 권고 (gate 정책상 COMMENT 등록) 내부 review loop 3회 완료. 마지막 리뷰어 verdict: **APPROVED**, must-fix-now 0건. ### Review trail | Round | Verdict | 반영 | |-------|---------|------| | Spec compliance | PASS-WITH-NICE-TO-FIX (must-fix 0, 3 nice-to-fix) | — | | Quality round 1 | APPROVED-WITH-MINOR (Important 3 + Minor 5) | `e0df429`, `557275c` | | Quality round 2 | **APPROVED** (must-fix-now 0) | — | ### 핵심 검증 | 항목 | 상태 | |------|------| | `kb_core::Inline` schema (§9 migration) | ✅ struct variants, 5/5 round-trip pass, BlockTuple 에 Inline 미포함 → ID stability 보존 | | `doc_id` / `block_id` per §4.2-3 | ✅ ordinal `(heading_path, kind)` scoped 0-based, 5개 block fixture 로 검증 | | `heading_path` NFC 핫픽스 | ✅ NFC=NFD 동치 보장 (id_for_block + CommonBlock wire form 양쪽), `nfc_nfd_korean_heading_path_same_block_id` | | AudioRef placeholder 제거 | ✅ skip + lift_warning emit, `AssetId("")` 유효성 위반 차단, TODO(P8) 명시 | | Provenance ordering + agent 귀속 | ✅ Discovered → Parsed → Normalized → upstream warnings → lift warnings, 각각 정확한 agent | | 결정적 ID 1000 iter < 1초 | ✅ non-empty 5-block fixture 로 검증 | | title/lang lift + user map cleanup | ✅ end-to-end snapshot + 3개 edge case test | ### Gate 통과 - `cargo check / build (RUSTFLAGS=-D warnings) / clippy --all-targets -D warnings` clean - `cargo test --workspace` → **147 passing** (was 142, +5) - `cargo tree -p kb-normalize --depth 1` parser-impl 부재 ### ⚠️ Bisect 주의 `606ce1c` → `cfccb36` 사이는 컴파일 안 됨 (cross-crate schema migration). `git bisect` 시 두 커밋 같이 skip 또는 squash 권장. ### 후속 자연 해소 (비차단) - kb-core `id_from` NFC 자체 적용 (현재 caller 측 보장) - list-item per-item BlockId (§4.2 recipe 확장 필요) - `Warning::WarningSource` 필드 (multi-stage 동일 kind 구조적 분리) - mixed upstream + lift warning ordering test (nice-to-fix) ### 결론 papered over 없음. self-review gate 정책상 본 코멘트는 COMMENT — author 측 수동 APPROVE + merge 부탁.
altair823 merged commit 4665910370 into main 2026-04-30 16:23:18 +00:00
altair823 deleted branch feat/p1-4-normalize 2026-04-30 16:23:19 +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#9