# Tool reference

Every description below is the text your client actually receives on `tools/list`, verbatim. It is
rendered from the running registry rather than transcribed, so this page cannot describe a tool
differently from the way the server describes it, and cannot list one the server does not register.

For which tools exist but are not served, and which are only proposed, see
[Status: served vs planned](/docs/status).

<!-- hs:gen:shapes -->
The engine ranks ONE binary outcome per entity. A `reading` tells it what shape your data is in and reduces it to one leak-safe row per entity as of a cutoff — you supply the outcome, or a reading derives it from your events; the engine never guesses which token means the outcome occurred. Free text in any column is engineered into signals on every run.

| `reading.kind` | what you hand it, and what comes back |
|---|---|
| `as-is@1` | a row per entity already — no reduction, and no cost to saying so |
| `sequential@1` | an event log → per-entity counts, tenure, top event types and transitions |
| `trace@1` | agent/job runs → each run gains its prior failure rate over STRICTLY EARLIER runs |
| `windowed@1` | a metric history → trailing-window aggregates, row count preserved |
| `panel@1` | entity × period → the outcome DERIVED by the engine, from a future window or a trend |
| `stream@1` | a sensor stream + fault intervals → snapshots with trailing aggregates |

Every kind states its own required roles (`—`, `identifier + time_axis`, `identifier + time_axis + outcome`, `identifier + time_axis + numeric_metric`, `identifier + time_axis`, `identifier + time_axis + signals` respectively) and its param bounds; `hs_describe_capabilities` publishes all of them, so a caller constructs a valid `reading` without guessing.
<!-- hs:end -->

<!-- hs:gen:tools-full -->
### hs_describe_capabilities
**Describe capabilities** — Free; no engine run.

Free; no engine run. Returns the input contract, supported problem shapes, trust guarantees (determinism, provenance, honest-empty, leak guard), limits (inline caps, k max), the reading kinds for event tables, the free sample dataset ids, and worked examples from several domains.
The engine ranks ONE binary outcome per entity. A `reading` tells it what shape your data is in and reduces it to one leak-safe row per entity as of a cutoff — you supply the outcome, or a reading derives it from your events; the engine never guesses which token means the outcome occurred. Free text in any column is engineered into signals on every run.

  as-is@1 — a row per entity already — no reduction, and no cost to saying so
  sequential@1 — an event log → per-entity counts, tenure, top event types and transitions
  trace@1 — agent/job runs → each run gains its prior failure rate over STRICTLY EARLIER runs
  windowed@1 — a metric history → trailing-window aggregates, row count preserved
  panel@1 — entity × period → the outcome DERIVED by the engine, from a future window or a trend
  stream@1 — a sensor stream + fault intervals → snapshots with trailing aggregates
USE WHEN you are unsure whether the user's problem is a yes/no ranking problem, how to shape the input, or which sample dataset to demonstrate with.
NOT FOR running anything — it reads nothing but the contract, and reports no finding.
RETURNS: contract, trust, limits, readings.kinds[], samples[], examples[], provenance. Every field describes what the engine CAN do, never what any run found.
EXAMPLE: call with no arguments; then hs_rank_topk({ data: { dataset_id: "sample:saas_churn" }, entity_column: "customer_id", outcome_column: "churned", subject_kind: "org" }).

### hs_provide_dataset
**Provide dataset** — Free; no engine run.

Free; no engine run. Register a dataset, then rank it by dataset_id. Two ways in: call with no arguments to receive { dataset_id, upload_url } and PUT the CSV yourself (curl -X PUT --data-binary @file.csv "<upload_url>"; the bytes stream straight to storage, so there is no row cap — ~1M rows is routine); or pass fetch_url and the server downloads a public https CSV.
USE WHEN the table is bigger than a small paste, or you will rank the same data more than once.
NOT FOR small tables (inline them as data.rows or data.csv in hs_rank_topk); private, internal, localhost or non-https URLs, which are refused. Note hs_rank_topk also accepts data.fetch_url directly, collapsing the two calls when you only need one run.
RETURNS: dataset_id, plus upload_url in upload mode. Runs on a dataset_id are ASYNC: hs_rank_topk returns { status: "pending", task_id } — poll hs_poll_task.
LIFETIME: a dataset_id is SINGLE-USE and short-lived. The raw data is dropped the moment its run reaches a terminal state — a ranking, an honest-empty, or a failure — so the SECOND hs_rank_topk on the same dataset_id returns unknown_dataset. That is the engine keeping no customer data it is not actively using, not an error and not an expiry. A registration that is never run expires 24 hours after it is created. To rank the same table again, register it again; you do not need to, and should not, re-register in order to ask more questions about a ranking you already have — hs_explain_drivers, hs_explain_levers, hs_model_quality and hs_context_brief all reuse the completed analysis by ranking_ref for an hour, free.
EXAMPLE: hs_provide_dataset({}) then PUT the file, then hs_rank_topk({ data: { dataset_id }, entity_column: "customer_id", outcome_column: "churned", subject_kind: "org" }).
MISTAKES: forgetting to PUT after receiving upload_url, which leaves the run with no data; tight-polling hs_poll_task instead of respecting retry_after_ms; holding a dataset_id and expecting to re-rank it later — it is consumed by its first completed run.

### hs_rank_topk
**Rank top-k by outcome likelihood** — Costs ONE RUN from quota — the only tool that does.

Costs ONE RUN from quota — the only tool that does. Rank the rows of a table by likelihood of a binary outcome and return the top-k entities with calibrated scores, an engine-authored band per entity, a model_ref for decision-time scoring, and a signed Verdict.
USE WHEN the user has tabular data (or an event log, via reading) with one column holding a historical yes/no result and asks who is most likely to convert, churn, renew, default, fail, respond, no-show, upsell, win or drop out — or which customers, accounts, leads, users, subscribers, machines, applications or tickets to prioritise, triage, target or work first. Lead scoring, propensity, risk scoring, target lists. Domain-general — business, operations, health-adjacent, education, sports, research, personal — provided the table has one binary outcome column that was historically known before the outcome occurred.
NOT FOR predicting a continuous number; data with no historical outcome column; pure time-series forecasting; causal guarantees.
COST NOTES: the run is refunded on honest-empty or error, so you are billed only for a ranking you received. The billing gate is the REALISED lift bar (>= 1.5, measured on every analysed row). A run can clear that and still return model_ref: null, because the engine caches a scorecard only when top_decile_lift clears 1.5 as well — you get the ranking you paid for, but the decision tools are unavailable for it. Sample dataset_ids are free and never touch quota.
PARAMS: exactly one of data.rows | data.csv (inline, synchronous, keep to a few hundred rows — your client's payload budget binds before the server's ~4MB / 2,000 rows / 64 columns) | data.dataset_id | data.fetch_url (async: returns { status: "pending", task_id }; poll hs_poll_task). outcome_column must be knowable BEFORE the outcome occurs — the leak guard quarantines post-outcome columns and says why. subject_kind "person" with a regulated outcome requires acknowledge_decision_support: true. reading reduces an event table to one row per entity first (kinds from hs_describe_capabilities). refit_of: a prior model_ref of the SAME analysis declares this run a refresh, so the response carries drift; an unknown, expired or foreign ref is refused before the run starts and nothing is billed. idempotency_key dedupes retries onto one run.
RETURNS: entities[] { entity_id, score, tier, band, max_autonomy }, ranking_ref (1 hour, for the free interrogation tools), model_ref (90 days, for hs_score_entity), verdict + signature, drift (only when refit_of was passed), top_decile_lift, calibration, validation { scheme, split_fraction, n_train, n_holdout, reason? }, leak_guard[], gate_verdicts, entity_replication; or a structured honest_empty { reasons[] } with no verdict and no model_ref. Never thresholds, weights, or arm names. A null statistic means no finding, NOT a pending field.
EXAMPLE: hs_rank_topk({ data: { dataset_id: "sample:saas_churn" }, entity_column: "customer_id", outcome_column: "churned", subject_kind: "org", page: { k: 20 } }).
MISTAKES: retrying an honest-empty instead of reading its reasons; reading a null statistic as zero; choosing an outcome column derived after the fact; comparing two scores for exact equality — compare bands.

### hs_poll_task
**Poll ranking task** — Free to call; the run it polls is the billable one.

Free to call; the run it polls is the billable one. Check a long-running ranking started by hs_rank_topk in an async mode (a dataset_id or fetch_url run). Returns status "pending" (poll again after the suggested interval; do not tight-loop) or the completed ranking envelope. A pending response may also carry a stage + append-only facts_so_far (leak-firewalled progress — never a partial ranking) and an optional status_url: a short-TTL signed link to a live status page a human can open to watch staged progress in real time. Common mistakes: polling in a tight loop - respect retry_after_ms; treating "pending" as failure.

### hs_explain_levers
**Explain counterfactual levers** — Free; no engine run.

Free; no engine run. For one or more entities already ranked by hs_rank_topk, compute the minimal set of feature changes - counterfactual levers - that would move each entity out of (or into) the pattern. It reuses the analysis behind the ranking_ref, so there is no new run and no extra cost.
USE WHEN the user asks what would have to change for this customer not to churn; how to intervene; what to do about this account; what would move this lead; what is driving this entity's risk; which retention play, save offer, discount or next-best-action to take; or "what if" for one named entity.
NOT FOR entities absent from a prior ranking; population-level "what drives this outcome" questions (use hs_explain_drivers); or any guarantee of real-world causal effect - these are minimal model-associated flips, not proven interventions.
RETURNS: per-entity minimal feature changes (each a feature label + a direction: increase / decrease / change), a coarse magnitude (substantial / notable / slight), and likelihood_direction (lower / higher / unchanged) - which way the change moves PREDICTED LIKELIHOOD of the outcome - plus provenance. Every lever is labeled association_not_causal. No raw scores, score deltas, thresholds or weights are returned.
likelihood_direction is a FACT, not a recommendation, and it is NOT fixed to "lower" - READ IT PER LEVER. Which way it reads follows the polarity the engine resolved for this outcome: on an ADVERSE outcome (churn, default, failure) the levers move an entity OUT of the high-likelihood pattern and read "lower"; on a DESIRABLE outcome (converted, renewed, closed) the engine returns COMPLETION levers that move an entity INTO it, and those read "higher". Assuming "lower" on a desirable outcome inverts every lever you present. Whether the direction you get is the direction you WANT depends on whether the outcome is desirable or adverse - you know which, and Hunter-Seeker does not infer it. Decide the good/bad reading yourself, or ask the user, before presenting a lever as an improvement.
EXAMPLE: hs_explain_levers({ ranking_ref, entity_ids: ["acct_1042"] }).
MISTAKES: interpreting levers as causal guarantees - present them as "what the model associates with a different outcome", especially in regulated or person-level domains; assuming the direction instead of reading likelihood_direction per lever.

### hs_explain_drivers
**Explain outcome drivers** — Free; no engine run.

Free; no engine run. For a ranking already produced by hs_rank_topk (pass its ranking_ref), return THE PATTERN the engine found: the COMBINATION of feature-conditions that, TOGETHER, predict the outcome - not independent per-feature effects. It reuses the analysis behind the ranking_ref, so there is no new run and no extra cost.
USE WHEN the user asks why - what drives churn, conversion, default or failure here; what the high-risk group has in common; what distinguishes the top decile; what converting customers share; what characterises the accounts that renew; what profile to target; what to segment on; or when you are priming context before acting, or writing a deep-dive on a dataset.
NOT FOR per-entity "what would have to change" questions (use hs_explain_levers); causal claims of any kind; ranking the conditions against each other; or reporting one condition on its own.
RETURNS: pattern.conditions[] - read as ONE joint profile - plus pattern.coverage (the exact share of entities matching the FULL pattern) and pattern.lift. Engine-authored facts passed through verbatim: never a raw arm name, a weight, or the search objective. Every condition is association_not_causal, the same firewall as levers.
A NUMERIC condition is a feature + direction (higher / lower) + the threshold where it turns (e.g. months_supply lower than 2.1), and it carries the exact predicate alongside: operator ("<=" or ">") and missing_values ("included" or "excluded"). BUILD YOUR FILTER FROM operator AND missing_values, NOT FROM THE DIRECTION WORD - the boundary is ASYMMETRIC and the English reading of "lower" is wrong. "lower" means operator "<=" with missing_values "included": it holds AT the threshold as well as below it, and also covers rows where that value is MISSING. "higher" means operator ">" with missing_values "excluded": strictly above, missing rows out. So "lower than t" names a WIDER cohort than a naive "< t" filter, and coverage counts those extra rows. This is not a rounding detail: on a dataset whose threshold lands on a common value, reading "lower than 168" as "< 168" selects ZERO rows while coverage reports 0.279 - a gate that fires on nothing while the envelope looks healthy. Both fields are absent on a categorical condition.
A CATEGORICAL condition has direction "different" (set membership has no high/low) and instead carries categories (the values involved) plus category_match, either "is_one_of" or "is_not_one_of" - take it LITERALLY and never drop the negation, because "is_not_one_of" ["annual"] is the opposite cohort from "is_one_of" ["annual"]. Both fields are absent on numeric conditions.
Describe the whole combination as a unit - "entities that are X and Y and Z together". Hunter-Seeker never phrases it for you.
EXAMPLE: hs_explain_drivers({ ranking_ref }) using the ranking_ref a cleared hs_rank_topk returned.
MISTAKES: passing a ranking_ref that has expired (cached one hour) or never cleared the bar - call hs_rank_topk first and use its ranking_ref verbatim; reading a driver as a cause; re-ordering the conditions or quoting one alone.

### hs_model_quality
**Model quality diagnostics** — Free; no engine run.

Free; no engine run. For a ranking already produced by hs_rank_topk (pass its ranking_ref), return the model DIAGNOSTICS so you can judge how much to trust it BEFORE acting on it. Returns: top_decile_lift (how concentrated the outcome is in the top-ranked group), calibration_error (ECE - lower is better-calibrated), validation (scheme: out-of-time, holdout, or none when no rows could be held back; n_holdout is null when nothing was held back; plus n_train and a plain-English reason), lift_curve (relative cumulative lift per decile), and leak_guard (columns quarantined as likely target leakage, each with a plain-English reason). These are validation statistics - never a threshold, weight, score, or arm. Best for: due diligence before acting, a governance / trust check, or a model-quality section in a report. A low top_decile_lift, a high calibration_error, or a populated leak_guard is a signal to be cautious. It reuses the analysis behind the ranking_ref - no new engine run. Common mistakes: passing an expired or never-cleared ranking_ref (call hs_rank_topk first); treating a null field as zero - it means there was no finding.

### hs_context_brief
**Portable context brief** — Free; no engine run.

Free; no engine run. For a ranking already produced by hs_rank_topk (pass its ranking_ref), return a PORTABLE BRIEF you can drop straight into your own agent's context - the whole analysis as one compact artifact instead of three separate calls. Returns: provenance (engine version, core hash, the ranking_ref, the analysis identity, and when the analysis was computed), outcome.label, trust (top-decile lift, calibration error, validation scheme, and any columns the leak guard quarantined), driver_group (the drivers the engine found, each with a direction, to be read as ONE combination), and limits. Best for: handing an analysis to another agent, filing an analysis in your own store so you can recognise the same analysis later, or building a domain expert on top of Hunter-Seeker - we supply the governed prediction, you supply the domain. Set format to "markdown" for prose instead of JSON; both carry identical numbers. It reuses the analysis behind the ranking_ref, so it costs nothing and can be called as often as you like - only hs_rank_topk consumes a run. Drivers are ASSOCIATIONS, not causes, and are only meaningful together: never re-order them, never rank one above another, never report one on its own. Common mistakes: passing a ranking_ref older than an hour (the analysis is cached for one hour, then you must re-run hs_rank_topk); treating a null statistic as zero - null means the engine did not report it; and inventing drivers when the brief returns an empty driver group, which is a real result and not a gap. Never restate a number this brief does not contain, and never compute change over time by comparing two briefs - that is you authoring a direction the engine never gave. If change over time matters, re-run.

### hs_score_entity
**Score one entity at decision time** — Costs ONE DECISION from your quota (never a run).

Costs ONE DECISION from your quota (never a run). Score ONE row against a scorecard the engine already fitted — use the model_ref that a cleared hs_rank_topk returned. Returns the calibrated score (0–1), an engine-authored band (act = certified likely; escalate = uncertain, ask a human; refuse = the engine will not vouch, with a reason), a max_autonomy hint (L0–L4: how much rope THIS decision earns), up to four principal_reasons (engine-ordered levers in the shape adverse-action rules require), and a SIGNED Verdict you can hand to a reviewer or verify with hs_verify_verdict. The score is the one the originating run assigned this row, from the same fitted scorecard — a cache of a deterministic fit, not a second model. It can differ in the last bit of the float (measured: 7 of 12 entities identical, the rest by one ULP, never more) because the run scores the whole table at once while this scores one row; the band, the reasons and the ordering are unaffected. Do not compare two scores for exact equality — compare bands. Best for: 'should my agent act on this lead / account / ticket right now'. Not for: rows from a different table than the model_ref was fitted on (columns must match). Firewall: no thresholds, weights, or condition counts are returned — read band and principal_reasons, never try to reconstruct the rule. subject_kind 'person' REQUIRES acknowledge_decision_support: true and caps max_autonomy at L2 (L0 without a human principal). A refuse band bills nothing. Common mistakes: reusing an expired model_ref (404 model_ref_expired → re-run hs_rank_topk, do not retry); treating band as advice — it is a fact about likelihood; on an adverse outcome 'act' means likely to churn/default/fail, on a desirable one likely to convert/renew — read verdict.outcome.polarity.

### hs_score_batch
**Score many rows against one scorecard** — Costs ONE DECISION PER ROW that is not refused.

Costs ONE DECISION PER ROW that is not refused. Score up to 10,000 rows against a model_ref in one call — for backfills, nightly re-scoring, or replaying a historical cohort. Same arithmetic, same firewall, same band semantics as hs_score_entity; returns one signed Verdict covering every entity. Common mistakes: sending rows with different columns than the fitted table; expecting per-row signatures — there is one signature over the whole Verdict.

### hs_verify_verdict
**Verify a Verdict's signature** — Free; no engine run.

Free; no engine run. Check that a Verdict was issued by Hunter-Seeker and has not been altered. Verification is credential-free on the REST endpoint POST https://hunter-seeker.io/api/v1/verify-verdict and in the published hs-verify / @hunter-seeker/verify libraries, which check a signature against the public JWKS with no server at all. Over MCP it still needs a credential like every other tool, because MCP authenticates the whole connection rather than each call — so hand a reviewer the REST endpoint or the library, not an MCP session.  Pass the verdict object and its signature exactly as received. Returns status: valid | invalid_signature | expired | unknown_key — and nothing else, so the endpoint cannot be probed. Best for: an approval step that must prove the score was not authored by the agent; an auditor; another vendor's agent. The same check runs offline with the open-source libraries hs-verify (pip) and @hunter-seeker/verify (npm) against the public keys at hunter-seeker.io/.well-known/jwks.json. Common mistakes: re-serialising the verdict before verifying (pass the object, the verifier canonicalises it — RFC 8785); treating 'expired' as valid — an expired verdict is re-scored, never reused.

### hs_report_outcome
**Report observed outcomes** — Free; writes YOUR observed outcomes (never fires an action, never retrains).

Free; writes YOUR observed outcomes (never fires an action, never retrains). For entities scored under a model_ref, append what actually happened: outcome is a binary (0/1, true/false, yes/no); observed_at is when you observed it; event_id is your idempotency key (send the same id on retry and the row is written once). Outcomes are EVIDENCE for hs_action_evidence and drift signals for hs_drift_status; they never change the scorecard, the band, or the pattern — a refit is an explicit hs_rank_topk with refit_of. Free text is refused (422 outcome_malformed) so this ledger cannot become a transcript store. Best for: closing the loop after acting, so the engine can tell you whether acting worked. Common mistakes: inferring the outcome from the trace or the agent's own judgement — report the real-world binary you observed; sending prose.

### hs_attest_action
**Attest that a lever was pulled** — Free; records that you acted on a lever.

Free; records that you acted on a lever. Pass the lever_token from hs_explain_levers and the entity's NEW value for that feature after your action. The engine evaluates compliance inside its boundary — was the target crossed, and how far did the entity move (dose_fraction 0–1) — and returns only {compliant, dose_fraction}; the threshold never crosses the wire. The attestation is what lets hs_action_evidence compare acted vs not-acted entities within the same pattern. Idempotent by event_id. Common mistakes: attesting before the change actually happened; passing the OLD value.

### hs_action_evidence
**Did acting on this pattern work?** — Free; no engine run.

Free; no engine run. For a model_ref, returns the live evidence block (from outcomes you reported, comparing entities with a compliant attestation against those without, within the same pattern). It carries rates, n, the difference, and a Newcombe hybrid-score confidence interval. live is NULL until each cell has at least 30 rows, and small_n is true below 100 per cell — a null here means 'not enough evidence yet', not zero effect. Best for: deciding whether to keep acting on a pattern; a governance report. Common mistakes: treating small_n evidence as settled; reading a null live block as zero effect rather than as not enough evidence yet.

### hs_drift_status
**Has the pattern moved?** — Free; no engine run.

Free; no engine run. For a model_ref that has been refit at least once (hs_rank_topk with refit_of), compare the current pattern with the prior: pattern is unchanged | changed | no_prior; pattern_diff names conditions added or removed and thresholds that moved (feature + direction only — values never leave the engine); cusum_fired says whether the calibration coefficient drifted; recommendation is keep | refit | abandon. One honest-empty on refresh is NOT drift — abandon requires two consecutive honest-empties on the same analysis. Never infer drift by comparing two briefs yourself; this tool is the engine's comparison. Common mistakes: refitting on 'changed' alone — refit is recommended only when the coefficient also drifted or across ≥ 2 cycles.
<!-- hs:end -->
USE WHEN you are unsure whether the user's problem is a yes/no ranking problem, how to shape the input, or which sample dataset to demonstrate with.
NOT FOR running anything — it reads nothing but the contract, and reports no finding.
RETURNS: contract, trust, limits, readings.kinds[], samples[], examples[], provenance. Every field describes what the engine CAN do, never what any run found.
EXAMPLE: call with no arguments; then hs_rank_topk({ data: { dataset_id: "sample:saas_churn" }, entity_column: "customer_id", outcome_column: "churned", subject_kind: "org" }).

### hs_provide_dataset
**Provide dataset** — Free; no engine run.

Free; no engine run. Register a dataset, then rank it by dataset_id. Two ways in: call with no arguments to receive { dataset_id, upload_url } and PUT the CSV yourself (curl -X PUT --data-binary @file.csv "<upload_url>"; the bytes stream straight to storage, so there is no row cap — ~1M rows is routine); or pass fetch_url and the server downloads a public https CSV.
USE WHEN the table is bigger than a small paste, or you will rank the same data more than once.
NOT FOR small tables (inline them as data.rows or data.csv in hs_rank_topk); private, internal, localhost or non-https URLs, which are refused. Note hs_rank_topk also accepts data.fetch_url directly, collapsing the two calls when you only need one run.
RETURNS: dataset_id, plus upload_url in upload mode. Runs on a dataset_id are ASYNC: hs_rank_topk returns { status: "pending", task_id } — poll hs_poll_task.
LIFETIME: a dataset_id is SINGLE-USE and short-lived. The raw data is dropped the moment its run reaches a terminal state — a ranking, an honest-empty, or a failure — so the SECOND hs_rank_topk on the same dataset_id returns unknown_dataset. That is the engine keeping no customer data it is not actively using, not an error and not an expiry. A registration that is never run expires 24 hours after it is created. To rank the same table again, register it again; you do not need to, and should not, re-register in order to ask more questions about a ranking you already have — hs_explain_drivers, hs_explain_levers, hs_model_quality and hs_context_brief all reuse the completed analysis by ranking_ref for an hour, free.
EXAMPLE: hs_provide_dataset({}) then PUT the file, then hs_rank_topk({ data: { dataset_id }, entity_column: "customer_id", outcome_column: "churned", subject_kind: "org" }).
MISTAKES: forgetting to PUT after receiving upload_url, which leaves the run with no data; tight-polling hs_poll_task instead of respecting retry_after_ms; holding a dataset_id and expecting to re-rank it later — it is consumed by its first completed run.

### hs_rank_topk
**Rank top-k by outcome likelihood** — Costs ONE RUN from quota — the only tool that does.

Costs ONE RUN from quota — the only tool that does. Rank the rows of a table by likelihood of a binary outcome and return the top-k entities with calibrated scores, an engine-authored band per entity, a model_ref for decision-time scoring, and a signed Verdict.
USE WHEN the user has tabular data (or an event log, via reading) with one column holding a historical yes/no result and asks who is most likely to convert, churn, fail, default, respond, or succeed, or who to prioritise. Any domain — business, operations, health-adjacent, education, sports, research, personal.
NOT FOR predicting a continuous number; data with no historical outcome column; pure time-series forecasting; causal guarantees.
COST NOTES: the run is refunded on honest-empty or error, so you are billed only for a ranking you received. The billing gate is the REALISED lift bar (>= 1.5, measured on every analysed row). A run can clear that and still return model_ref: null, because the engine caches a scorecard only when top_decile_lift clears 1.5 as well — you get the ranking you paid for, but the decision tools are unavailable for it. Sample dataset_ids are free and never touch quota.
PARAMS: exactly one of data.rows | data.csv (inline, synchronous, keep to a few hundred rows — your client's payload budget binds before the server's ~4MB / 2,000 rows / 64 columns) | data.dataset_id | data.fetch_url (async: returns { status: "pending", task_id }; poll hs_poll_task). outcome_column must be knowable BEFORE the outcome occurs — the leak guard quarantines post-outcome columns and says why. subject_kind "person" with a regulated outcome requires acknowledge_decision_support: true. reading reduces an event table to one row per entity first (kinds from hs_describe_capabilities). refit_of: a prior model_ref of the SAME analysis declares this run a refresh, so the response carries drift; an unknown, expired or foreign ref is refused before the run starts and nothing is billed. idempotency_key dedupes retries onto one run.
RETURNS: entities[] { entity_id, score, tier, band, max_autonomy }, ranking_ref (1 hour, for the free interrogation tools), model_ref (90 days, for hs_score_entity), verdict + signature, drift (only when refit_of was passed), top_decile_lift, calibration, validation { scheme, split_fraction, n_train, n_holdout, reason? }, leak_guard[], gate_verdicts, entity_replication; or a structured honest_empty { reasons[] } with no verdict and no model_ref. Never thresholds, weights, or arm names. A null statistic means no finding, NOT a pending field.
EXAMPLE: hs_rank_topk({ data: { dataset_id: "sample:saas_churn" }, entity_column: "customer_id", outcome_column: "churned", subject_kind: "org", page: { k: 20 } }).
MISTAKES: retrying an honest-empty instead of reading its reasons; reading a null statistic as zero; choosing an outcome column derived after the fact; comparing two scores for exact equality — compare bands.

### hs_poll_task
**Poll ranking task** — Free to call; the run it polls is the billable one.

Free to call; the run it polls is the billable one. Check a long-running ranking started by hs_rank_topk in an async mode (a dataset_id or fetch_url run). Returns status "pending" (poll again after the suggested interval; do not tight-loop) or the completed ranking envelope. A pending response may also carry a stage + append-only facts_so_far (leak-firewalled progress — never a partial ranking) and an optional status_url: a short-TTL signed link to a live status page a human can open to watch staged progress in real time. Common mistakes: polling in a tight loop - respect retry_after_ms; treating "pending" as failure.

### hs_explain_levers
**Explain counterfactual levers** — Free; no engine run.

Free; no engine run. For one or more entities already ranked by hs_rank_topk, compute the minimal set of feature changes (counterfactual levers) that would move the entity out of the high-risk / high-likelihood pattern. Best for: "what would have to change for this customer not to churn", "what's driving this risk", "how do I intervene". Not recommended for: entities not present in a prior ranking; guarantees of real-world causal effect (these are minimal model-based flips, not proven interventions). Returns: per-entity minimal feature changes (each a feature label + a direction: increase / decrease / change), a coarse magnitude (substantial / notable / slight), and likelihood_direction (lower / higher / unchanged) - which way the change moves PREDICTED LIKELIHOOD of the outcome - plus provenance; every lever is labeled association_not_causal. likelihood_direction is a FACT, not a recommendation, and it is NOT fixed to "lower" - READ IT PER LEVER. Which way it reads follows the polarity the engine resolved for this outcome: on an ADVERSE outcome (churn, default, failure) the levers move an entity OUT of the high-likelihood pattern and read "lower"; on a DESIRABLE outcome (converted, renewed, closed) the engine returns COMPLETION levers that move an entity INTO it, and those read "higher". Assuming "lower" on a desirable outcome inverts every lever you present. Whether the direction you get is the direction you want depends on whether the outcome is desirable (converted, renewed, closed) or adverse (churn, default, failure) - you know which, and Hunter-Seeker does not infer it. Decide the good/bad reading yourself, or ask the user, before presenting a lever as an improvement. No raw scores, score deltas, thresholds, or weights are returned - these are coarse, model-associated flips, not causal guarantees. Common mistakes: interpreting levers as causal guarantees - present them as "what the model associates with a different outcome", especially in regulated or person-level domains.

### hs_explain_drivers
**Explain outcome drivers** — Free; no engine run.

Free; no engine run. For a ranking already produced by hs_rank_topk (pass its ranking_ref), return THE PATTERN the engine found - the core of what Hunter-Seeker does: it discovers a COMBINATION of feature-conditions that, TOGETHER, predict the outcome (not independent per-feature effects). Returns pattern.conditions[] - read as ONE joint profile - plus pattern.coverage (exact share of entities matching the full pattern) and pattern.lift. A NUMERIC condition is a feature + direction (higher / lower) + the threshold where it turns (e.g. months_supply lower than 2.1), and it carries the exact predicate alongside: operator ("<=" or ">") and missing_values ("included" or "excluded"). BUILD YOUR FILTER FROM operator AND missing_values, NOT FROM THE DIRECTION WORD - the boundary is ASYMMETRIC and the English reading of "lower" is wrong. "lower" means operator "<=" with missing_values "included": it holds AT the threshold as well as below it, and also covers rows where that value is MISSING. "higher" means operator ">" with missing_values "excluded": strictly above, missing rows out. So "lower than t" names a WIDER cohort than a naive "< t" filter, and coverage counts those extra rows. This is not a rounding detail: on a dataset whose threshold lands on a common value, reading "lower than 168" as "< 168" selects ZERO rows while coverage reports 0.279 - a gate that fires on nothing while the envelope looks healthy. Both fields are absent on a categorical condition, whose cohort is defined by the set below instead. A CATEGORICAL condition has direction "different" (set membership has no high/low) and instead carries categories (the values involved) plus category_match, either "is_one_of" or "is_not_one_of" - take it LITERALLY and never drop the negation, because "is_not_one_of" ["annual"] is the opposite cohort from "is_one_of" ["annual"]. Both fields are absent on numeric conditions. This is what takes your agent from generalist to expert on THIS dataset - the structured pattern for YOU to phrase (describe the whole combination as a unit, "entities that are X and Y and Z together"); Hunter-Seeker never phrases it for you. Best for: "what pattern actually drives churn / conversion / failure here", priming context before you act, or a deep-dive report on a dataset. Not recommended for: per-entity "what would change" questions (use hs_explain_levers). Returns: pattern.conditions[] (feature + direction + threshold, plus categories + category_match on categorical conditions) + coverage + lift - engine-authored facts passed through verbatim, never a raw arm name, weight, or the search objective (association_not_causal, same firewall as levers). It reuses the analysis behind the ranking_ref - no new engine run, no extra cost. Common mistakes: passing a ranking_ref that has expired or never cleared the bar (call hs_rank_topk first and use its ranking_ref verbatim); reading a driver as a causal guarantee - it is what the model associates with the outcome.

### hs_model_quality
**Model quality diagnostics** — Free; no engine run.

Free; no engine run. For a ranking already produced by hs_rank_topk (pass its ranking_ref), return the model DIAGNOSTICS so you can judge how much to trust it BEFORE acting on it. Returns: top_decile_lift (how concentrated the outcome is in the top-ranked group), calibration_error (ECE - lower is better-calibrated), validation (scheme: out-of-time, holdout, or none when no rows could be held back; n_holdout is null when nothing was held back; plus n_train and a plain-English reason), lift_curve (relative cumulative lift per decile), and leak_guard (columns quarantined as likely target leakage, each with a plain-English reason). These are validation statistics - never a threshold, weight, score, or arm. Best for: due diligence before acting, a governance / trust check, or a model-quality section in a report. A low top_decile_lift, a high calibration_error, or a populated leak_guard is a signal to be cautious. It reuses the analysis behind the ranking_ref - no new engine run. Common mistakes: passing an expired or never-cleared ranking_ref (call hs_rank_topk first); treating a null field as zero - it means there was no finding.

### hs_context_brief
**Portable context brief** — Free; no engine run.

Free; no engine run. For a ranking already produced by hs_rank_topk (pass its ranking_ref), return a PORTABLE BRIEF you can drop straight into your own agent's context - the whole analysis as one compact artifact instead of three separate calls. Returns: provenance (engine version, core hash, the ranking_ref, the analysis identity, and when the analysis was computed), outcome.label, trust (top-decile lift, calibration error, validation scheme, and any columns the leak guard quarantined), driver_group (the drivers the engine found, each with a direction, to be read as ONE combination), and limits. Best for: handing an analysis to another agent, filing an analysis in your own store so you can recognise the same analysis later, or building a domain expert on top of Hunter-Seeker - we supply the governed prediction, you supply the domain. Set format to "markdown" for prose instead of JSON; both carry identical numbers. It reuses the analysis behind the ranking_ref, so it costs nothing and can be called as often as you like - only hs_rank_topk consumes a run. Drivers are ASSOCIATIONS, not causes, and are only meaningful together: never re-order them, never rank one above another, never report one on its own. Common mistakes: passing a ranking_ref older than an hour (the analysis is cached for one hour, then you must re-run hs_rank_topk); treating a null statistic as zero - null means the engine did not report it; and inventing drivers when the brief returns an empty driver group, which is a real result and not a gap. Never restate a number this brief does not contain, and never compute change over time by comparing two briefs - that is you authoring a direction the engine never gave. If change over time matters, re-run.

### hs_score_entity
**Score one entity at decision time** — Costs ONE DECISION from your quota (never a run).

Costs ONE DECISION from your quota (never a run). Score ONE row against a scorecard the engine already fitted — use the model_ref that a cleared hs_rank_topk returned. Returns the calibrated score (0–1), an engine-authored band (act = certified likely; escalate = uncertain, ask a human; refuse = the engine will not vouch, with a reason), a max_autonomy hint (L0–L4: how much rope THIS decision earns), up to four principal_reasons (engine-ordered levers in the shape adverse-action rules require), and a SIGNED Verdict you can hand to a reviewer or verify with hs_verify_verdict. The score is the one the originating run assigned this row, from the same fitted scorecard — a cache of a deterministic fit, not a second model. It can differ in the last bit of the float (measured: 7 of 12 entities identical, the rest by one ULP, never more) because the run scores the whole table at once while this scores one row; the band, the reasons and the ordering are unaffected. Do not compare two scores for exact equality — compare bands. Best for: 'should my agent act on this lead / account / ticket right now'. Not for: rows from a different table than the model_ref was fitted on (columns must match). Firewall: no thresholds, weights, or condition counts are returned — read band and principal_reasons, never try to reconstruct the rule. subject_kind 'person' REQUIRES acknowledge_decision_support: true and caps max_autonomy at L2 (L0 without a human principal). A refuse band bills nothing. Common mistakes: reusing an expired model_ref (404 model_ref_expired → re-run hs_rank_topk, do not retry); treating band as advice — it is a fact about likelihood; on an adverse outcome 'act' means likely to churn/default/fail, on a desirable one likely to convert/renew — read verdict.outcome.polarity.

### hs_score_batch
**Score many rows against one scorecard** — Costs ONE DECISION PER ROW that is not refused.

Costs ONE DECISION PER ROW that is not refused. Score up to 10,000 rows against a model_ref in one call — for backfills, nightly re-scoring, or replaying a historical cohort. Same arithmetic, same firewall, same band semantics as hs_score_entity; returns one signed Verdict covering every entity. Common mistakes: sending rows with different columns than the fitted table; expecting per-row signatures — there is one signature over the whole Verdict.

### hs_verify_verdict
**Verify a Verdict's signature** — Free; no engine run.

Free; no engine run. Check that a Verdict was issued by Hunter-Seeker and has not been altered. Verification is credential-free on the REST endpoint POST /v1/verify-verdict and in the published hs-verify / @hunter-seeker/verify libraries, which check a signature against the public JWKS with no server at all. Over MCP it still needs a credential like every other tool, because MCP authenticates the whole connection rather than each call — so hand a reviewer the REST endpoint or the library, not an MCP session.  Pass the verdict object and its signature exactly as received. Returns status: valid | invalid_signature | expired | unknown_key — and nothing else, so the endpoint cannot be probed. Best for: an approval step that must prove the score was not authored by the agent; an auditor; another vendor's agent. The same check runs offline with the open-source libraries hs-verify (pip) and @hunter-seeker/verify (npm) against the public keys at hunter-seeker.io/.well-known/jwks.json. Common mistakes: re-serialising the verdict before verifying (pass the object, the verifier canonicalises it — RFC 8785); treating 'expired' as valid — an expired verdict is re-scored, never reused.

### hs_report_outcome
**Report observed outcomes** — Free; writes YOUR observed outcomes (never fires an action, never retrains).

Free; writes YOUR observed outcomes (never fires an action, never retrains). For entities scored under a model_ref, append what actually happened: outcome is a binary (0/1, true/false, yes/no); observed_at is when you observed it; event_id is your idempotency key (send the same id on retry and the row is written once). Outcomes are EVIDENCE for hs_action_evidence and drift signals for hs_drift_status; they never change the scorecard, the band, or the pattern — a refit is an explicit hs_rank_topk with refit_of. Free text is refused (422 outcome_malformed) so this ledger cannot become a transcript store. Best for: closing the loop after acting, so the engine can tell you whether acting worked. Common mistakes: inferring the outcome from the trace or the agent's own judgement — report the real-world binary you observed; sending prose.

### hs_attest_action
**Attest that a lever was pulled** — Free; records that you acted on a lever.

Free; records that you acted on a lever. Pass the lever_token from hs_explain_levers and the entity's NEW value for that feature after your action. The engine evaluates compliance inside its boundary — was the target crossed, and how far did the entity move (dose_fraction 0–1) — and returns only {compliant, dose_fraction}; the threshold never crosses the wire. The attestation is what lets hs_action_evidence compare acted vs not-acted entities within the same pattern. Idempotent by event_id. Common mistakes: attesting before the change actually happened; passing the OLD value.

### hs_action_evidence
**Did acting on this pattern work?** — Free; no engine run.

Free; no engine run. For a model_ref, returns the live evidence block (from outcomes you reported, comparing entities with a compliant attestation against those without, within the same pattern). It carries rates, n, the difference, and a Newcombe hybrid-score confidence interval. live is NULL until each cell has at least 30 rows, and small_n is true below 100 per cell — a null here means 'not enough evidence yet', not zero effect. Best for: deciding whether to keep acting on a pattern; a governance report. Common mistakes: treating small_n evidence as settled; reading a null live block as zero effect rather than as not enough evidence yet.

### hs_drift_status
**Has the pattern moved?** — Free; no engine run.

Free; no engine run. For a model_ref that has been refit at least once (hs_rank_topk with refit_of), compare the current pattern with the prior: pattern is unchanged | changed | no_prior; pattern_diff names conditions added or removed and thresholds that moved (feature + direction only — values never leave the engine); cusum_fired says whether the calibration coefficient drifted; recommendation is keep | refit | abandon. One honest-empty on refresh is NOT drift — abandon requires two consecutive honest-empties on the same analysis. Never infer drift by comparing two briefs yourself; this tool is the engine's comparison. Common mistakes: refitting on 'changed' alone — refit is recommended only when the coefficient also drifted or across ≥ 2 cycles.
<!-- hs:end -->

---

## Provenance

<!-- hs:gen:pin -->
- **Engine pin.** Every response carries `{engine_version, core_hash}`. Current: engine `0.3.5`, core `4d24e9fc…`.
- **Reading contract.** A SEPARATE identity axis from the engine pin — it moves when the reading kinds change, which `core_hash` cannot see, so pin whichever you actually depend on. Current: `40498adb…`, grammar version 3.
<!-- hs:end -->

---

*See also:* [The two loops](/docs/two-loops) · [Refusal is a result](/docs/refusal-is-a-result) ·
[llms-full.txt](/llms-full.txt)
