1use std::collections::HashSet;
11use std::path::PathBuf;
12use std::sync::{Arc, Mutex};
13use std::time::Instant;
14
15use harn_vm::VmValue;
16
17use super::agents::AgentId;
18use super::file_table::{fnv1a64, FileId};
19use super::imports;
20use super::state::{now_unix_ms, IndexState};
21use super::trigram;
22use super::versions::EditOp;
23use crate::error::HostlibError;
24use crate::tools::args::{
25 build_dict, dict_arg, optional_bool, optional_int_list, optional_string, optional_string_list,
26 require_string, str_value,
27};
28use crate::value_args;
29
30pub type SharedIndex = Arc<Mutex<Option<IndexState>>>;
37
38pub(super) const BUILTIN_QUERY: &str = "hostlib_code_index_query";
45pub(super) const BUILTIN_REBUILD: &str = "hostlib_code_index_rebuild";
46pub(super) const BUILTIN_STATS: &str = "hostlib_code_index_stats";
47pub(super) const BUILTIN_IMPORTS_FOR: &str = "hostlib_code_index_imports_for";
48pub(super) const BUILTIN_IMPORTERS_OF: &str = "hostlib_code_index_importers_of";
49
50pub(super) const BUILTIN_PATH_TO_ID: &str = "hostlib_code_index_path_to_id";
51pub(super) const BUILTIN_ID_TO_PATH: &str = "hostlib_code_index_id_to_path";
52pub(super) const BUILTIN_FILE_IDS: &str = "hostlib_code_index_file_ids";
53pub(super) const BUILTIN_FILE_META: &str = "hostlib_code_index_file_meta";
54pub(super) const BUILTIN_FILE_HASH: &str = "hostlib_code_index_file_hash";
55
56pub(super) const BUILTIN_READ_RANGE: &str = "hostlib_code_index_read_range";
57pub(super) const BUILTIN_REINDEX_FILE: &str = "hostlib_code_index_reindex_file";
58pub(super) const BUILTIN_TRIGRAM_QUERY: &str = "hostlib_code_index_trigram_query";
59pub(super) const BUILTIN_EXTRACT_TRIGRAMS: &str = "hostlib_code_index_extract_trigrams";
60pub(super) const BUILTIN_WORD_GET: &str = "hostlib_code_index_word_get";
61pub(super) const BUILTIN_DEPS_GET: &str = "hostlib_code_index_deps_get";
62pub(super) const BUILTIN_OUTLINE_GET: &str = "hostlib_code_index_outline_get";
63
64pub(super) const BUILTIN_CURRENT_SEQ: &str = "hostlib_code_index_current_seq";
65pub(super) const BUILTIN_CHANGES_SINCE: &str = "hostlib_code_index_changes_since";
66pub(super) const BUILTIN_VERSION_RECORD: &str = "hostlib_code_index_version_record";
67
68pub(super) const BUILTIN_AGENT_REGISTER: &str = "hostlib_code_index_agent_register";
69pub(super) const BUILTIN_AGENT_HEARTBEAT: &str = "hostlib_code_index_agent_heartbeat";
70pub(super) const BUILTIN_AGENT_UNREGISTER: &str = "hostlib_code_index_agent_unregister";
71pub(super) const BUILTIN_LOCK_TRY: &str = "hostlib_code_index_lock_try";
72pub(super) const BUILTIN_LOCK_RELEASE: &str = "hostlib_code_index_lock_release";
73pub(super) const BUILTIN_STATUS: &str = "hostlib_code_index_status";
74pub(super) const BUILTIN_CURRENT_AGENT_ID: &str = "hostlib_code_index_current_agent_id";
75
76pub(super) const BUILTIN_CYPHER: &str = "hostlib_code_index_cypher";
77pub(super) const BUILTIN_BRANCH_OVERLAY: &str = "hostlib_code_index_branch_overlay";
78pub(super) const BUILTIN_FRESHNESS: &str = "hostlib_code_index_freshness";
79
80pub(super) fn run_query_merged(
88 index: &SharedIndex,
89 readonly: Option<&super::readonly::ReadonlyRoots>,
90 args: &[VmValue],
91) -> Result<VmValue, HostlibError> {
92 let raw = dict_arg(BUILTIN_QUERY, args)?;
93 let dict = raw.as_ref();
94 let needle = require_string(BUILTIN_QUERY, dict, "needle")?;
95 if needle.is_empty() {
96 return Err(HostlibError::InvalidParameter {
97 builtin: BUILTIN_QUERY,
98 param: "needle",
99 message: "must not be empty".to_string(),
100 });
101 }
102 let case_sensitive = optional_bool(BUILTIN_QUERY, dict, "case_sensitive", false)?;
103 let max_results = optional_positive_usize(BUILTIN_QUERY, dict, "max_results")?.unwrap_or(100);
104 let scope = optional_string_list(BUILTIN_QUERY, dict, "scope")?;
105
106 let mut hits: Vec<Hit> = Vec::new();
107 {
108 let guard = index.lock().expect("code_index mutex poisoned");
109 if let Some(state) = guard.as_ref() {
110 collect_hits_scoped(state, &needle, case_sensitive, &scope, &mut hits);
111 }
112 }
113 if let Some(readonly) = readonly {
114 if scope.is_empty() {
117 hits.extend(super::readonly::query_readonly_hits(
118 readonly,
119 &needle,
120 case_sensitive,
121 ));
122 }
123 }
124
125 hits.sort_by(|a, b| {
126 b.match_count
127 .cmp(&a.match_count)
128 .then_with(|| a.path.cmp(&b.path))
129 });
130 let truncated = hits.len() > max_results;
131 if truncated {
132 hits.truncate(max_results);
133 }
134 Ok(build_dict([
135 (
136 "results",
137 VmValue::List(Arc::new(hits.into_iter().map(hit_to_value).collect())),
138 ),
139 ("truncated", VmValue::Bool(truncated)),
140 ]))
141}
142
143fn collect_hits_scoped(
148 state: &IndexState,
149 needle: &str,
150 case_sensitive: bool,
151 scope: &[String],
152 hits: &mut Vec<Hit>,
153) {
154 let candidate_ids = candidates_for(state, needle);
155 for id in candidate_ids {
156 let Some(file) = state.files.get(&id) else {
157 continue;
158 };
159 if !scope_allows(scope, &file.relative_path) {
160 continue;
161 }
162 let Some(text) = read_file_text(&state.root, &file.relative_path) else {
163 continue;
164 };
165 let count = count_matches(&text, needle, case_sensitive);
166 if count == 0 {
167 continue;
168 }
169 hits.push(Hit {
170 path: file.relative_path.clone(),
171 match_count: count,
172 root: None,
173 });
174 }
175}
176
177pub(super) fn collect_hits_into(
181 state: &IndexState,
182 needle: &str,
183 case_sensitive: bool,
184 hits: &mut Vec<Hit>,
185) {
186 let before = hits.len();
187 collect_hits_scoped(state, needle, case_sensitive, &[], hits);
188 let root = state.root.to_string_lossy().to_string();
189 for hit in &mut hits[before..] {
190 hit.root = Some(root.clone());
191 }
192}
193
194pub(super) fn run_rebuild(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
195 let raw = dict_arg(BUILTIN_REBUILD, args)?;
196 let dict = raw.as_ref();
197 let _force = optional_bool(BUILTIN_REBUILD, dict, "force", false)?;
198 let root = optional_string(BUILTIN_REBUILD, dict, "root")?
199 .map(PathBuf::from)
200 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
201 if !root.exists() {
202 return Err(HostlibError::InvalidParameter {
203 builtin: BUILTIN_REBUILD,
204 param: "root",
205 message: format!("path `{}` does not exist", root.display()),
206 });
207 }
208 if !root.is_dir() {
209 return Err(HostlibError::InvalidParameter {
210 builtin: BUILTIN_REBUILD,
211 param: "root",
212 message: format!("path `{}` is not a directory", root.display()),
213 });
214 }
215 let started = Instant::now();
216 let (state, outcome) = IndexState::build_from_root(&root);
217 let elapsed_ms = started.elapsed().as_millis() as i64;
218 {
219 let mut guard = index.lock().expect("code_index mutex poisoned");
220 *guard = Some(state);
221 }
222 Ok(build_dict([
223 ("files_indexed", VmValue::Int(outcome.files_indexed as i64)),
224 ("files_skipped", VmValue::Int(outcome.files_skipped as i64)),
225 ("elapsed_ms", VmValue::Int(elapsed_ms)),
226 ]))
227}
228
229pub(super) fn run_stats(index: &SharedIndex, _args: &[VmValue]) -> Result<VmValue, HostlibError> {
230 let guard = index.lock().expect("code_index mutex poisoned");
231 let Some(state) = guard.as_ref() else {
232 return Ok(empty_stats_response());
233 };
234 Ok(build_dict([
235 ("indexed_files", VmValue::Int(state.files.len() as i64)),
236 (
237 "trigrams",
238 VmValue::Int(state.trigrams.distinct_trigrams() as i64),
239 ),
240 ("words", VmValue::Int(state.words.distinct_words() as i64)),
241 ("memory_bytes", VmValue::Int(state.estimated_bytes() as i64)),
242 (
243 "last_rebuild_unix_ms",
244 VmValue::Int(state.last_built_unix_ms),
245 ),
246 ]))
247}
248
249pub(super) fn run_imports_for(
250 index: &SharedIndex,
251 args: &[VmValue],
252) -> Result<VmValue, HostlibError> {
253 let raw = dict_arg(BUILTIN_IMPORTS_FOR, args)?;
254 let dict = raw.as_ref();
255 let path = require_string(BUILTIN_IMPORTS_FOR, dict, "path")?;
256 let guard = index.lock().expect("code_index mutex poisoned");
257 let Some(state) = guard.as_ref() else {
258 return Ok(empty_imports_response(&path));
259 };
260 let Some(file_id) = state.lookup_path(&path) else {
261 return Ok(empty_imports_response(&path));
262 };
263 let Some(file) = state.files.get(&file_id) else {
264 return Ok(empty_imports_response(&path));
265 };
266 let kind = imports::import_kind(&file.language).to_string();
267 let base_dir = imports::parent_dir(&file.relative_path);
268 let resolved_ids: HashSet<FileId> = state.deps.imports_of(file_id).into_iter().collect();
269 let mut entries: Vec<VmValue> = Vec::with_capacity(file.imports.len());
270 for raw_import in &file.imports {
271 let resolved_path =
272 imports::resolve_module(raw_import, &file.language, &base_dir, &state.path_to_id)
273 .filter(|id| resolved_ids.contains(id))
274 .and_then(|id| state.files.get(&id).map(|f| f.relative_path.clone()));
275 entries.push(import_entry(raw_import, resolved_path.as_deref(), &kind));
276 }
277 Ok(build_dict([
278 ("path", str_value(&file.relative_path)),
279 ("imports", VmValue::List(Arc::new(entries))),
280 ]))
281}
282
283pub(super) fn run_importers_of(
284 index: &SharedIndex,
285 args: &[VmValue],
286) -> Result<VmValue, HostlibError> {
287 let raw = dict_arg(BUILTIN_IMPORTERS_OF, args)?;
288 let dict = raw.as_ref();
289 let module = require_string(BUILTIN_IMPORTERS_OF, dict, "module")?;
290 let guard = index.lock().expect("code_index mutex poisoned");
291 let Some(state) = guard.as_ref() else {
292 return Ok(empty_importers_response(&module));
293 };
294
295 let target_id = state.lookup_path(&module).or_else(|| {
296 let needle = format!("/{module}");
300 state
301 .path_to_id
302 .iter()
303 .find(|(p, _)| p.ends_with(&needle) || *p == &module)
304 .map(|(_, id)| *id)
305 });
306
307 let mut importers: Vec<String> = match target_id {
308 Some(id) => state
309 .deps
310 .importers_of(id)
311 .into_iter()
312 .filter_map(|importer_id| {
313 state
314 .files
315 .get(&importer_id)
316 .map(|f| f.relative_path.clone())
317 })
318 .collect(),
319 None => Vec::new(),
320 };
321 importers.sort();
322 Ok(build_dict([
323 ("module", str_value(&module)),
324 (
325 "importers",
326 VmValue::List(Arc::new(importers.into_iter().map(str_value).collect())),
327 ),
328 ]))
329}
330
331pub(super) fn run_path_to_id(
334 index: &SharedIndex,
335 args: &[VmValue],
336) -> Result<VmValue, HostlibError> {
337 let raw = dict_arg(BUILTIN_PATH_TO_ID, args)?;
338 let path = require_string(BUILTIN_PATH_TO_ID, raw.as_ref(), "path")?;
339 let guard = index.lock().expect("code_index mutex poisoned");
340 let id = guard.as_ref().and_then(|s| s.lookup_path(&path));
341 Ok(match id {
342 Some(id) => VmValue::Int(id as i64),
343 None => VmValue::Nil,
344 })
345}
346
347pub(super) fn run_id_to_path(
348 index: &SharedIndex,
349 args: &[VmValue],
350) -> Result<VmValue, HostlibError> {
351 let raw = dict_arg(BUILTIN_ID_TO_PATH, args)?;
352 let id = require_positive_file_id(BUILTIN_ID_TO_PATH, raw.as_ref(), "file_id")?;
353 let guard = index.lock().expect("code_index mutex poisoned");
354 let path = guard
355 .as_ref()
356 .and_then(|s| s.files.get(&id))
357 .map(|f| f.relative_path.clone());
358 Ok(match path {
359 Some(p) => str_value(&p),
360 None => VmValue::Nil,
361 })
362}
363
364pub(super) fn run_file_ids(
365 index: &SharedIndex,
366 _args: &[VmValue],
367) -> Result<VmValue, HostlibError> {
368 let guard = index.lock().expect("code_index mutex poisoned");
369 let mut ids: Vec<FileId> = guard
370 .as_ref()
371 .map(|s| s.files.keys().copied().collect())
372 .unwrap_or_default();
373 ids.sort_unstable();
374 Ok(VmValue::List(Arc::new(
375 ids.into_iter().map(|id| VmValue::Int(id as i64)).collect(),
376 )))
377}
378
379pub(super) fn run_file_meta(
380 index: &SharedIndex,
381 args: &[VmValue],
382) -> Result<VmValue, HostlibError> {
383 let raw = dict_arg(BUILTIN_FILE_META, args)?;
384 let dict = raw.as_ref();
385 let guard = index.lock().expect("code_index mutex poisoned");
386 let Some(state) = guard.as_ref() else {
387 return Ok(VmValue::Nil);
388 };
389 let id_opt: Option<FileId> = if dict.contains_key("file_id") {
390 Some(require_positive_file_id(
391 BUILTIN_FILE_META,
392 dict,
393 "file_id",
394 )?)
395 } else if let Some(VmValue::String(p)) = dict.get("path") {
396 state.lookup_path(p)
397 } else {
398 return Err(HostlibError::MissingParameter {
399 builtin: BUILTIN_FILE_META,
400 param: "file_id|path",
401 });
402 };
403 let Some(id) = id_opt else {
404 return Ok(VmValue::Nil);
405 };
406 let Some(file) = state.files.get(&id) else {
407 return Ok(VmValue::Nil);
408 };
409 let last_edit_seq = state
410 .versions
411 .last_entry(&file.relative_path)
412 .map(|e| e.seq)
413 .unwrap_or(0);
414 Ok(build_dict([
415 ("id", VmValue::Int(file.id as i64)),
416 ("path", str_value(&file.relative_path)),
417 ("language", str_value(&file.language)),
418 ("size", VmValue::Int(file.size_bytes as i64)),
419 ("line_count", VmValue::Int(file.line_count as i64)),
420 ("hash", str_value(file.content_hash.to_string())),
421 ("mtime_ms", VmValue::Int(file.mtime_ms)),
422 ("last_edit_seq", VmValue::Int(last_edit_seq as i64)),
423 ]))
424}
425
426pub(super) fn run_file_hash(
427 index: &SharedIndex,
428 args: &[VmValue],
429) -> Result<VmValue, HostlibError> {
430 let raw = dict_arg(BUILTIN_FILE_HASH, args)?;
431 let path = require_string(BUILTIN_FILE_HASH, raw.as_ref(), "path")?;
432 let guard = index.lock().expect("code_index mutex poisoned");
433 let Some(state) = guard.as_ref() else {
434 return Ok(VmValue::Nil);
435 };
436 let Some(abs) = state.absolute_path(&path) else {
437 return Ok(VmValue::Nil);
438 };
439 let bytes = match crate::fs::read(&abs, None) {
440 Some(result) => result,
441 None => std::fs::read(&abs),
442 };
443 match bytes {
444 Ok(bytes) => Ok(str_value(fnv1a64(&bytes).to_string())),
445 Err(_) => Ok(VmValue::Nil),
446 }
447}
448
449pub(super) fn run_read_range_merged(
458 index: &SharedIndex,
459 readonly: Option<&super::readonly::ReadonlyRoots>,
460 args: &[VmValue],
461) -> Result<VmValue, HostlibError> {
462 let raw = dict_arg(BUILTIN_READ_RANGE, args)?;
463 let dict = raw.as_ref();
464 let path = require_string(BUILTIN_READ_RANGE, dict, "path")?;
465 let start = optional_positive_i64(BUILTIN_READ_RANGE, dict, "start")?;
466 let end = optional_positive_i64(BUILTIN_READ_RANGE, dict, "end")?;
467 let abs =
468 match readonly {
469 Some(readonly) => super::readonly::resolve_read_path(index, readonly, &path)
470 .ok_or_else(|| HostlibError::InvalidParameter {
471 builtin: BUILTIN_READ_RANGE,
472 param: "path",
473 message: "path must stay within the indexed workspace root or a read-only \
474 dependency root"
475 .to_string(),
476 })?,
477 None => {
478 let guard = index.lock().expect("code_index mutex poisoned");
479 match guard.as_ref() {
480 Some(state) => state.absolute_path(&path).ok_or_else(|| {
481 HostlibError::InvalidParameter {
482 builtin: BUILTIN_READ_RANGE,
483 param: "path",
484 message: "path must stay within the indexed workspace root".to_string(),
485 }
486 })?,
487 None => PathBuf::from(&path),
488 }
489 }
490 };
491
492 let content_result = match crate::fs::read_to_string(&abs, None) {
493 Some(result) => result,
494 None => std::fs::read_to_string(&abs),
495 };
496 let content = match content_result {
497 Ok(s) => s,
498 Err(_) => {
499 return Err(HostlibError::Backend {
500 builtin: BUILTIN_READ_RANGE,
501 message: format!("file not found: {path}"),
502 })
503 }
504 };
505
506 if start.is_none() && end.is_none() {
507 return Ok(build_dict([("content", str_value(&content))]));
508 }
509 let lines: Vec<&str> = content.split('\n').collect();
510 let total = lines.len() as i64;
511 let lo = (start.unwrap_or(1) - 1).max(0) as usize;
512 let hi = end.unwrap_or(total).min(total).max(0) as usize;
513 if lo >= hi {
514 return Ok(build_dict([
515 ("content", str_value("")),
516 ("start", VmValue::Int((lo as i64) + 1)),
517 ("end", VmValue::Int(hi as i64)),
518 ]));
519 }
520 let slice = lines[lo..hi].join("\n");
521 Ok(build_dict([
522 ("content", str_value(&slice)),
523 ("start", VmValue::Int((lo as i64) + 1)),
524 ("end", VmValue::Int(hi as i64)),
525 ]))
526}
527
528pub(super) fn run_reindex_file(
529 index: &SharedIndex,
530 args: &[VmValue],
531) -> Result<VmValue, HostlibError> {
532 let raw = dict_arg(BUILTIN_REINDEX_FILE, args)?;
533 let path = require_string(BUILTIN_REINDEX_FILE, raw.as_ref(), "path")?;
534 let mut guard = index.lock().expect("code_index mutex poisoned");
535 let Some(state) = guard.as_mut() else {
536 return Ok(build_dict([
537 ("indexed", VmValue::Bool(false)),
538 ("file_id", VmValue::Nil),
539 ]));
540 };
541 let Some(abs) = state.absolute_path(&path) else {
542 return Err(HostlibError::InvalidParameter {
543 builtin: BUILTIN_REINDEX_FILE,
544 param: "path",
545 message: "path must stay within the indexed workspace root".to_string(),
546 });
547 };
548 let id = state.reindex_file(&abs);
549 Ok(build_dict([
550 ("indexed", VmValue::Bool(id.is_some())),
551 (
552 "file_id",
553 id.map(|i| VmValue::Int(i as i64)).unwrap_or(VmValue::Nil),
554 ),
555 ]))
556}
557
558pub(super) fn run_trigram_query(
559 index: &SharedIndex,
560 args: &[VmValue],
561) -> Result<VmValue, HostlibError> {
562 let raw = dict_arg(BUILTIN_TRIGRAM_QUERY, args)?;
563 let dict = raw.as_ref();
564 let trigrams_raw = optional_int_list(BUILTIN_TRIGRAM_QUERY, dict, "trigrams")?;
565 let max_files = optional_positive_usize(BUILTIN_TRIGRAM_QUERY, dict, "max_files")?;
566 let mut trigrams = Vec::with_capacity(trigrams_raw.len());
567 for n in trigrams_raw {
568 if n < 0 {
569 return Err(HostlibError::InvalidParameter {
570 builtin: BUILTIN_TRIGRAM_QUERY,
571 param: "trigrams",
572 message: "entries must be >= 0".to_string(),
573 });
574 }
575 trigrams.push(n as u32);
576 }
577 let guard = index.lock().expect("code_index mutex poisoned");
578 let mut ids: Vec<FileId> = match guard.as_ref() {
579 Some(state) => state.trigrams.query(&trigrams).into_iter().collect(),
580 None => Vec::new(),
581 };
582 ids.sort_unstable();
583 if let Some(limit) = max_files {
584 ids.truncate(limit);
585 }
586 Ok(VmValue::List(Arc::new(
587 ids.into_iter().map(|id| VmValue::Int(id as i64)).collect(),
588 )))
589}
590
591pub(super) fn run_extract_trigrams(
592 _index: &SharedIndex,
593 args: &[VmValue],
594) -> Result<VmValue, HostlibError> {
595 let raw = dict_arg(BUILTIN_EXTRACT_TRIGRAMS, args)?;
596 let query = require_string(BUILTIN_EXTRACT_TRIGRAMS, raw.as_ref(), "query")?;
597 let mut tgs = trigram::query_trigrams(&query);
598 tgs.sort_unstable();
599 Ok(VmValue::List(Arc::new(
600 tgs.into_iter().map(|n| VmValue::Int(n as i64)).collect(),
601 )))
602}
603
604pub(super) fn run_word_get(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
605 let raw = dict_arg(BUILTIN_WORD_GET, args)?;
606 let word = require_string(BUILTIN_WORD_GET, raw.as_ref(), "word")?;
607 let guard = index.lock().expect("code_index mutex poisoned");
608 let hits: Vec<VmValue> = match guard.as_ref() {
609 Some(state) => state
610 .words
611 .get(&word)
612 .iter()
613 .map(|h| {
614 build_dict([
615 ("file_id", VmValue::Int(h.file as i64)),
616 ("line", VmValue::Int(h.line as i64)),
617 ])
618 })
619 .collect(),
620 None => Vec::new(),
621 };
622 Ok(VmValue::List(Arc::new(hits)))
623}
624
625pub(super) fn run_deps_get(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
626 let raw = dict_arg(BUILTIN_DEPS_GET, args)?;
627 let dict = raw.as_ref();
628 let id = require_positive_file_id(BUILTIN_DEPS_GET, dict, "file_id")?;
629 let direction = optional_string(BUILTIN_DEPS_GET, dict, "direction")?
630 .unwrap_or_else(|| "importers".to_string());
631 let guard = index.lock().expect("code_index mutex poisoned");
632 let mut neighbors: Vec<FileId> = match guard.as_ref() {
633 Some(state) => match direction.as_str() {
634 "importers" => state.deps.importers_of(id),
635 "imports" => state.deps.imports_of(id),
636 _ => {
637 return Err(HostlibError::InvalidParameter {
638 builtin: BUILTIN_DEPS_GET,
639 param: "direction",
640 message: format!("expected \"importers\" or \"imports\", got {direction:?}"),
641 })
642 }
643 },
644 None => Vec::new(),
645 };
646 neighbors.sort_unstable();
647 Ok(VmValue::List(Arc::new(
648 neighbors
649 .into_iter()
650 .map(|id| VmValue::Int(id as i64))
651 .collect(),
652 )))
653}
654
655pub(super) fn run_outline_get(
656 index: &SharedIndex,
657 args: &[VmValue],
658) -> Result<VmValue, HostlibError> {
659 let raw = dict_arg(BUILTIN_OUTLINE_GET, args)?;
660 let id = require_positive_file_id(BUILTIN_OUTLINE_GET, raw.as_ref(), "file_id")?;
661 let guard = index.lock().expect("code_index mutex poisoned");
662 let symbols: Vec<VmValue> = match guard.as_ref().and_then(|s| s.files.get(&id)) {
663 Some(file) => file
664 .symbols
665 .iter()
666 .map(|sym| {
667 build_dict([
668 ("name", str_value(&sym.name)),
669 ("kind", str_value(&sym.kind)),
670 ("start_line", VmValue::Int(sym.start_line as i64)),
671 ("end_line", VmValue::Int(sym.end_line as i64)),
672 ("signature", str_value(&sym.signature)),
673 ])
674 })
675 .collect(),
676 None => Vec::new(),
677 };
678 Ok(VmValue::List(Arc::new(symbols)))
679}
680
681pub(super) fn run_current_seq(
684 index: &SharedIndex,
685 _args: &[VmValue],
686) -> Result<VmValue, HostlibError> {
687 let guard = index.lock().expect("code_index mutex poisoned");
688 let seq = guard.as_ref().map(|s| s.versions.current_seq).unwrap_or(0);
689 Ok(VmValue::Int(seq as i64))
690}
691
692pub(super) fn run_changes_since(
693 index: &SharedIndex,
694 args: &[VmValue],
695) -> Result<VmValue, HostlibError> {
696 let raw = dict_arg(BUILTIN_CHANGES_SINCE, args)?;
697 let dict = raw.as_ref();
698 let seq = optional_non_negative_u64(BUILTIN_CHANGES_SINCE, dict, "seq", 0)?;
699 let limit = optional_positive_usize(BUILTIN_CHANGES_SINCE, dict, "limit")?;
700 let guard = index.lock().expect("code_index mutex poisoned");
701 let records = match guard.as_ref() {
702 Some(state) => state.versions.changes_since(seq, limit),
703 None => Vec::new(),
704 };
705 Ok(VmValue::List(Arc::new(
706 records
707 .into_iter()
708 .map(|r| {
709 build_dict([
710 ("path", str_value(&r.path)),
711 ("seq", VmValue::Int(r.seq as i64)),
712 ("agent_id", VmValue::Int(r.agent_id as i64)),
713 ("op", str_value(r.op.as_str())),
714 ("hash", str_value(r.hash.to_string())),
715 ("size", VmValue::Int(r.size as i64)),
716 ("timestamp_ms", VmValue::Int(r.timestamp_ms)),
717 ])
718 })
719 .collect(),
720 )))
721}
722
723pub(super) fn run_version_record(
724 index: &SharedIndex,
725 args: &[VmValue],
726) -> Result<VmValue, HostlibError> {
727 let raw = dict_arg(BUILTIN_VERSION_RECORD, args)?;
728 let dict = raw.as_ref();
729 let agent_id = require_non_negative_u64(BUILTIN_VERSION_RECORD, dict, "agent_id")?;
730 let path = require_string(BUILTIN_VERSION_RECORD, dict, "path")?;
731 let op_str =
732 optional_string(BUILTIN_VERSION_RECORD, dict, "op")?.unwrap_or_else(|| "write".to_string());
733 let op = EditOp::parse(&op_str).unwrap_or(EditOp::Write);
734 let hash = parse_hash(BUILTIN_VERSION_RECORD, dict, "hash")?;
735 let size = optional_non_negative_u64(BUILTIN_VERSION_RECORD, dict, "size", 0)?;
736 let now = now_unix_ms();
737 let mut guard = index.lock().expect("code_index mutex poisoned");
738 let state = ensure_state(BUILTIN_VERSION_RECORD, &mut guard)?;
739 let normalized = normalize_relative_path(state, &path);
740 let seq = state
741 .versions
742 .record(normalized, agent_id, op, hash, size, now);
743 state.agents.note_edit(agent_id, now);
744 Ok(VmValue::Int(seq as i64))
745}
746
747pub(super) fn run_agent_register(
750 index: &SharedIndex,
751 args: &[VmValue],
752) -> Result<VmValue, HostlibError> {
753 let raw = dict_arg(BUILTIN_AGENT_REGISTER, args)?;
754 let dict = raw.as_ref();
755 let name = optional_string(BUILTIN_AGENT_REGISTER, dict, "name")?
756 .unwrap_or_else(|| "agent".to_string());
757 let requested_id = optional_positive_u64(BUILTIN_AGENT_REGISTER, dict, "agent_id")?;
758 let now = now_unix_ms();
759 let mut guard = index.lock().expect("code_index mutex poisoned");
760 let state = ensure_state(BUILTIN_AGENT_REGISTER, &mut guard)?;
761 let id = match requested_id {
762 Some(id) => state.agents.register_with_id(id, name, now),
763 None => state.agents.register(name, now),
764 };
765 Ok(VmValue::Int(id as i64))
766}
767
768pub(super) fn run_agent_heartbeat(
769 index: &SharedIndex,
770 args: &[VmValue],
771) -> Result<VmValue, HostlibError> {
772 let raw = dict_arg(BUILTIN_AGENT_HEARTBEAT, args)?;
773 let id = require_positive_u64(BUILTIN_AGENT_HEARTBEAT, raw.as_ref(), "agent_id")?;
774 let now = now_unix_ms();
775 let mut guard = index.lock().expect("code_index mutex poisoned");
776 let state = ensure_state(BUILTIN_AGENT_HEARTBEAT, &mut guard)?;
777 state.agents.heartbeat(id, now);
778 Ok(VmValue::Bool(true))
779}
780
781pub(super) fn run_agent_unregister(
782 index: &SharedIndex,
783 args: &[VmValue],
784) -> Result<VmValue, HostlibError> {
785 let raw = dict_arg(BUILTIN_AGENT_UNREGISTER, args)?;
786 let id = require_positive_u64(BUILTIN_AGENT_UNREGISTER, raw.as_ref(), "agent_id")?;
787 let mut guard = index.lock().expect("code_index mutex poisoned");
788 let state = ensure_state(BUILTIN_AGENT_UNREGISTER, &mut guard)?;
789 state.agents.unregister(id);
790 Ok(VmValue::Bool(true))
791}
792
793pub(super) fn run_lock_try(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
794 let raw = dict_arg(BUILTIN_LOCK_TRY, args)?;
795 let dict = raw.as_ref();
796 let agent_id = require_positive_u64(BUILTIN_LOCK_TRY, dict, "agent_id")?;
797 let path = require_string(BUILTIN_LOCK_TRY, dict, "path")?;
798 let ttl = optional_positive_i64(BUILTIN_LOCK_TRY, dict, "ttl_ms")?;
799 let now = now_unix_ms();
800 let mut guard = index.lock().expect("code_index mutex poisoned");
801 let state = ensure_state(BUILTIN_LOCK_TRY, &mut guard)?;
802 let granted = state.agents.try_lock(agent_id, &path, ttl, now);
803 if granted {
804 return Ok(build_dict([
805 ("locked", VmValue::Bool(true)),
806 ("holder", VmValue::Int(agent_id as i64)),
807 ]));
808 }
809 let holder = state.agents.lock_holder(&path, now);
810 Ok(build_dict([
811 ("locked", VmValue::Bool(false)),
812 (
813 "holder",
814 holder
815 .map(|id| VmValue::Int(id as i64))
816 .unwrap_or(VmValue::Nil),
817 ),
818 ]))
819}
820
821pub(super) fn run_lock_release(
822 index: &SharedIndex,
823 args: &[VmValue],
824) -> Result<VmValue, HostlibError> {
825 let raw = dict_arg(BUILTIN_LOCK_RELEASE, args)?;
826 let dict = raw.as_ref();
827 let agent_id = require_positive_u64(BUILTIN_LOCK_RELEASE, dict, "agent_id")?;
828 let path = require_string(BUILTIN_LOCK_RELEASE, dict, "path")?;
829 let mut guard = index.lock().expect("code_index mutex poisoned");
830 let state = ensure_state(BUILTIN_LOCK_RELEASE, &mut guard)?;
831 state.agents.release_lock(agent_id, &path);
832 Ok(VmValue::Bool(true))
833}
834
835pub(super) fn run_status(index: &SharedIndex, _args: &[VmValue]) -> Result<VmValue, HostlibError> {
836 let guard = index.lock().expect("code_index mutex poisoned");
837 match guard.as_ref() {
838 Some(state) => Ok(build_dict([
839 ("file_count", VmValue::Int(state.files.len() as i64)),
840 (
841 "current_seq",
842 VmValue::Int(state.versions.current_seq as i64),
843 ),
844 ("last_indexed_at_ms", VmValue::Int(state.last_built_unix_ms)),
845 (
846 "git_head",
847 state
848 .git_head
849 .as_deref()
850 .map(str_value)
851 .unwrap_or(VmValue::Nil),
852 ),
853 (
854 "agents",
855 VmValue::List(Arc::new(
856 state
857 .agents
858 .agents()
859 .map(|info| {
860 build_dict([
861 ("id", VmValue::Int(info.id as i64)),
862 ("name", str_value(&info.name)),
863 (
864 "state",
865 str_value(match info.state {
866 super::agents::AgentState::Active => "active",
867 super::agents::AgentState::Crashed => "crashed",
868 super::agents::AgentState::Gone => "gone",
869 }),
870 ),
871 ("last_seen_ms", VmValue::Int(info.last_seen_ms)),
872 ("edit_count", VmValue::Int(info.edit_count as i64)),
873 ("lock_count", VmValue::Int(info.locked_paths.len() as i64)),
874 ])
875 })
876 .collect(),
877 )),
878 ),
879 ])),
880 None => Ok(build_dict([
881 ("file_count", VmValue::Int(0)),
882 ("current_seq", VmValue::Int(0)),
883 ("last_indexed_at_ms", VmValue::Int(0)),
884 ("git_head", VmValue::Nil),
885 ("agents", VmValue::List(Arc::new(Vec::new()))),
886 ])),
887 }
888}
889
890pub(super) fn run_current_agent_id(
891 slot: &Arc<Mutex<Option<AgentId>>>,
892 _args: &[VmValue],
893) -> Result<VmValue, HostlibError> {
894 let guard = slot.lock().expect("current_agent slot poisoned");
895 Ok(match *guard {
896 Some(id) => VmValue::Int(id as i64),
897 None => VmValue::Nil,
898 })
899}
900
901pub(super) fn run_cypher(index: &SharedIndex, args: &[VmValue]) -> Result<VmValue, HostlibError> {
904 let raw = dict_arg(BUILTIN_CYPHER, args)?;
905 let dict = raw.as_ref();
906 let query = require_string(BUILTIN_CYPHER, dict, "query")?;
907
908 let guard = index.lock().expect("code_index mutex poisoned");
909 let Some(state) = guard.as_ref() else {
910 return Ok(build_dict([
911 ("rows", VmValue::List(Arc::new(Vec::new()))),
912 ("overlay", VmValue::Nil),
913 ]));
914 };
915
916 let graph = state.overlays.graph(&state.symbols);
917 let rows = super::cypher::execute(&query, graph).map_err(|err| HostlibError::Backend {
918 builtin: BUILTIN_CYPHER,
919 message: err.to_string(),
920 })?;
921
922 let rows_vm: Vec<VmValue> = rows
923 .into_iter()
924 .map(|row| {
925 let mut map: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
926 for (k, v) in row {
927 map.insert(k, v.to_vm());
928 }
929 VmValue::dict(map)
930 })
931 .collect();
932
933 Ok(build_dict([
934 ("rows", VmValue::List(Arc::new(rows_vm))),
935 (
936 "overlay",
937 match state.overlays.active() {
938 Some(name) => str_value(name),
939 None => VmValue::Nil,
940 },
941 ),
942 ]))
943}
944
945pub(super) fn run_branch_overlay(
946 index: &SharedIndex,
947 args: &[VmValue],
948) -> Result<VmValue, HostlibError> {
949 let raw = dict_arg(BUILTIN_BRANCH_OVERLAY, args)?;
950 let dict = raw.as_ref();
951 let branch = optional_string(BUILTIN_BRANCH_OVERLAY, dict, "branch")?;
952 let activate = optional_bool(BUILTIN_BRANCH_OVERLAY, dict, "activate", true)?;
953 let action = optional_string(BUILTIN_BRANCH_OVERLAY, dict, "action")?;
954
955 let mut guard = index.lock().expect("code_index mutex poisoned");
956 let state = ensure_state(BUILTIN_BRANCH_OVERLAY, &mut guard)?;
957
958 let mut reuse: f64 = 1.0;
959 match action.as_deref().unwrap_or("activate") {
960 "deactivate" => {
961 state.overlays.activate(None);
962 }
963 "create" => {
964 let branch_name = branch.ok_or(HostlibError::MissingParameter {
965 builtin: BUILTIN_BRANCH_OVERLAY,
966 param: "branch",
967 })?;
968 let mut overlay = super::overlay::BranchOverlay::new(&branch_name);
969 overlay.materialize(&state.symbols);
970 state.overlays.set(overlay);
971 if activate {
972 state.overlays.activate(Some(branch_name));
973 }
974 reuse = state.overlays.reuse_fraction(&state.symbols);
975 }
976 "activate" => {
977 let branch_name = branch.ok_or(HostlibError::MissingParameter {
978 builtin: BUILTIN_BRANCH_OVERLAY,
979 param: "branch",
980 })?;
981 if state.overlays.get(&branch_name).is_none() {
985 let mut overlay = super::overlay::BranchOverlay::new(&branch_name);
986 overlay.materialize(&state.symbols);
987 state.overlays.set(overlay);
988 }
989 state.overlays.activate(Some(branch_name));
990 reuse = state.overlays.reuse_fraction(&state.symbols);
991 }
992 other => {
993 return Err(HostlibError::InvalidParameter {
994 builtin: BUILTIN_BRANCH_OVERLAY,
995 param: "action",
996 message: format!("expected one of activate|deactivate|create, got `{other}`"),
997 })
998 }
999 }
1000
1001 Ok(build_dict([
1002 (
1003 "active",
1004 match state.overlays.active() {
1005 Some(name) => str_value(name),
1006 None => VmValue::Nil,
1007 },
1008 ),
1009 ("reuse_fraction", VmValue::Float(reuse)),
1010 ]))
1011}
1012
1013pub(super) fn run_freshness(
1014 index: &SharedIndex,
1015 args: &[VmValue],
1016) -> Result<VmValue, HostlibError> {
1017 let raw = dict_arg(BUILTIN_FRESHNESS, args)?;
1018 let dict = raw.as_ref();
1019 let path = require_string(BUILTIN_FRESHNESS, dict, "path")?;
1020
1021 let guard = index.lock().expect("code_index mutex poisoned");
1022 let state = guard.as_ref().ok_or_else(|| HostlibError::Backend {
1023 builtin: BUILTIN_FRESHNESS,
1024 message: "code index has not been initialised — call \
1025 `hostlib_code_index_rebuild` first"
1026 .to_string(),
1027 })?;
1028
1029 let normalized = normalize_relative_path(state, &path);
1030 let file = state
1031 .lookup_path(&normalized)
1032 .and_then(|id| state.files.get(&id));
1033 let Some(file) = file else {
1034 return Ok(unknown_freshness_response(&path));
1035 };
1036
1037 let abs = state.root.join(&file.relative_path);
1038 let (disk_mtime, disk_hash) = match std::fs::read(&abs) {
1039 Ok(bytes) => {
1040 let hash = fnv1a64(&bytes);
1041 let mtime = std::fs::metadata(&abs)
1042 .ok()
1043 .and_then(|m| m.modified().ok())
1044 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
1045 .map(|d| d.as_millis() as i64)
1046 .unwrap_or(0);
1047 (mtime, Some(hash))
1048 }
1049 Err(_) => (0, None),
1050 };
1051 let stale = disk_hash != Some(file.content_hash);
1052 Ok(build_dict([
1053 ("path", str_value(&file.relative_path)),
1054 ("known", VmValue::Bool(true)),
1055 ("stale", VmValue::Bool(stale)),
1056 (
1057 "indexed_hash",
1058 VmValue::String(Arc::from(format!("{:016x}", file.content_hash).as_str())),
1059 ),
1060 ("indexed_mtime_ms", VmValue::Int(file.mtime_ms)),
1061 (
1062 "disk_hash",
1063 match disk_hash {
1064 Some(h) => VmValue::String(Arc::from(format!("{h:016x}").as_str())),
1065 None => VmValue::Nil,
1066 },
1067 ),
1068 ("disk_mtime_ms", VmValue::Int(disk_mtime)),
1069 ]))
1070}
1071
1072fn ensure_state<'a>(
1075 builtin: &'static str,
1076 guard: &'a mut std::sync::MutexGuard<'_, Option<IndexState>>,
1077) -> Result<&'a mut IndexState, HostlibError> {
1078 if guard.is_none() {
1079 return Err(HostlibError::Backend {
1080 builtin,
1081 message: "code index has not been initialised — call \
1082 `hostlib_code_index_rebuild` or restore from a snapshot first"
1083 .to_string(),
1084 });
1085 }
1086 Ok(guard.as_mut().unwrap())
1087}
1088
1089fn parse_hash(
1090 builtin: &'static str,
1091 dict: &harn_vm::value::DictMap,
1092 key: &'static str,
1093) -> Result<u64, HostlibError> {
1094 match dict.get(key) {
1095 None | Some(VmValue::Nil) => Ok(0),
1096 Some(VmValue::Int(n)) if *n >= 0 => Ok(*n as u64),
1097 Some(VmValue::Int(n)) => Err(HostlibError::InvalidParameter {
1098 builtin,
1099 param: key,
1100 message: format!("must be >= 0, got {n}"),
1101 }),
1102 Some(VmValue::String(s)) => s
1103 .parse::<u64>()
1104 .map_err(|_| HostlibError::InvalidParameter {
1105 builtin,
1106 param: key,
1107 message: format!("expected u64-parseable string, got {s:?}"),
1108 }),
1109 Some(other) => Err(HostlibError::InvalidParameter {
1110 builtin,
1111 param: key,
1112 message: format!(
1113 "expected integer or numeric string, got {}",
1114 other.type_name()
1115 ),
1116 }),
1117 }
1118}
1119
1120fn require_positive_u64(
1121 builtin: &'static str,
1122 dict: &harn_vm::value::DictMap,
1123 key: &'static str,
1124) -> Result<u64, HostlibError> {
1125 let raw = require_non_negative_u64(builtin, dict, key)?;
1126 if raw == 0 {
1127 return Err(HostlibError::InvalidParameter {
1128 builtin,
1129 param: key,
1130 message: "must be >= 1".to_string(),
1131 });
1132 }
1133 Ok(raw)
1134}
1135
1136fn require_positive_file_id(
1137 builtin: &'static str,
1138 dict: &harn_vm::value::DictMap,
1139 key: &'static str,
1140) -> Result<FileId, HostlibError> {
1141 let raw = require_positive_u64(builtin, dict, key)?;
1142 FileId::try_from(raw).map_err(|_| HostlibError::InvalidParameter {
1143 builtin,
1144 param: key,
1145 message: "does not fit in file id".to_string(),
1146 })
1147}
1148
1149fn require_non_negative_u64(
1150 builtin: &'static str,
1151 dict: &harn_vm::value::DictMap,
1152 key: &'static str,
1153) -> Result<u64, HostlibError> {
1154 match value_args::optional_i64_no_default(builtin, dict, key)? {
1155 Some(value) if value >= 0 => Ok(value as u64),
1156 Some(value) => Err(HostlibError::InvalidParameter {
1157 builtin,
1158 param: key,
1159 message: format!("must be >= 0, got {value}"),
1160 }),
1161 None => Err(HostlibError::MissingParameter {
1162 builtin,
1163 param: key,
1164 }),
1165 }
1166}
1167
1168fn optional_positive_u64(
1169 builtin: &'static str,
1170 dict: &harn_vm::value::DictMap,
1171 key: &'static str,
1172) -> Result<Option<u64>, HostlibError> {
1173 match dict.get(key) {
1174 None | Some(VmValue::Nil) => Ok(None),
1175 Some(_) => require_positive_u64(builtin, dict, key).map(Some),
1176 }
1177}
1178
1179fn optional_non_negative_u64(
1180 builtin: &'static str,
1181 dict: &harn_vm::value::DictMap,
1182 key: &'static str,
1183 default: u64,
1184) -> Result<u64, HostlibError> {
1185 match dict.get(key) {
1186 None | Some(VmValue::Nil) => Ok(default),
1187 Some(_) => require_non_negative_u64(builtin, dict, key),
1188 }
1189}
1190
1191fn optional_positive_i64(
1192 builtin: &'static str,
1193 dict: &harn_vm::value::DictMap,
1194 key: &'static str,
1195) -> Result<Option<i64>, HostlibError> {
1196 match value_args::optional_i64_no_default(builtin, dict, key)? {
1197 None => Ok(None),
1198 Some(value) if value >= 1 => Ok(Some(value)),
1199 Some(value) => Err(HostlibError::InvalidParameter {
1200 builtin,
1201 param: key,
1202 message: format!("must be >= 1, got {value}"),
1203 }),
1204 }
1205}
1206
1207fn optional_positive_usize(
1208 builtin: &'static str,
1209 dict: &harn_vm::value::DictMap,
1210 key: &'static str,
1211) -> Result<Option<usize>, HostlibError> {
1212 match optional_positive_u64(builtin, dict, key)? {
1213 Some(value) => {
1214 usize::try_from(value)
1215 .map(Some)
1216 .map_err(|_| HostlibError::InvalidParameter {
1217 builtin,
1218 param: key,
1219 message: "does not fit in usize".to_string(),
1220 })
1221 }
1222 None => Ok(None),
1223 }
1224}
1225
1226pub(super) fn normalize_relative_path_for(state: &IndexState, path: &str) -> String {
1232 normalize_relative_path(state, path)
1233}
1234
1235fn normalize_relative_path(state: &IndexState, path: &str) -> String {
1236 if let Some(rel) = state
1237 .lookup_path(path)
1238 .and_then(|id| state.files.get(&id))
1239 .map(|f| f.relative_path.clone())
1240 {
1241 return rel;
1242 }
1243 let p = std::path::Path::new(path);
1244 if p.is_absolute() {
1245 if let Ok(rel) = p.strip_prefix(&state.root) {
1246 return rel.to_string_lossy().replace('\\', "/");
1247 }
1248 }
1249 path.to_string()
1250}
1251
1252fn candidates_for(state: &IndexState, needle: &str) -> Vec<FileId> {
1253 if needle.len() >= 3 {
1254 let trigrams = trigram::query_trigrams(needle);
1255 return state.trigrams.query(&trigrams).into_iter().collect();
1256 }
1257 state.files.keys().copied().collect()
1258}
1259
1260fn read_file_text(root: &std::path::Path, relative: &str) -> Option<String> {
1261 let path = root.join(relative);
1262 match crate::fs::read_to_string(&path, None) {
1263 Some(result) => result.ok(),
1264 None => std::fs::read_to_string(path).ok(),
1265 }
1266}
1267
1268fn count_matches(haystack: &str, needle: &str, case_sensitive: bool) -> u64 {
1269 if case_sensitive {
1270 haystack.matches(needle).count() as u64
1271 } else {
1272 let lower_h = haystack.to_lowercase();
1273 let lower_n = needle.to_lowercase();
1274 lower_h.matches(&lower_n).count() as u64
1275 }
1276}
1277
1278fn scope_allows(scope: &[String], relative: &str) -> bool {
1279 if scope.is_empty() {
1280 return true;
1281 }
1282 scope
1283 .iter()
1284 .any(|s| relative == s || relative.starts_with(&format!("{s}/")) || s.is_empty())
1285}
1286
1287pub(super) struct Hit {
1288 pub(super) path: String,
1289 pub(super) match_count: u64,
1290 pub(super) root: Option<String>,
1293}
1294
1295fn hit_to_value(hit: Hit) -> VmValue {
1296 let Hit {
1297 path,
1298 match_count,
1299 root,
1300 } = hit;
1301 build_dict([
1302 ("path", str_value(&path)),
1303 ("score", VmValue::Float(match_count as f64)),
1304 ("match_count", VmValue::Int(match_count as i64)),
1305 (
1306 "root",
1307 match root {
1308 Some(r) => str_value(&r),
1309 None => VmValue::Nil,
1310 },
1311 ),
1312 ])
1313}
1314
1315fn import_entry(module: &str, resolved: Option<&str>, kind: &str) -> VmValue {
1316 let mut map: harn_vm::value::DictMap = harn_vm::value::DictMap::new();
1317 map.insert("module".into(), str_value(module));
1318 map.insert(
1319 "resolved_path".into(),
1320 match resolved {
1321 Some(p) => str_value(p),
1322 None => VmValue::Nil,
1323 },
1324 );
1325 map.insert("kind".into(), str_value(kind));
1326 VmValue::dict(map)
1327}
1328
1329fn empty_stats_response() -> VmValue {
1330 build_dict([
1331 ("indexed_files", VmValue::Int(0)),
1332 ("trigrams", VmValue::Int(0)),
1333 ("words", VmValue::Int(0)),
1334 ("memory_bytes", VmValue::Int(0)),
1335 ("last_rebuild_unix_ms", VmValue::Nil),
1336 ])
1337}
1338
1339fn empty_imports_response(path: &str) -> VmValue {
1340 build_dict([
1341 ("path", str_value(path)),
1342 ("imports", VmValue::List(Arc::new(Vec::new()))),
1343 ])
1344}
1345
1346fn empty_importers_response(module: &str) -> VmValue {
1347 build_dict([
1348 ("module", str_value(module)),
1349 ("importers", VmValue::List(Arc::new(Vec::new()))),
1350 ])
1351}
1352
1353fn unknown_freshness_response(path: &str) -> VmValue {
1354 build_dict([
1355 ("path", str_value(path)),
1356 ("known", VmValue::Bool(false)),
1357 ("stale", VmValue::Bool(true)),
1358 ("indexed_hash", VmValue::Nil),
1359 ("indexed_mtime_ms", VmValue::Nil),
1360 ("disk_hash", VmValue::Nil),
1361 ("disk_mtime_ms", VmValue::Nil),
1362 ])
1363}