Metadata-Version: 2.5
Name: stac-scout
Version: 0.5.0
Summary: Evidence-backed dataset selection and verification for STAC catalogs.
Project-URL: Repository, https://github.com/GeoGeekLab/stac-scout
Project-URL: Issues, https://github.com/GeoGeekLab/stac-scout/issues
Author: GeoGeekLab
License: MIT License
        
        Copyright (c) 2026 GeoGeekLab
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: earth-observation,geospatial,gis,stac
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.12
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.8
Requires-Dist: pyproj<4,>=3.6
Requires-Dist: pystac-client<1,>=0.8
Requires-Dist: rich<15,>=13.7
Requires-Dist: shapely<3,>=2.0
Requires-Dist: typer<1,>=0.12
Requires-Dist: urllib3<3,>=2
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == 'dev'
Requires-Dist: mypy<2,>=1.11; extra == 'dev'
Requires-Dist: pytest-cov<7,>=5; extra == 'dev'
Requires-Dist: pytest<10,>=8.3; extra == 'dev'
Requires-Dist: ruff<1,>=0.6; extra == 'dev'
Requires-Dist: types-shapely; extra == 'dev'
Provides-Extra: planetary-computer
Requires-Dist: planetary-computer>=1.0.0; extra == 'planetary-computer'
Provides-Extra: raster
Requires-Dist: odc-stac<1,>=0.4; extra == 'raster'
Requires-Dist: rasterio<2,>=1.4; extra == 'raster'
Requires-Dist: xarray>=2024.7; extra == 'raster'
Description-Content-Type: text/markdown

<div align="center">

# STAC Scout

**Find the data. Prove it exists. Know what you're getting.**

`intent → task semantics → catalog federation → live item evidence → asset plan → manifest`

[![CI](https://github.com/GeoGeekLab/stac-scout/actions/workflows/ci.yml/badge.svg)](https://github.com/GeoGeekLab/stac-scout/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/Python-3.12%20%7C%203.13-3776AB?style=flat-square&logo=python&logoColor=white)](.github/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-2ea44f?style=flat-square)](LICENSE)
[![STAC](https://img.shields.io/badge/STAC-evidence--first-111111?style=flat-square)](https://stacspec.org/)

A deterministic decision layer for the STAC ecosystem.

</div>

## STAC is syntax. Dataset choice is semantics.

A model can generate a valid STAC query.

That does not mean it chose the right dataset.

A Collection can advertise the right spatial and temporal extent.

That does not mean an Item actually exists for your AOI and date.

An asset can be called `B08`.

That does not mean a caller should silently assume what it contains.

STAC Scout lives in that gap.

```text
"find imagery for this task"
            │
            ▼
      what does the task need?
            │
            ▼
      which datasets fit?
            │
            ▼
      do matching items exist?
            │
            ▼
      which assets are actually required?
            │
            ▼
      how much data will this read?
            │
            ▼
      can the decision be replayed?
```

The operating rule is simple:

> **Never infer what can be inspected.**

## What Scout actually does

```text
natural language / structured request
              │
              ▼
         IntentDraft
              │
        task_type + facts
              ▼
     ┌───────────────────┐
     │    TaskAdvisor   │◄──── versioned task rules
     └────────┬─────────┘
              │
              ▼
        ScoutRequest
              │
      ┌───────┴────────┐
      │                │
      ▼                ▼
 ScoutEngine     FederatedScout
      │                │
      │         provider isolation
      │         dataset identity
      │         duplicate evidence
      │         explicit failures
      │                │
      └───────┬────────┘
              ▼
         live Item probe
              │
        AOI coverage
              │
         asset semantics
              │
         access planning
              │
              ▼
      manifest + replay
```

Scout does **not** replace `pystac-client`, `odc-stac`, or a STAC API.

It decides what should be asked, checks what came back, and records why.

## The invariants

These are not style preferences. They are the contract.

- **Collection metadata is not availability evidence.** Item Search is.
- **`intersects` is not AOI coverage.**
- **Unknown is not false.**
- **Required and preferred measurements are different things.**
- **Task rules do not invent dates.**
- **Explicit user constraints beat defaults.**
- **Probable duplicates stay visible.**
- **Provider health is not scientific quality.**
- **A language model may classify intent; it does not own geospatial truth.**
- **Every useful decision should be reproducible.**

Or shorter:

```text
metadata ≠ evidence
similarity ≠ identity
availability ≠ suitability
confidence ≠ proof
```

## Install

```bash
python -m pip install -e ".[dev]"
```

Python 3.12 or newer is required.

For Planetary Computer access recipes:

```bash
python -m pip install -e ".[planetary-computer]"
```

## Geo Task Intelligence

![Geo Task Intelligence workflow](docs/assets/geo-task-intelligence.svg)

The user often knows the problem, not the bands.

```text
"assess wildfire impact"
        ↓
optical
        ↓
NIR + SWIR2 required
        ↓
surface reflectance preferred
        ↓
before/after strategy
        ↓
comparison_windows still required from the user
```

Scout keeps that knowledge in a versioned, inspectable registry instead of hiding it in prompts.

List the built-in task archetypes:

```bash
stac-scout tasks
stac-scout task-profile wildfire_impact
```

Derive task-aware data requirements:

```bash
stac-scout advise request.json --task wildfire_impact
```

The output includes:

```text
TaskProfile
├── rule_id
├── rationale
├── sources
├── required measurements
├── preferred measurements
├── temporal strategy
├── processing preferences
└── mask preferences

TaskAdvice
├── enriched ScoutRequest
├── derivations[]
│   ├── rule_id
│   ├── field
│   ├── value
│   ├── required | preferred
│   └── rationale
├── follow_up_requirements[]
└── notes[]
```

Task rules live in:

```text
src/stac_scout/data/tasks.toml
```

The scientific basis and rule boundaries are documented in [`docs/TASK_RULES.md`](docs/TASK_RULES.md).

### Rules are allowed to say "I don't know"

For example, a before/after wildfire task does **not** cause Scout to hallucinate a pre-fire window.

It returns:

```json
{
  "follow_up_requirements": ["comparison_windows"]
}
```

Likewise, if the user explicitly asks for SAR while a task's default modality is optical, Scout preserves SAR and skips incompatible optical band defaults.

No spectral fan fiction.

## Intent boundary

STAC Scout deliberately does not ship an LLM SDK in the core package.

Any structured-output model can produce an `IntentDraft`.

Get the contract:

```bash
stac-scout intent-contract
stac-scout schema intent
```

The model may identify a supported `task_type`.

It is explicitly told **not** to invent:

- coordinates;
- dates;
- dataset names;
- measurement names;
- hidden constraints.

Those belong to deterministic code, explicit user input, or live metadata.

Resolve an intent directly:

```bash
stac-scout resolve-intent intent.json
```

Resolve it and apply task intelligence:

```bash
stac-scout advise-intent intent.json
```

If required information is still ambiguous, the draft stays unresolved.

That is a feature.

## Providers are adapters, not assumptions

List the built-in provider registry:

```bash
stac-scout providers
stac-scout providers --all
```

Check operational health:

```bash
stac-scout health
stac-scout health --provider earth-search
```

The default enabled providers are:

- Element 84 Earth Search
- Microsoft Planetary Computer

Provider quirks stay behind adapters.

Planetary Computer signing is recorded explicitly and generated recipes use the official `planetary_computer.sign_inplace` path instead of reimplementing SAS handling.

Provider latency and uptime are reported as operational evidence only.

Network behavior is bounded: adapters use explicit connect/read timeouts, a finite transient-retry
budget, capped backoff/Retry-After delays, and typed provider errors. A reachable provider with
invalid metadata is reported as degraded rather than falsely labeled unreachable.

A slow endpoint does not make a scientifically suitable dataset worse.

## Federation without pretending everything is the same

Search a single provider:

```bash
stac-scout discover request.json --provider earth-search
```

Or a raw STAC endpoint:

```bash
stac-scout discover request.json \
  --catalog https://earth-search.aws.element84.com/v1
```

Search across enabled providers:

```bash
stac-scout federate request.json
stac-scout federate request.json --max-workers 4 --overall-timeout 30
```

Federation isolates expected provider failures such as timeouts, rate limits, authentication
errors, malformed metadata, and unsupported capabilities. Each failure reports its type, HTTP
status when known, and whether retry is reasonable. Unexpected internal exceptions are not
converted into provider failures.

Dataset identity is conservative:

```text
sci:doi
  → exact

collection id + platform / constellation / instrument evidence
  → probable

everything else
  → local to that catalog
```

Only exact groups are considered safe to collapse.

Probable matches stay visible.

Because two catalogs agreeing on a name is not the same thing as two catalogs describing the same scientific product.

## The catalog says maybe. Items say yes or no.

Verification is item-level.

```bash
stac-scout verify request.json \
  --provider earth-search \
  --collection sentinel-2-l2a
```

Scout records:

- matching Item count;
- Item IDs;
- timestamps;
- asset keys;
- cloud metadata when available;
- AOI coverage ratio;
- fraction of each Item intersected by the AOI;
- warnings when geometry or metadata cannot be evaluated.

AOI coverage normalizes antimeridian-crossing geometry and measures overlap in an AOI-centered WGS84 equal-area projection rather than treating raw longitude/latitude as a flat Cartesian plane.

Because this is geospatial software.

## Asset planning

A scientifically correct Collection can still produce a bad access plan.

Scout resolves requested measurements against declared asset and band metadata rather than guessing asset names.

```bash
stac-scout plan request.json \
  --provider earth-search \
  --collection sentinel-2-l2a \
  --manifest scout.manifest.json \
  --recipe load.py
```

Planning can produce:

```text
measurement → asset + selection evidence
resampling strategy + evidence
windowed-read estimate
explicit target output resolution
provider signing requirements
warnings
odc-stac recipe
```

`max_source_resolution_m` answers “is this source data fine enough?” while
`target_resolution_m` answers “what output grid should I request?”. It requires an explicit
`target_crs` whose units are meters (or odc-stac's explicit `"utm"` selector), because odc-stac
interprets resolution in output-CRS units. Scout never reuses the source threshold as an output
resampling instruction. The legacy input name
`max_spatial_resolution_m` is accepted as a source-resolution alias for compatibility.

If multiple assets remain equally supported after semantic, role, coverage, media-type, and GSD
evidence, Scout reports the selection as ambiguous instead of choosing the alphabetically first
key. Unknown resampling semantics likewise remain unresolved rather than defaulting to bilinear.

When `file:size` exists, Scout estimates transfer volume from the AOI/item intersection fraction.

It is an estimate, not a bandwidth prophecy.

## Manifests: because catalogs move

A successful query today is not a frozen scientific record.

Scout writes a versioned provenance manifest containing the canonical request/query,
provider/catalog identity, search limit and completeness, Collection fingerprint, full access
plan, selected-asset metadata, and the observed decision-matching Item set. Ephemeral asset URLs
are not copied into the provenance snapshot.

Replay it later:

```bash
stac-scout replay scout.manifest.json
```

By default replay reuses the manifest's recorded `max_items`. You can override it explicitly,
but Scout records that the limits differ.

Replay reports:

```text
comparison status: complete / partial / inconclusive
retained Item IDs
confirmed missing Item IDs
confirmed new Item IDs
unresolved missing Item IDs
unresolved new Item IDs
Collection metadata changed: true / false / unknown
```

A search that returns exactly `max_items` is marked `limit_reached`, not assumed complete or
definitely truncated. If either historical or current completeness cannot be proven, Scout will
not promote ordering/sample differences into confirmed drift.

The manifest is evidence of the decision.

It is not a copy of the remote data.

## CLI map

```text
stac-scout
├── version
├── validate-request
├── schema
│
├── intent-contract
├── resolve-intent
├── advise-intent
│
├── tasks
├── task-profile
├── advise
│
├── providers
├── health
├── inspect-catalog
│
├── discover
├── federate
├── verify
├── plan
└── replay
```

## Repository map

```text
stac-scout/
├── src/stac_scout/
│   ├── catalogs/          # STAC access boundary
│   ├── data/
│   │   ├── providers.toml
│   │   └── tasks.toml
│   ├── discovery/         # candidate retrieval
│   ├── models/            # strict contracts
│   ├── normalize/         # provider metadata → stable models
│   ├── planning/          # assets, raster semantics, volume
│   ├── provenance/        # manifest + replay
│   ├── verify/            # live evidence + dateline-safe coverage
│   ├── federation.py
│   ├── health.py
│   ├── identity.py
│   ├── reasoning.py
│   ├── scout.py
│   ├── tasking.py
│   └── tasks.py
├── evals/
│   ├── cases/
│   ├── federation_cases/
│   ├── task_cases/
│   ├── runner.py
│   └── live.py
├── docs/
│   ├── ARCHITECTURE.md
│   └── TASK_RULES.md
├── skill/
│   └── SKILL.md
└── tests/
```

## Evals over vibes

The deterministic evaluation corpus checks contracts that affect dataset decisions:

```bash
python evals/runner.py
```

It covers:

- request invariants;
- cross-provider identity;
- task-derived measurements;
- task modality;
- temporal strategy;
- follow-up requirements.

Remote providers are intentionally kept out of ordinary CI.

Live checks run separately:

```bash
python evals/live.py
```

That separation is deliberate:

```text
deterministic behavior
  → CI gate

remote catalog state
  → live observation
```

## Development

```bash
ruff check .
ruff format --check .
mypy
pytest --cov=stac_scout --cov-report=term-missing
python evals/runner.py
python -m build
```

CI runs on Python 3.12 and 3.13.

Coverage must stay at or above 90%.

The wheel is also checked to ensure the provider and task registries are actually packaged.

## What Scout refuses to fake

```text
"the collection covers 2024, so data must exist"
"cloud_cover=8 means my AOI is clear"
"B08 probably means NIR"
"these two collections have similar names, merge them"
"the provider is fast, therefore the dataset is better"
"wildfire task means I'll invent a pre-fire date"
"the model sounded confident"
```

Those are shortcuts.

Scout's job is to turn them into inspectable claims.

## Philosophy

```text
STAC gives us a language.

Scout adds skepticism.
```

Or, in GeoGeek form:

> **The map can look right while the geography is wrong.  
> The query can run while the dataset choice is wrong.  
> Check the semantics. Check the evidence.**

## License

[MIT](LICENSE)

---

<div align="center">

**Find it. Verify it. Plan it. Replay it.**

`catalog metadata ≠ ground truth`

</div>
