1use std::{
5 collections::{BTreeMap, BTreeSet},
6 path::{Path, PathBuf},
7 time::Instant,
8};
9
10use ::objects::{HeddleError, error::Result, worktree::WorktreeStatus};
11use repo::{
12 Repository, Thread, ThreadManager, describe_thread_advice,
13 git_worktree_status::GitWorktreeEntryState, refresh_thread_freshness,
14};
15use schemars::JsonSchema;
16use serde::{Serialize, Serializer};
17use sley::{BString as GitBString, Index, Repository as SleyRepository};
18
19use crate::{
20 ExecutionContext, HeddleReport, MachineOutputKind, OutputDiscriminator, ReportContract,
21 schema_for_report,
22 status::{
23 RepositoryVerificationHealth, build_repository_verification_health_with_worktree_status,
24 default_remote_name, git_default_remote_name_from_repo,
25 next_action::remote_tracking_status,
26 },
27};
28
29#[derive(Clone)]
30pub struct VerifyOptions {
31 pub start_path: Option<PathBuf>,
32 pub machine_contract_input: MachineContractInput,
33 pub action_audience: ActionAudience,
34}
35
36impl VerifyOptions {
37 pub fn new() -> Self {
38 Self {
39 start_path: None,
40 machine_contract_input: MachineContractInput::default(),
41 action_audience: ActionAudience::Human,
42 }
43 }
44
45 pub fn with_start_path(mut self, start_path: impl Into<PathBuf>) -> Self {
46 self.start_path = Some(start_path.into());
47 self
48 }
49
50 pub fn with_machine_contract_input(mut self, input: MachineContractInput) -> Self {
51 self.machine_contract_input = input;
52 self
53 }
54
55 pub fn with_action_audience(mut self, audience: ActionAudience) -> Self {
56 self.action_audience = audience;
57 self
58 }
59}
60
61impl Default for VerifyOptions {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[derive(Debug, Clone, Copy, Serialize, JsonSchema, PartialEq, Eq)]
68#[serde(rename_all = "snake_case")]
69pub enum ActionAudience {
70 Human,
71 Agent,
72 Script,
73}
74
75#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
76pub struct MachineContractInput {
77 pub coverage: MachineContractCoverage,
78}
79
80impl MachineContractInput {
81 pub fn from_coverage(coverage: MachineContractCoverage) -> Self {
82 Self { coverage }
83 }
84}
85
86impl Default for MachineContractInput {
87 fn default() -> Self {
88 Self {
89 coverage: MachineContractCoverage::not_checked(),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
95pub struct VerifyReport {
96 pub output_kind: &'static str,
97 pub clean: bool,
98 pub repository_label: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub repository_context: Option<RepositoryContextInfo>,
101 #[serde(rename = "verification")]
102 pub trust: RepositoryVerificationState,
103 #[serde(skip)]
104 #[schemars(skip)]
105 pub profile: VerifyProfile,
106}
107
108impl VerifyReport {
109 pub const CONTRACT: ReportContract = ReportContract {
110 schema_name: "verify",
111 machine_output_kind: MachineOutputKind::Json,
112 output_discriminator: Some(OutputDiscriminator {
113 field: "output_kind",
114 value: "verify",
115 }),
116 schema: schema_for_report::<VerifyReport>,
117 };
118}
119
120impl HeddleReport for VerifyReport {
121 const CONTRACT: ReportContract = VerifyReport::CONTRACT;
122}
123
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
125pub struct VerifyProfile {
126 pub plain_git_probe_ms: u128,
127 pub repo_open_ms: u128,
128 pub verification_ms: u128,
129}
130
131#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
132pub struct RepositoryContextInfo {
133 pub kind: String,
134 pub parent_repository: Option<String>,
135 pub target_thread: Option<String>,
136 pub parent_thread: Option<String>,
137}
138
139#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
140pub struct RepositoryPresentation {
141 pub label: String,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub context: Option<RepositoryContextInfo>,
144}
145
146#[derive(Debug, Serialize, JsonSchema)]
147pub struct PlainGitVerifyProbe {
148 #[schemars(with = "String")]
149 pub root: PathBuf,
150 pub git_branch: Option<String>,
151 #[serde(skip)]
152 #[schemars(skip)]
153 pub changes: WorktreeStatus,
154 pub trust: RepositoryVerificationState,
155}
156
157#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
158pub struct ActionTemplate {
159 pub action: String,
160 pub argv_template: Vec<String>,
161 pub required_inputs: Vec<String>,
162 pub agent_may_fill: bool,
169}
170
171#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
172pub struct RepositoryVerificationState {
173 #[serde(rename = "verified")]
174 pub verified: bool,
175 pub status: String,
176 pub repository_mode: String,
177 pub heddle_initialized: bool,
178 pub git_branch: Option<String>,
179 pub heddle_thread: Option<String>,
180 pub worktree_dirty: bool,
181 pub worktree_state: String,
182 pub import_state: String,
183 pub mapping_state: String,
184 pub remote_drift: String,
185 pub active_operation: Option<String>,
186 pub default_remote: Option<String>,
187 pub clone_verification: String,
188 pub machine_contract: String,
189 pub machine_contract_coverage: MachineContractCoverage,
190 pub workflow_status: String,
191 pub workflow_summary: String,
192 pub summary: String,
193 #[serde(serialize_with = "serialize_empty_action_as_null")]
194 #[schemars(with = "Option<String>")]
195 pub recommended_action: String,
196 pub recommended_action_template: Option<ActionTemplate>,
197 pub recovery_commands: Vec<String>,
198 pub recovery_action_templates: Vec<ActionTemplate>,
199 pub checks: Vec<VerificationCheck>,
200}
201
202pub fn serialize_empty_action_as_null<S>(
203 action: &String,
204 serializer: S,
205) -> std::result::Result<S::Ok, S::Error>
206where
207 S: Serializer,
208{
209 if action.is_empty() {
210 serializer.serialize_none()
211 } else {
212 serializer.serialize_some(action)
213 }
214}
215
216#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
217pub struct MachineContractCoverage {
218 pub status: String,
219 #[serde(rename = "verified_scope")]
220 pub verified_scope: String,
221 pub advanced_scope: String,
222 pub summary: String,
223 pub catalog_commands_total: usize,
224 pub catalog_mutating_commands_total: usize,
225 pub json_commands_total: usize,
226 pub json_mutating_commands_total: usize,
227 pub json_commands_with_schema: usize,
228 pub json_commands_with_accepted_opaque_schema: usize,
229 pub json_commands_without_schema: usize,
230 #[serde(rename = "verified_scope_json_commands_total")]
231 pub verified_scope_json_commands_total: usize,
232 #[serde(rename = "verified_scope_json_commands_with_schema")]
233 pub verified_scope_json_commands_with_schema: usize,
234 #[serde(rename = "verified_scope_json_commands_with_accepted_opaque_schema")]
235 pub verified_scope_json_commands_with_accepted_opaque_schema: usize,
236 #[serde(rename = "verified_scope_json_commands_without_schema")]
237 pub verified_scope_json_commands_without_schema: usize,
238 pub advanced_scope_json_commands_total: usize,
239 pub advanced_scope_json_commands_with_accepted_opaque_schema: usize,
240 pub mutating_commands_total: usize,
241 pub mutating_commands_with_schema: usize,
242 pub mutating_commands_with_accepted_opaque_schema: usize,
243 pub mutating_commands_without_schema: usize,
244 #[serde(rename = "verified_scope_mutating_commands_total")]
245 pub verified_scope_mutating_commands_total: usize,
246 #[serde(rename = "verified_scope_mutating_commands_with_schema")]
247 pub verified_scope_mutating_commands_with_schema: usize,
248 #[serde(rename = "verified_scope_mutating_commands_with_accepted_opaque_schema")]
249 pub verified_scope_mutating_commands_with_accepted_opaque_schema: usize,
250 #[serde(rename = "verified_scope_mutating_commands_without_schema")]
251 pub verified_scope_mutating_commands_without_schema: usize,
252 pub advanced_scope_mutating_commands_total: usize,
253 pub advanced_scope_mutating_commands_with_accepted_opaque_schema: usize,
254 pub schema_verbs_total: usize,
255 pub documented_schema_verbs_total: usize,
256 pub undocumented_schema_verbs_total: usize,
257 pub opaque_schema_verbs_total: usize,
258 pub accepted_opaque_schema_verbs_total: usize,
259 pub unaccepted_opaque_schema_verbs_total: usize,
260 pub supports_op_id_total: usize,
261 pub jsonl_commands_total: usize,
262 pub missing_schema_examples: Vec<String>,
263 pub missing_mutating_schema_examples: Vec<String>,
264 pub verified_scope_missing_schema_examples: Vec<String>,
265 pub verified_scope_accepted_opaque_schema_examples: Vec<String>,
266 pub advanced_scope_accepted_opaque_schema_examples: Vec<String>,
267 pub accepted_opaque_schema_examples: Vec<String>,
268 pub unaccepted_opaque_schema_examples: Vec<String>,
269 pub undocumented_schema_examples: Vec<String>,
270}
271
272impl MachineContractCoverage {
273 pub fn not_checked() -> Self {
274 Self {
275 status: "not_checked".to_string(),
276 verified_scope: "not_checked".to_string(),
277 advanced_scope: "not_checked".to_string(),
278 summary: "Machine-contract proof was not supplied by this embedder".to_string(),
279 catalog_commands_total: 0,
280 catalog_mutating_commands_total: 0,
281 json_commands_total: 0,
282 json_mutating_commands_total: 0,
283 json_commands_with_schema: 0,
284 json_commands_with_accepted_opaque_schema: 0,
285 json_commands_without_schema: 0,
286 verified_scope_json_commands_total: 0,
287 verified_scope_json_commands_with_schema: 0,
288 verified_scope_json_commands_with_accepted_opaque_schema: 0,
289 verified_scope_json_commands_without_schema: 0,
290 advanced_scope_json_commands_total: 0,
291 advanced_scope_json_commands_with_accepted_opaque_schema: 0,
292 mutating_commands_total: 0,
293 mutating_commands_with_schema: 0,
294 mutating_commands_with_accepted_opaque_schema: 0,
295 mutating_commands_without_schema: 0,
296 verified_scope_mutating_commands_total: 0,
297 verified_scope_mutating_commands_with_schema: 0,
298 verified_scope_mutating_commands_with_accepted_opaque_schema: 0,
299 verified_scope_mutating_commands_without_schema: 0,
300 advanced_scope_mutating_commands_total: 0,
301 advanced_scope_mutating_commands_with_accepted_opaque_schema: 0,
302 schema_verbs_total: 0,
303 documented_schema_verbs_total: 0,
304 undocumented_schema_verbs_total: 0,
305 opaque_schema_verbs_total: 0,
306 accepted_opaque_schema_verbs_total: 0,
307 unaccepted_opaque_schema_verbs_total: 0,
308 supports_op_id_total: 0,
309 jsonl_commands_total: 0,
310 missing_schema_examples: Vec::new(),
311 missing_mutating_schema_examples: Vec::new(),
312 verified_scope_missing_schema_examples: Vec::new(),
313 verified_scope_accepted_opaque_schema_examples: Vec::new(),
314 advanced_scope_accepted_opaque_schema_examples: Vec::new(),
315 accepted_opaque_schema_examples: Vec::new(),
316 unaccepted_opaque_schema_examples: Vec::new(),
317 undocumented_schema_examples: Vec::new(),
318 }
319 }
320}
321
322#[derive(Debug, Clone, Serialize, JsonSchema, PartialEq, Eq)]
323pub struct VerificationCheck {
324 pub name: String,
325 pub status: String,
326 pub clean: bool,
327 pub summary: String,
328 pub recommended_action: Option<String>,
329 pub recommended_action_template: Option<ActionTemplate>,
330 pub recovery_commands: Vec<String>,
331 pub recovery_action_templates: Vec<ActionTemplate>,
332 #[serde(default)]
333 pub details: BTreeMap<String, String>,
334}
335
336pub fn build_plain_git_verification_probe(start: &Path) -> Result<Option<PlainGitVerifyProbe>> {
337 build_plain_git_verification_probe_with_machine_contract(
338 start,
339 &MachineContractInput::default(),
340 )
341}
342
343pub fn build_plain_git_verification_probe_with_machine_contract(
344 start: &Path,
345 machine_contract_input: &MachineContractInput,
346) -> Result<Option<PlainGitVerifyProbe>> {
347 let git_repo = match SleyRepository::discover(start) {
348 Ok(repo) => repo,
349 Err(_) => return Ok(None),
350 };
351 let Some(workdir) = git_repo.workdir() else {
352 return Ok(None);
353 };
354 let root = workdir
355 .canonicalize()
356 .unwrap_or_else(|_| workdir.to_path_buf());
357 if root.join(".heddle").exists() {
358 return Ok(None);
359 }
360
361 let git_branch = plain_git_current_branch(&git_repo);
362 let git_branches = plain_git_local_branches(&git_repo);
363 let git_tags = plain_git_local_tags(&git_repo);
364 let changes = plain_git_worktree_status(&root, &git_repo)?;
365
366 let default_remote = git_default_remote_name_from_repo(&git_repo);
367 let setup_action = "heddle init".to_string();
368 let recovery_commands = vec![setup_action.clone()];
369 let machine_contract_coverage = machine_contract_input.coverage.clone();
370 let mut details = BTreeMap::new();
371 details.insert("path".to_string(), root.display().to_string());
372 if let Some(branch) = &git_branch {
373 details.insert("git_branch".to_string(), branch.clone());
374 }
375 if let Some(remote) = &default_remote {
376 details.insert("default_remote".to_string(), remote.clone());
377 }
378 details.insert(
379 "git_branch_count".to_string(),
380 git_branches.len().to_string(),
381 );
382 details.insert("git_tag_count".to_string(), git_tags.len().to_string());
383
384 let mut checks = vec![
385 VerificationCheck {
386 name: "Git".to_string(),
387 status: "present".to_string(),
388 clean: true,
389 summary: "plain Git repository found".to_string(),
390 recommended_action: None,
391 recommended_action_template: None,
392 recovery_commands: Vec::new(),
393 recovery_action_templates: Vec::new(),
394 details,
395 },
396 VerificationCheck {
397 name: "Heddle".to_string(),
398 status: "needs_init".to_string(),
399 clean: false,
400 summary: "Heddle data is not initialized".to_string(),
401 recommended_action: Some(setup_action.clone()),
402 recommended_action_template: action_template(&setup_action),
403 recovery_commands: recovery_commands.clone(),
404 recovery_action_templates: action_templates(&recovery_commands),
405 details: BTreeMap::new(),
406 },
407 VerificationCheck {
408 name: "Mapping".to_string(),
409 status: "git_backed".to_string(),
410 clean: true,
411 summary: "Git refs will stay in Git storage after Heddle initialization".to_string(),
412 recommended_action: None,
413 recommended_action_template: None,
414 recovery_commands: Vec::new(),
415 recovery_action_templates: Vec::new(),
416 details: BTreeMap::new(),
417 },
418 ];
419 checks.push(verification_check(
420 "Worktree",
421 changes.is_clean(),
422 if changes.is_clean() {
423 "clean"
424 } else {
425 "dirty_worktree"
426 },
427 if changes.is_clean() {
428 "Git worktree is clean"
429 } else {
430 "Git worktree has uncommitted changes"
431 },
432 None,
433 Vec::new(),
434 ));
435 checks.push(verification_check(
436 "Remote",
437 false,
438 "unknown",
439 "remote drift is checked after Heddle initialization",
440 None,
441 Vec::new(),
442 ));
443 checks.push(verification_check(
444 "Operation",
445 true,
446 "clean",
447 "no Heddle operation in progress",
448 None,
449 Vec::new(),
450 ));
451 checks.push(verification_check(
452 "Workflow",
453 false,
454 "not_checked",
455 "workflow readiness is checked after Heddle initialization",
456 None,
457 Vec::new(),
458 ));
459 checks.push(machine_contract_verification_check(
460 &machine_contract_coverage,
461 ));
462 checks.push(verification_check(
463 "Clone",
464 true,
465 "not_applicable",
466 "clone verification is not applicable to this checkout",
467 None,
468 Vec::new(),
469 ));
470
471 let trust = RepositoryVerificationState {
472 verified: false,
473 status: "needs_init".to_string(),
474 repository_mode: "plain-git".to_string(),
475 heddle_initialized: false,
476 git_branch: git_branch.clone(),
477 heddle_thread: None,
478 worktree_dirty: !changes.is_clean(),
479 worktree_state: if changes.is_clean() { "clean" } else { "dirty" }.to_string(),
480 import_state: "git_backed".to_string(),
481 mapping_state: "git_backed".to_string(),
482 remote_drift: "unknown".to_string(),
483 active_operation: None,
484 default_remote,
485 clone_verification: "not_applicable".to_string(),
486 machine_contract: machine_contract_status(&machine_contract_coverage).to_string(),
487 machine_contract_coverage,
488 workflow_status: "not_checked".to_string(),
489 workflow_summary: "workflow readiness is checked after Heddle initialization".to_string(),
490 summary: "Git repository has not been initialized for Heddle".to_string(),
491 recommended_action: setup_action.clone(),
492 recommended_action_template: action_template(&setup_action),
493 recovery_commands: recovery_commands.clone(),
494 recovery_action_templates: action_templates(&recovery_commands),
495 checks,
496 };
497 Ok(Some(PlainGitVerifyProbe {
498 root,
499 git_branch,
500 changes,
501 trust,
502 }))
503}
504
505fn plain_git_current_branch(git_repo: &SleyRepository) -> Option<String> {
506 git_repo.head().ok()?.branch_name().map(str::to_string)
507}
508
509fn plain_git_local_branches(git_repo: &SleyRepository) -> Vec<String> {
510 let Ok(branches) = git_repo.references().list_refs() else {
511 return Vec::new();
512 };
513 let mut names = branches
514 .into_iter()
515 .filter_map(|branch| branch.name.strip_prefix("refs/heads/").map(str::to_string))
516 .filter(|branch| !branch.trim().is_empty())
517 .collect::<Vec<_>>();
518 names.sort();
519 names.dedup();
520 names
521}
522
523fn plain_git_local_tags(git_repo: &SleyRepository) -> Vec<String> {
524 let Ok(tags) = git_repo.references().list_refs() else {
525 return Vec::new();
526 };
527 let mut names = tags
528 .into_iter()
529 .filter_map(|tag| tag.name.strip_prefix("refs/tags/").map(str::to_string))
530 .filter(|tag| !tag.trim().is_empty())
531 .collect::<Vec<_>>();
532 names.sort();
533 names.dedup();
534 names
535}
536
537fn plain_git_worktree_status(root: &Path, git_repo: &SleyRepository) -> Result<WorktreeStatus> {
538 let index = plain_git_index_or_empty(git_repo).map_err(|error| {
539 HeddleError::Config(format!(
540 "failed to inspect Git index at '{}': {error}",
541 root.display()
542 ))
543 })?;
544 let head_index = plain_git_head_index_or_empty(git_repo).map_err(|error| {
545 HeddleError::Config(format!(
546 "failed to inspect Git HEAD tree at '{}': {error}",
547 root.display()
548 ))
549 })?;
550
551 let mut head_entries = BTreeMap::new();
552 for entry in &head_index.entries {
553 head_entries.insert(plain_git_path(&entry.path), (entry.oid, entry.mode));
554 }
555 let mut index_entries = BTreeMap::new();
556 for entry in &index.entries {
557 index_entries.insert(plain_git_path(&entry.path), (entry.oid, entry.mode));
558 }
559
560 let mut added = BTreeSet::new();
561 let mut modified = BTreeSet::new();
562 let mut deleted = BTreeSet::new();
563
564 for (path, (oid, mode)) in &index_entries {
565 match head_entries.get(path) {
566 None => {
567 added.insert(PathBuf::from(path));
568 }
569 Some((head_oid, head_mode)) if (head_oid, head_mode) != (oid, mode) => {
570 modified.insert(PathBuf::from(path));
571 }
572 Some(_) => {}
573 }
574 }
575 for path in head_entries.keys() {
576 if !index_entries.contains_key(path) {
577 deleted.insert(PathBuf::from(path));
578 }
579 }
580
581 for (path, (oid, mode)) in &index_entries {
582 match repo::git_worktree_status::git_worktree_entry_state(root, path, *oid, *mode, None)? {
583 GitWorktreeEntryState::Clean => {}
584 GitWorktreeEntryState::Deleted => {
585 deleted.insert(PathBuf::from(path));
586 }
587 GitWorktreeEntryState::Modified => {
588 modified.insert(PathBuf::from(path));
589 }
590 }
591 }
592
593 let tracked_paths: BTreeSet<&str> = index_entries.keys().map(String::as_str).collect();
594 for path in plain_git_untracked_paths(root, &tracked_paths)? {
595 added.insert(PathBuf::from(path));
596 }
597
598 for path in &added {
599 modified.remove(path);
600 }
601 for path in &deleted {
602 modified.remove(path);
603 }
604
605 Ok(WorktreeStatus {
606 modified: modified.into_iter().collect(),
607 added: added.into_iter().collect(),
608 deleted: deleted.into_iter().collect(),
609 })
610}
611
612fn plain_git_untracked_paths(root: &Path, tracked_paths: &BTreeSet<&str>) -> Result<Vec<String>> {
613 let mut paths = Vec::new();
614 let walker = ignore::WalkBuilder::new(root)
615 .hidden(false)
616 .git_ignore(true)
617 .git_exclude(true)
618 .git_global(true)
619 .filter_entry(|entry| !plain_git_excluded_walk_entry(entry.path()))
620 .build();
621 for entry in walker {
622 let entry = entry.map_err(|error| HeddleError::Config(error.to_string()))?;
623 let file_type = entry.file_type();
624 if !file_type.is_some_and(|file_type| file_type.is_file() || file_type.is_symlink()) {
625 continue;
626 }
627 let path = plain_git_repo_relative_path(root, entry.path())?;
628 if !tracked_paths.contains(path.as_str()) {
629 paths.push(path);
630 }
631 }
632 paths.sort();
633 paths.dedup();
634 Ok(paths)
635}
636
637fn plain_git_excluded_walk_entry(path: &Path) -> bool {
638 path.file_name()
639 .and_then(|name| name.to_str())
640 .is_some_and(|name| name == ".git" || name == ".heddle")
641}
642
643fn plain_git_repo_relative_path(root: &Path, path: &Path) -> Result<String> {
644 let relative = path.strip_prefix(root).map_err(|error| {
645 HeddleError::Config(format!(
646 "failed to relativize Git worktree path '{}': {}",
647 path.display(),
648 error
649 ))
650 })?;
651 Ok(path_to_plain_git_path(relative))
652}
653
654fn path_to_plain_git_path(path: &Path) -> String {
655 path.components()
656 .map(|component| component.as_os_str().to_string_lossy())
657 .collect::<Vec<_>>()
658 .join("/")
659}
660
661fn plain_git_empty_index() -> Index {
662 Index {
663 version: 2,
664 entries: Vec::new(),
665 extensions: Vec::new(),
666 checksum: None,
667 }
668}
669
670fn plain_git_index_or_empty(
671 git_repo: &SleyRepository,
672) -> std::result::Result<Index, sley::GitError> {
673 git_repo
674 .open_index()
675 .map(|index| index.unwrap_or_else(plain_git_empty_index))
676}
677
678fn plain_git_head_index_or_empty(
679 git_repo: &SleyRepository,
680) -> std::result::Result<Index, sley::GitError> {
681 let head = git_repo.head()?;
682 let Some(oid) = head.oid else {
683 return Ok(plain_git_empty_index());
684 };
685 let commit = git_repo.read_commit(&oid)?;
686 git_repo.index_from_tree(&commit.tree)
687}
688
689fn plain_git_path(path: &GitBString) -> String {
690 String::from_utf8_lossy(path.as_bytes()).into_owned()
691}
692
693pub fn build_repository_verification_state(
694 repo: &Repository,
695) -> Result<RepositoryVerificationState> {
696 build_repository_verification_state_with_machine_contract(
697 repo,
698 &MachineContractInput::default(),
699 )
700}
701
702pub fn build_repository_verification_state_with_machine_contract(
703 repo: &Repository,
704 machine_contract_input: &MachineContractInput,
705) -> Result<RepositoryVerificationState> {
706 let worktree_status = if repo.capability() == repo::RepositoryCapability::GitOverlay {
707 repo.git_overlay_worktree_status()
708 } else {
709 native_worktree_status(repo)
710 };
711 let health = build_repository_verification_health_with_worktree_status(repo, &worktree_status);
712 Ok(
713 build_repository_verification_state_with_worktree_status_and_machine_contract(
714 repo,
715 health,
716 &worktree_status,
717 machine_contract_input,
718 ),
719 )
720}
721
722fn native_worktree_status(repo: &Repository) -> Result<Option<WorktreeStatus>> {
723 let Some(state) = repo.current_state()? else {
724 return Ok(Some(WorktreeStatus::default()));
725 };
726 let tree = repo.require_tree(&state.tree)?;
727 repo.compare_worktree_cached(&tree).map(Some)
728}
729
730pub fn build_repository_verification_state_with_worktree_status(
731 repo: &Repository,
732 health: RepositoryVerificationHealth,
733 worktree_status: &Result<Option<WorktreeStatus>>,
734) -> RepositoryVerificationState {
735 build_repository_verification_state_with_worktree_status_and_machine_contract(
736 repo,
737 health,
738 worktree_status,
739 &MachineContractInput::default(),
740 )
741}
742
743pub fn build_repository_verification_state_with_worktree_status_and_machine_contract(
744 repo: &Repository,
745 health: RepositoryVerificationHealth,
746 worktree_status: &Result<Option<WorktreeStatus>>,
747 machine_contract_input: &MachineContractInput,
748) -> RepositoryVerificationState {
749 let git_branch = repo.git_overlay_current_branch().ok().flatten();
750 let heddle_thread = repo.current_lane().ok().flatten();
751 let active_operation = repo.operation_status().ok().flatten().map(|operation| {
752 format!(
753 "{} {} ({})",
754 operation.scope, operation.kind, operation.state
755 )
756 });
757 let remote_drift = repo
758 .git_remote_tracking_status()
759 .ok()
760 .flatten()
761 .map(|remote| remote_tracking_status(&remote).to_string())
762 .unwrap_or_else(|| "clean".to_string());
763 let is_git_overlay = repo.capability() == repo::RepositoryCapability::GitOverlay;
764 let import_state = health
765 .checks
766 .iter()
767 .find(|check| check.name == "import" && check.status != "clean")
768 .or_else(|| health.checks.iter().find(|check| check.name == "import"))
769 .map(|check| check.status.clone())
770 .unwrap_or_else(|| {
771 if is_git_overlay {
772 "git_backed".to_string()
773 } else {
774 "clean".to_string()
775 }
776 });
777 let mapping_state = health
778 .checks
779 .iter()
780 .find(|check| {
781 matches!(check.name.as_str(), "head_mapping" | "tag_mapping")
782 && !verification_status_is_clean(&check.status)
783 })
784 .or_else(|| {
785 health
786 .checks
787 .iter()
788 .find(|check| check.name == "head_mapping")
789 })
790 .map(|check| check.status.clone())
791 .unwrap_or_else(|| {
792 if is_git_overlay {
793 "git_backed".to_string()
794 } else {
795 "clean".to_string()
796 }
797 });
798 let git_worktree_dirty = matches!(
799 worktree_status,
800 Ok(Some(status)) if !status.is_clean()
801 );
802 let worktree_dirty = git_worktree_dirty
803 || health.checks.iter().any(|check| {
804 matches!(check.name.as_str(), "worktree" | "heddle_worktree") && check.status != "clean"
805 });
806 let machine_contract_coverage = machine_contract_input.coverage.clone();
807 let machine_contract_clean = machine_contract_is_clean(&machine_contract_coverage);
808 let mut recovery_commands = health.recovery_commands.clone();
809 let remote_action = remote_sync_action(&health);
810 let (workflow_status, workflow_summary) = workflow_status(repo, heddle_thread.as_deref());
811 let workflow_action = if health.clean && workflow_status == "ready" {
812 workflow_primary_action(repo)
813 } else {
814 None
815 };
816 if health.clean && !machine_contract_clean {
817 recovery_commands.push("heddle doctor schemas --output json".to_string());
818 }
819 let recommended_action = if health.clean {
820 if !machine_contract_clean {
821 "heddle doctor schemas --output json".to_string()
822 } else {
823 workflow_action
824 .clone()
825 .or_else(|| remote_action.clone())
826 .unwrap_or_default()
827 }
828 } else {
829 recovery_commands.first().cloned().unwrap_or_default()
830 };
831 let checks = verification_checks_from_health(
832 &health,
833 &machine_contract_coverage,
834 is_git_overlay,
835 &workflow_status,
836 &workflow_summary,
837 );
838 RepositoryVerificationState {
839 verified: health.clean && machine_contract_clean,
840 status: if health.clean && !machine_contract_clean {
841 "machine_contract_gaps".to_string()
842 } else {
843 health.status.clone()
844 },
845 repository_mode: repo.capability_label().to_string(),
846 heddle_initialized: true,
847 git_branch,
848 heddle_thread,
849 worktree_dirty,
850 worktree_state: if worktree_dirty { "dirty" } else { "clean" }.to_string(),
851 import_state,
852 mapping_state,
853 remote_drift,
854 active_operation,
855 default_remote: default_remote_name(repo),
856 clone_verification: if repo.capability() == repo::RepositoryCapability::GitOverlay {
857 if health.clean {
858 "verified"
859 } else if matches!(
860 health.status.as_str(),
861 "dirty_worktree" | "needs_checkpoint"
862 ) {
863 "not_checked"
864 } else {
865 "blocked"
866 }
867 } else {
868 "not_applicable"
869 }
870 .to_string(),
871 machine_contract: machine_contract_status(&machine_contract_coverage).to_string(),
872 machine_contract_coverage,
873 workflow_status,
874 workflow_summary,
875 summary: health.summary,
876 recommended_action: recommended_action.clone(),
877 recommended_action_template: action_template(&recommended_action),
878 recovery_commands: recovery_commands.clone(),
879 recovery_action_templates: action_templates(&recovery_commands),
880 checks,
881 }
882}
883
884fn verification_checks_from_health(
885 health: &RepositoryVerificationHealth,
886 coverage: &MachineContractCoverage,
887 is_git_overlay: bool,
888 workflow_status: &str,
889 workflow_summary: &str,
890) -> Vec<VerificationCheck> {
891 let mut checks = vec![
892 git_verification_check(is_git_overlay),
893 verification_check(
894 "Heddle",
895 true,
896 "clean",
897 "Heddle data is initialized",
898 None,
899 Vec::new(),
900 ),
901 mapping_verification_check(health, is_git_overlay),
902 worktree_verification_check(health),
903 remote_verification_check(health),
904 operation_verification_check(health),
905 workflow_verification_check(health, workflow_status, workflow_summary),
906 ];
907 checks.push(machine_contract_verification_check(coverage));
908 checks.push(clone_verification_check(health, is_git_overlay));
909 checks
910}
911
912fn machine_contract_verification_check(coverage: &MachineContractCoverage) -> VerificationCheck {
913 let mut details = BTreeMap::new();
914 details.insert("coverage_status".to_string(), coverage.status.clone());
915 details.insert("coverage_summary".to_string(), coverage.summary.clone());
916 details.insert(
917 "verified_scope".to_string(),
918 coverage.verified_scope.clone(),
919 );
920 details.insert(
921 "advanced_scope".to_string(),
922 coverage.advanced_scope.clone(),
923 );
924 details.insert(
925 "catalog_commands_total".to_string(),
926 coverage.catalog_commands_total.to_string(),
927 );
928 details.insert(
929 "json_commands_total".to_string(),
930 coverage.json_commands_total.to_string(),
931 );
932 details.insert(
933 "json_commands_with_schema".to_string(),
934 coverage.json_commands_with_schema.to_string(),
935 );
936 details.insert(
937 "json_commands_without_schema".to_string(),
938 coverage.json_commands_without_schema.to_string(),
939 );
940 details.insert(
941 "json_commands_with_accepted_opaque_schema".to_string(),
942 coverage
943 .json_commands_with_accepted_opaque_schema
944 .to_string(),
945 );
946 details.insert(
947 "verified_scope_json_commands_total".to_string(),
948 coverage.verified_scope_json_commands_total.to_string(),
949 );
950 let mut check = verification_check(
951 "Machine contract",
952 machine_contract_is_clean(coverage),
953 machine_contract_status(coverage),
954 &coverage.summary,
955 (!machine_contract_is_clean(coverage))
956 .then(|| "heddle doctor schemas --output json".to_string()),
957 if machine_contract_is_clean(coverage) {
958 Vec::new()
959 } else {
960 vec!["heddle doctor schemas --output json".to_string()]
961 },
962 );
963 check.details = details;
964 check
965}
966
967fn git_verification_check(is_git_overlay: bool) -> VerificationCheck {
968 if is_git_overlay {
969 verification_check(
970 "Git",
971 true,
972 "clean",
973 "Git overlay repository is present",
974 None,
975 Vec::new(),
976 )
977 } else {
978 verification_check(
979 "Git",
980 true,
981 "not_applicable",
982 "Heddle-native repository is running in non-overlay mode",
983 None,
984 Vec::new(),
985 )
986 }
987}
988
989fn mapping_verification_check(
990 health: &RepositoryVerificationHealth,
991 is_git_overlay: bool,
992) -> VerificationCheck {
993 if !is_git_overlay {
994 return verification_check(
995 "Mapping",
996 true,
997 "not_applicable",
998 "native Heddle refs do not require Git Projection Mapping",
999 None,
1000 Vec::new(),
1001 );
1002 }
1003 if let Some(check) = health
1004 .checks
1005 .iter()
1006 .find(|check| check.name == "head_mapping" && !verification_status_is_clean(&check.status))
1007 {
1008 return verification_check_from_health("Mapping", check, health);
1009 }
1010 if let Some(check) = find_health_check(health, "import")
1011 && check.status != "clean"
1012 {
1013 return verification_check_from_health("Mapping", check, health);
1014 }
1015 if let Some(check) = find_health_check(health, "tag_mapping")
1016 && check.status != "clean"
1017 {
1018 return verification_check_from_health("Mapping", check, health);
1019 }
1020 if let Some(check) = find_health_check(health, "head_mapping") {
1021 if check.status == "git_backed" && health.status == "dirty_worktree" {
1022 return verification_check(
1023 "Mapping",
1024 true,
1025 "clean",
1026 "Git-backed branch mapping is not blocking verification",
1027 None,
1028 Vec::new(),
1029 );
1030 }
1031 return verification_check_from_health("Mapping", check, health);
1032 }
1033 verification_check(
1034 "Mapping",
1035 true,
1036 "clean",
1037 "Git branch tips map to imported Heddle state",
1038 None,
1039 Vec::new(),
1040 )
1041}
1042
1043fn worktree_verification_check(health: &RepositoryVerificationHealth) -> VerificationCheck {
1044 for name in ["worktree", "heddle_worktree"] {
1045 if let Some(check) = find_health_check(health, name)
1046 && check.status != "clean"
1047 {
1048 return verification_check_from_health("Worktree", check, health);
1049 }
1050 }
1051 for name in ["worktree", "heddle_worktree"] {
1052 if let Some(check) = find_health_check(health, name) {
1053 return verification_check_from_health("Worktree", check, health);
1054 }
1055 }
1056 if !health.clean {
1057 return verification_check(
1058 "Worktree",
1059 false,
1060 "not_checked",
1061 "worktree agreement is checked after the primary verification blocker is resolved",
1062 health.recovery_commands.first().cloned(),
1063 health.recovery_commands.clone(),
1064 );
1065 }
1066 verification_check(
1067 "Worktree",
1068 true,
1069 "clean",
1070 "worktree has no uncommitted Git/Heddle disagreement",
1071 None,
1072 Vec::new(),
1073 )
1074}
1075
1076fn remote_verification_check(health: &RepositoryVerificationHealth) -> VerificationCheck {
1077 if let Some(check) = find_health_check(health, "remote_tracking") {
1078 if matches!(check.status.as_str(), "remote_ahead" | "remote_untracked") {
1079 let mut remote_check = verification_check(
1080 "Remote",
1081 true,
1082 &check.status,
1083 &check.summary,
1084 remote_sync_action(health),
1085 Vec::new(),
1086 );
1087 remote_check.details = check.details.clone();
1088 return remote_check;
1089 }
1090 return verification_check_from_health("Remote", check, health);
1091 }
1092 verification_check(
1093 "Remote",
1094 true,
1095 "clean",
1096 "remote tracking has no blocking drift",
1097 None,
1098 Vec::new(),
1099 )
1100}
1101
1102fn operation_verification_check(health: &RepositoryVerificationHealth) -> VerificationCheck {
1103 if let Some(check) = find_health_check(health, "operation") {
1104 return verification_check_from_health("Operation", check, health);
1105 }
1106 verification_check(
1107 "Operation",
1108 true,
1109 "clean",
1110 "no Git or Heddle operation in progress",
1111 None,
1112 Vec::new(),
1113 )
1114}
1115
1116fn workflow_verification_check(
1117 health: &RepositoryVerificationHealth,
1118 workflow_status: &str,
1119 workflow_summary: &str,
1120) -> VerificationCheck {
1121 if let Some(check) = find_health_check(health, "thread_integration_metadata")
1122 && check.status != "clean"
1123 {
1124 return verification_check_from_health("Workflow", check, health);
1125 }
1126 if !health.clean {
1127 return verification_check(
1128 "Workflow",
1129 false,
1130 "blocked",
1131 "workflow readiness is checked after the primary verification blocker is resolved",
1132 health.recovery_commands.first().cloned(),
1133 health.recovery_commands.clone(),
1134 );
1135 }
1136 verification_check(
1137 "Workflow",
1138 true,
1139 workflow_status,
1140 workflow_summary,
1141 None,
1142 Vec::new(),
1143 )
1144}
1145
1146fn clone_verification_check(
1147 health: &RepositoryVerificationHealth,
1148 is_git_overlay: bool,
1149) -> VerificationCheck {
1150 if !is_git_overlay {
1151 return verification_check(
1152 "Clone",
1153 true,
1154 "not_applicable",
1155 "native Heddle state is the checkout authority",
1156 None,
1157 Vec::new(),
1158 );
1159 }
1160 if health.clean {
1161 return verification_check(
1162 "Clone",
1163 true,
1164 "verified",
1165 "Git checkout and Heddle mapping agree",
1166 None,
1167 Vec::new(),
1168 );
1169 }
1170 if matches!(
1171 health.status.as_str(),
1172 "dirty_worktree" | "needs_checkpoint"
1173 ) {
1174 return verification_check(
1175 "Clone",
1176 true,
1177 "not_checked",
1178 "clone verification waits for a clean worktree",
1179 None,
1180 Vec::new(),
1181 );
1182 }
1183 verification_check(
1184 "Clone",
1185 false,
1186 "blocked",
1187 "clone verification is blocked until verification checks agree",
1188 health.recovery_commands.first().cloned(),
1189 health.recovery_commands.clone(),
1190 )
1191}
1192
1193fn verification_check_from_health(
1194 name: &str,
1195 health_check: &crate::status::RepositoryVerificationCheck,
1196 health: &RepositoryVerificationHealth,
1197) -> VerificationCheck {
1198 let recommended_action = (!verification_status_is_clean(&health_check.status))
1199 .then(|| health.recovery_commands.first().cloned())
1200 .flatten();
1201 let recovery_commands = if recommended_action.is_some() {
1202 health.recovery_commands.clone()
1203 } else {
1204 Vec::new()
1205 };
1206 let mut check = verification_check(
1207 name,
1208 verification_status_is_clean(&health_check.status),
1209 &health_check.status,
1210 &health_check.summary,
1211 recommended_action,
1212 recovery_commands,
1213 );
1214 check.details = health_check.details.clone();
1215 check
1216}
1217
1218fn remote_sync_action(health: &RepositoryVerificationHealth) -> Option<String> {
1219 find_health_check(health, "remote_tracking").and_then(|check| {
1220 matches!(check.status.as_str(), "remote_ahead" | "remote_untracked")
1221 .then(|| "heddle push".to_string())
1222 })
1223}
1224
1225fn find_health_check<'a>(
1226 health: &'a RepositoryVerificationHealth,
1227 name: &str,
1228) -> Option<&'a crate::status::RepositoryVerificationCheck> {
1229 health.checks.iter().find(|check| check.name == name)
1230}
1231
1232fn verification_status_is_clean(status: &str) -> bool {
1233 matches!(
1234 status,
1235 "clean"
1236 | "available"
1237 | "git_backed"
1238 | "not_applicable"
1239 | "verified"
1240 | "remote_ahead"
1241 | "remote_untracked"
1242 )
1243}
1244
1245fn workflow_status(repo: &Repository, current_thread: Option<&str>) -> (String, String) {
1246 let ready_threads = ThreadManager::new(repo.heddle_dir())
1247 .list()
1248 .unwrap_or_default()
1249 .into_iter()
1250 .filter(|thread| thread.state == repo::ThreadState::Ready)
1251 .collect::<Vec<_>>();
1252 if ready_threads.is_empty() {
1253 return (
1254 "clean".to_string(),
1255 "no ready thread actions require attention".to_string(),
1256 );
1257 }
1258 if ready_threads.iter().all(|thread| {
1259 thread
1260 .target_thread
1261 .as_deref()
1262 .zip(current_thread)
1263 .is_some_and(|(target, current)| target != current)
1264 }) {
1265 return (
1266 "clean".to_string(),
1267 "ready thread actions target another thread".to_string(),
1268 );
1269 }
1270 (
1271 "ready".to_string(),
1272 "ready thread actions are waiting to land".to_string(),
1273 )
1274}
1275
1276fn workflow_primary_action(repo: &Repository) -> Option<String> {
1277 let current_thread = repo.current_lane().ok().flatten();
1278 let opened_from_dedicated_checkout = repo
1279 .heddle_dir()
1280 .parent()
1281 .is_some_and(|main_root| main_root != repo.root());
1282 ThreadManager::new(repo.heddle_dir())
1283 .list()
1284 .ok()?
1285 .into_iter()
1286 .filter(|thread| thread.state == repo::ThreadState::Ready)
1287 .find_map(|mut thread| {
1288 let _ = refresh_thread_freshness(repo, &mut thread);
1289 let actionable = thread
1290 .target_thread
1291 .as_deref()
1292 .map(|target| {
1293 current_thread.as_deref() == Some(target) || opened_from_dedicated_checkout
1294 })
1295 .unwrap_or(true);
1296 if !actionable {
1297 return None;
1298 }
1299 let advice = describe_thread_advice(&thread, false, 0, false);
1300 (!advice.recommended_action.trim().is_empty()).then_some(advice.recommended_action)
1301 })
1302}
1303
1304fn verification_check(
1305 name: &str,
1306 clean: bool,
1307 status: &str,
1308 summary: &str,
1309 recommended_action: Option<String>,
1310 recovery_commands: Vec<String>,
1311) -> VerificationCheck {
1312 VerificationCheck {
1313 name: name.to_string(),
1314 status: status.to_string(),
1315 clean,
1316 summary: summary.to_string(),
1317 recommended_action: recommended_action.clone(),
1318 recommended_action_template: recommended_action.as_deref().and_then(action_template),
1319 recovery_action_templates: action_templates(&recovery_commands),
1320 recovery_commands,
1321 details: BTreeMap::new(),
1322 }
1323}
1324
1325pub fn action_template(action: &str) -> Option<ActionTemplate> {
1326 let trimmed = action.trim();
1327 if trimmed.is_empty() {
1328 return None;
1329 }
1330 recommended_action_templates()
1331 .iter()
1332 .find(|template| template.action == trimmed)
1333 .cloned()
1334 .or_else(|| concrete_action_template(trimmed))
1335}
1336
1337pub fn action_templates(commands: &[String]) -> Vec<ActionTemplate> {
1338 commands
1339 .iter()
1340 .filter_map(|command| action_template(command))
1341 .collect()
1342}
1343
1344fn concrete_action_template(action: &str) -> Option<ActionTemplate> {
1345 if action.contains("...") || (action.contains('<') && action.contains('>')) {
1346 return None;
1347 }
1348 let argv = split_action(action).ok()?;
1349 (argv.first().map(String::as_str) == Some("heddle")).then(|| ActionTemplate {
1350 action: action.to_string(),
1351 argv_template: normalize_heddle_argv(argv),
1352 required_inputs: Vec::new(),
1353 agent_may_fill: false,
1354 })
1355}
1356
1357fn recommended_action_templates() -> Vec<ActionTemplate> {
1358 [
1359 (
1360 "heddle capture -m \"...\"",
1361 &["heddle", "capture", "-m", "<message>"][..],
1362 &["message"][..],
1363 true,
1364 ),
1365 (
1366 "heddle checkpoint -m \"...\"",
1367 &["heddle", "checkpoint", "-m", "<message>"][..],
1368 &["message"][..],
1369 true,
1370 ),
1371 (
1372 "heddle commit -m \"...\"",
1373 &["heddle", "commit", "-m", "<message>"][..],
1374 &["message"][..],
1375 true,
1376 ),
1377 (
1378 "heddle commit --all -m \"...\"",
1379 &["heddle", "commit", "--all", "-m", "<message>"][..],
1380 &["message"][..],
1381 true,
1382 ),
1383 ("heddle init", &["heddle", "init"][..], &[][..], false),
1384 (
1385 "heddle init --principal-name <name> --principal-email <email>",
1386 &[
1387 "heddle",
1388 "init",
1389 "--principal-name",
1390 "<name>",
1391 "--principal-email",
1392 "<email>",
1393 ][..],
1394 &["name", "email"][..],
1395 true,
1396 ),
1397 (
1398 "heddle ready -m \"...\"",
1399 &["heddle", "ready", "-m", "<message>"][..],
1400 &["message"][..],
1401 true,
1402 ),
1403 ("heddle status", &["heddle", "status"][..], &[][..], false),
1404 (
1405 "heddle switch <branch>",
1406 &["heddle", "switch", "<branch>"][..],
1407 &["branch"][..],
1408 false,
1409 ),
1410 ("heddle verify", &["heddle", "verify"][..], &[][..], false),
1411 (
1412 "heddle diagnose",
1413 &["heddle", "diagnose"][..],
1414 &[][..],
1415 false,
1416 ),
1417 (
1418 "heddle doctor schemas --output json",
1419 &["heddle", "doctor", "schemas", "--output", "json"][..],
1420 &[][..],
1421 false,
1422 ),
1423 ]
1424 .into_iter()
1425 .map(
1426 |(action, argv_template, required_inputs, agent_may_fill)| ActionTemplate {
1427 action: action.to_string(),
1428 argv_template: normalize_heddle_argv(
1429 argv_template.iter().map(|arg| (*arg).to_string()).collect(),
1430 ),
1431 required_inputs: required_inputs
1432 .iter()
1433 .map(|input| (*input).to_string())
1434 .collect(),
1435 agent_may_fill,
1436 },
1437 )
1438 .collect()
1439}
1440
1441fn normalize_heddle_argv(mut argv: Vec<String>) -> Vec<String> {
1442 if argv.first().is_some_and(|first| first == "heddle") {
1443 argv[0] = heddle_argv0();
1444 }
1445 argv
1446}
1447
1448fn heddle_argv0() -> String {
1449 match std::env::current_exe() {
1450 Ok(path) => {
1451 let file_name = path.file_name().and_then(|name| name.to_str());
1452 if matches!(file_name, Some("heddle") | Some("heddle.exe")) {
1453 path.display().to_string()
1454 } else {
1455 "heddle".to_string()
1456 }
1457 }
1458 Err(_) => "heddle".to_string(),
1459 }
1460}
1461
1462fn split_action(action: &str) -> std::result::Result<Vec<String>, String> {
1463 let mut args = Vec::new();
1464 let mut current = String::new();
1465 let mut chars = action.chars().peekable();
1466 let mut in_single_quote = false;
1467 let mut in_double_quote = false;
1468 while let Some(ch) = chars.next() {
1469 match (ch, in_single_quote, in_double_quote) {
1470 ('\'', false, false) => in_single_quote = true,
1471 ('\'', true, false) => in_single_quote = false,
1472 ('"', false, false) => in_double_quote = true,
1473 ('"', false, true) => in_double_quote = false,
1474 ('\\', false, _) => match chars.next() {
1475 Some(next) => current.push(next),
1476 None => current.push('\\'),
1477 },
1478 (ch, false, false) if ch.is_whitespace() => {
1479 if !current.is_empty() {
1480 args.push(std::mem::take(&mut current));
1481 }
1482 }
1483 (ch, _, _) => current.push(ch),
1484 }
1485 }
1486 if in_single_quote || in_double_quote {
1487 return Err("unterminated quote".to_string());
1488 }
1489 if !current.is_empty() {
1490 args.push(current);
1491 }
1492 Ok(args)
1493}
1494
1495fn machine_contract_is_clean(coverage: &MachineContractCoverage) -> bool {
1496 if matches!(coverage.status.as_str(), "not_checked" | "not_applicable") {
1497 return true;
1498 }
1499 coverage.verified_scope_json_commands_without_schema == 0
1500 && coverage.verified_scope_mutating_commands_without_schema == 0
1501 && coverage.undocumented_schema_verbs_total == 0
1502 && coverage.unaccepted_opaque_schema_verbs_total == 0
1503}
1504
1505pub fn machine_contract_status(coverage: &MachineContractCoverage) -> &'static str {
1506 match coverage.status.as_str() {
1507 "not_checked" => "not_checked",
1508 "not_applicable" => "not_applicable",
1509 _ if machine_contract_is_clean(coverage) => "available",
1510 _ => "available_with_schema_gaps",
1511 }
1512}
1513
1514pub fn verify(ctx: &ExecutionContext, opts: VerifyOptions) -> Result<VerifyReport> {
1515 let fallback;
1516 let start = if let Some(start) = opts.start_path.as_deref() {
1517 start
1518 } else if let Some(start) = ctx.start_path() {
1519 start
1520 } else {
1521 fallback = std::env::current_dir().map_err(HeddleError::Io)?;
1522 fallback.as_path()
1523 };
1524
1525 let probe_start = Instant::now();
1526 let plain_git_probe = build_plain_git_verification_probe_with_machine_contract(
1527 start,
1528 &opts.machine_contract_input,
1529 )?;
1530 let plain_git_probe_ms = probe_start.elapsed().as_millis();
1531 let mut profile = VerifyProfile {
1532 plain_git_probe_ms,
1533 ..VerifyProfile::default()
1534 };
1535
1536 if let Some(probe) = plain_git_probe {
1537 return Ok(VerifyReport {
1538 output_kind: "verify",
1539 clean: probe.trust.verified,
1540 repository_label: repository_mode_label("plain-git", "git-only"),
1541 repository_context: None,
1542 trust: probe.trust,
1543 profile,
1544 });
1545 }
1546
1547 let opened;
1548 let repo = if let Some(repo) = ctx.repo() {
1549 repo
1550 } else {
1551 let repo_open_start = Instant::now();
1552 opened = Repository::open(start)?;
1553 profile.repo_open_ms = repo_open_start.elapsed().as_millis();
1554 &opened
1555 };
1556 let verification_start = Instant::now();
1557 let trust = build_repository_verification_state_with_machine_contract(
1558 repo,
1559 &opts.machine_contract_input,
1560 )?;
1561 profile.verification_ms = verification_start.elapsed().as_millis();
1562 let presentation = repository_presentation(repo, None, None);
1563 Ok(VerifyReport {
1564 output_kind: "verify",
1565 clean: trust.verified,
1566 repository_label: presentation.label,
1567 repository_context: presentation.context,
1568 trust,
1569 profile,
1570 })
1571}
1572
1573pub fn repository_mode_label(capability: &str, storage_model: &str) -> String {
1577 if capability == "git-overlay" || storage_model == "git+heddle-sidecar" {
1578 "Git + Heddle".to_string()
1579 } else if capability == "plain-git" || storage_model == "git-only" {
1580 "Git repo (setup needed)".to_string()
1581 } else if capability == "native"
1582 || capability == "native-heddle"
1583 || storage_model == "heddle-native"
1584 {
1585 "Heddle native".to_string()
1586 } else {
1587 capability.to_string()
1588 }
1589}
1590
1591pub fn repository_presentation(
1596 repo: &Repository,
1597 target_thread: Option<&str>,
1598 parent_thread: Option<&str>,
1599) -> RepositoryPresentation {
1600 if let Some(parent_root) = managed_git_overlay_parent_root(repo) {
1601 let thread = current_child_thread(repo);
1602 let target_thread = target_thread.map(ToString::to_string).or_else(|| {
1603 thread
1604 .as_ref()
1605 .and_then(|thread| thread.target_thread.clone())
1606 });
1607 let parent_thread = parent_thread.map(ToString::to_string).or_else(|| {
1608 thread
1609 .as_ref()
1610 .and_then(|thread| thread.parent_thread.clone())
1611 });
1612 return RepositoryPresentation {
1613 label: "Git + Heddle isolated checkout".to_string(),
1614 context: Some(RepositoryContextInfo {
1615 kind: "git-overlay-isolated-checkout".to_string(),
1616 parent_repository: Some(parent_root.display().to_string()),
1617 target_thread,
1618 parent_thread,
1619 }),
1620 };
1621 }
1622
1623 RepositoryPresentation {
1624 label: repository_mode_label(repo.capability_label(), repo.storage_model_label()),
1625 context: None,
1626 }
1627}
1628
1629fn managed_git_overlay_parent_root(repo: &Repository) -> Option<PathBuf> {
1630 let parent_root = repo.heddle_dir().parent()?;
1631 if paths_equal(parent_root, repo.root()) {
1632 return None;
1633 }
1634 parent_root
1635 .join(".git")
1636 .exists()
1637 .then(|| parent_root.to_path_buf())
1638}
1639
1640fn current_child_thread(repo: &Repository) -> Option<Thread> {
1641 let manager = ThreadManager::new(repo.heddle_dir());
1642 if let Ok(Some(thread)) = manager.find_by_execution_root(repo.root()) {
1643 return Some(thread);
1644 }
1645 let lane = repo.current_lane().ok().flatten()?;
1646 manager.find_by_thread(&lane).ok().flatten()
1647}
1648
1649fn paths_equal(left: &Path, right: &Path) -> bool {
1650 let left = left.canonicalize().unwrap_or_else(|_| left.to_path_buf());
1651 let right = right.canonicalize().unwrap_or_else(|_| right.to_path_buf());
1652 left == right
1653}
1654
1655pub fn dirty_path_count(status: &WorktreeStatus) -> usize {
1656 status.modified.len() + status.added.len() + status.deleted.len()
1657}