Skip to main content

fallow_api/
lib.rs

1//! Programmatic API contract types for fallow.
2//!
3//! Runtime execution for dead-code, duplication and health lives here. The
4//! default health runner, `EngineHealthRunner`, runs the engine pipeline.
5//! Tests and hosts can inject a different runner. This crate owns the
6//! CLI-independent option, error, and output contracts so NAPI, future Rust
7//! embedders, and the engine facade can share them without depending on the
8//! CLI crate.
9#![warn(missing_docs)]
10#![cfg_attr(
11    test,
12    allow(
13        clippy::expect_used,
14        reason = "tests use expect to keep fixture setup concise"
15    )
16)]
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20use std::sync::atomic::AtomicBool;
21
22use fallow_config::EmailMode;
23use fallow_output::EffortEstimate;
24use serde::Serialize;
25
26mod analysis_context;
27mod audit_complexity;
28/// Stable per-finding keys and audit ledgers that compare head results against
29/// a base snapshot, plus helpers that annotate output JSON with
30/// introduced-vs-pre-existing attribution.
31pub mod audit_keys;
32pub mod audit_output;
33pub mod audit_run;
34pub mod combined_output;
35/// One-line-per-finding compact text builders for dead-code, grouped, health,
36/// and duplication output.
37pub mod compact_output;
38pub mod coverage;
39pub mod dead_code_codeclimate;
40pub mod dead_code_sarif;
41pub mod decision_surface;
42pub mod dependency_deltas;
43pub mod diff_risk;
44pub mod doctor;
45pub mod dupes_output;
46pub mod editor;
47pub mod explain;
48pub mod grouped_output;
49pub mod health_codeclimate;
50mod health_groups_markdown;
51pub mod json_output;
52pub mod list_output;
53mod list_runtime;
54/// Markdown report builders for dead-code, grouped, duplication, health, and
55/// walkthrough output.
56pub mod markdown_output;
57pub mod mutation;
58mod next_steps;
59pub mod output_contracts;
60pub mod ownership;
61pub mod review_deltas;
62pub mod routing;
63pub mod runtime;
64mod runtime_json;
65mod runtime_output;
66pub mod sarif_output;
67/// JSON Schema documents and zero-config rule defaults re-exported from
68/// `fallow-config` for embedders such as the MCP resource surface.
69pub mod schemas;
70pub mod security_output;
71/// Local-only advisory similar-code discovery and provider status.
72pub mod similar_code;
73mod type_aware;
74pub use analysis_context::{ProgrammaticAnalysisContext, resolve_programmatic_analysis_context};
75pub use audit_output::{
76    AuditAttribution, AuditCodeClimateOutputInput, AuditJsonHeaderInput, AuditJsonOutputInput,
77    AuditSarifOutputInput, AuditSummary, AuditVerdict, attach_audit_diff_risk_attribution,
78    attach_audit_duplication_demotion_attribution, attach_audit_styling_attribution,
79    attach_audit_wire_attribution, build_audit_codeclimate, build_audit_codeclimate_issues,
80    build_audit_header_json, build_audit_header_map, build_audit_sarif,
81    build_diff_risk_codeclimate, build_review_brief_header, serialize_audit_json,
82};
83pub use combined_output::{
84    CombinedCheckJsonSection, CombinedJsonOutputInput, serialize_combined_dupes_json,
85    serialize_combined_health_json, serialize_combined_json,
86};
87pub use compact_output::{
88    build_compact_lines, build_diff_risk_compact_lines, build_duplication_compact_lines,
89    build_grouped_compact_lines, build_health_compact_lines,
90};
91pub use coverage::{
92    CoverageInputError, CoverageInputSource, CoverageInputs, resolve_coverage_inputs,
93};
94pub use dead_code_codeclimate::build_codeclimate;
95pub use dead_code_sarif::build_sarif;
96pub use doctor::{DoctorOptions, run_doctor, run_doctor_with_cache_dir};
97pub use dupes_output::{
98    AttributedCloneGroup, AttributedCloneGroupFinding, AttributedInstance, CloneDemotionReason,
99    CloneFamilyFinding, CloneGroupFinding, CombinedDupesSection, DupesReportPayload,
100    DuplicationGroup, DuplicationGrouping, build_duplication_codeclimate,
101};
102pub use editor::{
103    ChangedFilesError, EditorAnalysisOutput, EditorAnalysisResults, EditorAnalysisSession,
104    EditorCloneFamily, EditorCloneFingerprintSet, EditorCloneGroup, EditorCloneInstance,
105    EditorDeadCodeAnalysisOutput, EditorDuplicationReport, EditorDuplicationStats,
106    EditorInlineComplexityExceeded, EditorInlineComplexityFinding, EditorMirroredDirectory,
107    EditorProjectAnalysisOutput, EditorRefactoringKind, EditorRefactoringSuggestion,
108    EditorSessionParseCounts, collect_inline_complexity, editor_duplicates, editor_extract,
109    editor_results, editor_security, editor_suppress, filter_inline_complexity_by_change_scope,
110    filter_inline_complexity_by_changed_files, resolve_git_toplevel,
111    try_get_changed_files_with_toplevel,
112};
113pub use explain::{
114    AUDIT_RULES, CHECK_RULES, DUPES_RULES, FLAGS_RULES, HEALTH_RULES, RuleDef, RuleGuide,
115    SECURITY_RULES, all_rules, bare_rule_id, coverage_analyze_meta, coverage_setup_meta,
116    explain_issue_type, rule_by_id, rule_by_token, rule_command, rule_docs_url, rule_guide,
117    rule_severity_key, security_meta, serialize_explain_programmatic_json, unknown_explain_error,
118};
119pub use fallow_config::levenshtein::closest_match;
120pub use fallow_config::{AuditGate, HealthConfig, TypeAwareRequire};
121/// Engine-owned change scope of one run: a global changed-file set or the
122/// per-workspace Git baselines of `workspaces.changedSince`.
123pub use fallow_engine::change_scope::{
124    ChangeScope, ChangeScopeOwner, ChangeScopeRequest, first_package_baselines,
125    package_baseline_statuses,
126};
127/// Engine-owned per-workspace Git baseline state used by analysis surfaces.
128pub use fallow_engine::package_baselines::{PackageBaselineError, PackageChangeScope};
129/// Parsed modules that a long-lived process keeps across analysis calls.
130///
131/// A process that runs many calls on the same project, such as the MCP
132/// server, installs a store once. Each later analysis session then takes the
133/// modules of an unchanged file list from memory and does no parse work.
134pub use fallow_engine::warm_parse;
135/// Shared JSON and editor notification row for an applied package Git ref.
136pub use fallow_output::PackageBaselineStatus;
137pub use fallow_output::serialize_similar_code_json_output;
138pub use fallow_types::trace::{
139    CloneTrace, DependencyTrace, ExportReference, ExportTrace, FileTrace, ReExportChain,
140    TracedCloneGroup, TracedExport, TracedReExport,
141};
142pub use fallow_types::trace_usage::{
143    DependencyUsage, DependencyUsageQuery, SitePageRequest, UsageQueryError,
144};
145pub use grouped_output::{
146    ResultGroup, UNOWNED_GROUP_LABEL, build_duplication_grouping_with, group_analysis_results_with,
147    health_signal_header_part, largest_clone_group_owner_with,
148};
149pub use health_codeclimate::build_health_codeclimate;
150pub use health_groups_markdown::build_health_groups_markdown;
151pub use json_output::{
152    CheckJsonExtraOutputs, CheckJsonOutputInput, CheckJsonPayloadInput, DuplicationJsonOutputInput,
153    GroupedCheckJsonOutputInput, GroupedDuplicationJsonOutputInput, serialize_check_json,
154    serialize_check_json_payload, serialize_duplication_json, serialize_grouped_check_json,
155    serialize_grouped_duplication_json,
156};
157pub use list_output::{
158    ListJsonEnvelope, ListJsonOutputInput, build_list_json_output, serialize_list_json_output,
159};
160pub use list_runtime::{
161    BoundaryData, ListBoundariesOptions, ListBoundariesProgrammaticOutput, LogicalGroupInfo,
162    ProjectInfoOptions, ProjectInfoProgrammaticOutput, RuleInfo, ZoneInfo, boundary_data_to_output,
163    compute_boundary_data, run_list_boundaries, run_project_info,
164    serialize_list_boundaries_programmatic_json, serialize_project_info_programmatic_json,
165};
166pub use markdown_output::{
167    ComplexityListing, build_diff_risk_markdown, build_duplication_markdown,
168    build_grouped_markdown, build_health_markdown, build_markdown, build_walkthrough_markdown,
169};
170pub use mutation::{
171    MutationRunOptions, MutationRunScope, apply_mutation_verdicts, validate_mutation_verdicts,
172};
173pub use output_contracts::{
174    AuditOutput, BoundariesListLogicalGroup, BoundariesListRule, BoundariesListZone,
175    BoundariesListing, CombinedOutput, FallowOutput, ImpactOutput, ListBoundariesOutput,
176    ListEntryPointOutput, ListOutput, ListPluginOutput, ReviewBriefWireOutput, SecurityGate,
177    SecurityOutput, SecurityOutputConfig, SecuritySummaryOutput, SimilarCodeCandidateSnapshot,
178    SimilarCodeOutput, TraceOutput, WorkspacesOutput,
179};
180pub use runtime::TYPE_AWARE_CONFIG_UNSUPPORTED;
181pub use runtime::{
182    AUDIT_CONFIG_BASELINE_UNSUPPORTED, ArchitectureOutput, ArchitectureProgrammaticOutput,
183    AuditProgrammaticKeySnapshot, AuditProgrammaticOutput, BoundaryViolationsOutput,
184    BoundaryViolationsProgrammaticOutput, CircularDependenciesOutput,
185    CircularDependenciesProgrammaticOutput, CombinedProgrammaticOutput, DeadCodeOutput,
186    DeadCodeProgrammaticOutput, DecisionSurfaceProgrammaticOutput, DuplicationOutput,
187    DuplicationProgrammaticOutput, EngineHealthRunner, FeatureFlagsOutput,
188    FeatureFlagsProgrammaticOutput, HealthJsonReportInput, HealthProgrammaticOutput,
189    ProgrammaticHealthAnalysis, ProgrammaticHealthNextStepFacts, ProgrammaticHealthRun,
190    ProgrammaticHealthRunner, TraceClassMemberOutput, TraceCloneBenchmarkResult, TraceCloneOutput,
191    TraceCloneProgrammaticOutput, TraceDependencyOutput, TraceDependencyProgrammaticOutput,
192    TraceErrorOutput, TraceErrorProgrammaticOutput, TraceExportOutput,
193    TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOutput,
194    TraceFileProgrammaticOutput, TraceImportPathOutput, TraceImportPathProgrammaticOutput,
195    benchmark_trace_clone_compact_json, benchmark_trace_graph_family_compact_json,
196    inspect_similar_code, load_health_config, parse_similar_code_candidate_snapshot, plan_mutation,
197    review_similar_code, run_architecture, run_audit, run_boundary_violations,
198    run_circular_dependencies, run_combined, run_complexity_with_runner, run_dead_code,
199    run_dead_code_with_baseline, run_decision_surface, run_duplication, run_feature_flags,
200    run_health, run_health_with_runner, run_mutation, run_similar_code, run_trace_clone,
201    run_trace_dependency, run_trace_error, run_trace_export, run_trace_file, run_trace_import_path,
202    select_similar_code_candidate_snapshot, serialize_health_report_json,
203};
204pub use runtime_json::{
205    serialize_architecture_programmatic_json, serialize_audit_programmatic_json,
206    serialize_boundary_violations_programmatic_json,
207    serialize_circular_dependencies_programmatic_json, serialize_combined_programmatic_json,
208    serialize_dead_code_programmatic_json, serialize_decision_surface_programmatic_json,
209    serialize_duplication_programmatic_json, serialize_feature_flags_programmatic_json,
210    serialize_health_programmatic_json, serialize_trace_clone_programmatic_json,
211    serialize_trace_dependency_programmatic_json, serialize_trace_error_programmatic_json,
212    serialize_trace_export_programmatic_json, serialize_trace_file_programmatic_json,
213    serialize_trace_import_path_programmatic_json,
214};
215pub use sarif_output::{
216    annotate_sarif_results, build_duplication_sarif, build_grouped_duplication_sarif,
217    build_health_sarif,
218};
219pub use security_output::SecurityGateMode;
220pub use type_aware::{
221    SemanticCouplingOutcome, SemanticDeadCodeOutcome, SemanticInspectOutcome, TypeAwareError,
222    TypeAwareFileChanges, TypeAwareOutcome, TypeAwareSession, TypeAwareStatus,
223    discard_unverified_semantic_candidates, inspect_symbol as inspect_type_aware_symbol,
224    merge_type_aware_meta,
225    refine_configured_dead_code_results as refine_type_aware_results_with_config,
226    shutdown_type_aware_sidecars, status as type_aware_status,
227    symbol_impact as run_type_aware_symbol_impact, symbol_impact as type_aware_symbol_impact,
228    terminate_active_type_aware_sidecars, trace_symbol as run_type_aware_symbol_trace,
229    trace_symbol as trace_type_aware_symbol, type_coupling as analyze_type_coupling,
230};
231
232/// Long names of the analysis-affecting global CLI flags that
233/// [`AnalysisOptions`] mirrors for embedders.
234///
235/// A contract test in the CLI crate asserts this list stays in sync with the
236/// clap globals, so drift between the CLI surface and the programmatic
237/// options is caught at test time.
238pub const COMMON_ANALYSIS_OPTION_FLAGS: &[&str] = &[
239    "root",
240    "config",
241    "no-cache",
242    "threads",
243    "changed-since",
244    "diff-file",
245    "production",
246    "workspace",
247    "changed-workspaces",
248    "explain",
249    "allow-remote-extends",
250];
251
252/// Structured error surface for the programmatic API.
253#[derive(Debug, Clone, Serialize)]
254pub struct ProgrammaticError {
255    /// Human-readable description; also the `Display` output.
256    pub message: String,
257    /// Process exit code the CLI maps this failure to.
258    pub exit_code: u8,
259    /// Stable machine-readable code such as `FALLOW_INVALID_COVERAGE_PATH`.
260    pub code: Option<String>,
261    /// Optional remediation hint for the caller.
262    pub help: Option<String>,
263    /// Dotted path of the offending input, such as `health.coverage`.
264    pub context: Option<String>,
265}
266
267impl ProgrammaticError {
268    /// Create an error from the required message and exit code, with all
269    /// optional fields left empty.
270    #[must_use]
271    pub fn new(message: impl Into<String>, exit_code: u8) -> Self {
272        Self {
273            message: message.into(),
274            exit_code,
275            code: None,
276            help: None,
277            context: None,
278        }
279    }
280
281    /// Attach a remediation hint shown to the caller alongside the message.
282    #[must_use]
283    pub fn with_help(mut self, help: impl Into<String>) -> Self {
284        self.help = Some(help.into());
285        self
286    }
287
288    /// Attach a stable machine-readable code such as
289    /// `FALLOW_INVALID_COVERAGE_PATH`.
290    #[must_use]
291    pub fn with_code(mut self, code: impl Into<String>) -> Self {
292        self.code = Some(code.into());
293        self
294    }
295
296    /// Attach the dotted path of the offending input, such as
297    /// `health.coverage`.
298    #[must_use]
299    pub fn with_context(mut self, context: impl Into<String>) -> Self {
300        self.context = Some(context.into());
301        self
302    }
303}
304
305impl std::fmt::Display for ProgrammaticError {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        write!(f, "{}", self.message)
308    }
309}
310
311impl std::error::Error for ProgrammaticError {}
312
313/// Shared options for all one-shot analyses.
314#[derive(Debug, Clone, Default)]
315pub struct AnalysisOptions {
316    /// Project root to analyze. `None` resolves to the current working
317    /// directory.
318    pub root: Option<PathBuf>,
319    /// Explicit config file path. `None` uses config discovery from the root.
320    pub config_path: Option<PathBuf>,
321    /// Permit `https://` config inheritance for this analysis call.
322    pub allow_remote_extends: bool,
323    /// Bypass the on-disk analysis cache for this call.
324    pub no_cache: bool,
325    /// Worker thread count. `None` picks the default; `Some(0)` is rejected.
326    pub threads: Option<usize>,
327    /// Explicit unified diff file that scopes changed-code analysis.
328    ///
329    /// Relative file paths resolve against the analysis root. Patch headers
330    /// are placed relative to that root or its Git repository toplevel.
331    /// A readable patch with foreign or ambiguous paths leaves the analysis
332    /// at full scope and records a `not-applied` diff-filter request outcome.
333    /// Empty and deletion-only patches apply an empty scope.
334    ///
335    /// A file that cannot be read fails the call with
336    /// `FALLOW_INVALID_DIFF_FILE`: the caller named it and can fix it.
337    /// An explicit file never falls back to `ambient_diff_file`.
338    pub diff_file: Option<PathBuf>,
339    /// Unified diff file inherited from the environment (`FALLOW_DIFF_FILE`)
340    /// rather than named by the caller. Read only when `diff_file` is unset.
341    ///
342    /// A file that cannot be read or placed does not fail the call. The
343    /// analysis runs at full scope and `request_outcomes["diff-filter"]` states
344    /// the reason, exactly as the CLI does for the same variable: a caller
345    /// cannot fix a CI environment it inherited (issue #2799).
346    pub ambient_diff_file: Option<PathBuf>,
347    /// Legacy convenience override. `true` forces production mode; `false`
348    /// defers to config unless `production_override` is set.
349    pub production: bool,
350    /// Explicit production override from an embedder option. `None` means
351    /// use the project config for the current analysis.
352    pub production_override: Option<bool>,
353    /// Git base reference that scopes analysis to files changed since it.
354    ///
355    /// A ref that does not resolve fails the call with
356    /// `FALLOW_CHANGED_FILES_FAILED`: the caller named it and can fix it.
357    pub changed_since: Option<String>,
358    /// Git base reference inherited from the environment
359    /// (`FALLOW_CHANGED_SINCE`) rather than named by the caller. Read only when
360    /// `changed_since` is unset.
361    ///
362    /// A ref that does not resolve does not fail the call. The analysis runs at
363    /// full scope and `request_outcomes["changed-since"]` states the reason,
364    /// exactly as the CLI does for `--changed-since`.
365    pub ambient_changed_since: Option<String>,
366    /// Ignore the per-package refs of `workspaces.changedSince` for this call,
367    /// as `--no-package-baselines` does on the CLI. Every workspace package is
368    /// then analyzed in full scope.
369    pub no_package_baselines: bool,
370    /// Restrict analysis to the named workspace packages.
371    pub workspace: Option<Vec<String>>,
372    /// Restrict analysis to workspaces changed since the given git reference.
373    pub changed_workspaces: Option<String>,
374    /// Include rule and metric explanations (`_meta`) in machine output.
375    pub explain: bool,
376    /// Optional project-wide TypeScript semantic analysis. Disabled by default
377    /// and never changes compiler or typed-lint ownership.
378    pub type_aware: TypeAwareOptions,
379    /// Caller-owned cooperative cancellation for this analysis.
380    ///
381    /// Setting the flag asks the analysis to stop at its next stage boundary
382    /// and return a `FALLOW_CANCELLED` error. `None`, the default, is an
383    /// analysis that always runs to completion.
384    ///
385    /// The field is on the shared options struct, but the entry points do not
386    /// all honor it, so read the one being called before relying on it:
387    ///
388    /// - Cancelled at the entry, on both sides of the per-file parse loop, and
389    ///   at each pipeline stage boundary: [`run_dead_code`],
390    ///   [`run_circular_dependencies`], [`run_boundary_violations`],
391    ///   [`run_architecture`],
392    ///   [`run_combined`], [`run_duplication`], [`run_feature_flags`], and the
393    ///   four trace routes.
394    /// - Cancelled only at the boundaries around config load and file
395    ///   discovery, because they are the whole run: [`run_project_info`] and
396    ///   [`run_list_boundaries`].
397    /// - Cancelled only at the entry, then run to completion:
398    ///   [`run_health`], which hands off to a runner that builds its own
399    ///   session below these options.
400    /// - Not cancellable at all: [`run_decision_surface`] and
401    ///   [`run_similar_code`].
402    ///
403    /// The stop is cooperative everywhere it exists, so it has no upper bound:
404    /// it is only as prompt as the longest stage holding no check, and
405    /// duplication detection and the dead-code detectors are each seconds of
406    /// uninterruptible work on a large repository. A caller that needs a
407    /// bounded stop has to run Fallow as a child process and kill it.
408    pub cancellation: Option<Arc<AtomicBool>>,
409}
410
411/// Typed options for Fallow's optional TypeScript semantic companion.
412#[derive(Debug, Clone, Default, PartialEq, Eq)]
413pub struct TypeAwareOptions {
414    /// `Some(true)` turns on TypeScript semantic refinement for this call, as
415    /// `--type-aware` does. `Some(false)` keeps the call syntactic, as
416    /// `--no-type-aware` does, also when `FALLOW_TYPE_AWARE` or the config
417    /// turns it on. `None` follows `FALLOW_TYPE_AWARE` and the config. The
418    /// typed runners cannot run type-aware analysis from those layers, so they
419    /// return [`TYPE_AWARE_CONFIG_UNSUPPORTED`] when a layer turns it on.
420    pub enabled: Option<bool>,
421    /// Explicit TypeScript project paths handed to the semantic sidecar.
422    /// Empty defers to project discovery.
423    pub projects: Vec<PathBuf>,
424    /// How strictly semantic completion is required before results are kept.
425    pub require: fallow_config::TypeAwareRequire,
426}
427
428impl TypeAwareOptions {
429    /// The caller requested type-aware analysis for this call.
430    #[must_use]
431    pub const fn requested(&self) -> bool {
432        matches!(self.enabled, Some(true))
433    }
434}
435
436/// Issue-type filters for the dead-code analysis.
437///
438/// Each flag opts one issue type into the report. When no flag is enabled the
439/// analysis reports every issue type.
440#[derive(Debug, Clone, Default)]
441pub struct DeadCodeFilters {
442    /// Files never reached from any entry point.
443    pub unused_files: bool,
444    /// Exported symbols never imported elsewhere.
445    pub unused_exports: bool,
446    /// Declared dependencies never imported.
447    pub unused_deps: bool,
448    /// Exported types never referenced.
449    pub unused_types: bool,
450    /// Exported APIs that expose non-exported types.
451    pub private_type_leaks: bool,
452    /// Exports marked `@deprecated` that are still in use.
453    pub deprecated_exports_in_use: bool,
454    /// Enum members never read.
455    pub unused_enum_members: bool,
456    /// Class members never used outside their declaration.
457    pub unused_class_members: bool,
458    /// Store members (for example Pinia) never used outside the store.
459    pub unused_store_members: bool,
460    /// `inject` calls with no matching `provide`.
461    pub unprovided_injects: bool,
462    /// Components never rendered by any template or JSX.
463    pub unrendered_components: bool,
464    /// Declared component props never used.
465    pub unused_component_props: bool,
466    /// Review optional props omitted by known reachable callers.
467    pub absent_component_props: bool,
468    /// Declared component emits never used.
469    pub unused_component_emits: bool,
470    /// Declared component inputs never bound.
471    pub unused_component_inputs: bool,
472    /// Declared component outputs never listened to.
473    pub unused_component_outputs: bool,
474    /// Svelte component events never listened to.
475    pub unused_svelte_events: bool,
476    /// Effect layers composed only into unused layers.
477    pub unused_effect_layers: bool,
478    /// Effect tagged errors that no code produces.
479    pub unused_tagged_errors: bool,
480    /// `catchTag` handlers for a tag that no code produces.
481    pub unproduced_catch_tags: bool,
482    /// Server actions never invoked.
483    pub unused_server_actions: bool,
484    /// `load` data keys never read by the consuming page.
485    pub unused_load_data_keys: bool,
486    /// Imports that do not resolve to a file or package.
487    pub unresolved_imports: bool,
488    /// Imported packages missing from the dependency manifest.
489    pub unlisted_deps: bool,
490    /// The same symbol exported more than once.
491    pub duplicate_exports: bool,
492    /// Circular import chains.
493    pub circular_deps: bool,
494    /// Cycles formed through re-export chains.
495    pub re_export_cycles: bool,
496    /// Dependency cycles between workspace packages.
497    pub package_cycles: bool,
498    /// Imports that cross configured architecture boundaries.
499    pub boundary_violations: bool,
500    /// Violations of configured dependency policy rules.
501    pub policy_violations: bool,
502    /// Suppression comments that no longer match a finding.
503    pub stale_suppressions: bool,
504    /// Catalog entries never referenced by a workspace package.
505    pub unused_catalog_entries: bool,
506    /// Catalog groups that contain no entries.
507    pub empty_catalog_groups: bool,
508    /// `catalog:` references without a matching catalog entry.
509    pub unresolved_catalog_references: bool,
510    /// Dependency overrides that never affect a resolved package.
511    pub unused_dependency_overrides: bool,
512    /// Dependency overrides that cannot apply as written.
513    pub misconfigured_dependency_overrides: bool,
514}
515
516impl DeadCodeFilters {
517    /// Whether the report keeps dependency findings: no filter is active, or
518    /// `unused_deps` or `unlisted_deps` is one of the active filters. The CLI
519    /// applies the same rule to its `--unused-deps` and `--unlisted-deps`.
520    pub(crate) fn reports_dependency_findings(&self) -> bool {
521        !self.any_active() || self.unused_deps || self.unlisted_deps
522    }
523
524    fn any_active(&self) -> bool {
525        self.unused_files
526            || self.unused_exports
527            || self.unused_deps
528            || self.unused_types
529            || self.private_type_leaks
530            || self.deprecated_exports_in_use
531            || self.unused_enum_members
532            || self.unused_class_members
533            || self.unused_store_members
534            || self.unprovided_injects
535            || self.unrendered_components
536            || self.unused_component_props
537            || self.absent_component_props
538            || self.unused_component_emits
539            || self.unused_component_inputs
540            || self.unused_component_outputs
541            || self.unused_svelte_events
542            || self.unused_effect_layers
543            || self.unused_tagged_errors
544            || self.unproduced_catch_tags
545            || self.unused_server_actions
546            || self.unused_load_data_keys
547            || self.unresolved_imports
548            || self.unlisted_deps
549            || self.duplicate_exports
550            || self.circular_deps
551            || self.re_export_cycles
552            || self.package_cycles
553            || self.boundary_violations
554            || self.policy_violations
555            || self.stale_suppressions
556            || self.unused_catalog_entries
557            || self.empty_catalog_groups
558            || self.unresolved_catalog_references
559            || self.unused_dependency_overrides
560            || self.misconfigured_dependency_overrides
561    }
562
563    /// Enable the issue filter addressed by a shared registry selector.
564    ///
565    /// Returns `false` when the selector is not registered for dead-code
566    /// filtering. Callers that expose user input should surface their own
567    /// validation error with the accepted registry values.
568    pub fn enable_registry_selector(&mut self, selector: &str) -> bool {
569        let Some(flag) = fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS
570            .iter()
571            .find_map(|&(name, flag)| (name == selector).then_some(flag))
572        else {
573            return false;
574        };
575        self.enable_cli_filter_flag(flag);
576        true
577    }
578
579    fn enable_cli_filter_flag(&mut self, flag: &str) {
580        match flag {
581            "--unused-files" => self.unused_files = true,
582            "--unused-exports" => self.unused_exports = true,
583            "--unused-types" => self.unused_types = true,
584            "--private-type-leaks" => self.private_type_leaks = true,
585            "--deprecated-exports-in-use" => self.deprecated_exports_in_use = true,
586            "--unused-deps" => self.unused_deps = true,
587            "--unused-enum-members" => self.unused_enum_members = true,
588            "--unused-class-members" => self.unused_class_members = true,
589            "--unused-store-members" => self.unused_store_members = true,
590            "--unprovided-injects" => self.unprovided_injects = true,
591            "--unrendered-components" => self.unrendered_components = true,
592            "--unused-component-props" => self.unused_component_props = true,
593            "--absent-component-props" => self.absent_component_props = true,
594            "--unused-component-emits" => self.unused_component_emits = true,
595            "--unused-component-inputs" => self.unused_component_inputs = true,
596            "--unused-component-outputs" => self.unused_component_outputs = true,
597            "--unused-svelte-events" => self.unused_svelte_events = true,
598            "--unused-effect-layers" => self.unused_effect_layers = true,
599            "--unused-tagged-errors" => self.unused_tagged_errors = true,
600            "--unproduced-catch-tags" => self.unproduced_catch_tags = true,
601            "--unused-server-actions" => self.unused_server_actions = true,
602            "--unused-load-data-keys" => self.unused_load_data_keys = true,
603            "--unresolved-imports" => self.unresolved_imports = true,
604            "--unlisted-deps" => self.unlisted_deps = true,
605            "--duplicate-exports" => self.duplicate_exports = true,
606            "--circular-deps" => self.circular_deps = true,
607            "--re-export-cycles" => self.re_export_cycles = true,
608            "--package-cycles" => self.package_cycles = true,
609            "--boundary-violations" => self.boundary_violations = true,
610            "--policy-violations" => self.policy_violations = true,
611            "--stale-suppressions" => self.stale_suppressions = true,
612            "--unused-catalog-entries" => self.unused_catalog_entries = true,
613            "--empty-catalog-groups" => self.empty_catalog_groups = true,
614            "--unresolved-catalog-references" => self.unresolved_catalog_references = true,
615            "--unused-dependency-overrides" => self.unused_dependency_overrides = true,
616            "--misconfigured-dependency-overrides" => {
617                self.misconfigured_dependency_overrides = true;
618            }
619            _ => unreachable!("registry emitted unsupported dead-code filter flag: {flag}"),
620        }
621    }
622}
623
624/// Options for dead-code-oriented analyses.
625#[derive(Debug, Clone, Default)]
626pub struct DeadCodeOptions {
627    /// Shared analysis options.
628    pub analysis: AnalysisOptions,
629    /// Issue-type selection; everything is reported when no filter is set.
630    pub filters: DeadCodeFilters,
631    /// Restrict findings to these files when non-empty.
632    pub files: Vec<PathBuf>,
633    /// Also report unused exports declared in entry-point files.
634    pub include_entry_exports: bool,
635    /// Keep the export and member findings of unused files (`--show-cascade`).
636    pub show_cascade: bool,
637    /// Report only the findings with these `finding_id` values. The filter
638    /// runs last, after the baseline, and the output then carries
639    /// `finding_id_query`. Empty reports every finding. Only
640    /// [`run_dead_code`] and [`run_dead_code_with_baseline`] accept it.
641    pub finding_ids: Vec<String>,
642}
643
644/// Options for the analysis of `fallow architecture`: import cycles, boundary
645/// violations and rule-pack policy violations.
646///
647/// No selector reports every architecture issue type. Each selector keeps one
648/// kind, as `--cycles`, `--boundaries` and `--policy` do on the CLI.
649#[derive(Debug, Clone, Default)]
650pub struct ArchitectureOptions {
651    /// Shared analysis options.
652    pub analysis: AnalysisOptions,
653    /// Restrict findings to these files when non-empty.
654    pub files: Vec<PathBuf>,
655    /// Import cycles: circular dependencies, re-export cycles and workspace
656    /// package cycles.
657    pub cycles: bool,
658    /// Boundary violations, boundary coverage and forbidden calls.
659    pub boundaries: bool,
660    /// Rule-pack policy violations.
661    pub policy: bool,
662}
663
664impl ArchitectureOptions {
665    /// The dead-code issue-type filters that the selectors stand for.
666    #[must_use]
667    pub fn dead_code_filters(&self) -> DeadCodeFilters {
668        let all = !(self.cycles || self.boundaries || self.policy);
669        DeadCodeFilters {
670            circular_deps: self.cycles || all,
671            re_export_cycles: self.cycles || all,
672            package_cycles: self.cycles || all,
673            boundary_violations: self.boundaries || all,
674            policy_violations: self.policy || all,
675            ..DeadCodeFilters::default()
676        }
677    }
678
679    /// The dead-code options that run this analysis.
680    #[must_use]
681    pub fn dead_code_options(&self) -> DeadCodeOptions {
682        DeadCodeOptions {
683            analysis: self.analysis.clone(),
684            filters: self.dead_code_filters(),
685            files: self.files.clone(),
686            include_entry_exports: false,
687            show_cascade: false,
688            finding_ids: Vec::new(),
689        }
690    }
691}
692
693/// Options for changed-code audit analysis.
694#[derive(Debug, Clone, Default)]
695pub struct AuditOptions {
696    /// Shared analysis options.
697    pub analysis: AnalysisOptions,
698    /// Git base reference for the changed-code comparison. `None` lets the
699    /// audit detect a base itself.
700    pub base: Option<String>,
701    /// Force production mode for every audit domain.
702    pub production: bool,
703    /// Production override for the dead-code domain; `None` defers to config.
704    pub production_dead_code: Option<bool>,
705    /// Production override for the health domain; `None` defers to config.
706    pub production_health: Option<bool>,
707    /// Production override for the duplication domain; `None` defers to
708    /// config.
709    pub production_dupes: Option<bool>,
710    /// Run the dead-code domain; `None` defers to `audit.deadCode` in config,
711    /// which defaults to on. When off, the verdict comes from the domains that
712    /// run.
713    pub dead_code: Option<bool>,
714    /// Run the health domain; `None` defers to `audit.health` in config,
715    /// which defaults to on.
716    pub health: Option<bool>,
717    /// Run the duplication domain; `None` defers to `audit.dupes` in config,
718    /// which defaults to on.
719    pub duplication: Option<bool>,
720    /// `Some(false)` skips CSS / styling analysis, as `--no-css` does. `None`
721    /// and `Some(true)` defer to `audit.css` in config, which defaults to on.
722    pub css: Option<bool>,
723    /// Enable deep cross-file CSS analysis; `None` defers to `audit.cssDeep`
724    /// in config, which defaults to on. It runs only when CSS analysis runs.
725    pub css_deep: Option<bool>,
726    /// Gate mode deciding which findings fail the audit; `None` defers to
727    /// `audit.gate` in config, which defaults to `new-only`.
728    pub gate: Option<fallow_config::AuditGate>,
729    /// Fail the gate when a changed function exceeds this CRAP score.
730    pub max_crap: Option<f64>,
731    /// Test coverage report path for coverage-aware findings.
732    pub coverage: Option<PathBuf>,
733    /// Absolute path prefix the coverage report recorded its files under.
734    pub coverage_root: Option<PathBuf>,
735    /// Also report unused exports declared in entry-point files.
736    pub include_entry_exports: bool,
737    /// Keep the export and member findings of unused files (`--show-cascade`).
738    pub show_cascade: bool,
739    /// Runtime coverage capture merged into the audit.
740    pub runtime_coverage: Option<PathBuf>,
741    /// Minimum recorded invocations for a code path to count as hot.
742    pub min_invocations_hot: u64,
743}
744
745/// Options for bare combined analysis through the programmatic API.
746#[derive(Debug, Clone)]
747pub struct CombinedOptions {
748    /// Shared analysis options.
749    pub analysis: AnalysisOptions,
750    /// Run the dead-code domain.
751    pub dead_code: bool,
752    /// Run the duplication domain.
753    pub duplication: bool,
754    /// Run the health domain.
755    pub health: bool,
756    /// Also report unused exports declared in entry-point files.
757    pub include_entry_exports: bool,
758    /// Keep the export and member findings of unused files (`--show-cascade`).
759    pub show_cascade: bool,
760    /// Options for the duplication domain.
761    pub duplication_options: DuplicationOptions,
762    /// Options for the health domain.
763    pub health_options: ComplexityOptions,
764}
765
766impl Default for CombinedOptions {
767    fn default() -> Self {
768        Self {
769            analysis: AnalysisOptions::default(),
770            dead_code: true,
771            duplication: true,
772            health: true,
773            include_entry_exports: false,
774            show_cascade: false,
775            duplication_options: DuplicationOptions::default(),
776            health_options: ComplexityOptions::default(),
777        }
778    }
779}
780
781/// Options for changed-code decision-surface analysis.
782#[derive(Debug, Clone, Default)]
783pub struct DecisionSurfaceOptions {
784    /// Shared analysis options.
785    pub analysis: AnalysisOptions,
786    /// Git base reference for the changed-code comparison. `None` lets the
787    /// analysis detect a base itself.
788    pub base: Option<String>,
789    /// Cap on the number of decisions surfaced.
790    pub max_decisions: Option<usize>,
791}
792
793/// Options for feature-flag analysis.
794#[derive(Debug, Clone, Default)]
795pub struct FeatureFlagsOptions {
796    /// Shared analysis options.
797    pub analysis: AnalysisOptions,
798    /// Cap on the number of reported flags. With `retirement`, it also caps
799    /// the retirement rows.
800    pub top: Option<usize>,
801    /// Retirement report options. `None` leaves the `retirement` block out.
802    pub retirement: Option<FeatureFlagsRetirementOptions>,
803}
804
805/// Options of the flag retirement report, the `--retirement` mode of
806/// `fallow flags`.
807#[derive(Debug, Clone, Default)]
808pub struct FeatureFlagsRetirementOptions {
809    /// How to measure flag age.
810    pub flag_age: fallow_types::flag_retirement::FlagAgeMode,
811    /// Vendor flag export to read, like `--flag-state`.
812    pub flag_state: Option<std::path::PathBuf>,
813    /// Keep only rows with one of these reasons. Empty keeps all rows.
814    pub reasons: Vec<fallow_types::flag_retirement::RetirementReason>,
815    /// Row order.
816    pub sort: fallow_engine::flag_retirement::RetirementSort,
817    /// Keep only rows at least this many days old.
818    pub min_age_days: Option<u64>,
819    /// Report the flags older than this many days in `max_flag_age`.
820    pub max_flag_age: Option<u64>,
821}
822
823/// Programmatic duplication mode selection.
824#[derive(Debug, Clone, Copy, Default)]
825pub enum DuplicationMode {
826    /// Preserve all tokens, including identifier names and literal values
827    /// (Type-1 clones only).
828    Strict,
829    /// Default mode, equivalent to strict for AST-based tokenization.
830    #[default]
831    Mild,
832    /// Blind string literal values while preserving structure.
833    Weak,
834    /// Blind all identifiers and literal values for structural (Type-2)
835    /// detection.
836    Semantic,
837}
838
839/// Options for duplication analysis.
840#[derive(Debug, Clone, Default)]
841pub struct DuplicationOptions {
842    /// Shared analysis options.
843    pub analysis: AnalysisOptions,
844    /// Detection mode; `None` defers to the project config.
845    pub mode: Option<DuplicationMode>,
846    /// Detect function-scoped near-miss clones in addition to exact clones.
847    /// `None` defers to the project config.
848    pub near: Option<bool>,
849    /// Minimum number of tokens for a clone.
850    pub min_tokens: Option<usize>,
851    /// Minimum number of lines for a clone.
852    pub min_lines: Option<usize>,
853    /// Minimum number of occurrences before a clone group is reported.
854    /// Values below 2 are silently treated as 2 by the engine-facing adapter.
855    pub min_occurrences: Option<usize>,
856    /// Maximum allowed duplication percentage before the gate fails; 0 means
857    /// no limit. `None` defers to the project config.
858    pub threshold: Option<f64>,
859    /// Only report cross-directory duplicates. `None` defers to the project
860    /// config.
861    pub skip_local: Option<bool>,
862    /// Omit clone instances whose path is a symlink or lies under a symlinked
863    /// directory. `None` defers to the project config.
864    pub ignore_symlinks: Option<bool>,
865    /// Match clones across languages. `None` defers to the project config.
866    pub cross_language: Option<bool>,
867    /// Exclude module wiring from clone detection. `None` defers to the project
868    /// config.
869    pub ignore_imports: Option<bool>,
870    /// Cap on the number of reported clone groups.
871    pub top: Option<usize>,
872    /// Emit the verbatim source text on each clone instance. `None` keeps the
873    /// text, matching `fallow dupes` without `--no-fragments`; `Some(false)`
874    /// yields a location-only payload.
875    pub include_fragments: Option<bool>,
876}
877
878/// Options for local advisory similar-code discovery.
879#[derive(Debug, Clone, Default)]
880pub struct SimilarCodeOptions {
881    /// Shared project, config, cache, diff, and workspace options.
882    pub analysis: AnalysisOptions,
883    /// Model-specific cosine floor. `None` uses `similarCode.threshold`.
884    pub threshold: Option<f64>,
885    /// Minimum source lines per function. `None` uses
886    /// `similarCode.minLines`.
887    pub min_lines: Option<usize>,
888    /// Cap on displayed candidates after the full bounded comparison.
889    pub top: Option<usize>,
890    /// Restrict work and reported pairs to those where at least one side matches
891    /// one of these project-relative files. Deterministic background context is
892    /// retained only within the bounded scoped comparison budget.
893    pub files: Vec<PathBuf>,
894    /// Exact companion binary resolved and signature-verified by an official
895    /// distribution adapter such as the Node loader. `None` uses trusted
896    /// sibling discovery. Project config, PATH commands, and remote providers
897    /// must never populate this field.
898    #[doc(hidden)]
899    pub adapter_provider_path: Option<PathBuf>,
900}
901
902/// Options for one similar-code candidate inspection packet.
903#[derive(Debug, Clone)]
904pub struct SimilarCodeInspectOptions {
905    /// Shared project and configuration options used only for source validation
906    /// and deterministic enrichment. Inspect never reruns provider retrieval.
907    pub analysis: AnalysisOptions,
908    /// Bounded candidate handoff selected from the original discovery output.
909    pub snapshot: fallow_output::SimilarCodeCandidateSnapshot,
910}
911
912/// Options for export trace analysis.
913#[derive(Debug, Clone, Default)]
914pub struct TraceExportOptions {
915    /// Shared analysis options.
916    pub analysis: AnalysisOptions,
917    /// Path of the module that declares the export.
918    pub file: String,
919    /// Name of the export to trace.
920    pub export_name: String,
921}
922
923/// Options for file trace analysis.
924#[derive(Debug, Clone, Default)]
925pub struct TraceFileOptions {
926    /// Shared analysis options.
927    pub analysis: AnalysisOptions,
928    /// Path of the file to trace.
929    pub file: String,
930}
931
932/// Options for shortest-import-path trace analysis.
933#[derive(Debug, Clone, Default)]
934pub struct TraceImportPathOptions {
935    /// Shared analysis options.
936    pub analysis: AnalysisOptions,
937    /// Path of the module the walk starts from.
938    pub from: String,
939    /// Path of the module the walk is looking for.
940    pub to: String,
941    /// Follow only static imports that carry a runtime value, so the route
942    /// explains why `to` loads before `from` runs.
943    pub eager_only: bool,
944}
945
946/// Options for stack-trace frame resolution.
947#[derive(Debug, Clone, Default)]
948pub struct TraceErrorOptions {
949    /// Shared analysis options.
950    pub analysis: AnalysisOptions,
951    /// The runtime stack trace, verbatim.
952    pub trace: String,
953    /// Where the trace came from, reported back as the payload's `source`.
954    pub source: String,
955}
956
957/// Options for dependency trace analysis.
958#[derive(Debug, Clone, Default)]
959pub struct TraceDependencyOptions {
960    /// Shared analysis options.
961    pub analysis: AnalysisOptions,
962    /// Package whose importers are traced.
963    pub package_name: String,
964    /// The per-specifier usage to report. `None` keeps the output equal to
965    /// `fallow dead-code --trace-dependency`, with no `usage` object.
966    pub usage: Option<DependencyUsageQuery>,
967}
968
969/// Duplicate-code trace target.
970#[derive(Debug, Clone, PartialEq, Eq)]
971pub enum TraceCloneTarget {
972    /// Select the clone group covering this file and line.
973    Location {
974        /// Path of the file containing the clone instance.
975        file: String,
976        /// One-based line inside the clone instance.
977        line: usize,
978    },
979    /// Select the clone group by its fingerprint.
980    Fingerprint(String),
981}
982
983/// Options for duplicate-code trace analysis.
984#[derive(Debug, Clone)]
985pub struct TraceCloneOptions {
986    /// Duplication options controlling detection before the trace.
987    pub duplication: DuplicationOptions,
988    /// Clone group to trace.
989    pub target: TraceCloneTarget,
990}
991
992/// Sort criteria for complexity findings.
993#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
994pub enum ComplexitySort {
995    /// Sort by cyclomatic complexity (default).
996    #[default]
997    Cyclomatic,
998    /// Sort by cognitive complexity.
999    Cognitive,
1000    /// Sort by function length in lines.
1001    Lines,
1002    /// Sort by finding severity.
1003    Severity,
1004}
1005
1006/// Privacy mode for ownership-aware hotspot output.
1007#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1008pub enum OwnershipEmailMode {
1009    /// Show the raw email address as it appears in git history.
1010    Raw,
1011    /// Show only the local part before the `@` (default).
1012    #[default]
1013    Handle,
1014    /// Show a stable non-cryptographic pseudonym derived from the raw email.
1015    Anonymized,
1016    /// Legacy spelling retained for embedders that already pass `hash`.
1017    Hash,
1018}
1019
1020/// Effort filter for refactoring targets.
1021#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1022pub enum TargetEffort {
1023    /// Low estimated refactoring effort.
1024    Low,
1025    /// Medium estimated refactoring effort.
1026    Medium,
1027    /// High estimated refactoring effort.
1028    High,
1029}
1030
1031/// Options for complexity / health analysis.
1032#[derive(Debug, Clone, Default)]
1033pub struct ComplexityOptions {
1034    /// Shared analysis options.
1035    pub analysis: AnalysisOptions,
1036    /// Override for the cyclomatic complexity threshold.
1037    pub max_cyclomatic: Option<u16>,
1038    /// Override for the cognitive complexity threshold.
1039    pub max_cognitive: Option<u16>,
1040    /// Override for the CRAP score threshold.
1041    pub max_crap: Option<f64>,
1042    /// Cap on the number of reported findings.
1043    pub top: Option<usize>,
1044    /// Sort order for complexity findings.
1045    pub sort: ComplexitySort,
1046    /// Include the per-metric complexity breakdown with each finding.
1047    pub complexity_breakdown: bool,
1048    /// Request the complexity findings section.
1049    pub complexity: bool,
1050    /// Request the per-file score section.
1051    pub file_scores: bool,
1052    /// Request the coverage-gap section.
1053    pub coverage_gaps: bool,
1054    /// Request the churn hotspot section.
1055    pub hotspots: bool,
1056    /// Include ownership data with hotspots; implies the hotspot section.
1057    pub ownership: bool,
1058    /// Email privacy mode for ownership output; implies ownership when set.
1059    pub ownership_emails: Option<OwnershipEmailMode>,
1060    /// Request the refactoring targets section.
1061    pub targets: bool,
1062    /// Include CSS / styling health.
1063    pub css: bool,
1064    /// Enable deep cross-file CSS analysis.
1065    pub css_deep: bool,
1066    /// Filter refactoring targets by estimated effort; implies the targets
1067    /// section when set.
1068    pub effort: Option<TargetEffort>,
1069    /// Request the overall health score.
1070    pub score: bool,
1071    /// Git time window (for example `30d`) for churn-based sections.
1072    pub since: Option<String>,
1073    /// Minimum commit count for a file to count as a hotspot.
1074    pub min_commits: Option<u32>,
1075    /// Test coverage report path for coverage-aware sections.
1076    pub coverage: Option<PathBuf>,
1077    /// Absolute path prefix the coverage report recorded its files under.
1078    pub coverage_root: Option<PathBuf>,
1079    /// The coverage map was recorded against a different checkout of this
1080    /// project, so function line numbers may have drifted arbitrarily.
1081    /// Set internally by the audit base-worktree pass; leave `false` for
1082    /// same-checkout coverage.
1083    pub coverage_relocated: bool,
1084}
1085
1086/// Health threshold overrides accepted by the programmatic API.
1087#[derive(Debug, Clone, Copy, Default, PartialEq)]
1088pub struct ComplexityThresholdOverrides {
1089    /// Override for the cyclomatic complexity threshold.
1090    pub max_cyclomatic: Option<u16>,
1091    /// Override for the cognitive complexity threshold.
1092    pub max_cognitive: Option<u16>,
1093    /// Override for the CRAP score threshold.
1094    pub max_crap: Option<f64>,
1095}
1096
1097/// Coverage inputs accepted by the programmatic API.
1098#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1099pub struct ComplexityCoverageInputs<'a> {
1100    /// Test coverage report path.
1101    pub coverage: Option<&'a Path>,
1102    /// Absolute path prefix the coverage report recorded its files under.
1103    pub coverage_root: Option<&'a Path>,
1104    /// The coverage map was recorded against a different checkout of this
1105    /// project; tolerate arbitrary line drift for unambiguous name matches.
1106    pub coverage_relocated: bool,
1107}
1108
1109/// Input for deriving effective health sections from API-owned flags.
1110#[derive(Debug, Clone)]
1111pub struct HealthSectionOptions {
1112    /// Requested output format; `Badge` implies the score section.
1113    pub output: fallow_types::output_format::OutputFormat,
1114    /// The complexity findings section was requested.
1115    pub complexity: bool,
1116    /// The per-file score section was requested.
1117    pub file_scores: bool,
1118    /// The coverage-gap section was requested.
1119    pub coverage_gaps: bool,
1120    /// The churn hotspot section was requested.
1121    pub hotspots: bool,
1122    /// The refactoring targets section was requested.
1123    pub targets: bool,
1124    /// CSS / styling health was requested.
1125    pub css: bool,
1126    /// The overall health score was requested.
1127    pub score: bool,
1128    /// A score gate is active; implies the score section.
1129    pub score_gate: bool,
1130    /// A snapshot write was requested; forces full hidden inputs.
1131    pub snapshot_requested: bool,
1132    /// Trend output was requested; implies score and hotspot inputs.
1133    pub trend: bool,
1134}
1135
1136/// Derived section selection for health runs.
1137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1138pub struct DerivedHealthSections {
1139    /// At least one section was explicitly requested.
1140    pub any_section: bool,
1141    /// Emit the complexity findings section.
1142    pub complexity: bool,
1143    /// Compute the per-file score section.
1144    pub file_scores: bool,
1145    /// Emit the coverage-gap section.
1146    pub coverage_gaps: bool,
1147    /// Compute the churn hotspot section.
1148    pub hotspots: bool,
1149    /// Emit the refactoring targets section.
1150    pub targets: bool,
1151    /// Include CSS / styling health.
1152    pub css: bool,
1153    /// Compute the overall health score.
1154    pub score: bool,
1155    /// Compute full inputs even for sections that are not emitted.
1156    pub force_full: bool,
1157    /// Only the score should be printed.
1158    pub score_only_output: bool,
1159}
1160
1161/// Input for deriving effective programmatic complexity sections.
1162#[derive(Debug, Clone)]
1163pub struct ComplexitySectionOptions {
1164    /// The complexity findings section was requested.
1165    pub complexity: bool,
1166    /// The per-file score section was requested.
1167    pub file_scores: bool,
1168    /// The coverage-gap section was requested.
1169    pub coverage_gaps: bool,
1170    /// The churn hotspot section was requested.
1171    pub hotspots: bool,
1172    /// Ownership data was requested; implies the hotspot section.
1173    pub ownership: bool,
1174    /// The refactoring targets section was requested.
1175    pub targets: bool,
1176    /// CSS / styling health was requested.
1177    pub css: bool,
1178    /// The overall health score was requested.
1179    pub score: bool,
1180}
1181
1182/// Derived section selection for programmatic health / complexity runs.
1183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1184pub struct DerivedComplexityOptions {
1185    /// At least one section was explicitly requested.
1186    pub any_section: bool,
1187    /// Emit the complexity findings section.
1188    pub complexity: bool,
1189    /// Compute the per-file score section.
1190    pub file_scores: bool,
1191    /// Emit the coverage-gap section.
1192    pub coverage_gaps: bool,
1193    /// Compute the churn hotspot section.
1194    pub hotspots: bool,
1195    /// Include ownership data with hotspots.
1196    pub ownership: bool,
1197    /// Emit the refactoring targets section.
1198    pub targets: bool,
1199    /// Compute full inputs even for sections that are not emitted.
1200    pub force_full: bool,
1201    /// Only the score should be printed.
1202    pub score_only_output: bool,
1203    /// Compute the overall health score.
1204    pub score: bool,
1205}
1206
1207/// Normalized programmatic complexity / health inputs owned by `fallow-api`.
1208#[derive(Debug, Clone, PartialEq)]
1209pub struct ComplexityRunOptions<'a> {
1210    /// Complexity threshold overrides.
1211    pub thresholds: ComplexityThresholdOverrides,
1212    /// Cap on the number of reported findings.
1213    pub top: Option<usize>,
1214    /// Sort order for complexity findings.
1215    pub sort: ComplexitySort,
1216    /// Include the per-metric complexity breakdown with each finding.
1217    pub complexity_breakdown: bool,
1218    /// Derived effective section selection.
1219    pub sections: DerivedComplexityOptions,
1220    /// Email privacy mode for ownership output.
1221    pub ownership_emails: Option<OwnershipEmailMode>,
1222    /// Filter refactoring targets by estimated effort.
1223    pub effort: Option<TargetEffort>,
1224    /// Include CSS / styling health.
1225    pub css: bool,
1226    /// Enable deep cross-file CSS analysis.
1227    pub css_deep: bool,
1228    /// Git time window (for example `30d`) for churn-based sections.
1229    pub since: Option<&'a str>,
1230    /// Minimum commit count for a file to count as a hotspot.
1231    pub min_commits: Option<u32>,
1232    /// Test coverage inputs.
1233    pub coverage_inputs: ComplexityCoverageInputs<'a>,
1234}
1235
1236/// Derive effective health section flags for API consumers.
1237#[must_use]
1238pub fn derive_health_sections(options: &HealthSectionOptions) -> DerivedHealthSections {
1239    let score = options.score
1240        || options.score_gate
1241        || options.trend
1242        || matches!(
1243            options.output,
1244            fallow_types::output_format::OutputFormat::Badge
1245        );
1246    let any_section = options.complexity
1247        || options.file_scores
1248        || options.coverage_gaps
1249        || options.hotspots
1250        || options.targets
1251        || score;
1252    let effective_score = if any_section { score } else { true } || options.snapshot_requested;
1253    let force_full = options.snapshot_requested || effective_score;
1254
1255    DerivedHealthSections {
1256        any_section,
1257        complexity: if any_section {
1258            options.complexity
1259        } else {
1260            true
1261        },
1262        file_scores: if any_section {
1263            options.file_scores
1264        } else {
1265            true
1266        } || force_full,
1267        coverage_gaps: if any_section {
1268            options.coverage_gaps
1269        } else {
1270            false
1271        },
1272        hotspots: if any_section { options.hotspots } else { true }
1273            || options.snapshot_requested
1274            || options.trend,
1275        targets: if any_section { options.targets } else { true },
1276        css: options.css,
1277        score: effective_score,
1278        force_full,
1279        score_only_output: is_health_score_only_output(options, score),
1280    }
1281}
1282
1283/// Derive effective programmatic health / complexity section flags.
1284#[must_use]
1285pub fn derive_complexity_sections(options: &ComplexitySectionOptions) -> DerivedComplexityOptions {
1286    let requested_hotspots = options.hotspots || options.ownership;
1287    let sections = derive_health_sections(&HealthSectionOptions {
1288        output: fallow_types::output_format::OutputFormat::Human,
1289        complexity: options.complexity,
1290        file_scores: options.file_scores,
1291        coverage_gaps: options.coverage_gaps,
1292        hotspots: requested_hotspots,
1293        targets: options.targets,
1294        css: options.css,
1295        score: options.score,
1296        score_gate: false,
1297        snapshot_requested: false,
1298        trend: false,
1299    });
1300
1301    DerivedComplexityOptions {
1302        any_section: sections.any_section,
1303        complexity: sections.complexity,
1304        file_scores: sections.file_scores,
1305        coverage_gaps: sections.coverage_gaps,
1306        hotspots: sections.hotspots,
1307        ownership: options.ownership && sections.hotspots,
1308        targets: sections.targets,
1309        force_full: sections.force_full,
1310        score_only_output: sections.score_only_output,
1311        score: sections.score,
1312    }
1313}
1314
1315/// Derive effective programmatic health / complexity section flags.
1316#[must_use]
1317pub fn derive_complexity_options(options: &ComplexityOptions) -> DerivedComplexityOptions {
1318    derive_complexity_sections(&complexity_section_options(options))
1319}
1320
1321/// Normalize public API complexity options into engine-owned run contracts.
1322#[must_use]
1323pub fn derive_complexity_run_options(options: &ComplexityOptions) -> ComplexityRunOptions<'_> {
1324    ComplexityRunOptions {
1325        thresholds: ComplexityThresholdOverrides {
1326            max_cyclomatic: options.max_cyclomatic,
1327            max_cognitive: options.max_cognitive,
1328            max_crap: options.max_crap,
1329        },
1330        top: options.top,
1331        sort: options.sort,
1332        complexity_breakdown: options.complexity_breakdown,
1333        sections: derive_complexity_options(options),
1334        ownership_emails: options.ownership_emails,
1335        effort: options.effort,
1336        css: options.css,
1337        css_deep: options.css_deep,
1338        since: options.since.as_deref(),
1339        min_commits: options.min_commits,
1340        coverage_inputs: ComplexityCoverageInputs {
1341            coverage: options.coverage.as_deref(),
1342            coverage_root: options.coverage_root.as_deref(),
1343            coverage_relocated: options.coverage_relocated,
1344        },
1345    }
1346}
1347
1348/// Validate programmatic complexity / health inputs before invoking a concrete
1349/// runner.
1350///
1351/// These option contracts belong to the API boundary because NAPI and future
1352/// Rust embedders construct the same [`ComplexityOptions`] type.
1353///
1354/// A relative coverage path resolves against `analysis.root` (the cwd when
1355/// unset), exactly as the engine resolves it at read time, so a
1356/// project-relative `health.coverage` validates against the project and not
1357/// against the embedder's working directory.
1358///
1359/// The options carry no record of which layer supplied `coverage`, so the
1360/// path error's `context` is always `health.coverage`, whichever of the tool
1361/// parameter, `FALLOW_COVERAGE`, or the config field won
1362/// ([`crate::coverage::resolve_coverage_inputs`] resolves that order). The
1363/// sibling root error names its layer because
1364/// [`crate::coverage::CoverageInputError`] carries it.
1365///
1366/// # Errors
1367///
1368/// Returns a structured programmatic error when a coverage path does not exist
1369/// or when `coverage_root` is not an absolute prefix from the coverage data.
1370pub fn validate_complexity_options(options: &ComplexityOptions) -> Result<(), ProgrammaticError> {
1371    if let Some(path) = &options.coverage {
1372        let resolved = fallow_engine::health::scoring::resolve_relative_to_root(
1373            path,
1374            options.analysis.root.as_deref(),
1375        );
1376        if !resolved.exists() {
1377            return Err(ProgrammaticError::new(
1378                format!("coverage path does not exist: {}", resolved.display()),
1379                2,
1380            )
1381            .with_code("FALLOW_INVALID_COVERAGE_PATH")
1382            .with_context("health.coverage"));
1383        }
1384    }
1385    if let Err(message) =
1386        fallow_engine::health::validate_coverage_root_absolute(options.coverage_root.as_deref())
1387    {
1388        return Err(ProgrammaticError::new(message, 2)
1389            .with_code("FALLOW_INVALID_COVERAGE_ROOT")
1390            .with_context("health.coverage_root"));
1391    }
1392
1393    Ok(())
1394}
1395
1396fn complexity_section_options(options: &ComplexityOptions) -> ComplexitySectionOptions {
1397    let ownership = options.ownership || options.ownership_emails.is_some();
1398    let requested_targets = options.targets || options.effort.is_some();
1399    ComplexitySectionOptions {
1400        complexity: options.complexity,
1401        file_scores: options.file_scores,
1402        coverage_gaps: options.coverage_gaps,
1403        hotspots: options.hotspots,
1404        ownership,
1405        targets: requested_targets,
1406        css: options.css,
1407        score: options.score,
1408    }
1409}
1410
1411fn is_health_score_only_output(options: &HealthSectionOptions, score: bool) -> bool {
1412    score
1413        && !options.complexity
1414        && !options.file_scores
1415        && !options.coverage_gaps
1416        && !options.hotspots
1417        && !options.targets
1418        && !options.trend
1419}
1420
1421const fn thresholds_to_engine(
1422    thresholds: ComplexityThresholdOverrides,
1423) -> fallow_engine::health::HealthThresholdOverrides {
1424    fallow_engine::health::HealthThresholdOverrides {
1425        max_cyclomatic: thresholds.max_cyclomatic,
1426        max_cognitive: thresholds.max_cognitive,
1427        max_crap: thresholds.max_crap,
1428    }
1429}
1430
1431const fn complexity_sort_to_engine(sort: ComplexitySort) -> fallow_engine::health::HealthSort {
1432    match sort {
1433        ComplexitySort::Severity => fallow_engine::health::HealthSort::Severity,
1434        ComplexitySort::Cyclomatic => fallow_engine::health::HealthSort::Cyclomatic,
1435        ComplexitySort::Cognitive => fallow_engine::health::HealthSort::Cognitive,
1436        ComplexitySort::Lines => fallow_engine::health::HealthSort::Lines,
1437    }
1438}
1439
1440const fn coverage_inputs_to_engine(
1441    coverage_inputs: ComplexityCoverageInputs<'_>,
1442) -> fallow_engine::health::HealthCoverageInputs<'_> {
1443    fallow_engine::health::HealthCoverageInputs {
1444        coverage: coverage_inputs.coverage,
1445        coverage_root: coverage_inputs.coverage_root,
1446        coverage_relocated: coverage_inputs.coverage_relocated,
1447    }
1448}
1449
1450const fn ownership_email_mode_to_config(mode: OwnershipEmailMode) -> EmailMode {
1451    match mode {
1452        OwnershipEmailMode::Raw => EmailMode::Raw,
1453        OwnershipEmailMode::Handle => EmailMode::Handle,
1454        OwnershipEmailMode::Anonymized => EmailMode::Anonymized,
1455        OwnershipEmailMode::Hash => EmailMode::Hash,
1456    }
1457}
1458
1459const fn target_effort_to_output(effort: TargetEffort) -> EffortEstimate {
1460    match effort {
1461        TargetEffort::Low => EffortEstimate::Low,
1462        TargetEffort::Medium => EffortEstimate::Medium,
1463        TargetEffort::High => EffortEstimate::High,
1464    }
1465}
1466
1467#[cfg(test)]
1468mod tests {
1469    use super::*;
1470
1471    #[test]
1472    fn duplication_defaults_match_cli_contract() {
1473        let options = DuplicationOptions::default();
1474        assert!(options.mode.is_none());
1475        assert!(options.min_tokens.is_none());
1476        assert!(options.min_lines.is_none());
1477        assert!(options.min_occurrences.is_none());
1478    }
1479
1480    #[test]
1481    fn programmatic_error_builder_keeps_optional_fields() {
1482        let error = ProgrammaticError::new("boom", 2)
1483            .with_code("FALLOW_TEST")
1484            .with_help("Try again")
1485            .with_context("analysis.root");
1486
1487        assert_eq!(error.message, "boom");
1488        assert_eq!(error.exit_code, 2);
1489        assert_eq!(error.code.as_deref(), Some("FALLOW_TEST"));
1490        assert_eq!(error.help.as_deref(), Some("Try again"));
1491        assert_eq!(error.context.as_deref(), Some("analysis.root"));
1492    }
1493
1494    #[test]
1495    fn dead_code_filters_accept_shared_registry_selectors() {
1496        for (selector, _) in fallow_types::issue_meta::MCP_ISSUE_TYPE_FLAGS.iter() {
1497            let mut filters = DeadCodeFilters::default();
1498            assert!(
1499                filters.enable_registry_selector(selector),
1500                "{selector} should be accepted"
1501            );
1502        }
1503
1504        let mut filters = DeadCodeFilters::default();
1505        assert!(filters.enable_registry_selector("unused-files"));
1506        assert!(filters.unused_files);
1507        assert!(filters.enable_registry_selector("boundary-violations"));
1508        assert!(filters.boundary_violations);
1509        assert!(!filters.enable_registry_selector("not-a-real-selector"));
1510    }
1511
1512    #[test]
1513    fn default_complexity_options_match_programmatic_health_defaults() {
1514        let derived = derive_complexity_options(&ComplexityOptions::default());
1515
1516        assert!(!derived.any_section);
1517        assert!(derived.complexity);
1518        assert!(derived.file_scores);
1519        assert!(!derived.coverage_gaps);
1520        assert!(derived.hotspots);
1521        assert!(!derived.ownership);
1522        assert!(derived.targets);
1523        assert!(derived.force_full);
1524        assert!(!derived.score_only_output);
1525        assert!(derived.score);
1526    }
1527
1528    #[test]
1529    fn score_only_complexity_options_request_score_only_output() {
1530        let derived = derive_complexity_options(&ComplexityOptions {
1531            score: true,
1532            ..ComplexityOptions::default()
1533        });
1534
1535        assert!(derived.any_section);
1536        assert!(!derived.complexity);
1537        assert!(derived.file_scores);
1538        assert!(!derived.hotspots);
1539        assert!(!derived.targets);
1540        assert!(derived.force_full);
1541        assert!(derived.score_only_output);
1542        assert!(derived.score);
1543    }
1544
1545    #[test]
1546    fn ownership_implies_hotspots_when_requested() {
1547        let derived = derive_complexity_options(&ComplexityOptions {
1548            ownership: true,
1549            ..ComplexityOptions::default()
1550        });
1551
1552        assert!(derived.any_section);
1553        assert!(derived.hotspots);
1554        assert!(derived.ownership);
1555        assert!(!derived.targets);
1556    }
1557
1558    #[test]
1559    fn complexity_run_options_normalize_public_api_options() {
1560        let options = ComplexityOptions {
1561            max_cyclomatic: Some(42),
1562            max_cognitive: Some(21),
1563            max_crap: Some(18.5),
1564            top: Some(7),
1565            sort: ComplexitySort::Severity,
1566            complexity_breakdown: true,
1567            ownership_emails: Some(OwnershipEmailMode::Hash),
1568            effort: Some(TargetEffort::High),
1569            coverage: Some(PathBuf::from("coverage/coverage-final.json")),
1570            coverage_root: Some(PathBuf::from("/ci/workspace")),
1571            since: Some("30d".to_string()),
1572            min_commits: Some(4),
1573            ..ComplexityOptions::default()
1574        };
1575
1576        let run = derive_complexity_run_options(&options);
1577
1578        assert_eq!(run.thresholds.max_cyclomatic, Some(42));
1579        assert_eq!(run.thresholds.max_cognitive, Some(21));
1580        assert_eq!(run.thresholds.max_crap, Some(18.5));
1581        assert_eq!(run.top, Some(7));
1582        assert!(matches!(run.sort, ComplexitySort::Severity));
1583        assert!(run.complexity_breakdown);
1584        assert!(run.sections.hotspots);
1585        assert!(run.sections.ownership);
1586        assert!(run.sections.targets);
1587        assert!(matches!(
1588            run.ownership_emails,
1589            Some(OwnershipEmailMode::Hash)
1590        ));
1591        assert!(matches!(run.effort, Some(TargetEffort::High)));
1592        assert_eq!(run.since, Some("30d"));
1593        assert_eq!(run.min_commits, Some(4));
1594        assert_eq!(run.coverage_inputs.coverage, options.coverage.as_deref());
1595        assert_eq!(
1596            run.coverage_inputs.coverage_root,
1597            options.coverage_root.as_deref()
1598        );
1599    }
1600
1601    #[test]
1602    fn complexity_options_validation_accepts_existing_coverage_path_and_absolute_root() {
1603        let dir = tempfile::tempdir().expect("tempdir");
1604        let coverage = dir.path().join("coverage-final.json");
1605        std::fs::write(&coverage, "{}").expect("coverage fixture");
1606
1607        let result = validate_complexity_options(&ComplexityOptions {
1608            coverage: Some(coverage),
1609            coverage_root: Some(PathBuf::from("/ci/workspace")),
1610            ..ComplexityOptions::default()
1611        });
1612
1613        assert!(result.is_ok());
1614    }
1615
1616    #[test]
1617    fn complexity_options_validation_keeps_missing_coverage_error_contract() {
1618        let err = validate_complexity_options(&ComplexityOptions {
1619            coverage: Some(PathBuf::from("/missing/coverage-final.json")),
1620            ..ComplexityOptions::default()
1621        })
1622        .expect_err("missing coverage path should fail");
1623
1624        assert_eq!(err.exit_code, 2);
1625        assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1626        assert_eq!(err.context.as_deref(), Some("health.coverage"));
1627    }
1628
1629    #[test]
1630    fn complexity_options_validation_keeps_relative_coverage_root_error_contract() {
1631        let err = validate_complexity_options(&ComplexityOptions {
1632            coverage_root: Some(PathBuf::from("coverage")),
1633            ..ComplexityOptions::default()
1634        })
1635        .expect_err("relative coverage root should fail");
1636
1637        assert_eq!(err.exit_code, 2);
1638        assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_ROOT"));
1639        assert_eq!(err.context.as_deref(), Some("health.coverage_root"));
1640    }
1641
1642    /// #2368: a project-relative coverage path (the documented form of
1643    /// `health.coverage`) must be checked under the analysis root, not under
1644    /// the embedder's working directory.
1645    #[test]
1646    fn complexity_options_validation_resolves_relative_coverage_against_root() {
1647        let dir = tempfile::tempdir().expect("tempdir");
1648        std::fs::create_dir_all(dir.path().join("artifacts")).expect("artifacts dir");
1649        std::fs::write(dir.path().join("artifacts/coverage-final.json"), "{}")
1650            .expect("coverage fixture");
1651        let relative = PathBuf::from("artifacts/coverage-final.json");
1652        assert!(
1653            !relative.exists(),
1654            "the fixture must not also exist under the test cwd"
1655        );
1656
1657        let result = validate_complexity_options(&ComplexityOptions {
1658            analysis: AnalysisOptions {
1659                root: Some(dir.path().to_path_buf()),
1660                ..AnalysisOptions::default()
1661            },
1662            coverage: Some(relative.clone()),
1663            ..ComplexityOptions::default()
1664        });
1665        assert!(result.is_ok(), "{result:?}");
1666
1667        let err = validate_complexity_options(&ComplexityOptions {
1668            analysis: AnalysisOptions {
1669                root: Some(dir.path().join("elsewhere")),
1670                ..AnalysisOptions::default()
1671            },
1672            coverage: Some(relative),
1673            ..ComplexityOptions::default()
1674        })
1675        .expect_err("the path does not exist under the other root");
1676        assert_eq!(err.code.as_deref(), Some("FALLOW_INVALID_COVERAGE_PATH"));
1677        assert!(
1678            err.message.contains("elsewhere"),
1679            "the message names the resolved path: {}",
1680            err.message
1681        );
1682    }
1683
1684    #[test]
1685    fn default_health_sections_match_full_health_output() {
1686        let derived = derive_health_sections(&HealthSectionOptions {
1687            output: fallow_types::output_format::OutputFormat::Human,
1688            complexity: false,
1689            file_scores: false,
1690            coverage_gaps: false,
1691            hotspots: false,
1692            targets: false,
1693            css: false,
1694            score: false,
1695            score_gate: false,
1696            snapshot_requested: false,
1697            trend: false,
1698        });
1699
1700        assert!(!derived.any_section);
1701        assert!(derived.complexity);
1702        assert!(derived.file_scores);
1703        assert!(!derived.coverage_gaps);
1704        assert!(derived.hotspots);
1705        assert!(derived.targets);
1706        assert!(derived.score);
1707        assert!(derived.force_full);
1708        assert!(!derived.score_only_output);
1709    }
1710
1711    #[test]
1712    fn health_score_gate_requests_score_only_output() {
1713        let derived = derive_health_sections(&HealthSectionOptions {
1714            output: fallow_types::output_format::OutputFormat::Human,
1715            complexity: false,
1716            file_scores: false,
1717            coverage_gaps: false,
1718            hotspots: false,
1719            targets: false,
1720            css: false,
1721            score: false,
1722            score_gate: true,
1723            snapshot_requested: false,
1724            trend: false,
1725        });
1726
1727        assert!(derived.any_section);
1728        assert!(!derived.complexity);
1729        assert!(derived.file_scores);
1730        assert!(!derived.hotspots);
1731        assert!(!derived.targets);
1732        assert!(derived.score);
1733        assert!(derived.force_full);
1734        assert!(derived.score_only_output);
1735    }
1736
1737    #[test]
1738    fn health_snapshot_keeps_full_hidden_inputs_without_section_request() {
1739        let derived = derive_health_sections(&HealthSectionOptions {
1740            output: fallow_types::output_format::OutputFormat::Human,
1741            complexity: false,
1742            file_scores: false,
1743            coverage_gaps: false,
1744            hotspots: false,
1745            targets: false,
1746            css: true,
1747            score: false,
1748            score_gate: false,
1749            snapshot_requested: true,
1750            trend: false,
1751        });
1752
1753        assert!(!derived.any_section);
1754        assert!(derived.css);
1755        assert!(derived.file_scores);
1756        assert!(derived.hotspots);
1757        assert!(derived.score);
1758        assert!(derived.force_full);
1759    }
1760}