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