/** * @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. * @module services/variable-cache/variable-cache-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { CensusVariable, PredicateCheck, RecordDimension, UnsetPredicate } from './types.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 two 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. Or it can exist upstream and reject the * `NAME` column every query here requests, which is `cbp` before 2012 and `nonemp` 2008 through * 2011. `yearNotAvailable` therefore says a year cannot be queried rather than that the dataset * does not publish it, which would be false for the second kind. * * 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; /** 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; /** * 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. */ export declare function describeRecordRows(dataset: string, year: number, rowsPerGeography: number, observed: Record>): 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. * Returns variables sorted by relevance (exact concept match > label match > partial). */ searchVariables(params: { query: string; dataset: string; year: number; limit: number; }, ctx: Context): Promise<{ variables: CensusVariable[]; totalMatches: number; }>; /** * Get metadata for specific variable codes. Throws if any code is not found. */ getVariablesByCode(codes: 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 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; /** Get or fetch the variable map for a dataset+year. Cached in-memory with TTL. */ private getVariables; } export declare function initVariableCacheService(): void; export declare function getVariableCacheService(): VariableCacheService; //# sourceMappingURL=variable-cache-service.d.ts.map