diff --git a/.github/workflows/adr-validation.yml b/.github/workflows/adr-validation.yml index 08b00cd..aa796c3 100644 --- a/.github/workflows/adr-validation.yml +++ b/.github/workflows/adr-validation.yml @@ -22,7 +22,8 @@ jobs: steps: - name: Checkout repository # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # v6.0.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Validate ADR Format # main @@ -30,6 +31,7 @@ jobs: with: command: validate input-dir: docs/adr + pattern: "[0-9]*.md" - name: Get ADR Statistics # main diff --git a/docs/README.md b/docs/README.md index 3f2f172..9a9c35f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,76 @@ # Documentation Index -> All documentation for the nsip project. +> All documentation for the nsip project organized using the [Diataxis framework](https://diataxis.fr/). + +## Quick Start + +New to NSIP? Start here: + +| Document | Description | +|----------|-------------| +| [Getting Started Tutorial](tutorials/GETTING-STARTED.md) | 15-minute hands-on introduction to the NSIP CLI and library | +| [Understanding EBVs](explanation/EBV-EXPLAINED.md) | Learn what Estimated Breeding Values are and how to interpret them | +| [CLI Reference](reference/CLI.md) | Complete reference for every CLI subcommand, flag, and option | + +--- + +## Tutorials + +Learning-oriented guides that take you through practical exercises step by step. + +| Tutorial | Time | Description | +|----------|------|-------------| +| [Getting Started](tutorials/GETTING-STARTED.md) | 15 min | Install nsip, search for animals, retrieve genetic data | +| [Your First API Query](tutorials/FIRST-API-QUERY.md) | 10 min | Use the Rust library to query the NSIP Search API end-to-end | +| [MCP Server Setup](tutorials/MCP-SERVER-SETUP.md) | 10 min | Configure and start the MCP server for AI assistant integration | +| [Interpreting Search Results](tutorials/INTERPRETING-RESULTS.md) | 10 min | Read and understand animal search results, EBV traits, and accuracy | + +--- + +## How-To Guides + +Problem-oriented guides for accomplishing specific tasks. + +| Guide | Description | +|-------|-------------| +| [Configure Client](how-to/CONFIGURE-CLIENT.md) | Customize timeout, retries, and base URL for the HTTP client | +| [Compare Animals](how-to/COMPARE-ANIMALS.md) | Side-by-side genetic trait comparisons via CLI, library, or MCP | +| [Filter Search Results](how-to/FILTER-SEARCH-RESULTS.md) | Use SearchCriteria to filter by breed, gender, status, date, and trait ranges | +| [Use MCP Tools](how-to/USE-MCP-TOOLS.md) | Invoke the 13 MCP server tools from AI assistants | +| [Export JSON](how-to/EXPORT-JSON.md) | Export data as JSON using the `--json` flag or library serialization | +| [Batch Query Animals](how-to/BATCH-QUERY.md) | Query multiple animals concurrently with Tokio | +| [Scripting Integration](how-to/SCRIPTING-INTEGRATION.md) | Integrate nsip into shell scripts, CI pipelines, and automation workflows | + +--- + +## Explanation + +Understanding-oriented discussions of key concepts. + +| Document | Description | +|----------|-------------| +| [Understanding EBVs](explanation/EBV-EXPLAINED.md) | What EBVs are, how they're calculated, accuracy, and selection indexes | +| [NSIP Data Model](explanation/NSIP-DATA-MODEL.md) | Program structure: breed groups, breeds, flocks, animals, and their relationships | +| [Genetic Evaluation](explanation/GENETIC-EVALUATION.md) | How BLUP works, pedigree and genomic data, and the evaluation pipeline | +| [Breed Groups and Traits](explanation/BREED-GROUPS-AND-TRAITS.md) | Understanding breed group categories and the 13 EBV trait abbreviations | +| [From Data to Decisions](explanation/DATA-TO-DECISIONS.md) | How NSIP API data connects to real-world breeding decisions | + +--- + +## Reference + +Information-oriented technical descriptions. + +| Document | Description | +|----------|-------------| +| [CLI Reference](reference/CLI.md) | Every subcommand, flag, and option for the `nsip` binary | +| [Library API](reference/LIBRARY-API.md) | `NsipClient` methods, `SearchCriteria` builder, and all model types | +| [MCP Tools](reference/MCP-TOOLS.md) | All 13 MCP server tools with parameters, return types, and examples | +| [Error Handling](reference/ERROR-HANDLING.md) | Complete `Error` enum reference with handling patterns | +| [Configuration](reference/CONFIGURATION.md) | Client builder options, defaults, retry behavior, and environment | +| [MCP Server API](MCP.md) | Full MCP server reference: tools, resources, prompts, and analytics | + +--- ## Template Adoption Guides @@ -8,11 +78,11 @@ Guides for developers who just created a repository from this template. | Guide | Description | |-------|-------------| -| [Getting Started](template/GETTING-STARTED.md) | "Use this template" → first `cargo build` → first CI pass | +| [Getting Started](template/GETTING-STARTED.md) | "Use this template" to first `cargo build` to first CI pass | | [Configuration](template/CONFIGURATION.md) | Cargo.toml fields, placeholder replacement, feature flags, editor setup | | [CI Workflows](template/CI-WORKFLOWS.md) | Every included workflow: triggers, secrets, how to enable/disable | | [Customization](template/CUSTOMIZATION.md) | Add modules, remove examples, adjust lints, modify release targets | -| [GitHub Template Features](template/GITHUB-TEMPLATE-FEATURES.md) | What copies when using a template — and what doesn't | +| [GitHub Template Features](template/GITHUB-TEMPLATE-FEATURES.md) | What copies when using a template -- and what doesn't | | [Copilot Jumpstart](template/COPILOT-JUMPSTART.md) | Prompts for automatic project scaffolding with GitHub Copilot | ## Operational Runbooks @@ -21,12 +91,12 @@ Step-by-step procedures for ongoing project maintenance. | Runbook | Description | |---------|-------------| -| [Releasing](runbooks/RELEASING.md) | Version bump → tag → monitor workflows → verify artifacts | +| [Releasing](runbooks/RELEASING.md) | Version bump, tag, monitor workflows, verify artifacts | | [Dependency Updates](runbooks/DEPENDENCY-UPDATES.md) | Dependabot policy, manual cargo-deny audit, handling advisories | | [Security Response](runbooks/SECURITY-RESPONSE.md) | Vulnerability triage, fix, coordinated disclosure | | [CI Troubleshooting](runbooks/CI-TROUBLESHOOTING.md) | Common CI failure patterns and fixes | -## Reference Documentation +## Additional Reference Detailed reference material organized by topic. @@ -90,5 +160,6 @@ Detailed reference material organized by topic. |-----|-------------| | [ADR-0001](adr/0001-use-architectural-decision-records.md) | Use Architectural Decision Records | | [ADR-0002](adr/0002-documentation-directory-structure.md) | Documentation Directory Structure | +| [ADR-0003](adr/0003-adopt-diataxis-documentation-framework.md) | Adopt Diataxis Documentation Framework | See [docs/adr/README.md](adr/README.md) for the full ADR process and workflow. diff --git a/docs/adr/0001-use-architectural-decision-records.md b/docs/adr/0001-use-architectural-decision-records.md index c239734..9536ccf 100644 --- a/docs/adr/0001-use-architectural-decision-records.md +++ b/docs/adr/0001-use-architectural-decision-records.md @@ -1,8 +1,24 @@ -# Use Architectural Decision Records - -## Status +--- +title: "Use Architectural Decision Records" +description: "Adopt ADRs to document significant architectural decisions" +type: adr +category: process +tags: + - documentation + - process + - adr +status: accepted +created: "2026-02-16" +updated: "2026-02-16" +author: nsip maintainers +project: nsip +technologies: + - markdown +audience: + - developers +--- -Accepted +# Use Architectural Decision Records ## Context diff --git a/docs/adr/0002-documentation-directory-structure.md b/docs/adr/0002-documentation-directory-structure.md index 424d132..f63d1f2 100644 --- a/docs/adr/0002-documentation-directory-structure.md +++ b/docs/adr/0002-documentation-directory-structure.md @@ -1,8 +1,23 @@ -# Documentation Directory Structure - -## Status +--- +title: "Documentation Directory Structure" +description: "Organize docs into template guides and operational runbooks" +type: adr +category: documentation +tags: + - documentation + - structure +status: accepted +created: "2026-02-16" +updated: "2026-02-16" +author: nsip maintainers +project: nsip +technologies: + - markdown +audience: + - developers +--- -Accepted +# Documentation Directory Structure ## Context diff --git a/docs/adr/0003-adopt-diataxis-documentation-framework.md b/docs/adr/0003-adopt-diataxis-documentation-framework.md new file mode 100644 index 0000000..14cb3f4 --- /dev/null +++ b/docs/adr/0003-adopt-diataxis-documentation-framework.md @@ -0,0 +1,168 @@ +--- +title: "Adopt Diataxis Documentation Framework" +description: "Organize user documentation using the Diataxis framework" +type: adr +category: documentation +tags: + - documentation + - diataxis + - structure +status: accepted +created: "2026-02-16" +updated: "2026-02-16" +author: nsip maintainers +project: nsip +technologies: + - markdown +audience: + - developers +related: + - 0002-documentation-directory-structure.md +--- + +# Adopt Diataxis Documentation Framework + +## Context + +The repository had comprehensive technical reference documentation (11,562 lines across 39 markdown files) but lacked structured learning paths for new users. Existing documentation focused on: + +- Reference material (API docs, MCP server reference) +- Operational runbooks (releasing, CI troubleshooting) +- Template guides (for repository setup) + +However, there were no: +- **Tutorials** - Learning-oriented, hands-on lessons for newcomers +- **How-To Guides** - Problem-oriented solutions for specific tasks +- **Explanations** - Understanding-oriented conceptual documentation + +This created a documentation gap where users had to jump directly from the README examples to comprehensive API reference, with no intermediate learning materials. + +--- + +## Decision + +Adopt the [Diátaxis framework](https://diataxis.fr/) for organizing all user-facing documentation. + +### Framework Structure + +Diátaxis organizes documentation into four quadrants: + +| Type | Orientation | Focus | Example | +|------|------------|-------|---------| +| **Tutorials** | Learning | Practical steps | "Getting Started with NSIP" | +| **How-To Guides** | Problem-solving | Specific goals | "How to Compare Animals" | +| **Explanation** | Understanding | Concepts | "Understanding EBVs" | +| **Reference** | Information | Technical facts | "Error Handling Reference" | + +### Directory Structure + +```` +docs/ +├── README.md # Documentation index with Diátaxis organization +├── tutorials/ # Learning-oriented guides +├── how-to/ # Problem-oriented guides +├── explanation/ # Understanding-oriented discussions +├── reference/ # Information-oriented technical docs +├── runbooks/ # Operational procedures (existing) +├── template/ # Template adoption guides (existing) +└── workflows/ # CI/CD documentation (existing) +```` + +### Initial Documentation Set + +**Tutorials:** +- `tutorials/GETTING-STARTED.md` - 15-minute introduction to NSIP API + +**How-To Guides:** +- `how-to/CONFIGURE-CLIENT.md` - Customize timeout and retry settings +- `how-to/COMPARE-ANIMALS.md` - Side-by-side genetic trait comparisons + +**Explanation:** +- `explanation/EBV-EXPLAINED.md` - What EBVs are and how to use them + +**Reference:** +- `reference/ERROR-HANDLING.md` - Complete error type reference +- `MCP.md` - MCP server API reference (existing, moved to reference category) + +--- + +## Consequences + +### Positive + +✅ **Clear learning path** - New users can start with tutorials and progress naturally +✅ **Findability** - Users know where to look for different types of information +✅ **Progressive disclosure** - Information complexity increases with user expertise +✅ **Reduced cognitive load** - Each document has a single, clear purpose +✅ **Maintainability** - Clear categorization makes updates easier +✅ **Industry standard** - Diátaxis is used by Django, NumPy, and other major projects + +### Negative + +⚠️ **Migration effort** - Some existing docs may need recategorization +⚠️ **Link updates** - Internal references need updating to new paths +⚠️ **Duplication risk** - Clear guidelines needed to avoid content overlap + +### Mitigations + +- Existing documentation remains in place (runbooks, template guides, workflows) +- New categories augment rather than replace current structure +- Documentation index (`docs/README.md`) provides unified navigation +- Cross-references between quadrants help users discover related content + +--- + +## Alternatives Considered + +### 1. Keep Current Ad-Hoc Structure + +**Pros:** No changes needed +**Cons:** Continues documentation gaps, no clear user journey + +### 2. Use README-Only Approach + +**Pros:** Simple, single file +**Cons:** Doesn't scale, hard to navigate, poor SEO + +### 3. Auto-Generated API Docs Only + +**Pros:** Always in sync with code +**Cons:** No conceptual explanations, steep learning curve + +--- + +## References + +- [Diátaxis Framework](https://diataxis.fr/) +- [Django Documentation](https://docs.djangoproject.com/) - Diátaxis example +- [Write the Docs: Documentation Systems](https://www.writethedocs.org/guide/docs-as-code/) + +--- + +## Implementation Notes + +The `update-docs` workflow detected the documentation framework gap and created initial documentation in each quadrant. Future documentation should follow this structure: + +**When writing tutorials:** +- Use step-by-step instructions +- Build something tangible +- Assume minimal prior knowledge +- Provide complete working examples + +**When writing how-to guides:** +- Start with a clear problem statement +- Provide solution steps +- Focus on one specific task +- Include time estimates + +**When writing explanations:** +- Clarify concepts and theory +- Connect ideas +- Provide context and background +- Use diagrams where helpful + +**When writing reference:** +- Be accurate and complete +- Use consistent structure +- Include all parameters/options +- Provide examples diff --git a/docs/adr/README.md b/docs/adr/README.md index d5123ac..ef91a29 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -58,3 +58,4 @@ The HTML viewer is uploaded as a build artifact and can be downloaded from the A - [ADR-0001](0001-use-architectural-decision-records.md) - Use Architectural Decision Records - [ADR-0002](0002-documentation-directory-structure.md) - Documentation Directory Structure +- [ADR-0003](0003-adopt-diataxis-documentation-framework.md) - Adopt Diátaxis Documentation Framework diff --git a/docs/explanation/BREED-GROUPS-AND-TRAITS.md b/docs/explanation/BREED-GROUPS-AND-TRAITS.md new file mode 100644 index 0000000..d087043 --- /dev/null +++ b/docs/explanation/BREED-GROUPS-AND-TRAITS.md @@ -0,0 +1,247 @@ +# Breed Groups and Traits + +> How NSIP organizes sheep breeds into evaluation groups, and which EBV traits are relevant to each group. + +--- + +## Why Breed Groups Exist + +Sheep breeds vary enormously in their production characteristics. A Rambouillet (fine wool breed) and a Katahdin (hair breed) have fundamentally different selection objectives, trait profiles, and genetic parameters. Evaluating them together would be meaningless because the genetic correlations, heritabilities, and economic weights differ between production types. + +NSIP organizes breeds into **breed groups** that share similar production objectives and evaluation frameworks. Within a breed group, the genetic parameters used for BLUP evaluation are appropriate for all member breeds, and cross-breed comparisons (where connectedness exists) are more meaningful. + +--- + +## Breed Group Structure in the API + +Breed groups are the entry point to the NSIP data hierarchy. Each group has a numeric ID, a name, and a list of member breeds. + +```rust +pub struct BreedGroup { + pub id: i64, + pub name: String, + pub breeds: Vec, +} + +pub struct Breed { + pub id: i64, + pub name: String, +} +``` + +The breed group ID is required for many API operations, particularly searching for animals and querying trait ranges. The breed ID further narrows within a group. + +```bash +# List all breed groups +nsip breed-groups + +# Get trait ranges for a specific breed +nsip trait-ranges 640 +``` + +--- + +## Major Breed Group Categories + +NSIP organizes its 23 participating breeds into four primary groups, each reflecting distinct production objectives and evaluation frameworks: + +### USA Hair + +Shedding breeds that do not require shearing. They are selected primarily for meat production and maternal traits. Katahdin is the most-represented breed in the NSIP system, accounting for approximately 35% of all records. + +**Breeds:** Katahdin, Dorper, St. Croix + +**Key traits:** BWT, WWT, MWWT, PWWT, YWT, NLB, NLW, PEMD, PFAT, WFEC, PFEC, SC + +Hair sheep evaluations do not include wool traits. The **USA MAT-HAIR Index** is the primary selection index for this group -- it maximizes total weight of lamb weaned per ewe lambing by combining DWWT, MWWT, NLB, and NLW, with NLW receiving the heaviest economic weighting. + +### USA Terminal + +Terminal sires are used in crossbreeding programs. Their offspring are all destined for market (not kept as breeding replacements), so selection focuses on growth and carcass merit. + +**Breeds:** Suffolk, Hampshire, Texel, Dorset, White Suffolk, Southdown + +**Key traits:** BWT, WWT, PWWT, YWT, PEMD, PFAT. Maternal traits are less emphasized because daughters are not typically retained. + +The **USA Terminal Index** is the primary selection index for this group, emphasizing lean meat production and growth rate. + +### USA Maternal + +Breeds selected for maternal performance -- the ewe's ability to conceive, carry lambs to term, produce milk, and raise healthy offspring. In practice, these breeds are evaluated for both maternal and growth traits. + +**Breeds:** Polypay, Finnsheep, Coopworth, Border Leicester, Corriedale + +**Key traits:** NLB, NLW, MWWT, WWT, BWT, WFEC, PFEC + +### USA Range + +Western range and wool breeds that produce both meat and wool. Selection balances growth, carcass quality, and fleece characteristics. + +**Breeds:** Targhee, Rambouillet, Columbia, SAMM (South African Meat Merino) + +**Key traits:** All growth, carcass, and reproduction traits, plus wool traits (GFW, CFW, FD, SL, SS, FDCV, CURV) that are only meaningful for wool-producing breeds. + +### Other / Dual Purpose + +Several breeds fall outside the four primary groups: Romney, Cheviot, Clun Forest, Shropshire, Tunis, Black Welsh Mountain, and various Composite/Commercial/Terminal entries. These breeds may have more limited trait evaluations depending on data availability. + +--- + +## Trait Availability by Breed + +Not every breed has data for every trait. Trait availability depends on: + +1. **Relevance.** Wool traits are not measured in hair breeds. WFEC/PFEC are not routinely measured in all breeds. +2. **Data volume.** A trait must have sufficient performance records to estimate genetic parameters reliably. Small breeds or newly added traits may have limited data. +3. **Recording infrastructure.** Some traits (like ultrasound EMD and FAT) require specialized equipment that not all breeders have access to. + +Use the `trait_ranges` endpoint to discover which traits are available for a specific breed and what value ranges to expect: + +```rust +let ranges = client.trait_ranges(breed_id).await?; +for range in &ranges { + println!("{}: {:.2} to {:.2} {}", + range.trait_name, + range.min_value, + range.max_value, + range.unit.as_deref().unwrap_or(""), + ); +} +``` + +If a trait is not returned by `trait_ranges` for a given breed, that trait is either not evaluated for that breed or has insufficient data. + +--- + +## Understanding the 13 Traits in Context + +### Growth Traits: The Foundation + +Growth traits (BWT, WWT, MWWT, PWWT, YWT) are evaluated for virtually all breeds because weight gain is a fundamental economic driver in sheep production. All growth EBVs are expressed in **lbs** in the NSIP Search API. + +**Birth Weight (BWT)** stands apart from the other growth traits. While heavier weights at weaning and beyond are desirable, heavier birth weights increase the risk of dystocia (lambing difficulty), particularly in first-parity ewes. The genetic correlation between BWT and later growth traits means that selecting for heavier weaning weights tends to increase birth weight as well. This is a key trade-off that selection indexes address by assigning negative economic weight to BWT. + +**Weaning Weight (WWT)** is measured at 60 days and is the most widely recorded and economically important growth trait. It reflects the lamb's direct genetic growth potential. + +**Maternal Weaning Weight (MWWT)** is a distinct trait from WWT -- it measures the dam's genetic contribution to her lambs' weaning weight through milk production and maternal care. Higher MWWT indicates ewes that raise heavier lambs, making it a key trait for maternal selection. + +**Post-Weaning Weight (PWWT) and Yearling Weight (YWT)** measure growth potential beyond weaning, when the lamb is growing on its own nutrition rather than its dam's milk. These traits are particularly important for operations that sell feeder lambs or retain animals to heavier weights. + +### Carcass Traits: Meeting Market Specifications + +Carcass traits are standardized to a reference body weight of **55 kg (121 lbs)** to allow fair comparison across animals measured at different weights. + +**Post-Weaning Eye Muscle Depth (PEMD/EMD)** measures the cross-sectional area of the longissimus dorsi (loin) muscle via ultrasound scanning, in mm. Higher PEMD indicates greater lean meat yield and is almost always desirable. + +**Post-Weaning Fat Depth (PFAT/CF)** measures subcutaneous fat thickness via ultrasound, in mm. Lower values are preferred, indicating leaner carcasses with better dressing percentage. + +NSIP also provides a **Carcass Plus** composite that combines EMD, FAT, and PWWT into a single carcass merit value for simplified selection. + +### Reproduction Traits: Profitability Drivers + +Reproduction efficiency is the largest single driver of profitability in sheep enterprises. Reproduction traits are expressed as **% above breed average**. + +**Number of Lambs Born (NLB)** measures prolificacy. Higher NLB means more lambs per ewe per year. However, very high NLB (triplets and quads) comes with increased lamb mortality, higher labor requirements, and potential animal welfare concerns. + +**Number of Lambs Weaned (NLW)** captures the combined effect of prolificacy (NLB) and lamb survival. It is a more complete measure of reproductive success than NLB alone because it includes the ewe's ability to raise her lambs to weaning. NLW receives the heaviest economic weighting in the USA MAT-HAIR Index. + +**Scrotal Circumference (SC)** is a male fertility indicator measured in mm. Higher values correlate with improved fertility in both the ram and his daughters, making it valuable for indirect selection on female reproduction. + +### Parasite Resistance Traits: Reducing Input Costs + +**Weaning Fecal Egg Count (WFEC) and Post-Weaning Fecal Egg Count (PFEC)** measure resistance to internal parasites, specifically gastrointestinal nematodes. These are expressed as **% relative to breed average**, where **negative values indicate greater resistance**. For example, a ram with a WFEC of -90% has the potential to reduce worm burden in his lambs by approximately 45% (since half the genetics pass to offspring). + +Selecting for parasite resistance reduces the need for anthelmintic (deworming) treatments, which saves labor and chemical costs, slows the development of drug-resistant parasite populations, and improves animal welfare. These traits are gaining importance as anthelmintic resistance spreads globally. + +### Wool Traits (Wool Breeds Only) + +For USA Range and other wool-producing breeds, additional traits are evaluated: GFW (Greasy Fleece Weight), CFW (Clean Fleece Weight), FD (Fiber Diameter), SL (Staple Length), SS (Staple Strength), FDCV (Fiber Diameter CV), and CURV (Fiber Curvature). These traits are irrelevant for hair breeds. + +--- + +## Trait Interactions and Trade-offs + +Understanding trait correlations is essential for effective selection. The major interactions include: + +### Positive Correlations (Selecting for One Increases the Other) + +- BWT with WWT, PWWT, YWT -- growth genes tend to affect all stages +- WWT with PWWT -- early and late growth are strongly linked +- NLB with NLW -- more born usually means more weaned +- WFEC with PFEC -- both measure aspects of parasite resistance + +### Antagonistic Relationships (Trade-offs) + +- BWT with ease of lambing -- heavier lambs are harder to deliver +- NLB with individual lamb survival -- larger litters have higher per-lamb mortality +- Lean growth (EMD) with fat coverage -- pushing for extreme leanness reduces fat cover +- Growth rate with mature size -- faster-growing animals tend to reach larger mature weights, increasing maintenance feed costs for breeding ewes + +### Independent Traits + +Some trait pairs are largely independent, meaning selection on one has minimal effect on the other. These represent opportunities to improve multiple traits simultaneously without trade-offs. + +--- + +## Choosing Traits for Your Breeding Objective + +The appropriate traits to select depend on your production system and market: + +| Production System | Priority Traits | Secondary Traits | Recommended Index | +|---|---|---|---| +| Terminal sire (all lambs marketed) | WWT, PWWT, PEMD, PFAT | BWT (minimize) | USA Terminal Index | +| Self-replacing hair flock | NLW, MWWT, WWT | BWT, WFEC | USA MAT-HAIR Index | +| Dual-purpose (meat + wool) | WWT, NLW, GFW, FD | PEMD, PFAT | -- | +| Parasite-challenged environment | WFEC/PFEC, NLW | WWT, MWWT | -- | +| Low-input/extensive | NLW, MWWT | BWT (minimize), WFEC | USA MAT-HAIR Index | + +Published selection indexes combine these traits with appropriate economic weights. See [Understanding EBVs](EBV-EXPLAINED.md) for more on selection indexes. + +--- + +## Querying Breed and Trait Data + +### List All Available Breeds + +```bash +nsip breed-groups +``` + +```rust +let groups = client.breed_groups().await?; +``` + +### Get Trait Ranges for a Breed + +```bash +nsip trait-ranges +``` + +```rust +let ranges = client.trait_ranges(breed_id).await?; +``` + +### Search Within a Breed Group + +```bash +nsip search --breed-id 640 --status CURRENT --gender Male +``` + +```rust +let criteria = SearchCriteria::new() + .with_breed_id(640) + .with_status("CURRENT") + .with_gender("Male"); + +let results = client.search_animals(0, 25, 640, None, false, &criteria).await?; +``` + +--- + +## Further Reading + +- [Understanding EBVs](EBV-EXPLAINED.md) -- interpreting EBV values and selection indexes +- [Genetic Evaluation](GENETIC-EVALUATION.md) -- how BLUP produces breed-specific evaluations +- [NSIP Data Model](NSIP-DATA-MODEL.md) -- the data structures behind breed groups and traits +- [Data to Decisions](DATA-TO-DECISIONS.md) -- applying breed and trait knowledge to selection +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) -- comparing animals within a breed diff --git a/docs/explanation/DATA-TO-DECISIONS.md b/docs/explanation/DATA-TO-DECISIONS.md new file mode 100644 index 0000000..44715be --- /dev/null +++ b/docs/explanation/DATA-TO-DECISIONS.md @@ -0,0 +1,335 @@ +# From Data to Decisions + +> How NSIP API data connects to real-world sheep breeding decisions, from sire selection through mating plans to flock-level genetic improvement. + +--- + +## The Decision Pipeline + +Sheep breeding decisions follow a logical pipeline: define objectives, gather data, evaluate candidates, make selections, plan matings, and track progress. The NSIP system and the `nsip` crate provide data at every stage. + +``` +Define breeding objective + | + v +Identify candidate animals --> nsip search (with criteria) + | + v +Evaluate EBVs and accuracy --> nsip details / nsip profile + | + v +Compare candidates --> nsip compare + | + v +Check pedigree / inbreeding --> nsip lineage / MCP inbreeding_check + | + v +Plan matings --> MCP mating_recommendations + | + v +Monitor genetic trend --> nsip search (across years) +``` + +Each step maps to specific API calls and data types in the `nsip` crate. + +--- + +## Step 1: Define Your Breeding Objective + +Before querying the API, you need clarity on what you are selecting for. The breeding objective depends on your production system, market, and constraints. + +### Questions to Answer + +- **What is the end product?** Market lambs, breeding stock, wool, or a combination? +- **What are the major costs?** Feed, labor, animal health, replacements? +- **What traits limit profitability?** Low weaning rates, poor growth, parasite problems? +- **What are the market specifications?** Weight targets, fat coverage, timing? + +The answers determine which EBV traits to prioritize and what selection index to use. See [Breed Groups and Traits](BREED-GROUPS-AND-TRAITS.md) for guidance on matching traits to production systems. + +--- + +## Step 2: Identify Candidate Animals + +Use the `SearchCriteria` builder to narrow the candidate pool. Effective searches combine multiple filters: + +```rust +let criteria = SearchCriteria::new() + .with_breed_id(640) // Katahdin + .with_status("CURRENT") // Active animals only + .with_gender("Male") // Rams + .with_proven_only(true); // Progeny-tested + +let results = client.search_animals(0, 50, 640, Some("WWT"), true, &criteria).await?; +``` + +Or via the CLI: + +```bash +nsip search --breed-id 640 --status CURRENT --gender Male --proven-only --sort WWT --reverse +``` + +### Filtering by Trait Ranges + +If you have specific EBV thresholds, use trait range filters to exclude animals that do not meet minimum standards: + +```bash +nsip trait-ranges 640 +``` + +This returns the observed range for each trait within the breed. Use these ranges to set realistic filters -- filtering outside the observed range will return zero results. + +```rust +use std::collections::HashMap; +use nsip::{SearchCriteria, models::TraitRangeFilter}; + +let mut ranges = HashMap::new(); +ranges.insert("BWT".to_string(), TraitRangeFilter { min: -2.0, max: 0.5 }); +ranges.insert("WWT".to_string(), TraitRangeFilter { min: 2.0, max: 10.0 }); + +let criteria = SearchCriteria::new() + .with_breed_id(640) + .with_status("CURRENT") + .with_gender("Male") + .with_trait_ranges(ranges); +``` + +--- + +## Step 3: Evaluate Individual Animals + +Once you have a shortlist, fetch full details for each candidate: + +```rust +let profile = client.search_by_lpn("6400012006BWR107").await?; + +// Access EBV traits +for (abbrev, trait_data) in &profile.details.traits { + println!("{}: {:.2} (accuracy: {}%)", + abbrev, + trait_data.value, + trait_data.accuracy.unwrap_or(0), + ); +} +``` + +### What to Look For + +**EBV values relative to breed average.** An EBV of +3.0 for WWT means 3 lbs above the current breed average. But "above average" is only meaningful if you know the breed's range -- use `trait_ranges` for context. + +**Accuracy.** Prioritize animals with accuracy above 60% for traits critical to your breeding objective. Low-accuracy animals may look exceptional but carry higher risk. See [Understanding EBVs](EBV-EXPLAINED.md) for accuracy interpretation. + +**Trait balance.** An animal with outstanding WWT but very high BWT may cause lambing problems. Check all relevant traits, not just the one you are selecting for. + +**Genotyped status.** Animals with genomic data ("genotyped": "Yes") have more precise EBVs, particularly for young animals with limited progeny data. + +--- + +## Step 4: Compare Candidates + +Side-by-side comparison reveals relative strengths and weaknesses: + +```bash +nsip compare ANIMAL_A ANIMAL_B --traits BWT,WWT,PWWT,NLB,NLW +``` + +```rust +let profile_a = client.search_by_lpn("ANIMAL_A").await?; +let profile_b = client.search_by_lpn("ANIMAL_B").await?; + +let traits_to_compare = ["BWT", "WWT", "PWWT", "NLB", "NLW"]; +for trait_name in &traits_to_compare { + let val_a = profile_a.details.traits.get(*trait_name); + let val_b = profile_b.details.traits.get(*trait_name); + println!("{}: A={:.2} (acc {}%), B={:.2} (acc {}%)", + trait_name, + val_a.map(|t| t.value).unwrap_or(0.0), + val_a.and_then(|t| t.accuracy).unwrap_or(0), + val_b.map(|t| t.value).unwrap_or(0.0), + val_b.and_then(|t| t.accuracy).unwrap_or(0), + ); +} +``` + +### Decision Factors Beyond EBVs + +EBVs are the primary selection tool, but real-world decisions also consider: + +- **Physical soundness** -- structural correctness, feet, teeth, reproductive anatomy +- **Temperament** -- docile animals are safer to handle and less stressed +- **Price** -- the economic return must justify the purchase cost +- **Logistics** -- distance, quarantine requirements, health testing +- **Genetic diversity** -- avoid over-reliance on a single sire line + +--- + +## Step 5: Check Pedigree and Inbreeding + +Before finalizing mating decisions, examine the pedigree to assess genetic relationships: + +```bash +nsip lineage ANIMAL_ID +``` + +```rust +let lineage = client.lineage("ANIMAL_ID").await?; + +if let Some(sire) = &lineage.sire { + println!("Sire: {}", sire.lpn_id); +} +if let Some(dam) = &lineage.dam { + println!("Dam: {}", dam.lpn_id); +} + +// Check deeper ancestry +for (gen_idx, generation) in lineage.generations.iter().enumerate() { + println!("Generation {}: {} ancestors", gen_idx + 1, generation.len()); +} +``` + +### Inbreeding Concerns + +Mating related animals concentrates genes -- both desirable and undesirable ones. Inbreeding depression in sheep typically manifests as: + +- Reduced lamb survival +- Lower fertility +- Decreased immune function +- Slower growth + +The MCP server provides an `inbreeding_check` tool that calculates the Coefficient of Inbreeding (COI) for a proposed mating using Wright's path formula on the available pedigree data. Use it before committing to a mating plan. + +### Using Pedigree Data for Diversity + +The lineage data reveals how related animals in your shortlist are. If your top two ram candidates share a grandsire, using both in the same flock would concentrate that line. Consider selecting genetically diverse candidates to maintain long-term genetic variation. + +--- + +## Step 6: Plan Matings + +Mating allocation -- deciding which ram mates which ewes -- is where all the previous analysis comes together. The objectives are: + +1. **Maximize expected genetic progress** by matching superior sires with the most ewes +2. **Correct specific weaknesses** by pairing animals whose strengths compensate for each other's weaknesses +3. **Manage inbreeding** by avoiding matings between closely related animals +4. **Maintain genetic diversity** by not over-using any single sire + +### Corrective Mating + +If a ewe has a high BWT EBV (undesirable), mate her to a ram with low BWT to pull the offspring toward the desired range. The expected offspring EBV is approximately the average of the parents' EBVs: + +``` +Expected offspring EBV = (Sire EBV + Dam EBV) / 2 +``` + +This is an approximation -- actual offspring will vary around this expected value due to Mendelian sampling (the random 50% of each parent's genes that gets passed on). + +### Using the MCP Server for Mating Recommendations + +The MCP server's `mating_recommendations` tool automates the mating allocation process, considering EBV complementarity and inbreeding avoidance: + +```bash +nsip mcp +# Use the mating_recommendations tool with sire and dam LPN IDs +``` + +--- + +## Step 7: Monitor Progress + +Genetic improvement is a long-term process. Track progress by monitoring genetic trends across lamb crops: + +### Year-Over-Year Comparison + +Search for animals born in different years and compare average EBVs: + +```rust +let criteria_2024 = SearchCriteria::new() + .with_breed_id(640) + .with_born_after("2024-01-01") + .with_born_before("2024-12-31"); + +let criteria_2023 = SearchCriteria::new() + .with_breed_id(640) + .with_born_after("2023-01-01") + .with_born_before("2023-12-31"); + +let results_2024 = client.search_animals(0, 100, 640, None, false, &criteria_2024).await?; +let results_2023 = client.search_animals(0, 100, 640, None, false, &criteria_2023).await?; + +// Compare average EBVs between years +``` + +### Progeny Analysis + +For a sire in active use, monitor his progeny performance: + +```rust +let progeny = client.progeny("SIRE_LPN_ID", 0, 100).await?; +println!("Total progeny: {}", progeny.total_count); + +// Calculate average trait values across progeny +let wwt_values: Vec = progeny.animals.iter() + .filter_map(|a| a.traits.get("WWT").copied()) + .collect(); + +if !wwt_values.is_empty() { + let avg: f64 = wwt_values.iter().sum::() / wwt_values.len() as f64; + println!("Average WWT of progeny: {:.2}", avg); +} +``` + +--- + +## Putting It All Together: A Worked Example + +Consider a commercial Katahdin flock aiming to improve weaning weight while keeping birth weight under control. Katahdin is the most-represented breed in the NSIP system, accounting for approximately 35% of all records, which means there is a large pool of evaluated animals to choose from and generally higher accuracy EBVs. + +**Objective:** Increase WWT without increasing BWT. + +**Search criteria:** +- Breed: Katahdin (breed_id 640) +- Gender: Male (rams) +- Status: CURRENT +- Proven only: true (high accuracy) +- BWT range: -2.0 to 0.5 (keep birth weight low) +- WWT range: 3.0+ (above average growth) + +```bash +nsip search --breed-id 640 --status CURRENT --gender Male --proven-only --sort WWT --reverse +``` + +**Evaluate top candidates:** Fetch profiles for the top 5 rams. Check WWT, BWT, MWWT (maternal trait for daughters), NLW, accuracy, and genotyped status. Also check the **USA MAT-HAIR Index** value, which combines DWWT, MWWT, NLB, and NLW into a single selection score with NLW most heavily weighted. + +**Compare finalists:** Use `nsip compare` on the top 2-3 candidates. Look for the best balance of high WWT, low BWT, acceptable NLW, strong MWWT, and high accuracy. If parasite resistance matters in your environment, also check WFEC -- a ram with WFEC of -90% can reduce worm burden in his lambs by approximately 45%. + +**Check pedigree:** Verify the chosen ram is not related to existing flock sires. Use the lineage endpoint to inspect ancestry 3-4 generations deep. + +**Allocate matings:** Use the ram across the ewe flock. Consider splitting ewes between two genetically diverse rams to maintain variation. Use corrective mating for ewes with high BWT by pairing them with the ram having the lowest BWT. For Katahdin flocks, the USA MAT-HAIR Index provides a convenient single-number ranking that balances growth and maternal traits. + +**Track results:** In the next evaluation cycle, compare the new lamb crop's average EBVs to the previous year. A successful sire choice will show improvement in WWT without regression in BWT. Since NSIP data is submitted every two weeks and processed through LAMBPLAN, updated EBVs reflecting new progeny data become available on a regular cycle. + +--- + +## Common Decision Pitfalls + +**Chasing the highest number.** The animal with the highest EBV for one trait is rarely the best overall choice. Consider the full trait profile and accuracy. + +**Ignoring accuracy.** A young ram with EBV +8 at 25% accuracy is riskier than a proven ram with EBV +5 at 85% accuracy. The proven ram's true value is more certain. + +**Forgetting inbreeding.** Short-term gains from mating the best to the best can create long-term inbreeding problems. Maintain genetic diversity. + +**Neglecting maternal traits.** Terminal sire selection is straightforward, but self-replacing flocks must select for both growth and maternal performance. NLW and MWWT matter -- the USA MAT-HAIR Index combines these for hair sheep breeds. + +**Using outdated data.** Always check `nsip date-updated` to confirm you are working with the latest evaluation. EBVs change as new data arrives. + +--- + +## Further Reading + +- [Understanding EBVs](EBV-EXPLAINED.md) -- the foundation of genetic selection +- [Genetic Evaluation](GENETIC-EVALUATION.md) -- how EBVs are calculated +- [Breed Groups and Traits](BREED-GROUPS-AND-TRAITS.md) -- choosing the right traits for your system +- [NSIP Data Model](NSIP-DATA-MODEL.md) -- navigating the API data structures +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) -- step-by-step comparison guide +- [How to Configure Client](../how-to/CONFIGURE-CLIENT.md) -- customizing timeout and retry settings +- [MCP Server Reference](../MCP.md) -- using the analytics tools for ranking and mating diff --git a/docs/explanation/EBV-EXPLAINED.md b/docs/explanation/EBV-EXPLAINED.md new file mode 100644 index 0000000..053d6b1 --- /dev/null +++ b/docs/explanation/EBV-EXPLAINED.md @@ -0,0 +1,291 @@ +# Understanding Estimated Breeding Values (EBVs) + +> EBVs are the foundation of genetic improvement in sheep. This guide explains what they are, how they work, and why they matter for breeding decisions. + +--- + +## What is an EBV? + +An **Estimated Breeding Value** (EBV) predicts the genetic merit an animal will pass to its offspring for a specific trait. It is not a measurement of the animal's own performance -- it is an estimate of the *average genetic contribution* to its progeny. + +EBVs are expressed as deviations from a breed average baseline (typically zero). A positive EBV indicates above-average genetic potential; a negative EBV indicates below-average. The units match the trait being measured. + +For example, if a ram has a Weaning Weight (WWT) EBV of +2.5 lbs, his offspring are expected to be 2.5 lbs heavier at weaning than those of an average ram in the same breed, all else being equal. Because each parent contributes half the genetics, the expected offspring advantage is actually half the difference between the two parents' EBVs. This half-EBV concept is sometimes called an **Expected Progeny Difference (EPD)** -- the EPD is simply half the EBV difference between two animals. + +--- + +## Why EBVs, Not Raw Performance? + +Raw performance (phenotype) reflects both genetics and environment. A lamb raised in excellent conditions may outperform a genetically superior lamb raised in poor conditions. EBVs strip away environmental effects to isolate the heritable genetic component. + +This matters because: + +- **Genetics are heritable; environment is not.** A well-fed lamb does not pass its nutrition to offspring. +- **Fair comparison requires adjustment.** Animals from different flocks, years, and management systems can be compared on a level playing field. +- **Selection response is predictable.** Selecting on EBVs produces consistent genetic improvement across generations. + +--- + +## NSIP EBV Traits + +NSIP evaluates a range of traits organized into functional categories. Each trait has a standard abbreviation used throughout the API and CLI. Not all traits are evaluated for every breed -- trait availability depends on the breed group and data collection practices. + +> **Note on units:** The NSIP Search API reports growth traits in **lbs** (US customary) while the underlying LAMBPLAN evaluation system in Australia uses kg. The `nsip` CLI and library return values in the units provided by the API (lbs for growth, mm for carcass). The tables below show the API units. + +### Growth Traits + +| Abbreviation | Trait | Unit | Selection Direction | +|---|---|---|---| +| BWT | Birth Weight | lbs | Lower preferred (reduces dystocia) | +| WWT | Weaning Weight (60 days) | lbs | Higher preferred | +| MWWT | Maternal Weaning Weight | lbs | Higher preferred (more milk) | +| PWWT | Post-Weaning Weight | lbs | Higher preferred | +| YWT | Yearling Weight | lbs | Higher preferred | + +Birth weight is unusual: lower EBVs are generally preferred because heavier birth weights increase the risk of lambing difficulty (dystocia). The other growth traits follow the typical "higher is better" pattern for meat production. **MWWT (Maternal Weaning Weight)** is a distinct trait from WWT -- it measures the dam's genetic contribution to lamb weaning weight through milk production and maternal care, rather than the lamb's own direct growth potential. + +### Carcass Traits + +Carcass traits are standardized to a reference body weight of 55 kg (121 lbs) to allow fair comparison across animals measured at different weights. + +| Abbreviation | Trait | Unit | Selection Direction | +|---|---|---|---| +| PFAT (CF) | Post-Weaning Fat Depth | mm | Lower preferred (less fat) | +| PEMD (EMD) | Post-Weaning Eye Muscle Depth | mm | Higher preferred | + +Eye muscle depth measures the loin muscle cross-section and correlates with lean meat yield. Fat depth is measured as subcutaneous fat thickness via ultrasound -- lower values indicate leaner carcasses. NSIP also provides a **Carcass Plus** composite that combines EMD, FAT, and PWWT into a single carcass merit value. + +### Reproduction Traits + +| Abbreviation | Trait | Unit | Selection Direction | +|---|---|---|---| +| NLB | Number of Lambs Born | % above breed avg | Higher (with caution) | +| NLW | Number of Lambs Weaned | % above breed avg | Higher preferred | +| SC | Scrotal Circumference | mm | Higher preferred (fertility indicator) | + +NLB drives prolificacy but must be balanced against lamb survival -- triplets and quads have higher mortality. NLW captures the combined effect of prolificacy and lamb survival, making it a more practical selection target than NLB alone. SC (Scrotal Circumference) is a male fertility indicator -- higher values correlate with improved fertility in both the ram and his daughters. + +### Parasite Resistance Traits + +| Abbreviation | Trait | Unit | Selection Direction | +|---|---|---|---| +| WFEC | Weaning Fecal Egg Count | % | Lower/negative preferred | +| PFEC | Post-Weaning Fecal Egg Count | % | Lower/negative preferred | + +WFEC and PFEC measure parasite resistance as a percentage relative to the breed average. **Negative values indicate greater resistance.** For example, a ram with a WFEC of -90% has the potential to reduce worm burden in his lambs by approximately 45% (since half the genetics pass to offspring). Selecting for parasite resistance reduces the need for anthelmintic (deworming) treatments, slows the development of drug-resistant parasite populations, and improves animal welfare. + +### Wool Traits (Wool Breeds Only) + +For wool-producing breeds, additional traits may be evaluated including GFW (Greasy Fleece Weight), CFW (Clean Fleece Weight), FD (Fiber Diameter), SL (Staple Length), SS (Staple Strength), FDCV (Fiber Diameter CV), and CURV (Fiber Curvature). These traits are not relevant for hair sheep breeds. + +--- + +## How EBVs Are Calculated + +NSIP uses **BLUP (Best Linear Unbiased Prediction)**, the same statistical method used in cattle, pig, and poultry genetic evaluation worldwide. + +### Data Inputs + +BLUP combines three sources of information: + +1. **Performance records** -- measured traits from the animal and its contemporaries (birth weights, weaning weights, ultrasound scans, etc.) +2. **Pedigree data** -- ancestry relationships linking animals to their sire, dam, and extended family. This allows information to flow between relatives. +3. **Genomic data** (when available) -- DNA marker information that refines the estimated genetic relationships between animals. + +### What BLUP Does + +The BLUP model simultaneously estimates: + +- **Fixed effects** -- systematic environmental factors such as year of birth, flock of origin, age of dam, birth type (single/twin/triplet), and sex. These are "corrected out" so they do not bias the genetic estimates. +- **Random genetic effects** -- the actual breeding values. BLUP uses the pedigree (and genomic) relationships to borrow information from relatives, which is why a young animal with no progeny can still have an EBV based on its parents' and siblings' data. + +The key property of BLUP is that it produces **unbiased** estimates -- the EBVs are not systematically too high or too low for any group of animals. This is what makes across-flock comparison valid. + +### Genetic Connectedness + +For EBVs to be comparable across flocks, flocks must share genetic links. This happens when: + +- Rams are used across multiple flocks +- AI (artificial insemination) sires create connections +- Reference sires are shared through cooperative programs + +Without connectedness, EBVs from different flocks cannot be meaningfully compared, even within the same breed evaluation. + +--- + +## Understanding Accuracy + +Every EBV in the NSIP system has an associated **accuracy** value. In the `nsip` crate, accuracy is stored as an integer percentage (0--100) in the `Trait` struct: + +```rust +// From crates/models.rs +pub struct Trait { + pub name: String, + pub value: f64, + pub accuracy: Option, // Integer percentage 0-100 + pub units: Option, +} +``` + +### What Accuracy Means + +Accuracy measures the reliability of an EBV estimate -- how likely the EBV is to change as more data becomes available. It reflects the amount and quality of information behind the estimate. + +| Accuracy Range | Interpretation | Typical Source | +|---|---|---| +| 0--29% | Low | Parent average only, no own or progeny data | +| 30--59% | Moderate | Own performance and/or genomic data | +| 60--79% | High | Some progeny records | +| 80--100% | Very high | Extensive progeny data across multiple flocks | + +### Practical Implications + +- **High-accuracy EBVs are stable.** An animal with 85% accuracy for WWT is unlikely to see its EBV change significantly with additional data. +- **Low-accuracy EBVs carry risk.** A young ram with 25% accuracy might look exceptional, but his true breeding value could be substantially different. Use low-accuracy EBVs for screening, not final selection. +- **Proven sires have high accuracy.** Rams with many progeny across multiple flocks accumulate accuracy quickly. +- **Accuracy increases over time.** As an animal accumulates its own records and progeny data, its accuracy rises. + +### Accessing Accuracy via the CLI + +```bash +# View an animal's details including trait accuracies +nsip details 6400012006BWR107 + +# JSON output includes accuracy as integer percentage +nsip details 6400012006BWR107 --json +``` + +### Accessing Accuracy via the Library + +```rust +let details = client.animal_details("6400012006BWR107").await?; + +if let Some(bwt) = details.traits.get("BWT") { + println!("BWT EBV: {:.2} lbs", bwt.value); + if let Some(acc) = bwt.accuracy { + println!("Accuracy: {}%", acc); + if acc < 60 { + println!("Warning: low accuracy, treat with caution"); + } + } +} +``` + +--- + +## Genetic Trend and Base Changes + +EBV values are relative to a breed base that may shift over time as the breed average improves through selection. This has important consequences: + +- **An EBV of +5 today is not the same as +5 ten years ago.** If the breed has improved, the base has shifted upward, and today's +5 represents a higher absolute genetic level. +- **Always compare animals from the same evaluation run.** The NSIP database is periodically re-evaluated, and all EBVs are recalculated together. +- **Check the database date** to confirm you are working with current evaluations: + +```bash +nsip date-updated +``` + +```rust +let date_info = client.date_last_updated().await?; +``` + +--- + +## Selection Indexes + +Selecting on individual traits one at a time is inefficient and can cause problems. Pushing hard on weaning weight alone might inadvertently increase birth weight (and dystocia risk) because the traits are genetically correlated. + +**Selection indexes** solve this by combining multiple EBVs into a single score, weighted by their economic importance and adjusted for genetic correlations between traits. + +### How Indexes Work + +An index assigns economic weights to each trait based on its impact on profitability. For example, a simplified terminal sire index might look like: + +``` +Index = (w1 x WWT) + (w2 x PWWT) + (w3 x EMD) - (w4 x FAT) - (w5 x BWT) +``` + +The weights (w1 through w5) are derived from economic modeling and genetic parameters (heritabilities and correlations). Negative weight on BWT means the index penalizes animals that increase birth weight. + +### NSIP Indexes + +NSIP provides several selection indexes tailored to different production systems: + +- **USA MAT-HAIR Index** -- designed to maximize the total weight of lamb weaned per ewe lambing. It combines DWWT (Direct Weaning Weight), MWWT (Maternal Weaning Weight), NLB, and NLW, with NLW receiving the heaviest economic weighting. This index is used for hair sheep breeds such as Katahdin and Dorper. +- **USA Terminal Index** -- emphasizes growth and carcass traits for terminal sire breeds (Suffolk, Hampshire, Texel, etc.), prioritizing lean meat production. + +The NSIP API provides index values through the lineage endpoint. The `LineageAnimal` struct includes: + +- `us_index` -- the US index value (e.g., USA MAT-HAIR for hair breeds) +- `src_index` -- the SRC$ Index + +These pre-calculated indexes save producers from having to compute their own weighted combinations. + +### When to Use Indexes vs. Individual Traits + +| Scenario | Approach | +|---|---| +| General flock improvement | Use the published index | +| Corrective mating (fixing a specific weakness) | Emphasize the relevant individual trait | +| Research or custom breeding objectives | Build a custom index with appropriate weights | + +--- + +## Comparing Animals + +EBV comparisons are only meaningful under specific conditions: + +1. **Same breed.** EBVs are calculated within breed. A Katahdin with WWT +3.0 cannot be compared to a Suffolk with WWT +3.0 -- the breed bases are different. +2. **Same evaluation run.** EBVs from different evaluation dates may use different base adjustments. +3. **Consider accuracy.** When two animals have similar EBVs but different accuracies, the higher-accuracy animal is the safer choice. + +The `nsip` CLI provides a dedicated comparison command: + +```bash +nsip compare ANIMAL_ID_1 ANIMAL_ID_2 --traits BWT,WWT,PEMD +``` + +And the library supports fetching multiple animals for programmatic comparison: + +```rust +let profile_a = client.search_by_lpn("ANIMAL_ID_1").await?; +let profile_b = client.search_by_lpn("ANIMAL_ID_2").await?; + +// Compare specific traits +for trait_name in &["BWT", "WWT", "PEMD"] { + let val_a = profile_a.details.traits.get(*trait_name).map(|t| t.value); + let val_b = profile_b.details.traits.get(*trait_name).map(|t| t.value); + println!("{}: A={:?}, B={:?}", trait_name, val_a, val_b); +} +``` + +--- + +## Common Misconceptions + +**"A higher EBV is always better."** +Not true. For BWT, lower is preferred. For PFAT, lower is preferred (less fat). For WFEC and PFEC, lower/negative values are preferred (indicating greater parasite resistance). Always check the selection direction for each trait. + +**"EBVs predict an animal's own performance."** +EBVs predict genetic contribution to offspring, not the animal's own phenotype. A ewe with a high WWT EBV may have been a light lamb herself if she was raised in poor conditions. + +**"I can compare EBVs across breeds."** +EBVs are breed-specific. The genetic base, heritability estimates, and evaluation models differ between breeds. Cross-breed comparisons require special across-breed evaluation methodologies that NSIP does not currently provide. + +**"Low-accuracy EBVs are useless."** +They are less reliable, but still the best available estimate of an animal's genetic merit. A low-accuracy EBV from BLUP is better than no genetic information at all. Use them as screening tools while waiting for more data. + +**"Once calculated, an EBV never changes."** +EBVs are re-estimated with each evaluation run as new data arrives. An animal's EBV can change -- sometimes substantially for young animals with low accuracy -- as progeny records accumulate. + +--- + +## Further Reading + +- [Genetic Evaluation](GENETIC-EVALUATION.md) -- deeper dive into BLUP methodology and connectedness +- [Breed Groups and Traits](BREED-GROUPS-AND-TRAITS.md) -- understanding how breeds and traits are organized +- [NSIP Data Model](NSIP-DATA-MODEL.md) -- how the API structures animal, lineage, and progeny data +- [Data to Decisions](DATA-TO-DECISIONS.md) -- connecting API data to real-world breeding decisions +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) -- step-by-step comparison guide +- [Getting Started Tutorial](../tutorials/GETTING-STARTED.md) -- hands-on introduction to the NSIP API +- [Error Handling Reference](../reference/ERROR-HANDLING.md) -- complete error type reference diff --git a/docs/explanation/GENETIC-EVALUATION.md b/docs/explanation/GENETIC-EVALUATION.md new file mode 100644 index 0000000..3eec55f --- /dev/null +++ b/docs/explanation/GENETIC-EVALUATION.md @@ -0,0 +1,229 @@ +# Genetic Evaluation + +> How the NSIP system transforms raw performance data into Estimated Breeding Values using BLUP, and why the methodology matters for breeding decisions. + +--- + +## What is Genetic Evaluation? + +Genetic evaluation is the statistical process of estimating an animal's genetic merit from available data. The NSIP uses this process to produce Estimated Breeding Values (EBVs) for multiple traits across 23 sheep breeds in the United States. + +NSIP was founded in 1986 and completed its Phase I development in 1990. **Across-flock evaluations** -- the ability to compare animals from different flocks on a common genetic scale -- began around 1993, a major milestone that enabled meaningful national genetic improvement. Today, NSIP operates through a cooperative agreement with **Sheep Genetics (Australia)**, where flock data is processed through **Pedigree Master** software and sent to **LAMBPLAN** for BLUP genetic analysis. Data is submitted every two weeks for ongoing EBV development. + +The goal is to separate the heritable genetic component of a trait from the environmental influences. A lamb's weaning weight, for example, depends on its genetics, its dam's milk production, the pasture quality, the weather, and the management system. Genetic evaluation isolates the genetic signal from all that noise. + +--- + +## BLUP: The Engine Behind EBVs + +NSIP uses **Best Linear Unbiased Prediction** (BLUP), developed by Charles Henderson in the 1950s and now the standard method for livestock genetic evaluation worldwide. + +### Why "Best Linear Unbiased Prediction"? + +Each word in the name describes a specific statistical property: + +- **Best** -- among all linear unbiased predictors, BLUP minimizes the prediction error variance. In practical terms, BLUP produces the most accurate estimates possible given the available data. +- **Linear** -- the estimated breeding values are linear functions of the observed data. This makes computation tractable even for very large datasets. +- **Unbiased** -- the estimates have no systematic bias. Animals from different flocks, years, or management systems are evaluated on a fair basis. +- **Prediction** -- breeding values are predicted (not directly observed), because we can never measure an animal's true genetic merit directly. + +### The Mixed Model Equations + +BLUP works by solving a system called the **mixed model equations**. In simplified form: + +``` +y = Xb + Zu + e +``` + +Where: + +- **y** = vector of observed phenotypes (measured traits like birth weight, weaning weight) +- **X** = design matrix linking observations to fixed effects +- **b** = fixed effects (year, flock, sex, age of dam, birth type, etc.) +- **Z** = design matrix linking observations to animals +- **u** = random genetic effects (the breeding values we want to estimate) +- **e** = residual error (unexplained variation) + +The system simultaneously solves for the fixed effects (b) and the breeding values (u). This simultaneous estimation is critical -- it prevents environmental effects from biasing the genetic estimates and prevents genetic differences from biasing the environmental corrections. + +### The Role of the Relationship Matrix + +BLUP uses a **genetic relationship matrix** (A) that encodes the pedigree relationships between all animals in the evaluation. This matrix allows information to flow between relatives: + +- A sire's EBV is informed by his offspring's performance +- A young animal's EBV is informed by its parents' and siblings' data +- Even animals with no own performance data receive an EBV (parent average) + +The relationship matrix is why pedigree accuracy matters. Incorrect parentage assignments (wrong sire or dam) introduce errors that propagate through the evaluation. + +### Genomic Enhancement + +When DNA marker data is available, BLUP can be extended to **GBLUP** (Genomic BLUP) or **single-step genomic evaluation** by replacing or supplementing the pedigree-based relationship matrix with a genomic relationship matrix. Single-step methods combine pedigree, phenotypic, and genomic information simultaneously, which is the direction modern genetic evaluation is moving. This improves accuracy because: + +- Actual genetic sharing between relatives varies around the expected value (full siblings share 50% on average but the actual value ranges from roughly 38% to 62%) +- Genomic data captures the actual sharing, not just the expected value +- Animals without pedigree connections can be related through shared genomic segments + +The `genotyped` field in `AnimalDetails` indicates whether an animal has genomic data incorporated into its evaluation. + +--- + +## Fixed Effects: What Gets Corrected + +BLUP removes the influence of known environmental factors so that the remaining variation reflects genetics. The major fixed effects in sheep evaluation include: + +### Flock-Year-Season (Contemporary Group) + +The most important fixed effect. Animals are compared only against contemporaries raised in the same flock, during the same time period, under the same management. A lamb from a high-performing flock is not credited for its superior environment. + +### Age of Dam + +Older ewes (2nd through 5th parity) typically produce heavier lambs than first-parity ewes, because they are physically more mature and produce more milk. BLUP adjusts for this so that a lamb from a maiden ewe is not penalized. + +### Birth Type and Rearing Type + +Twins and triplets are lighter at birth and weaning than singles, because they share uterine space and maternal resources. BLUP adjusts for birth type (single, twin, triplet) and rearing type (how many lambs the ewe actually raised). + +### Sex + +Males are typically heavier than females at all ages. Separate sex effects prevent this from biasing EBVs. + +--- + +## Across-Flock Evaluation and Connectedness + +A key feature of NSIP's BLUP evaluation is that EBVs are comparable across flocks within the same breed. This is possible only when flocks are **genetically connected** -- they share genetic material through common ancestors. + +### How Connectedness Works + +``` + Flock A Flock B + | | + v v + Ram X (AI sire) used in both flocks + | | + v v + Progeny A Progeny B +``` + +When Ram X produces offspring in both Flock A and Flock B, the evaluation can separate the effect of the ram's genetics from the effect of each flock's environment. Without this link, the evaluation cannot tell whether Flock A's lambs are heavier because of better genetics or better feed. + +### Sources of Connectedness + +- **AI (artificial insemination) sires** -- the most common source of cross-flock links +- **Reference sire programs** -- cooperative programs where multiple flocks use designated rams +- **Ram purchases** -- when a flock buys a ram from another flock and both record progeny +- **Genomic links** -- DNA data can reveal relationships between animals in different flocks + +### Why Disconnected Comparisons Fail + +If two flocks have no genetic connections, their EBVs exist on independent scales. Flock A's "best" ram (EBV +5) might actually be genetically inferior to Flock B's "worst" ram (EBV -2) if Flock B's genetics are substantially better overall. The NSIP evaluation flags the degree of connectedness to help producers interpret cross-flock comparisons. + +--- + +## Multi-Trait Evaluation + +NSIP evaluates multiple traits simultaneously rather than one at a time. This multi-trait approach is important because: + +### Genetic Correlations + +Traits are not independent. Selecting for higher weaning weight also tends to increase birth weight, because the genes affecting growth are partially shared. The major genetic correlations in sheep include: + +| Trait Pair | Correlation Direction | Implication | +|---|---|---| +| BWT and WWT | Positive | Selecting for heavier weaning weights tends to increase birth weight | +| WWT and PWWT | Strongly positive | Growth traits track together | +| NLB and NWT | Moderate positive | More lambs born generally means more weaned, but not proportionally | +| EMD and FAT | Weakly positive | Muscular animals tend to carry slightly more fat | +| WEC and FEC | Strongly positive | Both measure parasite resistance | +| NLB and lamb survival | Negative | More lambs born per litter means lower individual survival | + +### Benefits of Multi-Trait Evaluation + +- **Correlated traits inform each other.** Even if weaning weight is not measured for an animal, its birth weight record provides indirect information about weaning weight through the known genetic correlation. +- **Missing data is handled naturally.** Not all traits are recorded for all animals. Multi-trait BLUP uses whatever data is available without discarding incomplete records. +- **Genetic trends are tracked correctly.** Correlated responses to selection (unintended changes in traits not under direct selection) are properly accounted for. + +--- + +## From Raw Data to EBV: The Pipeline + +The complete evaluation pipeline involves several stages: + +### 1. Data Collection + +Breeders record performance data on their animals: birth weights, weaning weights, ultrasound measurements (PEMD, PFAT), reproduction records (NLB, NLW), fecal egg counts (WFEC, PFEC), scrotal circumference (SC), and wool traits for wool breeds. This data is submitted to NSIP every two weeks along with pedigree information (sire, dam) and management details (flock, birth date, birth type). The data flows through Pedigree Master software to LAMBPLAN in Australia for BLUP processing. + +### 2. Data Validation + +Incoming data is checked for errors: impossible values (negative weights, animals born before their parents), missing pedigree links, duplicate records, and outliers. Invalid records are flagged for correction. + +### 3. Contemporary Group Formation + +Animals are grouped into contemporary groups based on flock, year, season, and management. These groups define the comparison set -- animals are evaluated relative to their contemporaries. + +### 4. BLUP Evaluation + +The mixed model equations are constructed and solved, incorporating all performance records, pedigree relationships, and (where available) genomic data. This produces EBVs and accuracies for every animal in the evaluation for all 13 traits. + +### 5. Publication + +Results are published to the NSIP Search API, where they can be accessed through the `nsip` CLI and library. The `date_last_updated` endpoint reports when the most recent evaluation was published. + +### EBVs and EPDs + +EBVs and **Expected Progeny Differences (EPDs)** are closely related concepts. An EPD is simply half the EBV difference between two animals -- because each parent contributes half its genetics to its offspring. Some livestock industries (notably U.S. beef cattle) use EPDs as the primary metric, while NSIP follows the Australian convention of reporting full EBVs. When comparing across systems, remember: EPD = EBV / 2 for a single parent's contribution. + +--- + +## Accuracy: A Measure of Information + +Accuracy quantifies how much information backs an EBV estimate. It ranges from 0% (no information) to 100% (perfect knowledge of the true breeding value). + +### Factors That Increase Accuracy + +| Factor | Effect on Accuracy | +|---|---| +| Own performance records | Moderate increase | +| Progeny records | Large increase per progeny | +| Number of flocks with progeny | Increases by removing flock-environment confounding | +| Genomic data | Moderate increase, especially for young animals | +| Trait heritability | Higher heritability means own records are more informative | +| Pedigree depth and completeness | Better pedigree improves parent average estimates | + +### Accuracy and Selection Risk + +The practical implication of accuracy is **selection risk**. A ram with EBV +5.0 and 30% accuracy might truly be anywhere from +1.0 to +9.0. The same EBV at 90% accuracy might range from +4.0 to +6.0. Producers pay more for accuracy because it reduces the chance of a disappointing outcome. + +In the `nsip` crate, accuracy is stored as `Option` (integer percentage) in the `Trait` struct. See [Understanding EBVs](EBV-EXPLAINED.md) for a detailed discussion of accuracy interpretation. + +--- + +## Genetic Trend + +Over time, selection drives genetic improvement. The breed average EBV for selected traits increases (or decreases for traits where lower is preferred). This genetic trend is a key measure of a breeding program's success. + +Because EBVs are expressed relative to a base, a breed undergoing strong selection for growth will see its average WWT EBV increase over the years. An animal that was above average ten years ago might be below average today even though its genetics have not changed -- the rest of the breed has improved around it. + +This is why the database update date matters: it tells you which evaluation run your data comes from. + +--- + +## Limitations of the System + +No evaluation system is perfect. Key limitations include: + +- **Data quality depends on breeders.** Incorrect parentage, inconsistent measurement protocols, or selective reporting (only recording good animals) can bias results. +- **Small breed populations** have fewer data points and fewer connectedness links, leading to lower accuracy and less reliable across-flock comparisons. +- **Genotype-by-environment interaction.** An animal's genetics may express differently in different environments. BLUP assumes the genetic rankings are consistent across environments, which is approximately but not perfectly true. +- **Non-additive genetic effects** (dominance, epistasis) are not captured by standard BLUP. These effects contribute to performance but are not heritable in the same predictable way as additive effects. + +--- + +## Further Reading + +- [Understanding EBVs](EBV-EXPLAINED.md) -- interpreting EBV values and accuracy +- [Breed Groups and Traits](BREED-GROUPS-AND-TRAITS.md) -- which traits are evaluated for which breeds +- [NSIP Data Model](NSIP-DATA-MODEL.md) -- how the API structures evaluation results +- [Data to Decisions](DATA-TO-DECISIONS.md) -- applying genetic evaluation to breeding programs +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) -- practical comparison techniques diff --git a/docs/explanation/NSIP-DATA-MODEL.md b/docs/explanation/NSIP-DATA-MODEL.md new file mode 100644 index 0000000..d661226 --- /dev/null +++ b/docs/explanation/NSIP-DATA-MODEL.md @@ -0,0 +1,377 @@ +# NSIP Data Model + +> How the NSIP Search API organizes sheep genetic evaluation data, from breed groups down to individual trait values. + +--- + +## Overview + +The **National Sheep Improvement Program (NSIP)** was founded in 1986 as a non-profit organization with a volunteer board of directors. It operates through a cooperative agreement with **Sheep Genetics (Australia)** -- flock data is processed through **Pedigree Master** software and sent to **LAMBPLAN** for BLUP genetic analysis. This partnership gives U.S. sheep producers access to the same world-class genetic evaluation methodology used by Australian sheep breeders. + +NSIP currently serves **23 participating breeds** organized into breed groups, with fee-based enrollment by active seedstock flock size. Data is submitted every two weeks for ongoing EBV development, and new members receive breed-specific mentors to help with data collection and interpretation. + +The NSIP data model represents a hierarchy of genetic evaluation data. At the top level, animals are organized into breed groups and breeds. Each animal carries a set of EBV traits, belongs to a flock, has pedigree connections (lineage), and may have progeny records. + +The `nsip` crate models this hierarchy through a set of Rust structs that map directly to the API's JSON responses. Understanding these structures is essential for writing effective queries and interpreting results. + +--- + +## The Data Hierarchy + +``` +Breed Group (e.g., "USA Hair", "USA Range", "USA Terminal") + | + +-- Breed (e.g., "Katahdin", "Suffolk", "Targhee") + | + +-- Flock (identified by flock_id, associated with ContactInfo) + | + +-- Animal (identified by LPN ID) + | + +-- Traits (HashMap of EBV values with accuracy) + +-- Lineage (pedigree tree: sire, dam, grandparents...) + +-- Progeny (offspring list with their traits) +``` + +Each level in this hierarchy corresponds to API endpoints and Rust types in the `nsip` crate. + +--- + +## Breed Groups and Breeds + +Breed groups are the top-level organizational unit. Each group contains one or more related breeds that share a common evaluation framework. + +```rust +pub struct BreedGroup { + pub id: i64, // e.g., 61 + pub name: String, // e.g., "Range" + pub breeds: Vec, +} + +pub struct Breed { + pub id: i64, // e.g., 486 + pub name: String, // e.g., "South African Meat Merino" +} +``` + +Breed group IDs and breed IDs are numeric identifiers assigned by the NSIP system. They are used as parameters when searching for animals or querying trait ranges. The 23 participating breeds include: Katahdin, Suffolk, Polypay, Targhee, Dorper, White Suffolk, Dorset, Hampshire, Rambouillet, Columbia, Texel, Romney, Coopworth, Finnsheep, Border Leicester, Southdown, Cheviot, Clun Forest, Shropshire, SAMM (South African Meat Merino), Tunis, Black Welsh Mountain, and various Composite/Commercial/Terminal entries. + +```bash +# List all breed groups and their breeds +nsip breed-groups +``` + +```rust +let groups = client.breed_groups().await?; +for group in &groups { + println!("Group: {} (ID: {})", group.name, group.id); + for breed in &group.breeds { + println!(" Breed: {} (ID: {})", breed.name, breed.id); + } +} +``` + +The grouping matters because traits are evaluated within breed groups. NSIP uses four primary group categories: **USA Hair** (Katahdin, Dorper, St. Croix), **USA Terminal** (Suffolk, Hampshire, Texel, Dorset, White Suffolk, Southdown), **USA Maternal** (Polypay, Finnsheep, Coopworth, Border Leicester), and **USA Range** (Targhee, Rambouillet, Columbia, SAMM). Not all traits are evaluated for all breeds -- for example, wool traits are only meaningful for wool breeds, and parasite resistance traits (WFEC, PFEC) depend on breed-specific data collection. + +See [Breed Groups and Traits](BREED-GROUPS-AND-TRAITS.md) for a detailed discussion of which traits apply to which breed groups. + +--- + +## Animal Identification + +Every animal in the NSIP system is identified by a **LPN ID** (Lamb Plan Number). This is a string identifier that uniquely identifies an animal across the entire NSIP database. + +LPN IDs are the primary key for looking up animal details, lineage, and progeny. They appear throughout the data model: + +- `AnimalDetails.lpn_id` -- the animal itself +- `AnimalDetails.sire` and `AnimalDetails.dam` -- parent references +- `LineageAnimal.lpn_id` -- nodes in the pedigree tree +- `ProgenyAnimal.lpn_id` -- offspring records + +--- + +## Animal Details + +The `AnimalDetails` struct is the core data type representing a single animal's record. It contains identification, demographics, and EBV trait values. + +```rust +pub struct AnimalDetails { + pub lpn_id: String, + pub breed: Option, + pub breed_group: Option, + pub date_of_birth: Option, + pub gender: Option, // "Male" or "Female" + pub status: Option, // "CURRENT", "SOLD", "DEAD", etc. + pub sire: Option, // Sire's LPN ID + pub dam: Option, // Dam's LPN ID + pub registration_number: Option, + pub total_progeny: Option, + pub flock_count: Option, + pub genotyped: Option, + pub traits: HashMap, // Keyed by trait abbreviation + pub contact_info: Option, +} +``` + +### Why Most Fields Are Optional + +The NSIP API returns different subsets of fields depending on the endpoint and the data available for a particular animal. A search result row contains fewer fields than a full detail response. Some animals lack registration numbers, genotyping data, or contact information. The `Option` wrapper handles this gracefully -- callers must explicitly handle the absence of data rather than encountering unexpected nulls. + +### The Traits Map + +Traits are stored as a `HashMap` keyed by the standard trait abbreviation (BWT, WWT, PWWT, etc.). This design allows the data model to accommodate any set of traits without hardcoding specific fields for each one. + +```rust +pub struct Trait { + pub name: String, // Trait abbreviation, e.g., "BWT" + pub value: f64, // The EBV value + pub accuracy: Option, // Integer percentage 0-100 + pub units: Option, // e.g., "lbs", "mm" +} +``` + +Note that `accuracy` is `Option`, not `f64`. The API sometimes returns accuracy as a decimal fraction (0.0--1.0) and sometimes as a percentage (0--100). The `nsip` crate normalizes these to integer percentages internally via the `convert_accuracy` function. + +### Animal Status + +The `status` field indicates an animal's current standing in the flock: + +| Status | Meaning | +|---|---| +| CURRENT | Active in the flock, available for breeding | +| SOLD | Transferred to another flock | +| DEAD | Deceased | + +Query the available statuses dynamically: + +```bash +nsip statuses +``` + +--- + +## Lineage (Pedigree) + +The lineage system represents an animal's ancestry as a tree structure. The NSIP API returns pedigree data as a recursive tree where each node has an `lpnId`, HTML `content` (containing farm name, index values, and demographics), and a `children` array (where index 0 is the sire and index 1 is the dam). + +The `nsip` crate parses this into a structured `Lineage` type: + +```rust +pub struct Lineage { + pub subject: Option, // The animal itself + pub sire: Option, // Father + pub dam: Option, // Mother + pub generations: Vec>, // Deeper ancestors +} + +pub struct LineageAnimal { + pub lpn_id: String, + pub farm_name: Option, + pub us_index: Option, // US (Hair) Index + pub src_index: Option, // SRC$ Index + pub date_of_birth: Option, + pub sex: Option, + pub status: Option, +} +``` + +### Generations Structure + +The `generations` field is a vector of vectors. Index 0 contains grandparents, index 1 contains great-grandparents, and so on. Within each generation, animals appear in pedigree order (sire's sire, sire's dam, dam's sire, dam's dam for the grandparent generation). + +```rust +let lineage = client.lineage("6400012006BWR107").await?; + +if let Some(sire) = &lineage.sire { + println!("Sire: {} ({})", sire.lpn_id, sire.farm_name.as_deref().unwrap_or("unknown")); +} + +// Grandparents +if let Some(grandparents) = lineage.generations.first() { + for gp in grandparents { + println!("Grandparent: {}", gp.lpn_id); + } +} +``` + +### Index Values in Lineage + +Lineage nodes include selection index values (`us_index`, `src_index`) that are not present in the main `AnimalDetails` struct. These indexes combine multiple EBVs into a single ranking score and are particularly useful for quick pedigree-level comparisons. + +--- + +## Progeny + +The progeny endpoint returns a paginated list of an animal's offspring, each with their own trait values. + +```rust +pub struct Progeny { + pub total_count: i64, + pub animals: Vec, + pub page: u32, + pub page_size: u32, +} + +pub struct ProgenyAnimal { + pub lpn_id: String, + pub sex: Option, + pub date_of_birth: Option, + pub traits: HashMap, // Trait values only, no accuracy +} +``` + +Note that progeny trait values are `f64` (not the full `Trait` struct). Accuracy is not included in progeny records -- only the EBV values themselves. + +```bash +nsip progeny 6400012006BWR107 +``` + +```rust +let progeny = client.progeny("6400012006BWR107", 0, 25).await?; +println!("Total offspring: {}", progeny.total_count); +for animal in &progeny.animals { + let wwt = animal.traits.get("WWT").copied().unwrap_or(0.0); + println!(" {} - WWT: {:.2}", animal.lpn_id, wwt); +} +``` + +--- + +## Search and Filtering + +The `SearchCriteria` struct provides a builder-pattern API for constructing search queries: + +```rust +pub struct SearchCriteria { + pub breed_group_id: Option, + pub breed_id: Option, + pub born_after: Option, + pub born_before: Option, + pub gender: Option, + pub proven_only: Option, + pub status: Option, + pub flock_id: Option, + pub trait_ranges: Option>, +} +``` + +Each field corresponds to a filter dimension. Only non-`None` fields are included in the API request body -- omitted fields apply no filter. + +### Trait Range Filtering + +The `trait_ranges` field allows filtering animals by EBV bounds: + +```rust +pub struct TraitRangeFilter { + pub min: f64, + pub max: f64, +} +``` + +Before setting trait range filters, query the valid ranges for a breed: + +```bash +nsip trait-ranges 640 +``` + +```rust +let ranges = client.trait_ranges(640).await?; +``` + +This returns `TraitRange` values showing the observed min/max for each trait within that breed, preventing you from constructing filters that would return zero results. + +### Search Results + +Search results are paginated and contain raw JSON values: + +```rust +pub struct SearchResults { + pub total_count: i64, + pub results: Vec, + pub page: u32, + pub page_size: u32, +} +``` + +The `results` are `serde_json::Value` because search result rows use a different field layout than full detail responses. You can parse individual results into `AnimalDetails` using `AnimalDetails::from_api_response()`. + +--- + +## Animal Profile (Combined View) + +The `AnimalProfile` struct aggregates details, lineage, and progeny into a single response. The `search_by_lpn` method fetches all three concurrently: + +```rust +pub struct AnimalProfile { + pub details: AnimalDetails, + pub lineage: Lineage, + pub progeny: Progeny, +} +``` + +```rust +let profile = client.search_by_lpn("6400012006BWR107").await?; +// profile.details, profile.lineage, and profile.progeny are all populated +``` + +This is the most efficient way to get a complete picture of an animal, as it issues the three API calls concurrently rather than sequentially. + +--- + +## Contact Information + +Each animal may have associated flock/owner contact information: + +```rust +pub struct ContactInfo { + pub farm_name: Option, + pub contact_name: Option, + pub phone: Option, + pub email: Option, + pub address: Option, + pub city: Option, + pub state: Option, + pub zip_code: Option, +} +``` + +Contact information is typically only present in full detail responses (not search result rows). + +--- + +## API Response Formats + +The NSIP API uses inconsistent casing across endpoints. The `nsip` crate handles this transparently: + +| Endpoint | Field Convention | Example | +|---|---|---| +| Animal details (nested) | camelCase | `lpnId`, `dateOfBirth` | +| Search results | camelCase | `lpnId`, `bwt`, `accbwt` | +| Legacy endpoints | PascalCase | `LpnId`, `DateOfBirth` | +| Breed groups | Mixed | `breedGroupId` / `Id` | + +The `AnimalDetails::from_api_response()` method auto-detects the format and parses accordingly, so callers do not need to handle format differences. + +--- + +## Date Last Updated + +The `DateLastUpdated` type wraps the raw response from the database timestamp endpoint: + +```rust +pub struct DateLastUpdated { + pub data: serde_json::Value, +} +``` + +This is intentionally kept as raw JSON because the API response format may vary. Always check this date before making breeding decisions to ensure you are working with current evaluation data. + +--- + +## Further Reading + +- [Understanding EBVs](EBV-EXPLAINED.md) -- what EBV values mean and how to interpret them +- [Genetic Evaluation](GENETIC-EVALUATION.md) -- how BLUP produces the EBV estimates +- [Breed Groups and Traits](BREED-GROUPS-AND-TRAITS.md) -- which traits apply to which breeds +- [Data to Decisions](DATA-TO-DECISIONS.md) -- practical application of NSIP data +- [Getting Started Tutorial](../tutorials/GETTING-STARTED.md) -- hands-on introduction +- [Error Handling Reference](../reference/ERROR-HANDLING.md) -- handling API errors diff --git a/docs/how-to/BATCH-QUERY.md b/docs/how-to/BATCH-QUERY.md new file mode 100644 index 0000000..6a114aa --- /dev/null +++ b/docs/how-to/BATCH-QUERY.md @@ -0,0 +1,218 @@ +# How to Batch Query Multiple Animals + +> **Problem:** You need to retrieve data for many animals at once, either from a list of LPN IDs or by paginating through search results. + +**Prerequisites:** +- `nsip` CLI installed, or `nsip` crate added to your `Cargo.toml` +- A list of LPN IDs or search criteria to identify the animals + +--- + +## CLI Method + +### Step 1: Query Multiple Animals from a List + +Use a shell loop to iterate over LPN IDs: + +```bash +for id in 430735-0032 430735-0041 430735-0058; do + nsip details "$id" -J +done +``` + +### Step 2: Query from a File + +If you have LPN IDs in a file (one per line): + +```bash +while IFS= read -r id; do + nsip details "$id" -J +done < lpn_ids.txt +``` + +### Step 3: Collect Results into a JSON Array + +Use `jq` to combine individual results into an array: + +```bash +while IFS= read -r id; do + nsip details "$id" -J +done < lpn_ids.txt | jq -s '.' +``` + +### Step 4: Paginate Through Search Results + +Fetch all pages of a search: + +```bash +page=0 +while true; do + result=$(nsip search --breed-id 486 --status CURRENT --page-size 100 -p "$page" -J) + echo "$result" | jq '.results[]' + + total=$(echo "$result" | jq '.total_count') + fetched=$(( (page + 1) * 100 )) + if [ "$fetched" -ge "$total" ]; then + break + fi + page=$((page + 1)) +done +``` + +--- + +## Library Method + +### Step 1: Fetch Multiple Animals Concurrently + +Use `tokio::join!` for a small, known set of animals: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let (a, b, c) = tokio::join!( + client.animal_details("430735-0032"), + client.animal_details("430735-0041"), + client.animal_details("430735-0058"), + ); + + let animals = vec![a?, b?, c?]; + + for animal in &animals { + println!("{}: {:?}", animal.lpn_id, animal.breed); + } + + Ok(()) +} +``` + +### Step 2: Fetch a Dynamic List with Controlled Concurrency + +For larger lists, process animals in batches to avoid overwhelming the API: + +```rust +use nsip::{AnimalDetails, NsipClient}; + +async fn fetch_batch( + client: &NsipClient, + ids: &[&str], + batch_size: usize, +) -> Vec> { + let mut results = Vec::new(); + + for chunk in ids.chunks(batch_size) { + let mut handles = Vec::new(); + for id in chunk { + handles.push(client.animal_details(id)); + } + + // Await all futures in this batch + for handle in handles { + results.push(handle.await); + } + } + + results +} + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let ids = vec!["430735-0032", "430735-0041", "430735-0058"]; + + let results = fetch_batch(&client, &ids, 5).await; + + for result in results { + match result { + Ok(animal) => println!("{}: {:?}", animal.lpn_id, animal.breed), + Err(e) => eprintln!("Error: {e}"), + } + } + + Ok(()) +} +``` + +### Step 3: Paginate Through All Search Results + +Collect all animals matching search criteria across multiple pages: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_status("CURRENT"); + + let page_size = 100; + let mut page = 0; + let mut all_results = Vec::new(); + + loop { + let results = client + .search_animals(page, page_size, Some(486), None, None, Some(&criteria)) + .await?; + + all_results.extend(results.results); + + if all_results.len() as i64 >= results.total_count { + break; + } + page += 1; + } + + println!("Fetched {} total animals", all_results.len()); + + Ok(()) +} +``` + +### Step 4: Fetch Full Profiles in Batch + +Use `search_by_lpn()` to get details, lineage, and progeny in a single call per animal: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let ids = ["430735-0032", "430735-0041"]; + + for id in &ids { + let profile = client.search_by_lpn(id).await?; + println!( + "{}: breed={:?}, progeny_count={}", + profile.details.lpn_id, + profile.details.breed, + profile.progeny.total_count, + ); + } + + Ok(()) +} +``` + +--- + +## Verify Results + +1. Compare the number of results fetched against `total_count` to confirm completeness. +2. Check for errors in batch results -- individual animals may return `NotFound` without affecting others. +3. For large batches, monitor API response times and adjust `batch_size` if requests start timing out. + +--- + +## See Also + +- [How to Filter Search Results](FILTER-SEARCH-RESULTS.md) -- narrow down which animals to query +- [How to Export JSON](EXPORT-JSON.md) -- save batch results to files +- [How to Integrate with Scripts](SCRIPTING-INTEGRATION.md) -- automate batch queries in pipelines diff --git a/docs/how-to/COMPARE-ANIMALS.md b/docs/how-to/COMPARE-ANIMALS.md new file mode 100644 index 0000000..8c7ac00 --- /dev/null +++ b/docs/how-to/COMPARE-ANIMALS.md @@ -0,0 +1,224 @@ +# How to Compare Animals + +> **Problem:** You need to compare EBV traits across multiple animals to inform breeding or selection decisions. + +**Prerequisites:** +- `nsip` CLI installed, or `nsip` crate added to your `Cargo.toml` +- LPN IDs of the animals you want to compare + +--- + +## CLI Method + +### Step 1: Run the Compare Command + +Compare two or more animals (up to 5) by their LPN IDs: + +```bash +nsip compare 430735-0032 430735-0041 430735-0058 +``` + +This outputs a side-by-side ASCII table with all EBV traits aligned for comparison. + +### Step 2: Filter to Specific Traits + +Use `--traits` to focus on the traits that matter for your breeding goal: + +```bash +nsip compare 430735-0032 430735-0041 --traits BWT,WWT,YWT,PEMD +``` + +### Step 3: Get JSON Output + +Add `-J` for machine-readable output: + +```bash +nsip compare 430735-0032 430735-0041 -J +``` + +--- + +## Library Method + +### Step 1: Fetch Animal Details + +Use `animal_details()` to retrieve EBV data for each animal: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let animal_a = client.animal_details("430735-0032").await?; + let animal_b = client.animal_details("430735-0041").await?; + + Ok(()) +} +``` + +### Step 2: Fetch Multiple Animals Concurrently + +Use `tokio::join!` to fetch details in parallel: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let (a, b, c) = tokio::join!( + client.animal_details("430735-0032"), + client.animal_details("430735-0041"), + client.animal_details("430735-0058"), + ); + + let animals = vec![a?, b?, c?]; + + Ok(()) +} +``` + +### Step 3: Compare Specific Traits + +Access the `traits` field on `AnimalDetails` to compare EBVs. Trait keys use standard abbreviations (BWT, WWT, YWT, EMD, etc.): + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let (a, b) = tokio::join!( + client.animal_details("430735-0032"), + client.animal_details("430735-0041"), + ); + let animals = vec![a?, b?]; + + let traits_of_interest = ["BWT", "WWT", "YWT", "PEMD"]; + + for animal in &animals { + println!("Animal: {}", animal.lpn_id); + for trait_name in &traits_of_interest { + if let Some(t) = animal.traits.get(*trait_name) { + println!( + " {}: {:.2} (accuracy: {}%)", + t.name, + t.value, + t.accuracy.unwrap_or(0), + ); + } + } + } + + Ok(()) +} +``` + +### Step 4: Calculate a Weighted Score + +Build a simple composite score to rank animals against a breeding objective: + +```rust +use std::collections::HashMap; +use nsip::{AnimalDetails, NsipClient}; + +fn weighted_score(animal: &AnimalDetails, weights: &HashMap<&str, f64>) -> f64 { + weights + .iter() + .filter_map(|(trait_name, weight)| { + animal.traits.get(*trait_name).map(|t| { + let accuracy = f64::from(t.accuracy.unwrap_or(50)) / 100.0; + t.value * weight * accuracy + }) + }) + .sum() +} + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let (a, b) = tokio::join!( + client.animal_details("430735-0032"), + client.animal_details("430735-0041"), + ); + let animals = vec![a?, b?]; + + // Terminal sire objective: penalize birth weight (lbs), reward growth (lbs) and muscle (mm) + let weights: HashMap<&str, f64> = HashMap::from([ + ("BWT", -1.0), + ("WWT", 2.0), + ("YWT", 1.5), + ("PEMD", 1.0), + ]); + + let mut scored: Vec<_> = animals + .iter() + .map(|a| (&a.lpn_id, weighted_score(a, &weights))) + .collect(); + + scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + for (lpn_id, score) in &scored { + println!("{lpn_id}: {score:.2}"); + } + + Ok(()) +} +``` + +--- + +## MCP Method + +If you are using the NSIP MCP server through an AI assistant: + +```json +{ + "tool": "compare", + "arguments": { + "animal_ids": ["430735-0032", "430735-0041", "430735-0058"], + "traits": "BWT,WWT,YWT,PEMD" + } +} +``` + +The `compare` tool returns a structured side-by-side comparison with all requested traits. Missing traits are clearly marked. + +For weighted ranking across a breed, use the `rank` tool instead: + +```json +{ + "tool": "rank", + "arguments": { + "breed_id": 486, + "weights": { "BWT": -1.0, "WWT": 2.0, "YWT": 1.5, "PEMD": 1.0 }, + "gender": "Male", + "status": "CURRENT", + "top_n": 5 + } +} +``` + +--- + +## Verify Results + +After comparing, confirm that: + +1. All requested animals were found (check for `NotFound` errors). +2. The traits you care about are present for each animal. Not all animals have all 13 EBV traits. +3. Accuracy values are reasonable -- low-accuracy EBVs (below 40%) should be treated with caution. + +--- + +## See Also + +- [How to Filter Search Results](FILTER-SEARCH-RESULTS.md) -- find candidates before comparing +- [How to Export JSON](EXPORT-JSON.md) -- export comparison data for further analysis +- [Understanding EBVs](../explanation/EBV-EXPLAINED.md) +- [MCP Compare Tool Reference](../MCP.md#compare) diff --git a/docs/how-to/CONFIGURE-CLIENT.md b/docs/how-to/CONFIGURE-CLIENT.md new file mode 100644 index 0000000..bc3b391 --- /dev/null +++ b/docs/how-to/CONFIGURE-CLIENT.md @@ -0,0 +1,177 @@ +# How to Configure the NSIP Client + +> **Problem:** You need to customize the HTTP client for timeouts, retries, or a different API endpoint. + +**Prerequisites:** +- `nsip` crate added to your `Cargo.toml` +- A Tokio async runtime + +--- + +## Step 1: Choose a Construction Method + +The `NsipClient` offers three ways to create an instance, depending on how much control you need. + +### Default Client + +Use `NsipClient::new()` when the defaults are acceptable: + +```rust +use nsip::NsipClient; + +let client = NsipClient::new(); +``` + +Defaults: +- **Base URL:** `http://nsipsearch.nsip.org/api` +- **Timeout:** 30 seconds per request +- **Max retries:** 3 (on status 500, 502, 503, 504) +- **Backoff:** Exponential (0.5 x 2^attempt seconds) + +### Custom Base URL Only + +Use `NsipClient::with_base_url()` when you only need to change the endpoint: + +```rust +use nsip::NsipClient; + +let client = NsipClient::with_base_url("http://localhost:8080/api"); +``` + +### Full Builder + +Use `NsipClient::builder()` when you need control over multiple settings: + +```rust +use nsip::NsipClient; + +let client = NsipClient::builder() + .base_url("http://nsipsearch.nsip.org/api") + .timeout_secs(60) + .max_retries(5) + .build()?; +``` + +--- + +## Step 2: Configure Timeout + +The timeout controls how long each individual HTTP request waits before failing. The default is 30 seconds. + +**Increase for slow networks or large responses:** + +```rust +let client = NsipClient::builder() + .timeout_secs(120) // 2 minutes + .build()?; +``` + +**Decrease for fast-fail scenarios:** + +```rust +let client = NsipClient::builder() + .timeout_secs(5) + .build()?; +``` + +--- + +## Step 3: Configure Retry Behavior + +The client automatically retries on server errors (HTTP 500, 502, 503, 504), timeouts, and connection failures. Client errors (4xx) are never retried. + +**Default:** 3 retries with exponential backoff (0.5s, 1s, 2s, 4s, ...). + +**Disable retries for time-sensitive applications:** + +```rust +let client = NsipClient::builder() + .max_retries(0) + .build()?; +``` + +**Aggressive retries for unreliable networks:** + +```rust +let client = NsipClient::builder() + .timeout_secs(60) + .max_retries(10) + .build()?; +``` + +With 10 retries, backoff delays are: 0.5s, 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s (total wait up to ~8.5 minutes before the final attempt). + +--- + +## Step 4: Configure Base URL + +Override the base URL for testing, proxies, or custom deployments: + +```rust +let client = NsipClient::builder() + .base_url("http://my-proxy.internal:3000/nsip-api") + .build()?; +``` + +You can verify the configured URL at any time: + +```rust +assert_eq!(client.base_url(), "http://my-proxy.internal:3000/nsip-api"); +``` + +--- + +## Step 5: Handle Builder Errors + +The `build()` method returns `Result`. Handle failures explicitly: + +```rust +use nsip::{NsipClient, Error}; + +match NsipClient::builder().timeout_secs(60).build() { + Ok(client) => { + let groups = client.breed_groups().await?; + } + Err(Error::Connection(msg)) => { + eprintln!("Failed to create HTTP client: {msg}"); + } + Err(e) => { + eprintln!("Unexpected error: {e}"); + } +} +``` + +--- + +## Verify It Works + +After building the client, make a lightweight call to verify connectivity: + +```rust +let client = NsipClient::builder() + .timeout_secs(10) + .max_retries(1) + .build()?; + +let updated = client.date_last_updated().await?; +println!("Database last updated: {:?}", updated.data); +``` + +If this returns successfully, the client is configured correctly. + +--- + +## Builder Options Reference + +| Method | Default | Description | +|-------------------|----------------------------------------|-------------------------------------------------| +| `base_url(url)` | `http://nsipsearch.nsip.org/api` | API base URL | +| `timeout_secs(n)` | 30 | Per-request timeout in seconds | +| `max_retries(n)` | 3 | Max retry attempts on server/connection errors | + +--- + +## See Also + +- [Error Handling Reference](../reference/ERROR-HANDLING.md) +- [Configuration Reference](../reference/CONFIGURATION.md) diff --git a/docs/how-to/EXPORT-JSON.md b/docs/how-to/EXPORT-JSON.md new file mode 100644 index 0000000..5243747 --- /dev/null +++ b/docs/how-to/EXPORT-JSON.md @@ -0,0 +1,193 @@ +# How to Export Data as JSON + +> **Problem:** You need to export NSIP animal data in JSON format for further processing, reporting, or integration with other tools. + +**Prerequisites:** +- `nsip` CLI installed, or `nsip` crate added to your `Cargo.toml` + +--- + +## CLI Method + +### Step 1: Add the JSON Flag + +Every CLI command supports the `--json` (or `-J`) global flag. Add it to any command to switch from human-readable ASCII tables to JSON output: + +```bash +nsip details 430735-0032 -J +``` + +### Step 2: Export Search Results + +```bash +nsip search --breed-id 486 --status CURRENT --sort-by WWT --reverse -J +``` + +Output is a JSON object with `total_count`, `page`, `page_size`, and a `results` array. + +### Step 3: Export Animal Profiles + +```bash +nsip profile 430735-0032 -J +``` + +Returns details, lineage, and progeny as a single JSON object. + +### Step 4: Export Breed Groups + +```bash +nsip breed-groups -J +``` + +### Step 5: Export Trait Ranges + +```bash +nsip trait-ranges 486 -J +``` + +### Step 6: Save to a File + +Redirect output to a file: + +```bash +nsip search --breed-id 486 --status CURRENT -J > current_animals.json +``` + +### Step 7: Pipe to Other Tools + +Combine with `jq` for extraction and transformation: + +```bash +# Extract just the LPN IDs from search results +nsip search --breed-id 486 --status CURRENT -J | jq '.results[].lpnId' + +# Get a specific trait value from animal details +nsip details 430735-0032 -J | jq '.traits.WWT' + +# Pretty-print the output +nsip profile 430735-0032 -J | jq . +``` + +--- + +## Library Method + +### Step 1: Serialize with serde_json + +All NSIP data types implement `Serialize`. Use `serde_json` to convert them: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let details = client.animal_details("430735-0032").await?; + + let json = serde_json::to_string_pretty(&details) + .expect("serialization should not fail"); + println!("{json}"); + + Ok(()) +} +``` + +### Step 2: Export Search Results + +```rust +use nsip::{NsipClient, SearchCriteria}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_status("CURRENT"); + + let results = client + .search_animals(0, 100, Some(486), Some("WWT"), Some(true), Some(&criteria)) + .await?; + + let json = serde_json::to_string_pretty(&results) + .expect("serialization should not fail"); + println!("{json}"); + + Ok(()) +} +``` + +### Step 3: Write to a File + +```rust +use std::fs::File; +use std::io::BufWriter; +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = NsipClient::new(); + let details = client.animal_details("430735-0032").await?; + + let file = File::create("animal_details.json")?; + let writer = BufWriter::new(file); + serde_json::to_writer_pretty(writer, &details)?; + + Ok(()) +} +``` + +### Step 4: Export Multiple Animals + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let ids = ["430735-0032", "430735-0041", "430735-0058"]; + + let mut animals = Vec::new(); + for id in &ids { + let details = client.animal_details(id).await?; + animals.push(details); + } + + let json = serde_json::to_string_pretty(&animals) + .expect("serialization should not fail"); + println!("{json}"); + + Ok(()) +} +``` + +--- + +## Verify the Output + +1. Validate the JSON structure with `jq`: + + ```bash + nsip details 430735-0032 -J | jq type + # Should output: "object" + ``` + +2. Check that expected fields are present: + + ```bash + nsip details 430735-0032 -J | jq 'keys' + ``` + +3. Verify the file is valid JSON if saved: + + ```bash + jq . < animal_details.json > /dev/null && echo "Valid JSON" + ``` + +--- + +## See Also + +- [How to Filter Search Results](FILTER-SEARCH-RESULTS.md) -- filter before exporting +- [How to Integrate with Scripts](SCRIPTING-INTEGRATION.md) -- use JSON in automation pipelines +- [How to Batch Query Multiple Animals](BATCH-QUERY.md) diff --git a/docs/how-to/FILTER-SEARCH-RESULTS.md b/docs/how-to/FILTER-SEARCH-RESULTS.md new file mode 100644 index 0000000..7e3b246 --- /dev/null +++ b/docs/how-to/FILTER-SEARCH-RESULTS.md @@ -0,0 +1,229 @@ +# How to Filter Search Results + +> **Problem:** You need to narrow down animals in the NSIP database by breed, gender, status, date range, flock, or trait values. + +**Prerequisites:** +- `nsip` CLI installed, or `nsip` crate added to your `Cargo.toml` +- Knowledge of the breed ID you want to search (use `nsip breed-groups` to find it) + +--- + +## CLI Method + +### Step 1: Search with Basic Filters + +Use flags to narrow the search: + +```bash +nsip search --breed-id 486 --gender Male --status CURRENT +``` + +Available filters: + +| Flag | Description | Example | +|---------------------|--------------------------------------------|----------------------------| +| `--breed-id N` | Filter by breed ID | `--breed-id 486` | +| `--breed-group-id N`| Filter by breed group ID | `--breed-group-id 61` | +| `--gender G` | `Male`, `Female`, or `Both` | `--gender Female` | +| `--status S` | `CURRENT`, `SOLD`, or `DEAD` | `--status CURRENT` | +| `--born-after D` | Born after date (YYYY-MM-DD) | `--born-after 2020-01-01` | +| `--born-before D` | Born before date (YYYY-MM-DD) | `--born-before 2023-12-31` | +| `--proven-only` | Only animals with proven (high-accuracy) EBVs | `--proven-only` | +| `--flock-id F` | Filter by flock ID | `--flock-id 430735` | + +### Step 2: Sort by a Trait + +Sort results by any EBV trait abbreviation: + +```bash +nsip search --breed-id 486 --status CURRENT --sort-by WWT +``` + +Add `--reverse` to sort in descending order (highest first): + +```bash +nsip search --breed-id 486 --status CURRENT --sort-by WWT --reverse +``` + +### Step 3: Paginate Results + +Control pagination with `-p` (page number, 0-indexed) and `--page-size`: + +```bash +nsip search --breed-id 486 --sort-by WWT --reverse -p 0 --page-size 25 +``` + +--- + +## Library Method + +### Step 1: Build Search Criteria + +Use the `SearchCriteria` builder to construct filters: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_gender("Male") + .with_status("CURRENT") + .with_born_after("2020-01-01") + .with_born_before("2023-12-31"); +``` + +### Step 2: Execute the Search + +Pass the criteria to `search_animals()`: + +```rust +let client = NsipClient::new(); + +let results = client + .search_animals( + 0, // page (0-indexed) + 25, // page_size (1-100) + Some(486), // breed_id + Some("WWT"), // sort by trait + Some(true), // reverse (descending) + Some(&criteria), + ) + .await?; + +println!("Total matches: {}", results.total_count); +println!("Page {}, showing {} results", results.page, results.results.len()); +``` + +### Step 3: Filter by Trait Ranges + +Use `with_trait_ranges()` to constrain results to animals within specific EBV bounds: + +```rust +use std::collections::HashMap; +use nsip::{NsipClient, SearchCriteria, TraitRangeFilter}; + +let mut ranges = HashMap::new(); +// BWT in lbs: filter for moderate birth weight EBVs +ranges.insert( + "BWT".to_string(), + TraitRangeFilter { min: -0.5, max: 0.5 }, +); +// WWT in lbs: filter for above-average weaning weight EBVs +ranges.insert( + "WWT".to_string(), + TraitRangeFilter { min: 2.0, max: 10.0 }, +); + +let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_status("CURRENT") + .with_trait_ranges(ranges); + +let client = NsipClient::new(); +let results = client + .search_animals(0, 25, Some(486), None, None, Some(&criteria)) + .await?; +``` + +### Step 4: Filter by Flock and Proven Status + +Narrow to a specific flock and only proven animals: + +```rust +let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_flock_id("430735") + .with_proven_only(true); +``` + +### Step 5: Paginate Through All Results + +Iterate through pages to collect all matching animals: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +async fn fetch_all_results( + client: &NsipClient, + criteria: &SearchCriteria, + breed_id: i64, +) -> Result, nsip::Error> { + let page_size = 100; + let mut all_results = Vec::new(); + let mut page = 0; + + loop { + let results = client + .search_animals(page, page_size, Some(breed_id), None, None, Some(criteria)) + .await?; + + all_results.extend(results.results); + + if all_results.len() as i64 >= results.total_count { + break; + } + page += 1; + } + + Ok(all_results) +} +``` + +--- + +## MCP Method + +Use the `search` tool with filter parameters: + +```json +{ + "tool": "search", + "arguments": { + "breed_id": 486, + "gender": "Male", + "status": "CURRENT", + "born_after": "2020-01-01", + "sort_by": "WWT", + "reverse": true, + "page_size": 10 + } +} +``` + +--- + +## Find Breed and Group IDs + +Before filtering by breed, look up the breed ID: + +```bash +nsip breed-groups +``` + +Or programmatically: + +```rust +let groups = client.breed_groups().await?; +for group in &groups { + for breed in &group.breeds { + println!("{}: {} (group: {})", breed.id, breed.name, group.name); + } +} +``` + +--- + +## Verify Results + +1. Check `total_count` on the result to confirm matches exist. +2. If results are empty, relax your filters (broader date range, remove trait ranges). +3. Use `nsip statuses` to verify valid status values for the breed group. + +--- + +## See Also + +- [How to Compare Animals](COMPARE-ANIMALS.md) -- compare filtered candidates +- [How to Export JSON](EXPORT-JSON.md) -- export filtered results +- [SearchCriteria Reference](../reference/SEARCH-CRITERIA.md) +- [EBV Trait Glossary](../MCP.md#ebv-trait-glossary) diff --git a/docs/how-to/SCRIPTING-INTEGRATION.md b/docs/how-to/SCRIPTING-INTEGRATION.md new file mode 100644 index 0000000..bf6465a --- /dev/null +++ b/docs/how-to/SCRIPTING-INTEGRATION.md @@ -0,0 +1,240 @@ +# How to Integrate NSIP into Scripts and Pipelines + +> **Problem:** You want to automate NSIP data retrieval in shell scripts, CI pipelines, or data processing workflows. + +**Prerequisites:** +- `nsip` CLI installed and available on `PATH` +- `jq` installed (for JSON processing in shell scripts) + +--- + +## Step 1: Verify CLI Availability + +Check that `nsip` is installed and reachable: + +```bash +nsip --version +``` + +In CI pipelines, install from crates.io or download a pre-built binary: + +```bash +# From crates.io +cargo install nsip + +# Or download a release binary (Linux x86_64 example) +curl -L -o nsip https://github.com/zircote/nsip/releases/latest/download/nsip-linux-amd64 +chmod +x nsip +``` + +--- + +## Step 2: Use JSON Output in Scripts + +All commands accept the `-J` flag for JSON output. This is the recommended mode for scripting because it provides structured, parseable data. + +### Extract Specific Fields + +```bash +# Get the breed of an animal +nsip details 430735-0032 -J | jq -r '.breed' + +# Get all LPN IDs from a search +nsip search --breed-id 486 --status CURRENT -J | jq -r '.results[].lpnId' + +# Get the database last-updated date +nsip date-updated -J | jq -r '.' +``` + +### Check Command Success + +```bash +if nsip details 430735-0032 -J > /dev/null 2>&1; then + echo "Animal found" +else + echo "Animal not found or API error" +fi +``` + +The CLI returns a non-zero exit code on errors, which integrates with standard shell error handling. + +--- + +## Step 3: Build a Data Collection Script + +Collect details for a list of animals and save as a JSON array: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +INPUT_FILE="${1:?Usage: $0 }" +OUTPUT_FILE="${2:-output.json}" + +results=() + +while IFS= read -r lpn_id; do + # Skip empty lines and comments + [[ -z "$lpn_id" || "$lpn_id" == \#* ]] && continue + + if data=$(nsip details "$lpn_id" -J 2>/dev/null); then + results+=("$data") + else + echo "Warning: failed to fetch $lpn_id" >&2 + fi +done < "$INPUT_FILE" + +# Combine into a JSON array +printf '%s\n' "${results[@]}" | jq -s '.' > "$OUTPUT_FILE" +echo "Wrote ${#results[@]} records to $OUTPUT_FILE" +``` + +Usage: + +```bash +chmod +x collect_animals.sh +./collect_animals.sh lpn_ids.txt animals.json +``` + +--- + +## Step 4: Build a Breed Report Script + +Generate a summary report for a breed: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +BREED_ID="${1:?Usage: $0 }" + +echo "=== Breed Report for ID: $BREED_ID ===" + +# Get trait ranges +echo "--- Trait Ranges ---" +nsip trait-ranges "$BREED_ID" -J | jq '.' + +# Get top animals by WWT +echo "--- Top 10 by Weaning Weight ---" +nsip search --breed-id "$BREED_ID" --status CURRENT \ + --sort-by WWT --reverse --page-size 10 -J | \ + jq -r '.results[] | "\(.lpnId)\t\(.wwt // "N/A")"' +``` + +--- + +## Step 5: Use in CI/CD Pipelines + +### GitHub Actions Example + +```yaml +jobs: + nsip-report: + runs-on: ubuntu-latest + steps: + - name: Install nsip + run: cargo install nsip + + - name: Check database freshness + run: | + last_updated=$(nsip date-updated -J | jq -r '.') + echo "NSIP database last updated: $last_updated" + + - name: Generate breed report + run: | + nsip search --breed-id 486 --status CURRENT \ + --sort-by WWT --reverse --page-size 50 -J > report.json + + - name: Upload report + uses: actions/upload-artifact@v4 + with: + name: nsip-report + path: report.json +``` + +--- + +## Step 6: Process Output with Standard Unix Tools + +### CSV Conversion + +Convert search results to CSV using `jq`: + +```bash +nsip search --breed-id 486 --status CURRENT -J | \ + jq -r '.results[] | [.lpnId, .breed, .status, .bwt, .wwt, .ywt] | @csv' +``` + +### Filter and Count + +```bash +# Count current males in a breed +nsip search --breed-id 486 --gender Male --status CURRENT -J | jq '.total_count' + +# Find animals born in a specific year +nsip search --breed-id 486 --born-after 2022-01-01 --born-before 2022-12-31 -J | \ + jq '.total_count' +``` + +### Combine with Other Tools + +```bash +# Compare two animals and extract only differing traits +diff <(nsip details 430735-0032 -J | jq '.traits') \ + <(nsip details 430735-0041 -J | jq '.traits') +``` + +--- + +## Step 7: Handle Errors Gracefully + +### Retry on Transient Failures + +```bash +fetch_with_retry() { + local id="$1" + local max_retries=3 + local attempt=0 + + while [ "$attempt" -lt "$max_retries" ]; do + if result=$(nsip details "$id" -J 2>/dev/null); then + echo "$result" + return 0 + fi + attempt=$((attempt + 1)) + sleep $((attempt * 2)) + done + + echo "Failed to fetch $id after $max_retries attempts" >&2 + return 1 +} +``` + +Note: The `nsip` CLI has built-in retry logic (3 retries by default), so script-level retries are a second layer for additional resilience. + +### Validate JSON Output + +```bash +result=$(nsip details 430735-0032 -J) +if echo "$result" | jq empty 2>/dev/null; then + echo "$result" | jq '.lpn_id' +else + echo "Invalid JSON response" >&2 +fi +``` + +--- + +## Verify the Integration + +1. Run your script with a known LPN ID and check the output format. +2. Test error handling with an invalid LPN ID to confirm graceful failure. +3. In CI, check the exit code: `nsip` returns non-zero on errors. + +--- + +## See Also + +- [How to Export JSON](EXPORT-JSON.md) -- JSON output details +- [How to Batch Query Multiple Animals](BATCH-QUERY.md) -- batch processing patterns +- [How to Filter Search Results](FILTER-SEARCH-RESULTS.md) -- narrowing queries for scripts diff --git a/docs/how-to/USE-MCP-TOOLS.md b/docs/how-to/USE-MCP-TOOLS.md new file mode 100644 index 0000000..3ecf7d2 --- /dev/null +++ b/docs/how-to/USE-MCP-TOOLS.md @@ -0,0 +1,283 @@ +# How to Use the MCP Server Tools + +> **Problem:** You want to use the NSIP MCP server with an AI assistant (Claude Desktop, Claude Code, Cursor, etc.) to query sheep genetic data interactively. + +**Prerequisites:** +- `nsip` binary installed ([installation options](../MCP.md#installation)) +- An MCP-compatible client (Claude Desktop, Claude Code, or similar) + +--- + +## Step 1: Configure the MCP Server + +Add the NSIP server to your MCP client configuration. + +### Claude Code + +Create or edit `.mcp.json` at your project root: + +```json +{ + "mcpServers": { + "nsip": { + "command": "nsip", + "args": ["mcp"] + } + } +} +``` + +### Claude Desktop + +Edit `claude_desktop_config.json` (on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "nsip": { + "command": "nsip", + "args": ["mcp"] + } + } +} +``` + +### Docker + +If you prefer not to install the binary: + +```json +{ + "mcpServers": { + "nsip": { + "command": "docker", + "args": ["run", "--rm", "-i", "ghcr.io/zircote/nsip", "mcp"] + } + } +} +``` + +--- + +## Step 2: Verify the Server Starts + +Test that the server responds to an MCP initialize request: + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | nsip mcp +``` + +A successful response includes `"result"` with server capabilities. + +--- + +## Step 3: Use Data Retrieval Tools + +### Check Database Status + +```json +{ "tool": "database_status", "arguments": {} } +``` + +Returns the last-updated date and available statuses. + +### List Breed Groups + +```json +{ "tool": "breed_groups", "arguments": {} } +``` + +Returns all breed groups with their breed IDs and names. Use these IDs in other tool calls. + +### Get Trait Ranges for a Breed + +```json +{ "tool": "trait_ranges", "arguments": { "breed_id": 486 } } +``` + +Returns min/max EBV values for the breed, useful for understanding norms. + +--- + +## Step 4: Look Up Individual Animals + +### Get Animal Details + +```json +{ + "tool": "details", + "arguments": { "animal_id": "430735-0032" } +} +``` + +Returns EBVs, breed, status, contact info, and lineage identifiers. + +### Get Full Profile + +```json +{ + "tool": "profile", + "arguments": { "animal_id": "430735-0032" } +} +``` + +Returns details, pedigree, and offspring in a single call. + +### Get Lineage + +```json +{ + "tool": "lineage", + "arguments": { "animal_id": "430735-0032" } +} +``` + +Returns the ancestry tree (parents, grandparents). + +### Get Progeny + +```json +{ + "tool": "progeny", + "arguments": { "animal_id": "430735-0032", "page": 0, "page_size": 20 } +} +``` + +Returns paginated offspring list. + +--- + +## Step 5: Search and Compare + +### Search Animals + +```json +{ + "tool": "search", + "arguments": { + "breed_id": 486, + "gender": "Male", + "status": "CURRENT", + "sort_by": "WWT", + "reverse": true, + "page_size": 10 + } +} +``` + +### Compare Animals Side-by-Side + +```json +{ + "tool": "compare", + "arguments": { + "animal_ids": ["430735-0032", "430735-0041"], + "traits": "BWT,WWT,YWT,PEMD" + } +} +``` + +--- + +## Step 6: Use Breeding Analytics Tools + +### Rank Animals by Weighted Traits + +```json +{ + "tool": "rank", + "arguments": { + "breed_id": 486, + "weights": { "BWT": -1.0, "WWT": 2.0, "YWT": 1.5, "PEMD": 1.0 }, + "gender": "Male", + "status": "CURRENT", + "top_n": 5 + } +} +``` + +### Check Inbreeding Coefficient + +```json +{ + "tool": "inbreeding_check", + "arguments": { + "sire_id": "430735-0032", + "dam_id": "430735-0089" + } +} +``` + +Returns Wright's COI with a traffic-light rating (Green/Yellow/Red). + +### Get Mating Recommendations + +```json +{ + "tool": "mating_recommendations", + "arguments": { + "animal_id": "430735-0032", + "breed_id": 486, + "target_traits": "WWT,PEMD,NLB", + "max_results": 3 + } +} +``` + +### Summarize a Flock + +```json +{ + "tool": "flock_summary", + "arguments": { + "flock_id": "430735", + "breed_id": 486 + } +} +``` + +--- + +## Step 7: Use Guided Prompts + +MCP prompts are pre-built workflows that fetch data and construct structured breeding advice. Ask your AI assistant to use them: + +| Prompt | Description | Required Arguments | +|---------------------------|-----------------------------------------------------|---------------------------------| +| `evaluate-ram` | Assess a ram's breeding value | `lpn_id` | +| `evaluate-ewe` | Assess a ewe's breeding value | `lpn_id` | +| `compare-breeding-stock` | Side-by-side trait analysis of 2-5 animals | `animal_ids` (comma-separated) | +| `plan-mating` | Mating assessment with COI and trait complementarity | `sire_id`, `dam_id` | +| `flock-improvement` | Identify trait gaps and improvement opportunities | `breed_id`, optional `flock_id` | +| `select-replacement` | Find top replacement candidates | `breed_id`, `gender`, `target_trait` | +| `interpret-ebvs` | Plain-language EBV explanation | `lpn_id` | + +--- + +## Step 8: Access Resources + +MCP resources provide static and dynamic data by URI: + +``` +nsip://glossary -- EBV trait definitions +nsip://breeds -- Live breed directory +nsip://guide/selection -- Selection guide +nsip://guide/inbreeding -- Inbreeding guide +nsip://status -- Database status +nsip://animal/{lpn_id} -- Animal profile +nsip://breed/{breed_id}/ranges -- Trait ranges for a breed +``` + +--- + +## Verify It Works + +After configuration, ask your AI assistant a question like "What breeds are available in the NSIP database?" If it uses the `breed_groups` tool and returns results, the server is working correctly. + +--- + +## See Also + +- [MCP Server Reference](../MCP.md) -- complete tool, resource, and prompt documentation +- [How to Compare Animals](COMPARE-ANIMALS.md) +- [How to Filter Search Results](FILTER-SEARCH-RESULTS.md) diff --git a/docs/reference/CLI.md b/docs/reference/CLI.md new file mode 100644 index 0000000..e01e633 --- /dev/null +++ b/docs/reference/CLI.md @@ -0,0 +1,429 @@ +# CLI Reference + +Complete reference for the `nsip` command-line interface. + +--- + +## Synopsis + +``` +nsip [OPTIONS] +``` + +## Global Options + +| Option | Short | Description | +|--------|-------|-------------| +| `--json` | `-J` | Output raw JSON instead of human-readable format | +| `--version` | `-V` | Print version information | +| `--help` | `-h` | Print help information | + +The `--json` flag is global and applies to all subcommands. When set, the output is the raw JSON response from the NSIP API. When omitted (the default), output is formatted as human-readable ASCII tables. + +--- + +## Commands + +### date-updated + +Get the date when the NSIP database was last updated. + +``` +nsip date-updated +nsip -J date-updated +``` + +**Arguments:** None + +**Output:** The last-updated date from the NSIP Search API. Always outputs JSON regardless of the `--json` flag. + +--- + +### breed-groups + +List all available breed groups and the individual breeds within each group. + +``` +nsip breed-groups +nsip -J breed-groups +``` + +**Arguments:** None + +**Output (default):** ASCII table of breed groups with their breeds and IDs. + +**Output (JSON):** Array of `BreedGroup` objects, each containing an `id`, `name`, and `breeds` array. + +--- + +### statuses + +List all available animal statuses. + +``` +nsip statuses +nsip -J statuses +``` + +**Arguments:** None + +**Output (default):** Bullet list of status strings (e.g., `CURRENT`, `SOLD`, `DEAD`). + +**Output (JSON):** Array of status strings. + +--- + +### trait-ranges + +Get the minimum and maximum EBV trait values for a specific breed. + +``` +nsip trait-ranges +nsip -J trait-ranges +``` + +**Arguments:** + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `BREED_ID` | integer | yes | Breed ID to query trait ranges for | + +**Validation:** `breed_id` must be greater than 0. + +**Example:** + +```bash +nsip trait-ranges 486 +nsip -J trait-ranges 640 +``` + +--- + +### search + +Search for animals in the NSIP database with filters for breed, gender, status, date range, flock, and sorting. + +``` +nsip search [OPTIONS] +``` + +**Options:** + +| Option | Short | Type | Default | Description | +|--------|-------|------|---------|-------------| +| `--breed-id` | `-b` | integer | -- | Breed ID to filter by | +| `--breed-group-id` | -- | integer | -- | Breed group ID to filter by | +| `--status` | `-s` | string | -- | Animal status filter (`CURRENT`, `SOLD`, `DEAD`) | +| `--gender` | `-g` | string | -- | Gender filter (`Male`, `Female`, `Both`) | +| `--born-after` | -- | string | -- | Only animals born after this date (`YYYY-MM-DD`) | +| `--born-before` | -- | string | -- | Only animals born before this date (`YYYY-MM-DD`) | +| `--proven-only` | -- | flag | false | Only return proven animals | +| `--flock-id` | -- | string | -- | Flock ID to filter by | +| `--sort-by` | -- | string | -- | Trait abbreviation to sort by (e.g., `BWT`, `WWT`) | +| `--reverse` | -- | flag | false | Reverse the sort order | +| `--page` | `-p` | integer | 0 | Page number (0-indexed) | +| `--page-size` | -- | integer | 15 | Results per page (1-100) | + +**Validation:** `page_size` must be between 1 and 100. + +**Examples:** + +```bash +# Search for current male Dorper sheep sorted by weaning weight +nsip search --breed-id 486 --gender Male --status CURRENT --sort-by WWT + +# Get page 2 with 25 results per page +nsip search --breed-id 486 --page 2 --page-size 25 + +# Search with date range and JSON output +nsip -J search --breed-id 640 --born-after 2020-01-01 --born-before 2023-12-31 + +# Only proven animals from a specific flock +nsip search --breed-id 486 --flock-id 430735 --proven-only +``` + +--- + +### details + +Get detailed information about a specific animal, including EBV traits, breed, contact info, and status. + +``` +nsip details +nsip -J details +``` + +**Arguments:** + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `SEARCH_STRING` | string | yes | LPN ID or registration number of the animal | + +**Validation:** `search_string` must not be empty or whitespace-only. + +**Examples:** + +```bash +nsip details 430735-0032 +nsip -J details 430735-0032 +``` + +--- + +### lineage + +Get lineage (ancestry) information for a specific animal, including sire, dam, and extended pedigree. + +``` +nsip lineage +nsip -J lineage +``` + +**Arguments:** + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `LPN_ID` | string | yes | LPN ID of the animal | + +**Validation:** `lpn_id` must not be empty or whitespace-only. + +**Examples:** + +```bash +nsip lineage 430735-0032 +nsip -J lineage 430735-0032 +``` + +--- + +### progeny + +Get progeny (offspring) information for a specific animal with pagination. + +``` +nsip progeny [OPTIONS] +``` + +**Arguments:** + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `LPN_ID` | string | yes | LPN ID of the animal | + +**Options:** + +| Option | Short | Type | Default | Description | +|--------|-------|------|---------|-------------| +| `--page` | `-p` | integer | 0 | Page number (0-indexed) | +| `--page-size` | -- | integer | 10 | Results per page | + +**Validation:** `lpn_id` must not be empty. `page_size` must be greater than 0. + +**Examples:** + +```bash +nsip progeny 430735-0032 +nsip progeny 430735-0032 --page 1 --page-size 20 +nsip -J progeny 430735-0032 +``` + +--- + +### profile + +Get a full profile for an animal, combining details, lineage, and progeny in a single call. Internally fetches all three concurrently using `tokio::join!`. + +``` +nsip profile +nsip -J profile +``` + +**Arguments:** + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `LPN_ID` | string | yes | LPN ID of the animal | + +**Validation:** `lpn_id` must not be empty or whitespace-only. + +**Examples:** + +```bash +nsip profile 430735-0032 +nsip -J profile 430735-0032 +``` + +--- + +### compare + +Compare two or more animals side-by-side on their EBV traits. Fetches details for all animals concurrently. + +``` +nsip compare [OPTIONS] ... +``` + +**Arguments:** + +| Argument | Type | Required | Description | +|----------|------|----------|-------------| +| `LPN_IDS` | string (2-5) | yes | LPN IDs of animals to compare | + +**Options:** + +| Option | Type | Description | +|--------|------|-------------| +| `--traits` | string | Comma-separated list of traits to display (e.g., `BWT,WWT,YWT`) | + +**Validation:** Requires 2 to 5 LPN IDs. + +**Examples:** + +```bash +# Compare two animals on all traits +nsip compare 430735-0032 430735-0041 + +# Compare three animals on specific traits +nsip compare 430735-0032 430735-0041 430735-0058 --traits BWT,WWT,YWT,EMD + +# JSON output +nsip -J compare 430735-0032 430735-0041 +``` + +--- + +### completions + +Generate shell completions for your shell. Write the output to the appropriate completions directory for your shell. + +``` +nsip completions +``` + +**Arguments:** + +| Argument | Type | Required | Values | +|----------|------|----------|--------| +| `SHELL` | string | yes | `bash`, `zsh`, `fish`, `powershell` | + +**Examples:** + +```bash +# Bash +nsip completions bash > ~/.local/share/bash-completion/completions/nsip + +# Zsh +nsip completions zsh > ~/.zfunc/_nsip + +# Fish +nsip completions fish > ~/.config/fish/completions/nsip.fish + +# PowerShell +nsip completions powershell > nsip.ps1 +``` + +--- + +### man-pages + +Generate man pages. Writes the main man page to stdout by default, or generates all man pages (including subcommand pages) to a directory. + +``` +nsip man-pages [OPTIONS] +``` + +**Options:** + +| Option | Type | Description | +|--------|------|-------------| +| `--out-dir` | string | Output directory for man pages. If omitted, writes the main page to stdout. | + +**Examples:** + +```bash +# View main man page +nsip man-pages | man -l - + +# Generate all man pages to a directory +nsip man-pages --out-dir ./man/man1 + +# Install system-wide +sudo nsip man-pages --out-dir /usr/local/share/man/man1 +``` + +--- + +### mcp + +Start the MCP (Model Context Protocol) server for AI assistant integration. Communicates over stdio using JSON-RPC. + +``` +nsip mcp +``` + +**Arguments:** None + +**Notes:** +- Runs as a long-lived process reading JSON-RPC from stdin and writing to stdout +- Logging goes to stderr +- See [MCP Tools Reference](MCP-TOOLS.md) for the full tool catalog + +--- + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | Error (validation, API, connection, timeout, parse, or not found) | + +On error, the error message is printed to stderr in the format `Error: {message}`. + +--- + +## Output Modes + +The CLI supports two output modes controlled by the global `--json` / `-J` flag: + +**Human-readable (default):** Formatted ASCII tables and structured text output designed for terminal use. + +**JSON (`--json`):** Raw JSON output from the NSIP API, pretty-printed with indentation. Suitable for piping to `jq` or other JSON-processing tools. + +```bash +# Pipe JSON output to jq +nsip -J breed-groups | jq '.[0].breeds' + +# Save search results to a file +nsip -J search --breed-id 486 > results.json +``` + +--- + +## EBV Trait Abbreviations + +These abbreviations are used with `--sort-by` and `--traits` options: + +| Abbreviation | Name | Unit | +|--------------|------|------| +| BWT | Birth Weight | lbs | +| WWT | Weaning Weight | lbs | +| PWWT | Post-Weaning Weight | lbs | +| YWT | Yearling Weight | lbs | +| FAT | Fat Depth | mm | +| EMD | Eye Muscle Depth | mm | +| NLB | Number of Lambs Born | lambs | +| NWT | Number of Lambs Weaned | lambs | +| PWT | Pounds Weaned | lbs | +| DAG | Dag Score | score | +| WGR | Wool Growth Rate | g/day | +| WEC | Worm Egg Count | eggs/g | +| FEC | Fecal Egg Count | eggs/g | + +--- + +## See Also + +- [Library API Reference](LIBRARY-API.md) -- programmatic access to the same functionality +- [MCP Tools Reference](MCP-TOOLS.md) -- AI assistant integration +- [Configuration Reference](CONFIGURATION.md) -- environment and client configuration +- [Getting Started](../tutorials/GETTING-STARTED.md) diff --git a/docs/reference/CONFIGURATION.md b/docs/reference/CONFIGURATION.md new file mode 100644 index 0000000..c929e6b --- /dev/null +++ b/docs/reference/CONFIGURATION.md @@ -0,0 +1,206 @@ +# Configuration Reference + +Complete reference for configuring the `nsip` CLI and library. + +--- + +## Client Configuration + +The `NsipClient` is configured through its builder or constructor methods. There are no environment variables or configuration files. + +### Defaults + +| Setting | Default value | Description | +|---------|---------------|-------------| +| Base URL | `http://nsipsearch.nsip.org/api` | NSIP Search API endpoint (HTTP-only; the upstream API has no valid TLS certificate) | +| Timeout | 30 seconds | Per-request timeout | +| Max retries | 3 | Automatic retries on server errors (HTTP 500, 502, 503, 504) | +| Backoff factor | 0.5 | Retry delay multiplier (not configurable) | +| Retry delay | `0.5 * 2^attempt` seconds | Exponential backoff formula | + +### Constructor Methods + +Three ways to create a client: + +```rust +use nsip::NsipClient; + +// 1. All defaults +let client = NsipClient::new(); + +// 2. Custom base URL, default timeout and retries +let client = NsipClient::with_base_url("http://localhost:8080/api"); + +// 3. Full control via builder +let client = NsipClient::builder() + .base_url("http://localhost:8080/api") + .timeout_secs(60) + .max_retries(5) + .build()?; +``` + +### Builder Options + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `base_url(url)` | `impl Into` | `http://nsipsearch.nsip.org/api` | API base URL | +| `timeout_secs(secs)` | `u64` | 30 | Request timeout in seconds | +| `max_retries(retries)` | `u32` | 3 | Max retries for 5xx errors | + +The `build()` method returns `Result`. It returns `Error::Connection` if the underlying `reqwest::Client` cannot be constructed (rare in practice). + +--- + +## Retry Policy + +The client automatically retries failed requests that receive specific HTTP status codes. + +**Retried status codes:** 500, 502, 503, 504 + +**Backoff schedule:** Exponential with a factor of 0.5. + +| Attempt | Delay | +|---------|-------| +| 1 | 0.5 seconds | +| 2 | 1.0 seconds | +| 3 | 2.0 seconds | +| 4 | 4.0 seconds | +| 5 | 8.0 seconds | + +After exhausting all retries, the final server error is returned as `Error::Api`. + +**Disable retries:** + +```rust +let client = NsipClient::builder() + .max_retries(0) + .build()?; +``` + +--- + +## CLI Configuration + +The CLI binary has no configuration file. All options are provided as command-line flags and arguments. + +### Global Flag + +| Flag | Short | Description | +|------|-------|-------------| +| `--json` | `-J` | Output raw JSON instead of human-readable ASCII tables | + +This flag is global and applies to all subcommands. + +### Pagination Defaults + +| Command | Default page | Default page_size | +|---------|-------------|-------------------| +| `search` | 0 | 15 | +| `progeny` | 0 | 10 | + +--- + +## MCP Server Configuration + +The MCP server is started with `nsip mcp` and communicates over stdio (JSON-RPC). It has no configuration options of its own beyond the MCP client configuration. + +### Claude Code (`.mcp.json`) + +Place at your project root or `~/.mcp.json`: + +```json +{ + "mcpServers": { + "nsip": { + "command": "nsip", + "args": ["mcp"] + } + } +} +``` + +### Claude Desktop (`claude_desktop_config.json`) + +```json +{ + "mcpServers": { + "nsip": { + "command": "nsip", + "args": ["mcp"] + } + } +} +``` + +On macOS the config file is at `~/Library/Application Support/Claude/claude_desktop_config.json`. + +### Docker Transport + +```json +{ + "mcpServers": { + "nsip": { + "command": "docker", + "args": ["run", "--rm", "-i", "ghcr.io/zircote/nsip", "mcp"] + } + } +} +``` + +--- + +## Search Criteria Defaults + +When using `SearchCriteria` programmatically, all fields default to `None` (no filter applied). The `search` CLI subcommand applies these defaults: + +| Option | Default | +|--------|---------| +| `--page` | 0 | +| `--page-size` | 15 | +| `--proven-only` | false | +| `--reverse` | false | +| All other filters | not set (no filtering) | + +--- + +## Validation Constraints + +These constraints are enforced at the client level before any API request is made: + +| Parameter | Constraint | Error | +|-----------|-----------|-------| +| `breed_id` (for `trait_ranges`) | Must be > 0 | `Error::Validation` | +| `page_size` (for `search_animals`) | Must be 1-100 | `Error::Validation` | +| `page_size` (for `progeny`) | Must be > 0 | `Error::Validation` | +| `search_string` (for `animal_details`) | Must not be empty/whitespace | `Error::Validation` | +| `lpn_id` (for `lineage`) | Must not be empty/whitespace | `Error::Validation` | +| `lpn_id` (for `progeny`) | Must not be empty | `Error::Validation` | +| `lpn_id` (for `search_by_lpn`) | Must not be empty/whitespace | `Error::Validation` | +| `lpn_ids` (for CLI `compare`) | Must provide 2-5 IDs | Clap argument validation | + +--- + +## API Endpoint Mapping + +The client translates method calls to these NSIP API endpoints: + +| Client method | HTTP method | API path | +|--------------|-------------|----------| +| `date_last_updated()` | GET | `search/getDateLastUpdated` | +| `breed_groups()` | GET | `search/getAvailableBreedGroups` | +| `statuses()` | GET | `search/getStatusesByBreedGroup` | +| `trait_ranges(breed_id)` | GET | `search/getTraitRangesByBreed?breedId={id}` | +| `search_animals(...)` | POST | `search/getPageOfSearchResults` | +| `animal_details(search_string)` | GET | `details/getAnimalDetails?searchString={s}` | +| `lineage(lpn_id)` | GET | `details/getLineage?lpnId={id}` | +| `progeny(lpn_id, ...)` | GET | `details/getPageOfProgeny` | +| `search_by_lpn(lpn_id)` | -- | Concurrent: `animal_details` + `lineage` + `progeny` | + +--- + +## See Also + +- [Library API Reference](LIBRARY-API.md) -- full method signatures +- [CLI Reference](CLI.md) -- all command-line options +- [Error Handling Reference](ERROR-HANDLING.md) -- error types and retry behavior +- [How to Configure Timeout and Retries](../how-to/CONFIGURE-CLIENT.md) diff --git a/docs/reference/ERROR-HANDLING.md b/docs/reference/ERROR-HANDLING.md new file mode 100644 index 0000000..f455edf --- /dev/null +++ b/docs/reference/ERROR-HANDLING.md @@ -0,0 +1,391 @@ +# Error Handling Reference + +Complete reference for error handling in the `nsip` crate. + +--- + +## Error Type + +The crate defines a single error enum with six variants, implemented using `thiserror`: + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("validation error: {0}")] + Validation(String), + + #[error("API error (HTTP {status}): {message}")] + Api { status: u16, message: String }, + + #[error("not found: {0}")] + NotFound(String), + + #[error("request timed out: {0}")] + Timeout(String), + + #[error("connection error: {0}")] + Connection(String), + + #[error("parse error: {0}")] + Parse(String), +} +``` + +All variants implement `std::fmt::Display` and `std::error::Error`. + +--- + +## Result Type Alias + +The crate provides a convenience alias: + +```rust +pub type Result = std::result::Result; +``` + +Use it in your own functions to propagate `nsip` errors: + +```rust +async fn fetch_animal(lpn_id: &str) -> nsip::Result { + let client = NsipClient::new(); + client.animal_details(lpn_id).await +} +``` + +--- + +## Error Variants + +### `Error::Validation` + +Returned when input parameters fail local validation before a request is sent to the API. + +**Display format:** `validation error: {message}` + +**Triggered by:** + +| Method | Condition | +|--------|-----------| +| `trait_ranges(breed_id)` | `breed_id <= 0` | +| `search_animals(page, page_size, ...)` | `page_size == 0` or `page_size > 100` | +| `animal_details(search_string)` | `search_string` is empty or whitespace-only | +| `lineage(lpn_id)` | `lpn_id` is empty or whitespace-only | +| `progeny(lpn_id, page, page_size)` | `lpn_id` is empty, or `page_size == 0` | +| `search_by_lpn(lpn_id)` | `lpn_id` is empty or whitespace-only | +| `NsipClientBuilder::build()` | (not applicable -- see `Error::Connection`) | + +**Example:** + +```rust +use nsip::{NsipClient, Error}; + +let client = NsipClient::new(); + +// page_size of 0 triggers Validation +match client.search_animals(0, 0, None, None, None, None).await { + Err(Error::Validation(msg)) => { + eprintln!("Invalid input: {}", msg); + // Fix the input -- do not retry + } + Ok(results) => { /* process results */ } + Err(e) => eprintln!("Other error: {}", e), +} +``` + +**Recovery:** Fix the input parameters. Never retry on validation errors. + +--- + +### `Error::Api` + +Returned when the NSIP API responds with a non-success HTTP status code that is not 404 and not retryable (or retries are exhausted for 5xx codes). + +**Display format:** `API error (HTTP {status}): {message}` + +**Fields:** +- `status: u16` -- the HTTP status code +- `message: String` -- human-readable description + +**Common status codes:** + +| Status | Meaning | +|--------|---------| +| 400 | Bad request -- malformed search criteria | +| 403 | Forbidden -- access denied | +| 500 | Internal server error (after retries exhausted) | +| 502 | Bad gateway (after retries exhausted) | +| 503 | Service unavailable (after retries exhausted) | +| 504 | Gateway timeout (after retries exhausted) | + +**Example:** + +```rust +match client.breed_groups().await { + Err(Error::Api { status, message }) => { + match status { + 400 => eprintln!("Bad request: {}", message), + 500..=599 => eprintln!("Server error ({}): {}", status, message), + _ => eprintln!("HTTP {}: {}", status, message), + } + } + Ok(groups) => { /* process groups */ } + Err(e) => eprintln!("Other error: {}", e), +} +``` + +**Recovery:** For 4xx errors, check your request parameters. For 5xx errors, the client has already retried according to its retry policy (see [Retry Behavior](#retry-behavior)). You may wait and retry later. + +--- + +### `Error::NotFound` + +Returned when the API responds with HTTP 404 -- the requested resource does not exist. + +**Display format:** `not found: {message}` + +**Triggered by:** +- `animal_details()` when the animal is not in the database +- `lineage()` when the LPN ID has no lineage data +- `progeny()` when the LPN ID has no progeny data +- Any endpoint that returns HTTP 404 + +**Example:** + +```rust +match client.animal_details("NONEXISTENT-ID").await { + Err(Error::NotFound(msg)) => { + eprintln!("Not found: {}", msg); + // Prompt user for a different ID + } + Ok(animal) => println!("Found: {}", animal.lpn_id), + Err(e) => eprintln!("Other error: {}", e), +} +``` + +**Recovery:** Verify the LPN ID or search string is correct. Do not retry with the same identifier. + +--- + +### `Error::Timeout` + +Returned when the HTTP request exceeds the configured timeout duration. The default timeout is 30 seconds. + +**Display format:** `request timed out: {message}` + +**Triggered by:** +- Slow network connections +- Large result sets +- Server overload + +**Example:** + +```rust +match client.search_animals(0, 100, None, None, None, None).await { + Err(Error::Timeout(msg)) => { + eprintln!("Timed out: {}", msg); + // Reduce page size or increase timeout + let client = NsipClient::builder() + .timeout_secs(120) + .build()?; + } + Ok(results) => { /* process results */ } + Err(e) => eprintln!("Other error: {}", e), +} +``` + +**Recovery:** Increase the timeout with `NsipClient::builder().timeout_secs()`, reduce the page size, or retry after a delay. + +--- + +### `Error::Connection` + +Returned when the HTTP client cannot establish a connection to the API server. + +**Display format:** `connection error: {message}` + +**Triggered by:** +- No internet connectivity +- DNS resolution failure +- Firewall blocking the request +- Invalid base URL configured via `NsipClient::with_base_url()` or `NsipClientBuilder::base_url()` +- Failure to build the `reqwest::Client` in `NsipClientBuilder::build()` + +**Example:** + +```rust +use std::time::Duration; + +match client.breed_groups().await { + Err(Error::Connection(msg)) => { + eprintln!("Connection failed: {}", msg); + // Check network, then retry + tokio::time::sleep(Duration::from_secs(5)).await; + } + Ok(groups) => { /* process groups */ } + Err(e) => eprintln!("Other error: {}", e), +} +``` + +**Recovery:** Check network connectivity and the configured base URL. Retry with exponential backoff. + +--- + +### `Error::Parse` + +Returned when the API response cannot be deserialized into the expected data type. + +**Display format:** `parse error: {message}` + +**Triggered by:** +- Unexpected JSON structure from the API +- Missing required fields in the response +- Invalid data types in the response +- API format changes + +**Example:** + +```rust +match client.trait_ranges(640).await { + Err(Error::Parse(msg)) => { + eprintln!("Parse error: {}", msg); + // Likely an API change -- report as a bug + } + Ok(ranges) => { /* process ranges */ } + Err(e) => eprintln!("Other error: {}", e), +} +``` + +**Recovery:** Parse errors typically indicate an API-side change. Report it as a bug. Do not retry with the same request. + +--- + +## Retry Behavior + +The `NsipClient` automatically retries requests that fail with specific server error codes. Retries happen transparently before any error is returned to the caller. + +**Retried status codes:** 500, 502, 503, 504 + +**Default retry policy:** + +| Setting | Default | Builder method | +|---------|---------|----------------| +| Max retries | 3 | `NsipClientBuilder::max_retries()` | +| Backoff factor | 0.5 | Not configurable | +| Backoff formula | `0.5 * 2^attempt` seconds | -- | + +**Retry delay schedule (with defaults):** + +| Attempt | Delay | +|---------|-------| +| 1 | 0.5 seconds | +| 2 | 1.0 seconds | +| 3 | 2.0 seconds | + +If all retries are exhausted, the final error is returned as `Error::Api`. + +**Customize retry policy:** + +```rust +// More aggressive retries +let client = NsipClient::builder() + .max_retries(5) + .build()?; + +// No retries (fail fast) +let client = NsipClient::builder() + .max_retries(0) + .build()?; +``` + +--- + +## Error Display Messages + +Each variant produces a distinct display prefix: + +| Variant | Display prefix | +|---------|---------------| +| `Validation(msg)` | `validation error: {msg}` | +| `Api { status, message }` | `API error (HTTP {status}): {message}` | +| `NotFound(msg)` | `not found: {msg}` | +| `Timeout(msg)` | `request timed out: {msg}` | +| `Connection(msg)` | `connection error: {msg}` | +| `Parse(msg)` | `parse error: {msg}` | + +--- + +## Matching All Variants + +A comprehensive match on all error variants: + +```rust +use nsip::{NsipClient, Error}; + +let client = NsipClient::new(); + +match client.animal_details("430735-0032").await { + Ok(animal) => { + println!("Retrieved: {}", animal.lpn_id); + } + Err(Error::Validation(msg)) => { + // Bad input -- fix and do not retry + eprintln!("Invalid input: {}", msg); + } + Err(Error::Api { status, message }) => { + // Server returned an error HTTP status + eprintln!("API error (HTTP {}): {}", status, message); + } + Err(Error::NotFound(msg)) => { + // Resource does not exist + eprintln!("Not found: {}", msg); + } + Err(Error::Timeout(msg)) => { + // Request exceeded timeout + eprintln!("Timed out: {}", msg); + } + Err(Error::Connection(msg)) => { + // Network-level failure + eprintln!("Connection error: {}", msg); + } + Err(Error::Parse(msg)) => { + // Response deserialization failed + eprintln!("Parse error: {}", msg); + } +} +``` + +--- + +## Wrapping in Application Errors + +Use `#[from]` with `thiserror` to convert `nsip::Error` into your application's error type: + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("NSIP error: {0}")] + Nsip(#[from] nsip::Error), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), +} + +async fn process_animal(lpn_id: &str) -> Result<(), AppError> { + let client = nsip::NsipClient::new(); + let animal = client.animal_details(lpn_id).await?; // converts via From + Ok(()) +} +``` + +--- + +## See Also + +- [Configuration Reference](CONFIGURATION.md) -- timeout and retry settings +- [Library API Reference](LIBRARY-API.md) -- method signatures and validation rules +- [How to Configure Timeout and Retries](../how-to/CONFIGURE-CLIENT.md) +- [NSIP Data Model](../explanation/NSIP-DATA-MODEL.md) diff --git a/docs/reference/LIBRARY-API.md b/docs/reference/LIBRARY-API.md new file mode 100644 index 0000000..ee21493 --- /dev/null +++ b/docs/reference/LIBRARY-API.md @@ -0,0 +1,569 @@ +# Library API Reference + +Complete reference for the `nsip` Rust library crate. + +--- + +## Crate Exports + +```rust +pub use client::NsipClient; +pub use models::{ + AnimalDetails, AnimalProfile, Breed, BreedGroup, ContactInfo, + DateLastUpdated, Lineage, LineageAnimal, Progeny, ProgenyAnimal, + SearchCriteria, SearchResults, Trait, TraitRange, TraitRangeFilter, +}; +pub mod mcp; +pub enum Error { /* ... */ } +pub type Result = std::result::Result; +``` + +--- + +## NsipClient + +HTTP client for the NSIP Search API at `nsipsearch.nsip.org/api`. All data-fetching methods are `async` and require a Tokio runtime. + +### Construction + +#### `NsipClient::new() -> Self` + +Create a client with default settings (base URL `http://nsipsearch.nsip.org/api`, 30-second timeout, 3 retries). + +```rust +use nsip::NsipClient; + +let client = NsipClient::new(); +``` + +#### `NsipClient::with_base_url(base_url: impl Into) -> Self` + +Create a client with a custom base URL. Uses default timeout and retry settings. + +```rust +let client = NsipClient::with_base_url("http://localhost:8080/api"); +``` + +#### `NsipClient::builder() -> NsipClientBuilder` + +Create a builder for full control over client configuration. + +```rust +let client = NsipClient::builder() + .base_url("http://localhost:8080/api") + .timeout_secs(60) + .max_retries(5) + .build()?; +``` + +#### `NsipClient::base_url(&self) -> &str` + +Return the configured base URL. + +```rust +let client = NsipClient::new(); +assert_eq!(client.base_url(), "http://nsipsearch.nsip.org/api"); +``` + +--- + +### NsipClientBuilder + +Builder for constructing an `NsipClient` with custom settings. + +#### `base_url(self, url: impl Into) -> Self` + +Set the API base URL. Default: `http://nsipsearch.nsip.org/api`. + +#### `timeout_secs(self, secs: u64) -> Self` + +Set the request timeout in seconds. Default: 30. This is a `const fn`. + +#### `max_retries(self, retries: u32) -> Self` + +Set the maximum number of retries for server errors (HTTP 500, 502, 503, 504). Default: 3. This is a `const fn`. + +#### `build(self) -> Result` + +Build the client. Returns `Error::Connection` if the underlying HTTP client cannot be constructed. + +```rust +let client = NsipClient::builder() + .timeout_secs(60) + .max_retries(5) + .build()?; +``` + +--- + +### Methods + +All methods are `async` and return `Result`. + +#### `date_last_updated(&self) -> Result` + +Get the date when the NSIP database was last updated. + +**API endpoint:** `GET search/getDateLastUpdated` + +**Errors:** `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let updated = client.date_last_updated().await?; +println!("{}", serde_json::to_string_pretty(&updated.data)?); +``` + +--- + +#### `breed_groups(&self) -> Result>` + +List all available breed groups and the individual breeds within each group. + +**API endpoint:** `GET search/getAvailableBreedGroups` + +**Errors:** `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let groups = client.breed_groups().await?; +for group in &groups { + println!("{}: {} breeds", group.name, group.breeds.len()); +} +``` + +--- + +#### `statuses(&self) -> Result>` + +List all available animal statuses. + +**API endpoint:** `GET search/getStatusesByBreedGroup` + +**Errors:** `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let statuses = client.statuses().await?; +// e.g., ["CURRENT", "SOLD", "DEAD"] +``` + +--- + +#### `trait_ranges(&self, breed_id: i64) -> Result` + +Get the minimum and maximum EBV trait values for a specific breed. + +**API endpoint:** `GET search/getTraitRangesByBreed?breedId={breed_id}` + +**Parameters:** + +| Parameter | Type | Validation | +|-----------|------|------------| +| `breed_id` | `i64` | Must be > 0 | + +**Errors:** `Validation` (if `breed_id <= 0`), `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let ranges = client.trait_ranges(486).await?; +``` + +--- + +#### `search_animals(&self, page: u32, page_size: u32, breed_id: Option, sorted_trait: Option<&str>, reverse: Option, criteria: Option<&SearchCriteria>) -> Result` + +Search for animals with filters and pagination. + +**API endpoint:** `POST search/getPageOfSearchResults` + +**Parameters:** + +| Parameter | Type | Validation | Description | +|-----------|------|------------|-------------| +| `page` | `u32` | -- | Page number (0-indexed) | +| `page_size` | `u32` | 1-100 | Results per page | +| `breed_id` | `Option` | -- | Breed filter | +| `sorted_trait` | `Option<&str>` | -- | Trait abbreviation to sort by | +| `reverse` | `Option` | -- | Reverse sort order | +| `criteria` | `Option<&SearchCriteria>` | -- | Additional search criteria | + +**Errors:** `Validation` (if `page_size == 0` or `page_size > 100`), `Connection`, `Timeout`, `Api`, `Parse` + +```rust +use nsip::{NsipClient, SearchCriteria}; + +let client = NsipClient::new(); +let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_status("CURRENT") + .with_gender("Male"); + +let results = client.search_animals( + 0, // page + 15, // page_size + Some(486), // breed_id + Some("WWT"), // sort by weaning weight + None, // default sort order + Some(&criteria), +).await?; + +println!("Found {} animals", results.total_count); +``` + +--- + +#### `animal_details(&self, search_string: &str) -> Result` + +Get detailed EBV data, breed, contact info, and status for a specific animal. + +**API endpoint:** `GET details/getAnimalDetails?searchString={search_string}` + +**Parameters:** + +| Parameter | Type | Validation | +|-----------|------|------------| +| `search_string` | `&str` | Must not be empty or whitespace-only | + +**Errors:** `Validation` (if empty/whitespace), `NotFound`, `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let animal = client.animal_details("430735-0032").await?; +println!("LPN: {}, Breed: {:?}", animal.lpn_id, animal.breed); +``` + +--- + +#### `lineage(&self, lpn_id: &str) -> Result` + +Get pedigree / ancestry tree for a specific animal including parents and grandparents. + +**API endpoint:** `GET details/getLineage?lpnId={lpn_id}` + +**Parameters:** + +| Parameter | Type | Validation | +|-----------|------|------------| +| `lpn_id` | `&str` | Must not be empty or whitespace-only | + +**Errors:** `Validation` (if empty/whitespace), `NotFound`, `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let lineage = client.lineage("430735-0032").await?; +if let Some(sire) = &lineage.sire { + println!("Sire: {}", sire.lpn_id); +} +if let Some(dam) = &lineage.dam { + println!("Dam: {}", dam.lpn_id); +} +``` + +--- + +#### `progeny(&self, lpn_id: &str, page: u32, page_size: u32) -> Result` + +Get a paginated list of offspring for a specific animal. + +**API endpoint:** `GET details/getPageOfProgeny` + +**Parameters:** + +| Parameter | Type | Validation | +|-----------|------|------------| +| `lpn_id` | `&str` | Must not be empty | +| `page` | `u32` | -- | +| `page_size` | `u32` | Must be > 0 | + +**Errors:** `Validation` (if `lpn_id` empty or `page_size == 0`), `NotFound`, `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let progeny = client.progeny("430735-0032", 0, 10).await?; +println!("{} total offspring", progeny.total_count); +for animal in &progeny.animals { + println!(" {}", animal.lpn_id); +} +``` + +--- + +#### `search_by_lpn(&self, lpn_id: &str) -> Result` + +Get a complete profile for an animal: details, lineage, and progeny fetched concurrently via `tokio::join!`. + +**Parameters:** + +| Parameter | Type | Validation | +|-----------|------|------------| +| `lpn_id` | `&str` | Must not be empty or whitespace-only | + +**Errors:** `Validation` (if empty/whitespace), `NotFound`, `Connection`, `Timeout`, `Api`, `Parse` + +```rust +let profile = client.search_by_lpn("430735-0032").await?; +println!("Details: {}", profile.details.lpn_id); +println!("Sire: {:?}", profile.lineage.sire); +println!("Offspring: {}", profile.progeny.total_count); +``` + +--- + +## SearchCriteria + +Builder for constructing search filters. All builder methods consume and return `self`, allowing method chaining. + +### Construction + +#### `SearchCriteria::new() -> Self` + +Create an empty criteria with all fields set to `None`. This is a `const fn`. + +```rust +let criteria = SearchCriteria::new(); +``` + +### Builder Methods + +All builder methods consume `self` and return a new `SearchCriteria` with the field set. + +| Method | Parameter type | Description | +|--------|---------------|-------------| +| `with_breed_group_id(self, id: i64)` | `i64` | Set breed group ID filter (`const fn`) | +| `with_breed_id(self, id: i64)` | `i64` | Set breed ID filter (`const fn`) | +| `with_born_after(self, date: impl Into)` | `String` | Only animals born after this date (`YYYY-MM-DD`) | +| `with_born_before(self, date: impl Into)` | `String` | Only animals born before this date (`YYYY-MM-DD`) | +| `with_gender(self, gender: impl Into)` | `String` | Gender filter: `"Male"`, `"Female"`, `"Both"` | +| `with_proven_only(self, proven: bool)` | `bool` | Only proven animals (`const fn`) | +| `with_status(self, status: impl Into)` | `String` | Status filter: `"CURRENT"`, `"SOLD"`, `"DEAD"` | +| `with_flock_id(self, flock_id: impl Into)` | `String` | Flock ID filter | +| `with_trait_ranges(self, ranges: HashMap)` | `HashMap` | Per-trait min/max filters | + +### Fields + +All fields are `pub` and `Option`-wrapped. The struct derives `Debug`, `Clone`, `Default`, `Serialize`, and `Deserialize`. JSON serialization uses `camelCase` field names and skips `None` values. + +| Field | Type | JSON key | +|-------|------|----------| +| `breed_group_id` | `Option` | `breedGroupId` | +| `breed_id` | `Option` | `breedId` | +| `born_after` | `Option` | `bornAfter` | +| `born_before` | `Option` | `bornBefore` | +| `gender` | `Option` | `gender` | +| `proven_only` | `Option` | `provenOnly` | +| `status` | `Option` | `status` | +| `flock_id` | `Option` | `flockId` | +| `trait_ranges` | `Option>` | `traitRanges` | + +### Example + +```rust +use std::collections::HashMap; +use nsip::{SearchCriteria, TraitRangeFilter}; + +let criteria = SearchCriteria::new() + .with_breed_id(486) + .with_status("CURRENT") + .with_gender("Female") + .with_born_after("2020-01-01") + .with_proven_only(true) + .with_trait_ranges(HashMap::from([ + ("BWT".to_string(), TraitRangeFilter { min: -1.0, max: 1.0 }), + ("WWT".to_string(), TraitRangeFilter { min: 5.0, max: 20.0 }), + ])); +``` + +--- + +## Model Types + +### AnimalDetails + +Detailed information about a single animal including EBV traits, breed, and contact info. + +| Field | Type | Description | +|-------|------|-------------| +| `lpn_id` | `String` | Unique LPN identifier | +| `breed` | `Option` | Breed name | +| `breed_group` | `Option` | Breed group name | +| `date_of_birth` | `Option` | Date of birth | +| `gender` | `Option` | `"Male"` or `"Female"` | +| `status` | `Option` | `"CURRENT"`, `"SOLD"`, `"DEAD"`, etc. | +| `sire` | `Option` | Sire LPN identifier | +| `dam` | `Option` | Dam LPN identifier | +| `registration_number` | `Option` | Registration number | +| `total_progeny` | `Option` | Total number of progeny | +| `flock_count` | `Option` | Number of flocks | +| `genotyped` | `Option` | Genotyped status | +| `traits` | `HashMap` | EBV traits keyed by abbreviation (e.g. `"BWT"`, `"WWT"`) | +| `contact_info` | `Option` | Owner/flock contact information | + +--- + +### AnimalProfile + +Combined profile returned by `search_by_lpn()`. + +| Field | Type | Description | +|-------|------|-------------| +| `details` | `AnimalDetails` | Animal details and EBVs | +| `lineage` | `Lineage` | Pedigree / ancestry data | +| `progeny` | `Progeny` | Offspring list | + +--- + +### Breed + +A single breed within a breed group. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `i64` | Breed identifier | +| `name` | `String` | Breed name | + +--- + +### BreedGroup + +A group of related breeds. + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `i64` | Group identifier | +| `name` | `String` | Group name | +| `breeds` | `Vec` | Breeds in this group | + +--- + +### ContactInfo + +Breeder contact information associated with an animal. + +| Field | Type | Description | +|-------|------|-------------| +| `farm_name` | `Option` | Farm name | +| `contact_name` | `Option` | Contact person | +| `phone` | `Option` | Phone number | +| `email` | `Option` | Email address | +| `address` | `Option` | Street address | +| `city` | `Option` | City | +| `state` | `Option` | State | +| `zip_code` | `Option` | ZIP code | + +--- + +### DateLastUpdated + +Response from the database last-updated endpoint. + +| Field | Type | Description | +|-------|------|-------------| +| `data` | `serde_json::Value` | Raw JSON response containing the date | + +--- + +### Lineage + +Pedigree / ancestry tree for an animal. + +| Field | Type | Description | +|-------|------|-------------| +| `subject` | `Option` | The animal itself | +| `sire` | `Option` | Father | +| `dam` | `Option` | Mother | +| `generations` | `Vec>` | Extended pedigree by generation depth | + +--- + +### LineageAnimal + +A single animal within a pedigree tree. + +| Field | Type | Description | +|-------|------|-------------| +| `lpn_id` | `String` | LPN identifier | +| `farm_name` | `Option` | Farm name | +| `us_index` | `Option` | US selection index | +| `src_index` | `Option` | Source index | +| `date_of_birth` | `Option` | Date of birth | +| `sex` | `Option` | Sex | +| `status` | `Option` | Status | + +--- + +### Progeny + +Paginated list of offspring. + +| Field | Type | Description | +|-------|------|-------------| +| `total_count` | `i64` | Total number of offspring | +| `animals` | `Vec` | Offspring on this page | +| `page` | `u32` | Current page number | +| `page_size` | `u32` | Page size | + +--- + +### ProgenyAnimal + +A single offspring animal. + +| Field | Type | Description | +|-------|------|-------------| +| `lpn_id` | `String` | LPN identifier | +| `sex` | `Option` | Sex | +| `date_of_birth` | `Option` | Date of birth | +| `traits` | `HashMap` | Trait abbreviation to EBV value | + +--- + +### SearchResults + +Paginated search results. + +| Field | Type | Description | +|-------|------|-------------| +| `total_count` | `i64` | Total matching animals | +| `results` | `Vec` | Raw result objects for this page | +| `page` | `u32` | Current page number | +| `page_size` | `u32` | Page size | + +--- + +### Trait + +A single EBV trait value for an animal. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `String` | Trait abbreviation (e.g., `BWT`, `WWT`) | +| `value` | `f64` | EBV value | +| `accuracy` | `Option` | Accuracy percentage (0-100) | +| `units` | `Option` | Unit of measurement | + +--- + +### TraitRange + +Breed-level minimum and maximum for a trait. + +| Field | Type | Description | +|-------|------|-------------| +| `trait_name` | `String` | Trait abbreviation | +| `min_value` | `f64` | Minimum value in the breed | +| `max_value` | `f64` | Maximum value in the breed | +| `unit` | `Option` | Unit of measurement | + +--- + +### TraitRangeFilter + +Min/max bounds for filtering animals by trait value in search criteria. + +| Field | Type | Description | +|-------|------|-------------| +| `min` | `f64` | Minimum value (inclusive) | +| `max` | `f64` | Maximum value (inclusive) | + +--- + +## See Also + +- [Error Handling Reference](ERROR-HANDLING.md) -- error types and recovery strategies +- [Configuration Reference](CONFIGURATION.md) -- client configuration options +- [CLI Reference](CLI.md) -- command-line interface for the same functionality +- [Getting Started](../tutorials/GETTING-STARTED.md) diff --git a/docs/reference/MCP-TOOLS.md b/docs/reference/MCP-TOOLS.md new file mode 100644 index 0000000..4bebf59 --- /dev/null +++ b/docs/reference/MCP-TOOLS.md @@ -0,0 +1,511 @@ +# MCP Tools Reference + +Complete reference for the 13 tools exposed by the `nsip mcp` server. + +For installation, configuration, resources, and prompts, see [MCP Server Reference](../MCP.md). + +--- + +## Overview + +The MCP server exposes 13 tools over the Model Context Protocol (stdio transport, protocol version `2024-11-05`). All tools return JSON results as text content. Errors use standard MCP error codes. + +| Tool | Description | +|------|-------------| +| [search](#search) | Search for animals with filters and sorting | +| [details](#details) | Get detailed EBV data for an animal | +| [lineage](#lineage) | Get pedigree / ancestry tree | +| [progeny](#progeny) | Get offspring list (paginated) | +| [profile](#profile) | Get complete animal profile in one call | +| [breed_groups](#breed_groups) | List all breed groups and breeds | +| [trait_ranges](#trait_ranges) | Get min/max EBV ranges for a breed | +| [compare](#compare) | Compare 2-5 animals side-by-side | +| [rank](#rank) | Rank animals by weighted EBV traits | +| [inbreeding_check](#inbreeding_check) | Calculate Wright's COI for a mating pair | +| [mating_recommendations](#mating_recommendations) | Find optimal mates for an animal | +| [flock_summary](#flock_summary) | Summarize a flock's animals and trait averages | +| [database_status](#database_status) | Get database last-updated date and statuses | + +--- + +## search + +Search for animals in the NSIP database with filters for breed, gender, status, date range, flock, and sorting. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `breed_group_id` | integer | no | -- | Breed group ID to filter by | +| `breed_id` | integer | no | -- | Breed ID to filter by | +| `status` | string | no | -- | `"CURRENT"`, `"SOLD"`, or `"DEAD"` | +| `gender` | string | no | -- | `"Male"`, `"Female"`, or `"Both"` | +| `born_after` | string | no | -- | Only animals born after this date (`YYYY-MM-DD`) | +| `born_before` | string | no | -- | Only animals born before this date (`YYYY-MM-DD`) | +| `proven_only` | boolean | no | false | Only return proven animals | +| `flock_id` | string | no | -- | Flock ID to filter by | +| `sort_by` | string | no | -- | Trait abbreviation to sort by (e.g., `"WWT"`) | +| `reverse` | boolean | no | -- | Reverse the sort order | +| `page` | integer | no | 0 | Page number (0-indexed) | +| `page_size` | integer | no | 15 | Results per page (1-100) | + +**Returns:** `SearchResults` -- total count, result objects, page, and page size. + +**Example:** + +```json +{ + "tool": "search", + "arguments": { + "breed_id": 486, + "gender": "Male", + "status": "CURRENT", + "sort_by": "WWT", + "page_size": 10 + } +} +``` + +--- + +## details + +Get detailed EBV data, breed, contact info, and status for a specific animal by LPN ID. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `animal_id` | string | yes | LPN ID or registration number | + +**Returns:** `AnimalDetails` -- LPN ID, breed, sex, date of birth, status, EBV traits, and contact info. + +**Example:** + +```json +{ + "tool": "details", + "arguments": { + "animal_id": "430735-0032" + } +} +``` + +--- + +## lineage + +Get pedigree / ancestry tree for a specific animal including parents and grandparents. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `animal_id` | string | yes | LPN ID of the animal | + +**Returns:** `Lineage` -- subject, sire, dam, and extended generations. + +**Example:** + +```json +{ + "tool": "lineage", + "arguments": { + "animal_id": "430735-0032" + } +} +``` + +--- + +## progeny + +Get a paginated list of offspring for a specific animal. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `animal_id` | string | yes | -- | LPN ID of the animal | +| `page` | integer | no | 0 | Page number (0-indexed) | +| `page_size` | integer | no | 10 | Results per page | + +**Returns:** `Progeny` -- total count, offspring animals with their traits, page, and page size. + +**Example:** + +```json +{ + "tool": "progeny", + "arguments": { + "animal_id": "430735-0032", + "page": 0, + "page_size": 20 + } +} +``` + +--- + +## profile + +Get a complete profile for an animal: details, pedigree, and offspring in one call. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `animal_id` | string | yes | LPN ID of the animal | + +**Returns:** `AnimalProfile` -- combined details, lineage, and progeny. + +**Example:** + +```json +{ + "tool": "profile", + "arguments": { + "animal_id": "430735-0032" + } +} +``` + +--- + +## breed_groups + +List all breed groups and individual breeds in the NSIP database. + +**Parameters:** None. + +**Returns:** Array of `BreedGroup` objects, each containing `id`, `name`, and a `breeds` array. + +**Example:** + +```json +{ + "tool": "breed_groups", + "arguments": {} +} +``` + +--- + +## trait_ranges + +Get the minimum and maximum EBV trait ranges for a specific breed. Useful for understanding breed norms and setting trait filters. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `breed_id` | integer | yes | Breed ID to query | + +**Returns:** JSON object with per-trait min/max values. + +**Example:** + +```json +{ + "tool": "trait_ranges", + "arguments": { + "breed_id": 486 + } +} +``` + +--- + +## compare + +Compare 2-5 animals side-by-side on their EBV traits. Optionally filter to specific traits. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `animal_ids` | array of strings | yes | LPN IDs to compare (2-5 items) | +| `traits` | string | no | Comma-separated trait filter (e.g., `"BWT,WWT,YWT"`) | + +**Returns:** Array of `AnimalDetails` objects for the requested animals. + +**Example:** + +```json +{ + "tool": "compare", + "arguments": { + "animal_ids": ["430735-0032", "430735-0041", "430735-0058"], + "traits": "BWT,WWT,YWT,EMD" + } +} +``` + +--- + +## rank + +Rank animals within a breed by weighted EBV traits. Specify trait weights to prioritize breeding goals. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `breed_id` | integer | yes | -- | Breed ID to search within | +| `weights` | object | yes | -- | Trait weights as `{"TRAIT": weight}` (e.g., `{"BWT": -1.0, "WWT": 2.0}`) | +| `gender` | string | no | -- | `"Male"`, `"Female"`, or `"Both"` | +| `status` | string | no | -- | Animal status filter (e.g., `"CURRENT"`) | +| `top_n` | integer | no | 10 | Number of top-ranked results to return | + +**Ranking formula:** `Score = sum(trait_value * weight * accuracy / 100)` for each trait where both a weight and value exist. + +**Returns:** Ranked list of animals with their composite scores and individual trait values. + +**Example -- terminal sire selection:** + +```json +{ + "tool": "rank", + "arguments": { + "breed_id": 486, + "weights": { + "BWT": -1.0, + "WWT": 2.0, + "YWT": 1.5, + "EMD": 1.0 + }, + "gender": "Male", + "status": "CURRENT", + "top_n": 5 + } +} +``` + +**Example -- maternal sire selection:** + +```json +{ + "tool": "rank", + "arguments": { + "breed_id": 486, + "weights": { + "NLB": 2.0, + "NWT": 2.0, + "PWT": 1.5, + "BWT": -0.5 + }, + "gender": "Male", + "top_n": 10 + } +} +``` + +--- + +## inbreeding_check + +Calculate Wright's coefficient of inbreeding (COI) for a potential sire-dam mating. Returns the COI value, a traffic-light rating, and shared ancestors. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sire_id` | string | yes | LPN ID of the sire | +| `dam_id` | string | yes | LPN ID of the dam | + +**COI formula:** `COI = sum[(0.5)^(n1 + n2 + 1)]` where `n1` and `n2` are path lengths from sire and dam to each common ancestor. + +**Traffic-light thresholds:** + +| Rating | COI range | Interpretation | +|--------|-----------|----------------| +| Green | < 6.25% | Acceptable -- proceed with mating | +| Yellow | 6.25% - 12.5% | Elevated -- consider alternatives | +| Red | > 12.5% | High -- generally avoid | + +**Returns:** COI coefficient, rating, and list of shared ancestors with path depths. + +**Example:** + +```json +{ + "tool": "inbreeding_check", + "arguments": { + "sire_id": "430735-0032", + "dam_id": "430735-0089" + } +} +``` + +**Example response:** + +```json +{ + "coefficient": 0.03125, + "rating": "Green", + "shared_ancestors": [ + { + "lpn_id": "410220-0015", + "sire_depth": 2, + "dam_depth": 2 + } + ] +} +``` + +--- + +## mating_recommendations + +Find optimal mates for an animal. Searches the breed for candidates, checks inbreeding, and ranks by trait complementarity. + +**Parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `animal_id` | string | yes | -- | LPN ID of the animal to find mates for | +| `breed_id` | integer | yes | -- | Breed ID to search for potential mates | +| `target_traits` | string | no | `WWT,BWT,NLB` | Traits to optimize (comma-separated) | +| `max_results` | integer | no | 5 | Maximum number of recommendations | + +**Default trait weights (when `target_traits` is omitted):** +- WWT: 1.0 +- BWT: -0.5 +- NLB: 0.5 + +Traits where lower values are preferred (`BWT`, `DAG`, `WEC`, `FEC`) automatically receive negative weights. + +**Offspring EBV prediction:** `predicted_offspring_EBV = (sire_EBV + dam_EBV) / 2` + +**Returns:** Ranked list of recommended mates, each with a score, COI check, and predicted offspring EBVs. + +**Example:** + +```json +{ + "tool": "mating_recommendations", + "arguments": { + "animal_id": "430735-0032", + "breed_id": 486, + "target_traits": "WWT,EMD,NLB", + "max_results": 3 + } +} +``` + +**Example response:** + +```json +[ + { + "mate_lpn_id": "430735-0089", + "rank_score": 18.42, + "coi": { + "coefficient": 0.015, + "rating": "Green" + }, + "predicted_offspring_ebvs": { + "BWT": 0.15, + "WWT": 11.3, + "EMD": 1.8, + "NLB": 0.12 + } + } +] +``` + +--- + +## flock_summary + +Summarize a flock's animals: count, gender breakdown, and average EBV traits. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `flock_id` | string | yes | Flock ID to summarize | +| `breed_id` | integer | no | Breed ID to filter within the flock | + +**Returns:** Flock summary with total count, sample size, male/female counts, and trait averages. + +**Example:** + +```json +{ + "tool": "flock_summary", + "arguments": { + "flock_id": "430735", + "breed_id": 486 + } +} +``` + +**Example response:** + +```json +{ + "flock_id": "430735", + "total_count": 87, + "sample_size": 87, + "males": 12, + "females": 75, + "trait_averages": { + "BWT": 0.32, + "WWT": 8.45, + "YWT": 12.10, + "NLB": 0.08, + "EMD": 0.95 + } +} +``` + +--- + +## database_status + +Get the NSIP database last-updated date and available animal statuses. + +**Parameters:** None. + +**Returns:** Database status object with last-updated date and available statuses. + +**Example:** + +```json +{ + "tool": "database_status", + "arguments": {} +} +``` + +--- + +## EBV Trait Abbreviations + +These abbreviations are used in `sort_by`, `traits`, `weights`, and `target_traits` parameters: + +| Abbreviation | Name | Unit | Selection Direction | +|--------------|------|------|---------------------| +| BWT | Birth Weight | lbs | Lower preferred | +| WWT | Weaning Weight | lbs | Higher preferred | +| PWWT | Post-Weaning Weight | lbs | Higher preferred | +| YWT | Yearling Weight | lbs | Higher preferred | +| FAT | Fat Depth | mm | Moderate preferred | +| EMD | Eye Muscle Depth | mm | Higher preferred | +| NLB | Number of Lambs Born | lambs | Higher preferred | +| NWT | Number of Lambs Weaned | lambs | Higher preferred | +| PWT | Pounds Weaned | lbs | Higher preferred | +| DAG | Dag Score | score | Lower preferred | +| WGR | Wool Growth Rate | g/day | Higher preferred | +| WEC | Worm Egg Count | eggs/g | Lower preferred | +| FEC | Fecal Egg Count | eggs/g | Lower preferred | + +--- + +## See Also + +- [MCP Server Reference](../MCP.md) -- installation, configuration, resources, and prompts +- [CLI Reference](CLI.md) -- command-line interface +- [Library API Reference](LIBRARY-API.md) -- Rust library API +- [Configuration Reference](CONFIGURATION.md) -- environment and client settings diff --git a/docs/tutorials/FIRST-API-QUERY.md b/docs/tutorials/FIRST-API-QUERY.md new file mode 100644 index 0000000..cf11efd --- /dev/null +++ b/docs/tutorials/FIRST-API-QUERY.md @@ -0,0 +1,297 @@ +# Your First API Query + +> **Learning Goal:** By the end of this tutorial, you will know how to construct targeted searches using `SearchCriteria`, paginate through large result sets, sort by genetic traits, and combine multiple filters to find exactly the animals you need. + +**Time to complete:** 15 minutes +**Prerequisites:** +- Completed the [Getting Started](GETTING-STARTED.md) tutorial +- A Rust project with `nsip` and `tokio` in your dependencies + +--- + +## What You Will Build + +A Rust program that: + +1. Discovers available breeds and their IDs +2. Builds targeted search queries with multiple filters +3. Pages through results and sorts by genetic traits +4. Retrieves detailed profiles for interesting animals + +--- + +## Step 1: Discover Breeds + +Before searching for animals, you need to know which breed IDs to use. Create a new file `src/main.rs`: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let breed_groups = client.breed_groups().await?; + + for group in &breed_groups { + println!("{} (group ID: {})", group.name, group.id); + for breed in &group.breeds { + println!(" {} (breed ID: {})", breed.name, breed.id); + } + } + + Ok(()) +} +``` + +Run it and note the breed IDs you are interested in: + +```bash +cargo run +``` + +**What just happened?** Each breed group has an ID, and each breed within a group has its own ID. You will use breed IDs when searching for animals. For example, if "Poll Dorset" has breed ID 645, you would pass `Some(645)` to `search_animals`. + +--- + +## Step 2: Build a Basic Search + +Now search for animals in a specific breed. Replace `src/main.rs`: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + // Search for current female animals + let criteria = SearchCriteria::new() + .with_status("CURRENT") + .with_gender("Female"); + + let results = client + .search_animals( + 0, // first page + 10, // 10 results per page + Some(640), // breed_id -- replace with your breed + None, // no trait sorting + None, // default sort order + Some(&criteria), + ) + .await?; + + println!("Total matches: {}", results.total_count); + println!("Page: {}, page size: {}", results.page, results.page_size); + println!("Results on this page: {}\n", results.results.len()); + + for (i, animal) in results.results.iter().enumerate() { + println!(" [{}] {}", i + 1, animal); + } + + Ok(()) +} +``` + +Run it: + +```bash +cargo run +``` + +**What just happened?** + +- `SearchCriteria` uses the builder pattern. Each `.with_*` method returns a new `SearchCriteria` with the added filter, so you can chain them. +- `with_status("CURRENT")` filters to animals that are alive and actively evaluated. Other valid values: `"SOLD"`, `"DEAD"`. +- `with_gender("Female")` limits results to ewes. Valid values: `"Male"`, `"Female"`, `"Both"`. +- The first argument to `search_animals` is the page number (0-based), and the second is the page size. + +--- + +## Step 3: Add More Filters + +The `SearchCriteria` builder supports several additional filters. Try combining them: + +```rust +use std::collections::HashMap; +use nsip::{NsipClient, SearchCriteria, TraitRangeFilter}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + // Find proven rams born after 2020 + let criteria = SearchCriteria::new() + .with_status("CURRENT") + .with_gender("Male") + .with_proven_only(true) + .with_born_after("2020-01-01"); + + let results = client + .search_animals(0, 10, Some(640), None, None, Some(&criteria)) + .await?; + + println!("Proven rams born after 2020: {}\n", results.total_count); + + // Now add trait range filters + let mut trait_ranges = HashMap::new(); + trait_ranges.insert( + "BWT".to_string(), + TraitRangeFilter { min: -1.0, max: 2.0 }, + ); + + let criteria_with_traits = SearchCriteria::new() + .with_status("CURRENT") + .with_gender("Male") + .with_born_after("2020-01-01") + .with_trait_ranges(trait_ranges); + + let filtered = client + .search_animals(0, 10, Some(640), None, None, Some(&criteria_with_traits)) + .await?; + + println!("With BWT between -1.0 and 2.0: {}", filtered.total_count); + + Ok(()) +} +``` + +**What just happened?** + +- `with_proven_only(true)` limits results to animals with progeny records, meaning their EBV estimates are backed by offspring data. +- `with_born_after("2020-01-01")` filters by date of birth. There is also `with_born_before()` for the upper bound. +- `with_trait_ranges()` takes a `HashMap` to filter animals by EBV values. The example filters for animals with Birth Weight (BWT) EBV between -1.0 and 2.0 lbs. +- You can check what trait ranges are valid for a breed using `client.trait_ranges(breed_id)`. + +--- + +## Step 4: Sort Results by a Trait + +To find top-performing animals, sort the search results by a specific EBV trait: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let criteria = SearchCriteria::new() + .with_status("CURRENT") + .with_gender("Male"); + + // Sort by Weaning Weight (WWT), highest first + let top_weaners = client + .search_animals( + 0, + 5, + Some(640), + Some("WWT"), // sort by Weaning Weight + Some(true), // reverse = true means descending (highest first) + Some(&criteria), + ) + .await?; + + println!("Top 5 rams by Weaning Weight:\n"); + for animal in &top_weaners.results { + println!(" {}", animal); + } + + // Sort by Birth Weight (BWT), lowest first (lighter birth weights are preferred) + let low_bwt = client + .search_animals( + 0, + 5, + Some(640), + Some("BWT"), // sort by Birth Weight + Some(false), // reverse = false means ascending (lowest first) + Some(&criteria), + ) + .await?; + + println!("\nTop 5 rams by lowest Birth Weight:\n"); + for animal in &low_bwt.results { + println!(" {}", animal); + } + + Ok(()) +} +``` + +**What just happened?** + +- The `sorted_trait` parameter accepts a trait abbreviation like `"WWT"` (Weaning Weight), `"BWT"` (Birth Weight), or any available EBV trait for the breed. +- The `reverse` parameter controls sort direction: `Some(true)` for descending (highest first), `Some(false)` for ascending (lowest first). +- Which direction is "best" depends on the trait. For growth traits (WWT, PWWT, YWT), higher is generally better. For Birth Weight, lower is often preferred to reduce lambing difficulty. + +--- + +## Step 5: Paginate Through All Results + +When a search returns more animals than one page, use pagination to walk through the full result set: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let criteria = SearchCriteria::new().with_status("CURRENT"); + let page_size = 20; + let mut page = 0; + let mut total_fetched = 0; + + loop { + let results = client + .search_animals(page, page_size, Some(640), None, None, Some(&criteria)) + .await?; + + let count = results.results.len(); + total_fetched += count; + + println!( + "Page {}: {} results (total so far: {}/{})", + page, count, total_fetched, results.total_count + ); + + // Stop when we have fetched all results or received an empty page + if count == 0 || total_fetched as i64 >= results.total_count { + break; + } + + page += 1; + } + + println!("\nDone. Fetched {} animals total.", total_fetched); + + Ok(()) +} +``` + +**What just happened?** + +- Each call to `search_animals` returns one page of results. The `total_count` field tells you the overall number of matching animals. +- Increment the `page` parameter to fetch the next page. Pages are 0-indexed. +- The loop stops when either the returned page is empty or we have fetched all matching results. +- Be mindful of rate limits when paginating through large result sets. A page size of 20-50 is reasonable for most use cases. + +--- + +## What You Learned + +In this tutorial you: + +- Discovered breed groups and their IDs with `breed_groups()` +- Built searches with `SearchCriteria` using status, gender, date, and trait range filters +- Sorted results by EBV traits in ascending or descending order +- Paginated through large result sets + +--- + +## Next Steps + +- [Interpreting Results](INTERPRETING-RESULTS.md) -- understand what the EBV numbers mean and how to compare them +- [Understanding EBVs](../explanation/EBV-EXPLAINED.md) -- background on Estimated Breeding Values and accuracy +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) -- side-by-side trait comparisons using the CLI or library +- [Error Handling Reference](../reference/ERROR-HANDLING.md) -- handle API errors gracefully diff --git a/docs/tutorials/GETTING-STARTED.md b/docs/tutorials/GETTING-STARTED.md new file mode 100644 index 0000000..543797b --- /dev/null +++ b/docs/tutorials/GETTING-STARTED.md @@ -0,0 +1,345 @@ +# Getting Started with NSIP + +> **Learning Goal:** By the end of this tutorial, you will have a working Rust program that connects to the NSIP Search API, lists sheep breed groups, searches for animals, and retrieves detailed genetic data. + +**Time to complete:** 15 minutes +**Prerequisites:** Rust 1.92+ installed ([rustup.rs](https://rustup.rs/)) + +--- + +## What You Will Build + +A command-line Rust program that: + +1. Connects to the NSIP Search API +2. Lists available breed groups +3. Searches for animals by breed +4. Retrieves detailed genetic data for a specific animal + +--- + +## Step 1: Create a New Project + +Open a terminal and create a new Rust project: + +```bash +cargo new nsip-demo +cd nsip-demo +``` + +Add the required dependencies: + +```bash +cargo add nsip tokio --features tokio/full +``` + +Your `Cargo.toml` should now include: + +```toml +[dependencies] +nsip = "0.3" +tokio = { version = "1", features = ["full"] } +``` + +**What just happened?** You created a new Rust binary project and added the `nsip` crate (the NSIP API client) and `tokio` (an async runtime required for making HTTP requests). + +--- + +## Step 2: List Breed Groups + +Replace the contents of `src/main.rs` with: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + // Create a new client with default settings + let client = NsipClient::new(); + + // Fetch all breed groups from the NSIP database + let breed_groups = client.breed_groups().await?; + + println!("Available Breed Groups:\n"); + for group in &breed_groups { + println!(" {} (ID: {})", group.name, group.id); + for breed in &group.breeds { + println!(" - {} (ID: {})", breed.name, breed.id); + } + println!(); + } + + Ok(()) +} +``` + +Run the program: + +```bash +cargo run +``` + +You should see output similar to: + +``` +Available Breed Groups: + + USA Hair (ID: 61) + - Katahdin (ID: 640) + - Dorper (ID: 644) + - St. Croix (ID: 648) + ... +``` + +**What just happened?** + +- `NsipClient::new()` creates a client with default settings (30-second timeout, 3 retries on server errors). +- `breed_groups()` is an async method that fetches all available sheep breeds from the NSIP database. +- The API organizes breeds into groups (USA Hair, USA Terminal, USA Maternal, USA Range, etc.). Each group contains one or more breeds. +- The `?` operator propagates any errors up to `main`, which returns `Result`. + +--- + +## Step 3: Search for Animals + +Now replace `src/main.rs` with a program that searches for animals: + +```rust +use nsip::{NsipClient, SearchCriteria}; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + // Build search criteria using the builder pattern + let criteria = SearchCriteria::new() + .with_status("CURRENT") + .with_gender("Male"); + + // Search for animals in breed 640 (first page, 5 results) + let results = client + .search_animals( + 0, // page number (0-based) + 5, // results per page + Some(640), // breed_id + None, // sorted_trait (no sorting) + None, // reverse sort + Some(&criteria), + ) + .await?; + + println!("Found {} animals total\n", results.total_count); + println!("Showing page {} ({} results):\n", results.page, results.results.len()); + + for animal in &results.results { + println!(" {}", animal); + } + + Ok(()) +} +``` + +Run it: + +```bash +cargo run +``` + +**What just happened?** + +- `SearchCriteria::new()` creates an empty filter. The builder methods (`with_status`, `with_gender`) add constraints. +- `with_status("CURRENT")` limits results to active, living animals. +- `with_gender("Male")` filters to rams only. Valid values are `"Male"`, `"Female"`, and `"Both"`. +- `search_animals()` takes pagination parameters (page number and page size), an optional breed ID, optional sorting, and optional search criteria. +- The `results.results` field contains the matching animals as JSON values. The `total_count` tells you how many animals matched overall. + +--- + +## Step 4: Get Animal Details + +To fetch detailed genetic information for a specific animal, use the `animal_details` method. Replace `src/main.rs`: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + // Look up a specific animal by search string + let details = client.animal_details("400001").await?; + + println!("Animal: {}", details.lpn_id); + + if let Some(breed) = &details.breed { + println!("Breed: {}", breed); + } + if let Some(gender) = &details.gender { + println!("Gender: {}", gender); + } + if let Some(status) = &details.status { + println!("Status: {}", status); + } + if let Some(dob) = &details.date_of_birth { + println!("Date of Birth: {}", dob); + } + + // Display EBV traits + if !details.traits.is_empty() { + println!("\nEBV Traits:"); + for (abbreviation, trait_data) in &details.traits { + print!(" {} ({}) = {:.2}", + abbreviation, + trait_data.name, + trait_data.value, + ); + if let Some(acc) = trait_data.accuracy { + print!(" (accuracy: {}%)", acc); + } + if let Some(units) = &trait_data.units { + print!(" {}", units); + } + println!(); + } + } + + Ok(()) +} +``` + +Run it: + +```bash +cargo run +``` + +**What just happened?** + +- `animal_details()` fetches comprehensive data for a single animal, including breed information, status, and all EBV (Estimated Breeding Value) traits. +- The `traits` field is a `HashMap` keyed by trait abbreviation (e.g., `"BWT"` for Birth Weight, `"WWT"` for Weaning Weight). +- Each `Trait` contains the full name, numeric value, optional accuracy percentage, and optional units. +- Most fields on `AnimalDetails` are `Option` types because not all data is available for every animal. + +--- + +## Step 5: Fetch a Complete Profile + +The `search_by_lpn` method combines details, lineage, and progeny into a single `AnimalProfile`: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + + let profile = client.search_by_lpn("400001").await?; + + // Details + println!("Animal: {}", profile.details.lpn_id); + if let Some(breed) = &profile.details.breed { + println!("Breed: {}", breed); + } + + // Lineage + if let Some(sire) = &profile.lineage.sire { + println!("Sire: {}", sire.lpn_id); + } + if let Some(dam) = &profile.lineage.dam { + println!("Dam: {}", dam.lpn_id); + } + + // Progeny summary + println!("Total progeny: {}", profile.progeny.total_count); + for offspring in &profile.progeny.animals { + print!(" {} ", offspring.lpn_id); + if let Some(sex) = &offspring.sex { + print!("({})", sex); + } + println!(); + } + + Ok(()) +} +``` + +**What just happened?** + +- `search_by_lpn()` returns an `AnimalProfile` that bundles three pieces of data: `details` (an `AnimalDetails`), `lineage` (a `Lineage` with sire, dam, and multi-generational pedigree), and `progeny` (a `Progeny` with offspring list). +- Unlike `animal_details()`, which returns only the animal's own data, `search_by_lpn()` gives you the full picture in one call. +- The `lineage.generations` field contains a nested vector of ancestors organized by generation depth. + +--- + +## Step 6: Handle Errors + +NSIP API calls can fail for various reasons. Here is how to handle errors gracefully: + +```rust +use nsip::{Error, NsipClient}; + +#[tokio::main] +async fn main() { + let client = NsipClient::new(); + + match client.animal_details("INVALID_ID").await { + Ok(details) => { + println!("Found: {}", details.lpn_id); + } + Err(Error::NotFound(msg)) => { + eprintln!("Animal not found: {}", msg); + } + Err(Error::Timeout(msg)) => { + eprintln!("Request timed out: {}", msg); + } + Err(Error::Api { status, message }) => { + eprintln!("API error (HTTP {}): {}", status, message); + } + Err(Error::Connection(msg)) => { + eprintln!("Connection failed: {}", msg); + } + Err(e) => { + eprintln!("Unexpected error: {}", e); + } + } +} +``` + +**What just happened?** + +- The `nsip::Error` enum has six variants covering all failure modes: `Validation`, `Api`, `NotFound`, `Timeout`, `Connection`, and `Parse`. +- Pattern matching lets you provide specific user-facing messages for each error type. +- See the [Error Handling Reference](../reference/ERROR-HANDLING.md) for full details on each variant. + +--- + +## What You Learned + +In this tutorial you: + +- Created an `NsipClient` to connect to the NSIP Search API +- Used `breed_groups()` to discover available sheep breeds +- Built search filters with `SearchCriteria` and the builder pattern +- Retrieved individual animal details and EBV traits +- Fetched complete profiles including lineage and progeny +- Handled API errors with pattern matching + +--- + +## Next Steps + +Now that you have a working setup, continue with these tutorials: + +- [Your First API Query](FIRST-API-QUERY.md) -- a deeper dive into searching and filtering animals +- [Interpreting Results](INTERPRETING-RESULTS.md) -- understand what the genetic data means +- [MCP Server Setup](MCP-SERVER-SETUP.md) -- connect AI assistants to NSIP data + +For task-oriented instructions, see the How-To Guides: + +- [How to Configure Timeout and Retries](../how-to/CONFIGURE-CLIENT.md) +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) + +For background reading: + +- [Understanding EBVs](../explanation/EBV-EXPLAINED.md) -- what Estimated Breeding Values mean +- [Error Handling Reference](../reference/ERROR-HANDLING.md) -- complete error type documentation diff --git a/docs/tutorials/INTERPRETING-RESULTS.md b/docs/tutorials/INTERPRETING-RESULTS.md new file mode 100644 index 0000000..caa6f59 --- /dev/null +++ b/docs/tutorials/INTERPRETING-RESULTS.md @@ -0,0 +1,354 @@ +# Interpreting Results + +> **Learning Goal:** By the end of this tutorial, you will understand how to read the data returned by the NSIP API -- what EBV values mean, how to assess accuracy, how to read lineage data, and how to make sense of an animal's complete profile. + +**Time to complete:** 10 minutes +**Prerequisites:** +- Completed the [Getting Started](GETTING-STARTED.md) tutorial +- Familiarity with the basic API calls (`animal_details`, `search_by_lpn`) + +--- + +## What You Will Learn + +1. How to read EBV trait values and their units +2. What accuracy means and why it matters +3. How to interpret lineage (pedigree) data +4. How to read progeny records +5. How to put it all together with a complete animal profile + +--- + +## Step 1: Understanding EBV Traits + +Fetch an animal's details and examine its traits: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let details = client.animal_details("400001").await?; + + println!("Traits for {}:\n", details.lpn_id); + + for (abbrev, trait_data) in &details.traits { + println!(" {} ({})", abbrev, trait_data.name); + println!(" Value: {:.2}", trait_data.value); + if let Some(acc) = trait_data.accuracy { + println!(" Accuracy: {}%", acc); + } + if let Some(units) = &trait_data.units { + println!(" Units: {}", units); + } + println!(); + } + + Ok(()) +} +``` + +You might see output like: + +``` +Traits for 400001: + + BWT (Birth Weight) + Value: 0.35 + Accuracy: 72% + Units: lbs + + WWT (Weaning Weight) + Value: 4.20 + Accuracy: 65% + Units: lbs + + NLB (Number Lambs Born) + Value: 0.12 + Accuracy: 45% + Units: % +``` + +**What do these numbers mean?** + +An EBV (Estimated Breeding Value) is not the animal's own weight or measurement. It is a prediction of the **genetic merit** the animal will pass to its offspring, expressed as a deviation from the breed average. + +- **BWT = 0.35 lbs** means this animal's offspring are expected to be 0.35 lbs heavier at birth than the breed average. A small positive BWT is typical; very high BWT can indicate lambing difficulty. +- **WWT = 4.20 lbs** means offspring are expected to weigh 4.20 lbs more at weaning (60 days) than average. Higher is generally better for growth traits. +- **NLB = 0.12** means this animal's genetics predict 0.12% more lambs born per lambing than the breed average. This is a reproductive trait expressed as a percentage. + +The common EBV traits in the NSIP system are: + +**Growth traits:** + +| Abbreviation | Trait | Units | Higher means... | +|-------------|-------|-------|-----------------| +| BWT | Birth Weight | lbs | Heavier lambs at birth | +| WWT | Weaning Weight (60 days) | lbs | Faster early growth | +| MWWT | Maternal Weaning Weight | lbs | Better mothering ability | +| PWWT | Post-Weaning Weight | lbs | Faster later growth | +| YWT | Yearling Weight | lbs | Heavier at one year | + +**Carcass traits** (standardized to 55 kg / 121 lbs body weight): + +| Abbreviation | Trait | Units | Higher means... | +|-------------|-------|-------|-----------------| +| PEMD (EMD) | Eye Muscle Depth | mm | More muscle | +| PFAT (CF) | Fat Depth | mm | More fat cover | + +**Reproduction traits:** + +| Abbreviation | Trait | Units | Higher means... | +|-------------|-------|-------|-----------------| +| NLB | Number Lambs Born | % | More prolific | +| NLW | Number Lambs Weaned | % | Better lamb survival | +| SC | Scrotal Circumference | mm | Higher fertility (rams) | + +**Parasite resistance traits:** + +| Abbreviation | Trait | Units | Higher means... | +|-------------|-------|-------|-----------------| +| WFEC | Weaning Fecal Egg Count | % | More worm eggs (undesirable) | +| PFEC | Post-Weaning Fecal Egg Count | % | More parasites (undesirable) | + +For parasite traits (WFEC, PFEC), **negative** values are desirable -- they indicate genetic resistance to internal parasites. For example, a ram with a WFEC EBV of -90% would be expected to reduce worm burden in his lambs by approximately 45% (half the EBV, since each parent contributes half). + +Wool breeds may also have additional traits such as GFW (Greasy Fleece Weight), CFW (Clean Fleece Weight), FD (Fibre Diameter), SL (Staple Length), SS (Staple Strength), FDCV (Fibre Diameter CV), and CURV (Curvature). + +--- + +## Step 2: Assessing Accuracy + +The accuracy value tells you how reliable the EBV estimate is: + +``` +Accuracy: 72% -- fairly reliable +Accuracy: 45% -- use with caution +Accuracy: 90% -- very reliable +``` + +Accuracy ranges from 0 to 100. It increases as more data becomes available: + +| Accuracy Range | Meaning | Typical Source | +|---------------|---------|----------------| +| 0-29% | Low confidence | Pedigree data only | +| 30-59% | Moderate confidence | Some progeny records or genomic data | +| 60-79% | Good confidence | Multiple progeny records | +| 80-99% | High confidence | Extensive progeny and genomic data | + +**Practical guidance:** + +- For important breeding decisions (selecting sires), prefer animals with accuracy of 60% or higher on your priority traits. +- Low-accuracy EBVs can change significantly as more data comes in. Treat them as rough estimates. +- A high accuracy on a mediocre EBV is more informative than a low accuracy on an excellent EBV. + +--- + +## Step 3: Reading Lineage Data + +Lineage shows an animal's pedigree -- its parents, grandparents, and further ancestors: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let lineage = client.lineage("400001").await?; + + // The subject animal + if let Some(subject) = &lineage.subject { + println!("Subject: {} (born {})", + subject.lpn_id, + subject.date_of_birth.as_deref().unwrap_or("unknown"), + ); + } + + // Parents + if let Some(sire) = &lineage.sire { + println!("Sire (father): {}", sire.lpn_id); + if let Some(idx) = sire.us_index { + println!(" US Index: {:.2}", idx); + } + } + + if let Some(dam) = &lineage.dam { + println!("Dam (mother): {}", dam.lpn_id); + if let Some(idx) = dam.us_index { + println!(" US Index: {:.2}", idx); + } + } + + // Further generations + for (gen_num, generation) in lineage.generations.iter().enumerate() { + println!("\nGeneration {} ({} ancestors):", gen_num + 1, generation.len()); + for ancestor in generation { + print!(" {}", ancestor.lpn_id); + if let Some(sex) = &ancestor.sex { + print!(" ({})", sex); + } + if let Some(status) = &ancestor.status { + print!(" [{}]", status); + } + println!(); + } + } + + Ok(()) +} +``` + +**What to look for in lineage data:** + +- **Selection indexes** on ancestors (such as the USA MAT-HAIR Index for hair sheep or the USA Terminal Index for terminal sires) indicate their overall genetic merit. These are composite indexes that combine multiple EBV traits into a single dollar-value score. +- **Status** tells you if ancestors are still `CURRENT`, `SOLD`, or `DEAD`. Dead ancestors still contribute valuable pedigree information. +- **Depth of pedigree** -- more generations of known ancestry generally means more accurate EBV estimates for the subject animal. + +--- + +## Step 4: Reading Progeny Records + +Progeny data shows an animal's offspring and their traits: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let progeny = client.progeny("400001", 0, 10).await?; + + println!("Total offspring: {}\n", progeny.total_count); + + for offspring in &progeny.animals { + print!("{}", offspring.lpn_id); + if let Some(sex) = &offspring.sex { + print!(" ({})", sex); + } + if let Some(dob) = &offspring.date_of_birth { + print!(" born {}", dob); + } + println!(); + + // Display offspring trait values + for (trait_name, value) in &offspring.traits { + println!(" {} = {:.2}", trait_name, value); + } + println!(); + } + + Ok(()) +} +``` + +**What to look for in progeny data:** + +- **Number of offspring** -- more progeny means the parent's EBV estimates are more accurate. +- **Trait consistency** -- if most offspring show similar trait values, the parent is a reliable transmitter of those genetics. +- **Sex distribution** -- relevant for reproductive traits (NLB, NLW) which are primarily expressed in female offspring. +- Progeny data is paginated. Use the `page` and `page_size` parameters to retrieve more offspring. + +--- + +## Step 5: Putting It All Together + +The `search_by_lpn` method returns a complete `AnimalProfile` that combines details, lineage, and progeny. Here is how to evaluate an animal holistically: + +```rust +use nsip::NsipClient; + +#[tokio::main] +async fn main() -> Result<(), nsip::Error> { + let client = NsipClient::new(); + let profile = client.search_by_lpn("400001").await?; + + // 1. Basic identification + println!("=== Animal Profile ===\n"); + println!("LPN ID: {}", profile.details.lpn_id); + if let Some(breed) = &profile.details.breed { + println!("Breed: {}", breed); + } + if let Some(gender) = &profile.details.gender { + println!("Gender: {}", gender); + } + if let Some(status) = &profile.details.status { + println!("Status: {}", status); + } + + // 2. Key growth traits + println!("\n--- Growth Traits ---"); + let growth_traits = ["BWT", "WWT", "PWWT", "YWT"]; + for abbrev in &growth_traits { + if let Some(t) = profile.details.traits.get(*abbrev) { + let acc_str = t.accuracy + .map(|a| format!(" (acc: {}%)", a)) + .unwrap_or_default(); + let units_str = t.units.as_deref().unwrap_or(""); + println!(" {} = {:.2} {}{}", abbrev, t.value, units_str, acc_str); + } + } + + // 3. Reproduction traits + println!("\n--- Reproduction Traits ---"); + let repro_traits = ["NLB", "NLW"]; + for abbrev in &repro_traits { + if let Some(t) = profile.details.traits.get(*abbrev) { + let acc_str = t.accuracy + .map(|a| format!(" (acc: {}%)", a)) + .unwrap_or_default(); + println!(" {} = {:.2}{}", abbrev, t.value, acc_str); + } + } + + // 4. Parentage + println!("\n--- Parentage ---"); + if let Some(sire) = &profile.lineage.sire { + println!(" Sire: {}", sire.lpn_id); + } + if let Some(dam) = &profile.lineage.dam { + println!(" Dam: {}", dam.lpn_id); + } + + // 5. Progeny summary + println!("\n--- Progeny ---"); + println!(" Total offspring: {}", profile.progeny.total_count); + + Ok(()) +} +``` + +**How to evaluate an animal:** + +1. **Check status** -- only `CURRENT` animals are actively evaluated and available for breeding. +2. **Prioritize high-accuracy traits** -- focus on traits with accuracy above 60% for breeding decisions. +3. **Consider the breeding objective** -- terminal sire breeds (Suffolk, Hampshire, Texel) prioritize growth and carcass traits (WWT, YWT, PEMD); maternal breeds (Polypay, Finnsheep) prioritize reproduction (NLB, NLW) and lamb survival; hair sheep breeds (Katahdin, Dorper) use the USA MAT-HAIR Index which combines growth and maternal traits. +4. **Look at the pedigree** -- strong ancestors with high indexes suggest the animal's genetics are well-supported. +5. **Check progeny count** -- animals with more offspring have more reliable EBVs. + +--- + +## What You Learned + +In this tutorial you: + +- Read and interpreted EBV trait values and their units +- Understood what accuracy percentages mean and how to use them +- Explored lineage data including parent and grandparent records +- Read progeny records and trait transmission data +- Combined all data points to evaluate an animal holistically + +--- + +## Next Steps + +For deeper understanding: + +- [Understanding EBVs](../explanation/EBV-EXPLAINED.md) -- the theory behind Estimated Breeding Values +- [How to Compare Animals](../how-to/COMPARE-ANIMALS.md) -- side-by-side comparison of multiple animals +- [MCP Server Setup](MCP-SERVER-SETUP.md) -- use AI assistants to query and analyze NSIP data interactively + +For reference: + +- [Error Handling Reference](../reference/ERROR-HANDLING.md) -- handle edge cases in API responses +- [MCP Server Reference](../MCP.md) -- complete API and tool documentation diff --git a/docs/tutorials/MCP-SERVER-SETUP.md b/docs/tutorials/MCP-SERVER-SETUP.md new file mode 100644 index 0000000..1bdaaaf --- /dev/null +++ b/docs/tutorials/MCP-SERVER-SETUP.md @@ -0,0 +1,217 @@ +# Setting Up the NSIP MCP Server + +> **Learning Goal:** By the end of this tutorial, you will have the NSIP MCP server running and connected to an AI assistant (Claude Desktop or Claude Code), ready to query sheep genetic data through natural language. + +**Time to complete:** 10 minutes +**Prerequisites:** One of the following AI clients installed: +- [Claude Desktop](https://claude.ai/download) +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) + +--- + +## What You Will Build + +A working MCP (Model Context Protocol) integration that lets your AI assistant: + +1. Search the NSIP sheep genetics database +2. Look up individual animal profiles +3. Compare animals by their genetic traits +4. Access breed group and trait reference data + +--- + +## Step 1: Install the NSIP Binary + +The MCP server is built into the `nsip` command-line tool. Choose one installation method: + +**From crates.io (requires Rust 1.92+):** + +```bash +cargo install nsip +``` + +**From pre-built binaries:** + +Download the binary for your platform from [GitHub Releases](https://github.com/zircote/nsip/releases): + +| Platform | Binary | +|----------------|---------------------------| +| Linux x86_64 | `nsip-linux-amd64` | +| Linux ARM64 | `nsip-linux-arm64` | +| macOS x86_64 | `nsip-macos-amd64` | +| macOS ARM64 | `nsip-macos-arm64` | +| Windows x86_64 | `nsip-windows-amd64.exe` | + +After downloading, make it executable and move it to your PATH: + +```bash +chmod +x nsip-macos-arm64 +sudo mv nsip-macos-arm64 /usr/local/bin/nsip +``` + +**Via Docker:** + +```bash +docker pull ghcr.io/zircote/nsip +``` + +**Verify the installation:** + +```bash +nsip --version +``` + +--- + +## Step 2: Test the MCP Server Locally + +Before connecting to an AI client, verify that the MCP server starts correctly: + +```bash +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}' | nsip mcp +``` + +You should see a JSON response containing the server's capabilities (tools, resources, and prompts). Press Ctrl+C to stop. + +**What just happened?** The `nsip mcp` command starts a stdio-based MCP server. It reads JSON-RPC messages from stdin and writes responses to stdout. The `initialize` message is the first step of the MCP handshake. + +--- + +## Step 3: Configure Your AI Client + +Choose the client you want to use: + +### Option A: Claude Code + +Create a `.mcp.json` file in your project root (or at `~/.mcp.json` for global access): + +```json +{ + "mcpServers": { + "nsip": { + "command": "nsip", + "args": ["mcp"] + } + } +} +``` + +Restart Claude Code or open a new session. The NSIP tools will be available automatically. + +### Option B: Claude Desktop + +Open the Claude Desktop configuration file: + +- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` +- **Linux:** `~/.config/Claude/claude_desktop_config.json` + +Add the NSIP server to the `mcpServers` section: + +```json +{ + "mcpServers": { + "nsip": { + "command": "nsip", + "args": ["mcp"] + } + } +} +``` + +Restart Claude Desktop. You should see the NSIP tools listed in the tools panel. + +### Option C: Docker Transport + +If you installed via Docker, use this configuration instead (works with both Claude Code and Claude Desktop): + +```json +{ + "mcpServers": { + "nsip": { + "command": "docker", + "args": ["run", "--rm", "-i", "ghcr.io/zircote/nsip", "mcp"] + } + } +} +``` + +**What just happened?** The configuration tells your AI client how to launch the NSIP MCP server. When the client starts, it spawns the `nsip mcp` process and communicates with it over stdio using the MCP protocol. + +--- + +## Step 4: Verify the Connection + +In your AI client, try asking a question that uses the NSIP tools: + +``` +What breed groups are available in the NSIP database? +``` + +The AI assistant should use the `list_breed_groups` tool and return a list of breed groups with their breeds. + +Try a few more queries: + +``` +Search for current female animals in breed 640 +``` + +``` +Look up the profile for animal 400001 +``` + +``` +When was the NSIP database last updated? +``` + +**What just happened?** The AI client matched your natural language query to one of the 13 NSIP MCP tools and called it automatically. The server fetched the data from the NSIP Search API and returned it to the client for display. + +--- + +## Step 5: Explore Available Tools + +The NSIP MCP server provides 13 tools: + +| Tool | Description | +|------|-------------| +| `search_animals` | Search animals with filters (breed, gender, status, date range) | +| `animal_details` | Get detailed information for a specific animal | +| `animal_profile` | Get a complete profile (details + lineage + progeny) | +| `animal_lineage` | Get multi-generational pedigree data | +| `animal_progeny` | List an animal's offspring | +| `compare_animals` | Side-by-side trait comparison of multiple animals | +| `list_breed_groups` | List all breed groups and their breeds | +| `list_statuses` | List valid animal status values | +| `trait_ranges` | Get min/max trait values for a breed | +| `date_last_updated` | Check when the database was last updated | +| `breed_group_details` | Get details for a specific breed group | +| `trait_definitions` | Get EBV trait definitions and units | +| `flock_search` | Search for flocks by criteria | + +The server also provides 7 guided prompts that help structure common queries. Ask your AI assistant to list the available prompts for more details. + +**What just happened?** Each MCP tool maps to one or more NSIP API endpoints. The server handles parameter validation, API calls, and response formatting so the AI client receives clean, structured data. + +--- + +## What You Learned + +In this tutorial you: + +- Installed the `nsip` binary which includes the MCP server +- Tested the MCP server locally with a raw JSON-RPC message +- Configured Claude Desktop or Claude Code to use the NSIP MCP server +- Verified the connection by querying breed groups and animal data +- Explored the 13 available MCP tools + +--- + +## Next Steps + +Now that your MCP server is running: + +- [Getting Started](GETTING-STARTED.md) -- use the NSIP library directly in Rust code +- [Interpreting Results](INTERPRETING-RESULTS.md) -- understand the genetic data returned by queries +- [Understanding EBVs](../explanation/EBV-EXPLAINED.md) -- background on Estimated Breeding Values + +For the complete MCP API reference, see the [MCP Server Reference](../MCP.md).