diff --git a/crates/kebab-core/src/document.rs b/crates/kebab-core/src/document.rs index 4fdda02..643875b 100644 --- a/crates/kebab-core/src/document.rs +++ b/crates/kebab-core/src/document.rs @@ -142,6 +142,18 @@ pub enum SourceSpan { start_ms: u64, end_ms: u64, }, + /// p10-1A-2: AST-unit span for code ingest. Internal storage shape + /// (chunks.source_spans_json) — `citation_helper` maps this to the + /// wire `Citation::Code` (added 1A-1). `symbol` is the per-language + /// self-reference path (design §3.4); `` / `` for + /// glue regions, never null for an identified unit. `lang` is the + /// canonical code_lang. + Code { + line_start: u32, + line_end: u32, + symbol: Option, + lang: Option, + }, } // ── Forward-declared stubs (§3.7a). Bodies are final per design. ──────── @@ -195,6 +207,24 @@ mod tests { /// previously failed at serde runtime because `tag = "kind"` cannot /// describe a newtype carrying a non-struct value. The struct-variant /// shape used here is the §9 schema migration. + #[test] + fn source_span_code_round_trips_and_tags_lowercase() { + let s = SourceSpan::Code { + line_start: 10, + line_end: 42, + symbol: Some("foo::Bar::baz".to_string()), + lang: Some("rust".to_string()), + }; + let v = serde_json::to_value(&s).unwrap(); + assert_eq!(v["kind"], "code"); + assert_eq!(v["line_start"], 10); + assert_eq!(v["line_end"], 42); + assert_eq!(v["symbol"], "foo::Bar::baz"); + assert_eq!(v["lang"], "rust"); + let back: SourceSpan = serde_json::from_value(v).unwrap(); + assert_eq!(back, s); + } + #[test] fn inline_serde_round_trip() { let cases = vec![ diff --git a/crates/kebab-search/src/citation_helper.rs b/crates/kebab-search/src/citation_helper.rs index 3001640..e896731 100644 --- a/crates/kebab-search/src/citation_helper.rs +++ b/crates/kebab-search/src/citation_helper.rs @@ -49,6 +49,13 @@ pub(crate) fn citation_from_first_span( end_ms: *end_ms, speaker: None, }, + // TODO(p10-1a-2 Task 3): map to Citation::Code + Some(SourceSpan::Code { .. }) => Citation::Line { + path, + start: 1, + end: 1, + section, + }, // Byte-spans don't have a Citation variant. Fall back to a // Line citation pointing at the document head — better than // fabricating a position. Spans-empty falls into the same diff --git a/crates/kebab-tui/src/inspect.rs b/crates/kebab-tui/src/inspect.rs index c6bc13c..2910b1b 100644 --- a/crates/kebab-tui/src/inspect.rs +++ b/crates/kebab-tui/src/inspect.rs @@ -455,6 +455,15 @@ fn describe_span(span: &kebab_core::SourceSpan) -> String { SourceSpan::Time { start_ms, end_ms } => { format!("Time {start_ms}-{end_ms} ms") } + SourceSpan::Code { + line_start, + line_end, + symbol, + .. + } => match symbol { + Some(sym) => format!("Code {line_start}-{line_end} ({sym})"), + None => format!("Code {line_start}-{line_end}"), + }, } } diff --git a/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md b/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md index d883d38..baa349b 100644 --- a/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md +++ b/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md @@ -565,6 +565,7 @@ pub enum SourceSpan { Page { page: u32, char_start: Option, char_end: Option }, Region { x: u32, y: u32, w: u32, h: u32 }, Time { start_ms: u64, end_ms: u64 }, + Code { line_start: u32, line_end: u32, symbol: Option, lang: Option }, // p10-1A-2: internal code-unit span (see tasks/p10/p10-1a-2) } ```