/** * @fileoverview Census variable cache service. Fetches and caches variables.json per dataset+year * with a configurable TTL, then performs client-side keyword search across label and concept * fields. Attribute columns and table universes, which variables.json does not carry, are fetched * on first lookup and cached with it. * @module services/variable-cache/variable-cache-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { WildcardColumn } from '../census-api/census-api-service.js'; import type { CensusVariable, PredicateCheck, RecordDimension, UnsetPredicate } from './types.js'; import { type VariableSearchResult } from './variable-search.js'; /** * Vintages each dataset can be queried for, and the only place they are written down — * `KNOWN_DATASETS`, `DATASET_LATEST_YEARS`, and the `available_years` census_list_datasets * advertises all derive from it, so a caller cannot be pointed at a year the query path refuses. * * The list is what a query here can answer with, which is narrower than what the Census API * hosts, for three separate reasons. A vintage can be absent upstream: `pep/charv` publishes the * 2023 vintage alone, and its 2020 through 2022 numbers are values of that vintage's own `YEAR` * dimension, so `variables.json` 404s for those paths. It can exist upstream and reject the * `NAME` column every query here requests, which is `cbp` before 2012 and `nonemp` 2008 through * 2011. Or upstream can fail on it: `acs/acs1/spp` answers HTTP 500 to `POPGROUP_LABEL` on 2008 * and to every `us` query on 2010. `yearNotAvailable` therefore says a year cannot be queried * rather than that the dataset does not publish it, which would be false for the last two kinds. * * A vintage the Census publishes that is missing here is refused before the network, so the * lists are checked against `api.census.gov/data/.json` — the per-vintage catalog — rather * than trimmed by hand. Adding a vintage the Census releases is an edit here; until it is made, * the new year fails with `year_not_available`. */ export declare const DATASET_AVAILABLE_YEARS: Record; /** Known dataset codes for validation. */ export declare const KNOWN_DATASETS: Set; /** * Resolve a caller's dataset code to the registered one, or throw `dataset_not_found`. * * The input is trimmed and lowercased — every registered code is lowercase, so each code accepted * as spelled still resolves to itself. A blank input is `fallback` when the tool's dataset is * optional and an error when it is required. A single segment resolves when it is the second * segment of exactly one two-segment code (`acs5` is `acs/acs5`, `pl` is `dec/pl`). A third segment * (`profile`, `subject`) never resolves, because it is or can become shared — acs/acs5/subject * stopped being the only `subject` when acs/acs1/subject was added — so its error names the codes * that end in it instead. */ export declare function resolveDataset(input: string | undefined, fallback?: string): string; /** Map of dataset to latest available year. */ export declare const DATASET_LATEST_YEARS: Record; /** * True for the ACS dataset family, the only family where the `E` estimate / `M` margin-of-error * suffix convention holds. ACS omits its `M` codes from variables.json but serves them from the * data API, so they are inferred; other families use `E`-final codes for unrelated fields * (`ecnbasic` `INVTOTE`, `cbp` `STATE`) that have no margin of error at all. */ export declare function isAcsDataset(dataset: string): boolean; /** * Required predicates that carry no subject-matter choice. `GEOCOMP` selects a geography's * component and defaults to the whole geography, and every Census dataset declares it — listing * it beside real filter dimensions like `NAICS2017` would bury the ones that change the answer. */ export declare const NON_FILTERING_PREDICATES: Set; /** * Map each unset dimension to the attribute column that echoes back the label of the default * the Census API applied, skipping the dimensions that publish none. Both data tools share it * so the row echo and the warning wording cannot drift apart. * * Requesting the bare predicate code instead of its attribute would flip the API from applying * one default to enumerating every category of it. */ export declare function defaultLabelColumnsFor(unset: UnsetPredicate[]): Record; /** Map each record dimension to the attribute column carrying its per-row label. */ export declare function recordLabelColumnsFor(dimensions: RecordDimension[]): Record; /** * Map each requested measure that publishes a flag column to that column. A withheld business * value holds `0`, so the flag is what the response needs to read the number at all. */ export declare function flagColumnsFor(codes: readonly string[], metadata: ReadonlyMap): Record; /** * The wildcarded dimensions to label, each with its own `_LABEL`/`_DESC` column when the dataset * publishes one. A dimension that is already a record column is labelled that way and skipped. */ export declare function wildcardColumnsFor(wildcards: readonly string[], recordColumns: Record, metadata: ReadonlyMap): WildcardColumn[]; /** * Word the warning that one geography came back on several rows. Each is a separate record the * dataset publishes, not a repeat of the same number, so reading either one as "the" answer picks * a record the query never asked for. The values observed in the response are what a caller pins * the record with. * * A dimension the caller set to `"*"` splits the rows too, but that split is the breakdown the * caller asked for, so it is worded as one row per category rather than as a choice to make. */ export declare function describeRecordRows(dataset: string, year: number, rowsPerGeography: number, observed: Record>, wildcards?: readonly string[]): string; /** * Word the failure that a comparison came back with several rows per geography. A rank is a * statement about one geography, so a ranking that lists the same one twice with two different * numbers is wrong however the rows are labelled — the fix is to pin the record, and the values * observed in the response are what the caller pins it to. */ export declare function describeAmbiguousRows(dataset: string, year: number, rowsPerGeography: number, observed: Record>): string; /** * Word the warning that a query left filter dimensions unset. Both data tools share it so the * two cannot drift into warning about the same silent default with different force. * * The default the Census API substitutes is not one shape: `cbp` defaults `NAICS2017` to the * all-industries total, while `dec/ddhca` defaults `POPGROUP` to a single population group and * `ecnbasic` defaults its NAICS dimension to one sector. Calling every default an all-category * total would be wrong, so `applied` carries the label the API echoed back per dimension and the * warning names it — that label is what separates a total from one ordinary category. * * A dimension that publishes no label attribute (`pep/charv` `YEAR`, the `nonemp` NAICS codes * before 2012) echoes nothing, so it is named as unreadable rather than left looking like a * dimension no default was applied to. */ export declare function describeUnsetPredicates(unset: UnsetPredicate[], dataset: string, year: number, applied?: Record): string; /** * Word why a predicated query came back empty, for the `no_data` recovery hint. Covers both * causes: a dimension left unset (`ecnbasic` publishes nothing below the national level until an * industry is named) and a supplied value that does not exist (an unknown `NAICS2017` value is a * `204`, not a `400`). Returns `''` when neither applies, so the caller falls back to its own * dataset-aware hint. */ export declare function describeEmptyPredicatedResult(unset: UnsetPredicate[], supplied: string[], dataset: string, year: number): string; export declare class VariableCacheService { private readonly cache; /** * Search variables by keyword across label and concept fields. `rankVariables` holds the * matching and ordering rule. */ searchVariables(params: { query: string; dataset: string; year: number; limit: number; }, ctx: Context): Promise; /** * Get metadata for specific variable codes, in the order given, with each one's table universe. * Throws `variable_not_found` naming every code the dataset has no column for, before any * further request goes out. * * A code variables.json lists only inside an entry's `attributes` is resolved from the * per-variable endpoint, which publishes its label and the column it belongs to. An uppercased * code resolves to the dataset's own spelling where that differs. */ getVariablesByCode(requested: string[], dataset: string, year: number, ctx: Context): Promise; /** * Check a caller's predicate map against the dataset's own variables.json. * * Census datasets mark some variables `required`, but the API does not enforce them: a query * that omits one succeeds and silently returns the aggregate across that whole dimension. A * `cbp` establishment count without `NAICS2017` is every industry, not the one that was asked * for. Reporting the unset dimensions is the only way a caller can tell those apart. */ checkPredicates(params: { dataset: string; year: number; supplied: string[]; }, ctx: Context): Promise; /** * Look up each code on its own, leaving out the ones the dataset's `variables.json` has no entry * for rather than failing the rest. The data API accepts columns that file lists only inside an * entry's `attributes` (`B19013_001EA`, `EMP_F`), so a miss here says nothing about whether a * query will succeed — it only means there is no label to show. */ lookupVariables(codes: readonly string[], dataset: string, year: number, ctx: Context): Promise>; /** Look up one variable, returning undefined rather than throwing when it is not defined. */ findVariable(code: string, dataset: string, year: number, ctx: Context): Promise; /** The dataset's filter dimensions — every variable it marks required that changes the answer. */ getFilterDimensions(dataset: string, year: number, ctx: Context): Promise; /** * Columns a geography's rows vary over that the dataset does not mark required — the reason * one geography can come back on more than one row. See `RecordDimension` for how they are * recognized and why the shape is read off variables.json instead of a per-dataset list. */ getRecordDimensions(dataset: string, year: number, ctx: Context): Promise; /** * Pick the measure variable to probe with when checking which of a dimension's codes the * dataset actually publishes rows for. * * A wildcard group-by answers from the dataset's published value map when every requested * column can be served from metadata, and only reads the data file when a measure is among * them — so which measure is requested decides the answer. Coverage is per table and nests: * on `dec/ddhca` the total-population table publishes 2,996 population groups where the * 23-category sex-by-age table publishes 551 of the same ones. The coarsest table therefore * gives the widest set, and cell count is what says which table is coarsest. */ findPublicationProbe(dataset: string, year: number, ctx: Context): Promise; /** * Validate that a dataset serves the requested vintage, before a query spends a round trip on * a path the Census API answers with a 404 and an HTML error page. Every data and discovery * path runs through the variable cache, so this is the one place the check has to sit. */ validateYear(dataset: string, year: number): void; /** Validate that a dataset code is known. */ validateDataset(dataset: string): void; /** The variable map for a dataset+year. */ private getVariables; /** * GET one Census metadata document and parse it, retrying transient failures. `mapError` * translates a failed fetch (unchanged by default); an HTML page or unparseable body is * `variables_unavailable`. */ private fetchMetadata; /** The metadata a failed per-variable or groups.json fetch leaves the lookup without. */ private metadataUnavailable; /** * Resolve one attribute column from the per-variable endpoint. Any failure is * `variables_unavailable`: the code was found in the dataset's own attributes index, so a miss * here is the endpoint failing rather than the code not existing. */ private fetchAttribute; /** * Universe per table, from groups.json, cached on the entry so it expires with variables.json. * A dataset that publishes no groups.json (a 404) has no universes; any other failure is * `variables_unavailable`, since leaving the field out would claim the table publishes none. */ private getUniverses; /** Get or fetch the cache entry for a dataset+year. Cached in-memory with TTL. */ private getEntry; } export declare function initVariableCacheService(): void; export declare function getVariableCacheService(): VariableCacheService; //# sourceMappingURL=variable-cache-service.d.ts.map