Skip to main content

heddle_core/
verify.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Repository verification facade.
3
4use 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    /// Whether an agent may replace placeholders in `argv_template`.
163    ///
164    /// When `agent_may_fill` is false, treat `action` and `argv_template` as
165    /// display-only: do not substitute `<name>`/`<url>` placeholders. Surface
166    /// the template to a human or discard it. Substituting and running it will
167    /// pass literal `<name>` to Heddle and fail.
168    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
1573/// Human-facing repository mode label. JSON keeps the exact repository mode
1574/// values; text output uses product language instead of storage implementation
1575/// names.
1576pub 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
1591/// Presentation-only repository identity. This deliberately leaves
1592/// `Repository::capability_label()` untouched: an isolated checkout that shares
1593/// a Git-overlay object store is still technically opened through the native
1594/// Heddle storage path, but user-facing status should say what manages it.
1595pub 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}