rskim-core 2.4.1

Core library for the most intelligent context optimization engine for coding agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//! Skim Core - Smart code reading and transformation library
//!
//! # Overview
//!
//! `skim-core` is a pure library for transforming source code by stripping
//! implementation details while preserving structure, signatures, and types.
//! Optimized for AI/LLM context windows.
//!
//! # Architecture
//!
//! **IMPORTANT: This is a LIBRARY with NO I/O.**
//! - Accepts `&str` (source code), not file paths
//! - Returns `Result<String>`, not stdout writes
//! - Pure transformations, no side effects
//!
//! CLI/SDK/MCP interfaces handle I/O separately.
//!
//! # Example
//!
//! ```no_run
//! use rskim_core::{transform, Language, Mode};
//!
//! let source = "function add(a: number, b: number) { return a + b; }";
//! let result = transform(source, Language::TypeScript, Mode::Structure)?;
//!
//! // Result: "function add(a: number, b: number) { /* ... */ }"
//! # Ok::<(), rskim_core::SkimError>(())
//! ```
//!
//! # API Stability
//!
//! As of v1.0.0, all publicly exported types and functions are considered stable.
//! Breaking changes will follow semver (major version bump).
//!
//! # Design Principles
//!
//! 1. **Zero-copy where possible** - Use `&str` slices, avoid allocations
//! 2. **Result types everywhere** - NO panics (enforced by clippy)
//! 3. **Dependency injection** - NO global state
//! 4. **Type-first** - Complete type schema before implementation

// Public API — stable as of v1.0.0
pub use types::{Language, Mode, Parser, Result, SkimError, TransformConfig, TransformResult};

mod parser;
mod transform;
mod types;

// NOTE: Caching is implemented at the CLI layer (rskim binary), not in the core library.
// The core library remains pure and I/O-free.
// See: crates/rskim/src/cache.rs for the caching implementation.

// ============================================================================
// Public API - Core Transformation Functions
// ============================================================================

/// Transform source code based on mode
///
/// This is the PRIMARY function for transformation.
///
/// # Arguments
///
/// * `source` - Source code as string slice (zero-copy)
/// * `language` - Programming language for parsing
/// * `mode` - Transformation mode (Structure, Signatures, Types, Full, Minimal, Pseudo)
///
/// # Returns
///
/// Transformed source code as `String`, or error if parsing fails.
///
/// # Performance
///
/// Target: <50ms for 1000-line files
/// - Parse: ~5-10ms (tree-sitter)
/// - Transform: ~10-20ms (AST traversal)
/// - String building: ~5-10ms
///
/// # Errors
///
/// - `SkimError::ParseError` - tree-sitter failed to parse
/// - `SkimError::TreeSitterError` - Grammar loading failed
///
/// # Examples
///
/// ```no_run
/// use rskim_core::{transform, Language, Mode};
///
/// let typescript = "function greet(name: string) { console.log(`Hello, ${name}`); }";
/// let result = transform(typescript, Language::TypeScript, Mode::Structure)?;
///
/// assert!(result.contains("function greet(name: string)"));
/// assert!(!result.contains("console.log"));
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn transform(source: &str, language: Language, mode: Mode) -> Result<String> {
    // ARCHITECTURE: Use default config for simple API
    transform_with_config(source, language, &TransformConfig::with_mode(mode))
}

/// Transform source code with custom configuration
///
/// Advanced API that accepts full configuration struct.
///
/// # Arguments
///
/// * `source` - Source code as string slice
/// * `language` - Programming language
/// * `config` - Full transformation configuration
///
/// # Examples
///
/// ```no_run
/// use rskim_core::{transform_with_config, Language, Mode, TransformConfig};
///
/// let config = TransformConfig::with_mode(Mode::Signatures)
///     .preserve_comments(false);
///
/// let result = transform_with_config("fn main() {}", Language::Rust, &config)?;
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn transform_with_config(
    source: &str,
    language: Language,
    config: &TransformConfig,
) -> Result<String> {
    // ARCHITECTURE: Language encapsulates parsing strategy (tree-sitter vs serde_json)
    // This eliminates special-case conditionals - each language handles its own parsing
    let (content, _has_errors) = language.transform_source(source, config)?;
    Ok(content)
}

/// Transform source code and return both content and parse quality flag
///
/// Like `transform_with_config` but also returns whether the parser encountered
/// syntax errors. Callers can use this to determine the `parse_tier`:
/// - `Mode::Full` → "passthrough" (no transformation applied)
/// - `has_errors == true` → "degraded" (syntax errors present)
/// - `has_errors == false` → "full" (clean parse)
///
/// # Examples
///
/// ```no_run
/// use rskim_core::{transform_with_quality, Language, Mode, TransformConfig};
///
/// let config = TransformConfig::with_mode(Mode::Structure);
/// let (content, has_errors) = transform_with_quality("fn main() {}", Language::Rust, &config)?;
/// assert!(!has_errors);
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn transform_with_quality(
    source: &str,
    language: Language,
    config: &TransformConfig,
) -> Result<(String, bool)> {
    language.transform_source(source, config)
}

/// Transform source code with automatic language detection from file path
///
/// Convenience function that detects language from file extension.
///
/// # Arguments
///
/// * `source` - Source code as string slice
/// * `path` - File path for language detection (NOT read from disk)
/// * `mode` - Transformation mode
///
/// # Errors
///
/// - `SkimError::UnsupportedLanguage` - Could not detect language from path
/// - All errors from `transform()`
///
/// # Examples
///
/// ```no_run
/// use rskim_core::{transform_auto, Mode};
/// use std::path::Path;
///
/// let source = "def hello(): pass";
/// let path = Path::new("script.py");
/// let result = transform_auto(source, path, Mode::Structure)?;
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn transform_auto(source: &str, path: &std::path::Path, mode: Mode) -> Result<String> {
    let language = Language::from_path(path)
        .ok_or_else(|| SkimError::UnsupportedLanguage(path.to_path_buf()))?;

    transform(source, language, mode)
}

/// Transform source code with automatic language detection and custom configuration
///
/// Convenience function that detects language from file extension and applies
/// the provided configuration. Useful for applying max_lines truncation with
/// auto-detected language.
///
/// # Arguments
///
/// * `source` - Source code as string slice
/// * `path` - File path for language detection (NOT read from disk)
/// * `config` - Full transformation configuration
///
/// # Errors
///
/// - `SkimError::UnsupportedLanguage` - Could not detect language from path
/// - All errors from `transform_with_config()`
///
/// # Examples
///
/// ```no_run
/// use rskim_core::{transform_auto_with_config, Mode, TransformConfig};
/// use std::path::Path;
///
/// let config = TransformConfig::with_mode(Mode::Structure)
///     .with_max_lines(50);
///
/// let source = "def hello(): pass";
/// let path = Path::new("script.py");
/// let result = transform_auto_with_config(source, path, &config)?;
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn transform_auto_with_config(
    source: &str,
    path: &std::path::Path,
    config: &TransformConfig,
) -> Result<String> {
    let language = Language::from_path(path)
        .ok_or_else(|| SkimError::UnsupportedLanguage(path.to_path_buf()))?;

    transform_with_config(source, language, config)
}

/// Transform source code with full result metadata
///
/// Returns `TransformResult` with optional token counts and timing.
/// Useful for benchmarking and analysis.
///
/// # Phase 3 Feature
///
/// Token counting requires `token-counting` feature flag.
///
/// # Examples
///
/// ```no_run
/// use rskim_core::{transform_detailed, Language, Mode};
///
/// let result = transform_detailed("code", Language::Python, Mode::Structure)?;
///
/// println!("Transformed: {}", result.content);
/// if let Some(reduction) = result.reduction_percentage() {
///     println!("Token reduction: {:.1}%", reduction);
/// }
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn transform_detailed(source: &str, language: Language, mode: Mode) -> Result<TransformResult> {
    let start = std::time::Instant::now();

    let content = transform(source, language, mode)?;

    let duration_ms = start.elapsed().as_millis() as u64;

    Ok(TransformResult {
        content,
        original_tokens: None, // Token counting is performed at the CLI layer (see rskim/src/tokens.rs)
        transformed_tokens: None, // Token counting is performed at the CLI layer (see rskim/src/tokens.rs)
        duration_ms: Some(duration_ms),
    })
}

// ============================================================================
// Token Budget Truncation
// ============================================================================

/// Truncate transformed output to fit within a token budget
///
/// Uses binary search to find the maximum number of lines that fit
/// within the budget, then appends a language-appropriate omission marker.
/// If the text already fits, it is returned unchanged.
///
/// # Arguments
/// * `text` - Previously transformed output to truncate
/// * `language` - Language for comment syntax in omission markers
/// * `token_budget` - Maximum number of tokens allowed
/// * `count_tokens` - Closure that counts tokens in a string slice
/// * `known_token_count` - Pre-computed token count of `text`, if available.
///   When `Some(count)`, skips the initial full-text tokenization.
///   Pass `None` when the count is unknown.
///
/// # Returns
/// Text fitting within the token budget, with omission marker if truncated.
/// If `token_budget` is 0 or smaller than the omission marker itself (~5-7
/// tokens), an empty string is returned rather than violating the budget
/// invariant. Callers should validate the budget upstream or handle the
/// empty-string edge case.
///
/// # Examples
///
/// ```
/// use rskim_core::{truncate_to_token_budget, Language};
///
/// let output = "line 1\nline 2\nline 3\nline 4\nline 5";
/// let word_count = |s: &str| -> usize { s.split_whitespace().count() };
/// let truncated = truncate_to_token_budget(output, Language::TypeScript, 5, word_count, None)?;
/// # Ok::<(), rskim_core::SkimError>(())
/// ```
pub fn truncate_to_token_budget<F>(
    text: &str,
    language: Language,
    token_budget: usize,
    count_tokens: F,
    known_token_count: Option<usize>,
) -> Result<String>
where
    F: Fn(&str) -> usize,
{
    transform::truncate::truncate_to_token_budget(
        text,
        language,
        token_budget,
        count_tokens,
        known_token_count,
    )
}

// ============================================================================
// Language Detection Utilities
// ============================================================================

/// Detect language from file extension
///
/// # Examples
///
/// ```
/// use rskim_core::{detect_language, Language};
///
/// assert_eq!(detect_language("ts"), Some(Language::TypeScript));
/// assert_eq!(detect_language("py"), Some(Language::Python));
/// assert_eq!(detect_language("unknown"), None);
/// ```
pub fn detect_language(extension: &str) -> Option<Language> {
    Language::from_extension(extension)
}

/// Detect language from file path
///
/// # Examples
///
/// ```
/// use rskim_core::{detect_language_from_path, Language};
/// use std::path::Path;
///
/// let path = Path::new("src/main.rs");
/// assert_eq!(detect_language_from_path(path), Some(Language::Rust));
/// ```
pub fn detect_language_from_path(path: &std::path::Path) -> Option<Language> {
    Language::from_path(path)
}

// ============================================================================
// Version Information
// ============================================================================

/// Get library version
pub fn version() -> &'static str {
    env!("CARGO_PKG_VERSION")
}

/// Get list of supported languages
pub fn supported_languages() -> &'static [Language] {
    &[
        Language::TypeScript,
        Language::JavaScript,
        Language::Python,
        Language::Rust,
        Language::Go,
        Language::Java,
        Language::Markdown,
        Language::Json,
        Language::Yaml,
        Language::C,
        Language::Cpp,
        Language::Toml,
        Language::CSharp,
        Language::Ruby,
        Language::Sql,
        Language::Kotlin,
        Language::Swift,
    ]
}

// ============================================================================
// Module Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_version() {
        assert!(!version().is_empty());
    }

    #[test]
    fn test_supported_languages() {
        assert_eq!(supported_languages().len(), 17);
        assert!(supported_languages().contains(&Language::Markdown));
        assert!(supported_languages().contains(&Language::Json));
        assert!(supported_languages().contains(&Language::Yaml));
    }

    #[test]
    fn test_detect_language() {
        assert_eq!(detect_language("ts"), Some(Language::TypeScript));
        assert_eq!(detect_language("unknown"), None);
    }

    // ========================================================================
    // transform_with_quality tests (B3)
    // ========================================================================

    #[test]
    fn test_transform_source_has_errors_false() {
        // Valid TypeScript source should parse without errors
        let source = "function add(a: number, b: number): number { return a + b; }";
        let config = TransformConfig::with_mode(Mode::Structure);
        let (content, has_errors) = Language::TypeScript
            .transform_source(source, &config)
            .expect("valid TypeScript should transform without failure");
        assert!(
            !has_errors,
            "valid TypeScript source should have has_errors=false"
        );
        assert!(
            content.contains("function add"),
            "output should preserve signature"
        );
    }

    #[test]
    fn test_transform_source_has_errors_true() {
        // Broken Rust syntax should trigger has_errors=true
        let source = "fn broken {{ this is not valid rust";
        let config = TransformConfig::with_mode(Mode::Structure);
        let (_, has_errors) = Language::Rust
            .transform_source(source, &config)
            .expect("tree-sitter is error-tolerant and should not fail outright");
        assert!(has_errors, "broken Rust syntax should have has_errors=true");
    }

    #[test]
    fn test_transform_with_quality_valid_source() {
        let source = "function greet(name: string): void { console.log(name); }";
        let config = TransformConfig::with_mode(Mode::Structure);
        let (content, has_errors) = transform_with_quality(source, Language::TypeScript, &config)
            .expect("transform_with_quality should succeed for valid TypeScript");
        assert!(!has_errors, "valid source should have has_errors=false");
        assert!(
            content.contains("function greet"),
            "output should preserve signature"
        );
    }

    #[test]
    fn test_transform_with_quality_broken_source() {
        let source = "fn broken {{ this is not valid rust";
        let config = TransformConfig::with_mode(Mode::Structure);
        let (_content, has_errors) = transform_with_quality(source, Language::Rust, &config)
            .expect("transform_with_quality should not fail outright on broken source");
        assert!(has_errors, "broken syntax should have has_errors=true");
    }

    #[test]
    fn test_transform_with_quality_json_no_errors() {
        // JSON uses serde parser — always reports no parse errors on success
        let source = r#"{"key": "value", "n": 42}"#;
        let config = TransformConfig::with_mode(Mode::Structure);
        let (_content, has_errors) = transform_with_quality(source, Language::Json, &config)
            .expect("valid JSON should transform without failure");
        assert!(
            !has_errors,
            "serde-based JSON should always report has_errors=false"
        );
    }

    #[test]
    fn test_transform_with_quality_full_mode_no_errors() {
        // Full mode is passthrough for all languages — always no errors
        let source = "fn broken {{ this is not valid rust";
        let config = TransformConfig::with_mode(Mode::Full);
        let (content, has_errors) = transform_with_quality(source, Language::Rust, &config)
            .expect("Full mode passthrough should always succeed");
        assert!(
            !has_errors,
            "Full mode (passthrough) should always report has_errors=false"
        );
        assert_eq!(content, source, "Full mode should return source unchanged");
    }
}