frigg 0.3.2

Local-first MCP server for code understanding.
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
500
501
502
503
504
505
506
507
use std::collections::BTreeMap;

use super::{MetadataObject, ReadPresentationMode, ResponseMode};
use crate::domain::{
    ChannelHealthStatus, EvidenceAnchor, PathClass, SourceClass, model::SymbolMatch,
    model::TextMatch,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SearchPatternType {
    Literal,
    Regex,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExploreOperation {
    Probe,
    Zoom,
    Refine,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreAnchor {
    pub start_line: usize,
    pub start_column: usize,
    pub end_line: usize,
    pub end_column: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreCursor {
    pub line: usize,
    pub column: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreLineWindow {
    pub start_line: usize,
    pub end_line: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreWindow {
    pub start_line: usize,
    pub end_line: usize,
    pub bytes: usize,
    pub content: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreMatch {
    pub match_id: String,
    pub start_line: usize,
    pub start_column: usize,
    pub end_line: usize,
    pub end_column: usize,
    pub excerpt: String,
    pub window: ExploreWindow,
    pub anchor: ExploreAnchor,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreMetadata {
    pub lossy_utf8: bool,
    pub effective_context_lines: usize,
    pub effective_max_matches: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreParams {
    /// Artifact path using the same canonical repository-relative semantics as `read_file`.
    pub path: String,
    /// Optional repository scope from `list_repositories`.
    pub repository_id: Option<String>,
    /// Explorer mode: `probe` scans an artifact, `zoom` returns a bounded window, and `refine` searches only inside an anchor-derived window.
    pub operation: ExploreOperation,
    /// Search query for `probe` or `refine`. Leading and trailing whitespace is trimmed.
    pub query: Option<String>,
    /// Match mode for `query`. Omit for exact literal search or set `regex` for safe-regex search.
    pub pattern_type: Option<SearchPatternType>,
    /// Explicit anchor used by `zoom` and `refine`.
    pub anchor: Option<ExploreAnchor>,
    /// Context lines to include around anchors and match windows. Omit to use the explorer default.
    pub context_lines: Option<usize>,
    /// Max match rows to return. Omit to use the explorer default.
    pub max_matches: Option<usize>,
    /// Explicit continuation cursor for `probe` or `refine`.
    pub resume_from: Option<ExploreCursor>,
    /// Read-surface presentation mode. Defaults to `text` for `zoom` and `json` for `probe`/`refine`.
    pub presentation_mode: Option<ReadPresentationMode>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ExploreResponse {
    pub repository_id: String,
    pub path: String,
    pub operation: ExploreOperation,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub query: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pattern_type: Option<SearchPatternType>,
    pub total_lines: usize,
    pub scan_scope: ExploreLineWindow,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub window: Option<ExploreWindow>,
    pub total_matches: usize,
    pub matches: Vec<ExploreMatch>,
    pub truncated: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resume_from: Option<ExploreCursor>,
    pub metadata: ExploreMetadata,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct SearchTextParams {
    /// Text or regex pattern to search for. Leading and trailing whitespace is trimmed.
    pub query: String,
    /// Match mode for `query`. Omit for exact literal search or set `regex` for safe-regex search.
    pub pattern_type: Option<SearchPatternType>,
    /// Optional repository scope from `list_repositories`.
    pub repository_id: Option<String>,
    /// Optional safe regex over canonical repository-relative paths.
    /// Use this to narrow code, docs, or runtime slices.
    pub path_regex: Option<String>,
    /// Optional max matches. Frigg clamps the effective limit to the server search budget.
    pub limit: Option<usize>,
    /// Optional inline excerpt window expansion for simple review flows. Omit to keep one-line excerpts.
    pub context_lines: Option<usize>,
    /// Optional bound on returned hits per file after lexical matching.
    pub max_matches_per_file: Option<usize>,
    /// Optional repeated-path collapse mode for noisy lexical result sets.
    pub collapse_by_file: Option<bool>,
    /// Response detail profile. Omit to default to `compact`.
    pub response_mode: Option<ResponseMode>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchTextResponse {
    pub total_matches: usize,
    pub matches: Vec<TextMatch>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result_handle: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<SearchTextMetadata>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SearchLexicalBackendMetadata {
    Native,
    Ripgrep,
    Mixed,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchTextMetadata {
    pub lexical_backend: SearchLexicalBackendMetadata,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lexical_backend_note: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridChannelWeightsParams {
    pub lexical: Option<f32>,
    pub graph: Option<f32>,
    pub semantic: Option<f32>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridParams {
    /// Broad natural-language or exact-phrase repository query.
    pub query: String,
    /// Optional repository scope.
    pub repository_id: Option<String>,
    /// Optional language filter for source-backed follow-up.
    pub language: Option<String>,
    /// Optional max matches.
    pub limit: Option<usize>,
    /// Optional channel-weight overrides.
    pub weights: Option<SearchHybridChannelWeightsParams>,
    /// Optional semantic-channel toggle.
    pub semantic: Option<bool>,
    /// Response detail profile. Omit to default to `compact`.
    pub response_mode: Option<ResponseMode>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridMatch {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub match_id: Option<String>,
    pub repository_id: String,
    pub path: String,
    pub line: usize,
    pub column: usize,
    pub excerpt: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub anchor: Option<EvidenceAnchor>,
    pub blended_score: f32,
    pub lexical_score: f32,
    pub graph_score: f32,
    pub semantic_score: f32,
    pub lexical_sources: Vec<String>,
    pub graph_sources: Vec<String>,
    pub semantic_sources: Vec<String>,
    /// Generic path-class hint for choosing a first navigation pivot.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path_class: Option<PathClass>,
    /// Generic source-class hint from shared runtime/support/project classification.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_class: Option<SourceClass>,
    /// Generic surface-family hints such as `runtime`, `tests`, or `entrypoint`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub surface_families: Vec<String>,
    /// Live-navigation hint describing whether this match is a good follow-up pivot.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub navigation_hint: Option<SearchHybridNavigationHint>,
    /// Concise explanation of the strongest signals that lifted this match.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub rank_reasons: Vec<SearchHybridRankReason>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SearchHybridRankReason {
    ExactSymbolMatch,
    ExactTextMatch,
    StrongLexicalAnchor,
    GraphAdjacency,
    SemanticContribution,
    WitnessOnlyFallback,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridNavigationHint {
    /// True when the match is a reasonable first pivot for `read_file` or symbol follow-up.
    pub pivotable: bool,
    /// True when `document_symbols` is expected to be useful on this path.
    pub document_symbols: bool,
    /// True when symbol/anchor follow-up is likely to support `go_to_definition`.
    pub go_to_definition: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridUtilitySummary {
    /// Count of returned matches that look like useful live-navigation pivots.
    pub pivotable_match_count: usize,
    /// One-based rank of the best generic pivot inside the returned result set.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub best_pivot_rank: Option<usize>,
    /// Canonical path of the best generic pivot inside the returned result set.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub best_pivot_path: Option<String>,
    /// Repository id for the best generic pivot when cross-repository search is used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub best_pivot_repository_id: Option<String>,
    /// True when the returned set contains at least one pivot that likely supports symbol follow-up.
    pub symbol_navigation_ready: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridChannelDiagnostic {
    pub code: String,
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridChannelMetadata {
    pub status: ChannelHealthStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    pub candidate_count: usize,
    pub hit_count: usize,
    pub match_count: usize,
    pub diagnostic_count: usize,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub diagnostics: Vec<SearchHybridChannelDiagnostic>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridDiagnosticsSummary {
    pub walk: usize,
    pub read: usize,
    pub total: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridStageSample {
    pub elapsed_us: u64,
    pub input_count: usize,
    pub output_count: usize,
}

impl From<&crate::searcher::SearchStageSample> for SearchHybridStageSample {
    fn from(value: &crate::searcher::SearchStageSample) -> Self {
        Self {
            elapsed_us: value.elapsed_us,
            input_count: value.input_count,
            output_count: value.output_count,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridStageAttribution {
    pub candidate_intake: SearchHybridStageSample,
    pub freshness_validation: SearchHybridStageSample,
    pub scan: SearchHybridStageSample,
    pub witness_scoring: SearchHybridStageSample,
    pub graph_expansion: SearchHybridStageSample,
    pub semantic_retrieval: SearchHybridStageSample,
    pub anchor_blending: SearchHybridStageSample,
    pub document_aggregation: SearchHybridStageSample,
    pub final_diversification: SearchHybridStageSample,
}

impl From<&crate::searcher::SearchStageAttribution> for SearchHybridStageAttribution {
    fn from(value: &crate::searcher::SearchStageAttribution) -> Self {
        Self {
            candidate_intake: SearchHybridStageSample::from(&value.candidate_intake),
            freshness_validation: SearchHybridStageSample::from(&value.freshness_validation),
            scan: SearchHybridStageSample::from(&value.scan),
            witness_scoring: SearchHybridStageSample::from(&value.witness_scoring),
            graph_expansion: SearchHybridStageSample::from(&value.graph_expansion),
            semantic_retrieval: SearchHybridStageSample::from(&value.semantic_retrieval),
            anchor_blending: SearchHybridStageSample::from(&value.anchor_blending),
            document_aggregation: SearchHybridStageSample::from(&value.document_aggregation),
            final_diversification: SearchHybridStageSample::from(&value.final_diversification),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ResponseFreshnessRepositoryMetadata {
    pub repository_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub snapshot_id: Option<String>,
    pub manifest: String,
    pub semantic: String,
    pub dirty_root: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ResponseFreshnessBasisMetadata {
    pub mode: String,
    pub cacheable: bool,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub repositories: Vec<ResponseFreshnessRepositoryMetadata>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridSemanticAcceleratorMetadata {
    pub tier: String,
    pub state: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<ChannelHealthStatus>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridLanguageCapabilityMetadata {
    pub requested_language: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
    pub semantic_chunking: String,
    pub semantic_accelerator: SearchHybridSemanticAcceleratorMetadata,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub capabilities: BTreeMap<String, String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SearchHybridQueryShape {
    BroadNaturalLanguage,
    CodeShaped,
    Neutral,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridExactPivotAssistance {
    pub applied: bool,
    pub exact_symbol_hit_count: usize,
    pub exact_text_hit_count: usize,
    pub boosted_match_count: usize,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridMetadata {
    pub channels: BTreeMap<String, SearchHybridChannelMetadata>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lexical_backend: Option<SearchLexicalBackendMetadata>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lexical_backend_note: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_requested: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_status: Option<ChannelHealthStatus>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_candidate_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_hit_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_match_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lexical_only_mode: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub query_shape: Option<SearchHybridQueryShape>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub warning: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exact_pivot_assistance: Option<SearchHybridExactPivotAssistance>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub witness_demotion_applied: Option<bool>,
    pub diagnostics_count: usize,
    pub diagnostics: SearchHybridDiagnosticsSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stage_attribution: Option<SearchHybridStageAttribution>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_capability: Option<SearchHybridLanguageCapabilityMetadata>,
    /// Utility summary for discovery-to-navigation workflows.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub utility: Option<SearchHybridUtilitySummary>,
    pub freshness_basis: ResponseFreshnessBasisMetadata,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchHybridResponse {
    pub matches: Vec<SearchHybridMatch>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result_handle: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_requested: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_status: Option<ChannelHealthStatus>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_reason: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_hit_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub semantic_match_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub warning: Option<String>,
    /// Structured diagnostics payload for live responses.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<SearchHybridMetadata>,
    /// Human-readable summary note.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct SearchSymbolParams {
    /// API, type, or function name to search in indexed symbols.
    pub query: String,
    /// Optional repository scope.
    pub repository_id: Option<String>,
    /// Optional path class filter: `runtime`, `support`, or `project`.
    pub path_class: Option<SearchSymbolPathClass>,
    /// Optional safe regex over canonical repository-relative symbol paths.
    pub path_regex: Option<String>,
    /// Optional max matches.
    pub limit: Option<usize>,
    /// Response detail profile. Omit to default to `compact`.
    pub response_mode: Option<ResponseMode>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchSymbolResponse {
    pub matches: Vec<SymbolMatch>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result_handle: Option<String>,
    #[schemars(schema_with = "super::metadata_object_field_schema")]
    pub metadata: Option<MetadataObject>,
    pub note: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SearchSymbolPathClass {
    Runtime,
    Project,
    Support,
}

impl SearchSymbolPathClass {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Runtime => "runtime",
            Self::Project => "project",
            Self::Support => "support",
        }
    }
}