/** * Suite filter — normalize tags and match evals against suite filters. * * A filter has an include side and an exclude side. Include semantics: * - AND across keys: every include key must match. * - OR within values: at least one value for each include key must match. * - Empty include matches all evals. * - Missing tag key on an eval means no match for that key. * * Exclude semantics reject rather than select: * - An eval is dropped if any exclude key is present on it with one of the * excluded values (OR within values, OR across keys). * - A missing exclude key never drops an eval. * - Exclude takes precedence: an eval matching any exclude condition is dropped * even when it also satisfies an include on a different key or value. */ import picomatch from "picomatch"; import type { RawStimulus } from "./types.js"; /** * A parsed tag filter with an include side (positive selection) and an exclude * side (negation). Both map a tag key to the values that select or reject. */ export interface SuiteFilter { include: Record; exclude: Record; } /** Build an empty {@link SuiteFilter} (matches everything). */ export declare function emptySuiteFilter(): SuiteFilter; /** Wrap an include-only tag map (e.g. from suite config) as a {@link SuiteFilter}. */ export declare function includeOnlyFilter(include: Record): SuiteFilter; /** True when the filter selects everything (no include and no exclude constraints). */ export declare function isSuiteFilterEmpty(filter: SuiteFilter): boolean; /** * Accepted input for the filter APIs: either a full {@link SuiteFilter} or the * legacy include-only tag map (`Record`) that these APIs took * before the exclude side existed. {@link toSuiteFilter} normalizes either into * a {@link SuiteFilter}, so existing callers passing an include map keep working. */ export type SuiteFilterInput = SuiteFilter | Record; /** * Normalize a {@link SuiteFilterInput} into a {@link SuiteFilter}. * * A {@link SuiteFilter}'s `include` is a `Record` (a non-array object); a legacy * include map's values are `string[]` (arrays), and it has no `include` key. * That distinction is used to tell the two shapes apart. */ export declare function toSuiteFilter(input: SuiteFilterInput): SuiteFilter; /** Human-readable one-line description of a filter, for diagnostics. */ export declare function describeSuiteFilter(filter: SuiteFilter): string; /** * Normalize tags from the eval.yaml/suite filter format where values * can be a bare string or string[] into a consistent Record. */ export declare function normalizeTags(tags: Record): Record; /** * Check whether an eval's tags satisfy a suite filter. * * - AND across include keys: every key in `include` must be present in `tags`. * - OR within values: for each include key, at least one value must appear * in the eval's tag values. * - Empty include (`{}`) matches all evals. * - If an include key is not present in `tags`, the eval does not match. * - Exclude rejects: if any `exclude` key is present in `tags` with one of the * excluded values, the eval does not match — even if it satisfied `include`. */ export declare function matchesSuiteFilter(tags: Record, include: Record, exclude?: Record): boolean; /** * Compute effective tags for a stimulus by merging eval tags with stimulus tags. * Stimulus tags override eval tags on the same key (child-wins). */ export declare function resolveEffectiveTags(evalTags: Record | undefined, stimulusTags: Record | undefined): Record; /** * Filter stimuli by a suite/tag filter. Returns matched stimuli and skip info. * * Accepts either a {@link SuiteFilter} or the legacy include-only tag map for * backward compatibility; the input is normalized via {@link toSuiteFilter}. */ export declare function filterStimuli(stimuli: RawStimulus[], evalTags: Record | undefined, filter: SuiteFilterInput): { matched: RawStimulus[]; skipped: string[]; total: number; }; /** * Parse `--tag` CLI flags into an include-only tag map. * * Each entry is `key=value[,value...]`. Values are comma-separated and * OR-combined; repeated keys merge. This is the original include-only parser; * it does not interpret the `key!=value` exclusion form (a leading `!` is * treated as part of the key). Use {@link parseSuiteFilter} for include and * exclude parsing. */ export declare function parseTagFilters(raw: string[]): Record; /** * Parse `--tag` CLI flags into a {@link SuiteFilter}. * * Each entry is `key=value[,value...]` (include) or `key!=value[,value...]` * (exclude). Values are comma-separated and OR-combined; repeated keys merge. */ export declare function parseSuiteFilter(raw: string[]): SuiteFilter; /** * Resolve suite eval globs against the filesystem. * * This is an independent discovery mechanism — it resolves globs directly, * not a filter on standard discovery. Paths are resolved relative to * `projectRoot` (the directory containing `.vally.yaml`, or an experiment * directory), but a pattern may reach outside that root via `..` — matching * the `paths.evals` and `-e/--eval-spec` mechanisms, which also resolve * arbitrary paths. Glob walks are re-anchored at the pattern's static base so * that a dotdir base (e.g. `.github/skills/**`) or a parent-relative base * (e.g. `../../tools/**`) is reached by path resolution rather than being * pruned by the walker's descent filter. * * @returns Array of resolved absolute file paths that match the globs. */ export declare function resolveSuiteEvals(suiteEvals: string[], projectRoot: string): Promise; /** * Verify that a resolved absolute path exists on disk with EXACTLY the requested * casing at every component. This enforces case-sensitive matching for the parts * of a pattern that are resolved through the filesystem — the static base of a * glob and exact (non-glob) paths — which `resolve`/`lstat` would otherwise * accept case-insensitively on macOS/Windows. The glob remainder is already * case-sensitive because it matches the real names `readdir` returns. * * The check does its own case-sensitive `readdir().includes()` per component, so * it enforces casing regardless of the filesystem's case sensitivity (and is * directly testable on case-sensitive Linux). This covers only the base/exact * portion; the glob remainder's case-sensitivity comes from the matcher * (see {@link tryCompileSuiteMatcher}'s invariant). Both must stay case-sensitive * for end-to-end cross-platform parity. `absTarget` must be an already * resolved absolute path (no `.`/`..` segments); the walk starts at its * filesystem/drive/UNC root, so the root's own casing (e.g. a Windows drive * letter) is the walk's anchor and is not itself verified. `readdir` results are * memoized in `cache` across one resolution pass. * * Each component is classified against its parent's `readdir` listing: * 1. Listed verbatim → correct casing, accept. * 2. Listed only under a different casing → a genuine casing mismatch of a * real entry, reject (this is the case-sensitivity contract). * 3. Not listed under any casing → either an 8.3 short-name / OS alias of a * real entry (e.g. `RUNNER~1` ⇒ `runneradmin`, which `readdir` never * surfaces since it returns long names only) or a nonexistent path. We * `realpath` the component to decide: if it fails to resolve the path is * nonexistent → reject; if it resolves to an entry actually listed in this * directory → tolerate the alias; otherwise (resolves to something not * listed here, e.g. an NTFS alternate-data-stream path) → reject. * * Case 3 tolerates short names whether they were machine-provided (a temp/anchor * ancestor) or typed by the user — both name a real entry that resolves on the * OS. `realpath` runs only for case-3 components (rare); ordinary correctly-cased * paths take case 1 with no extra syscalls. On case-sensitive filesystems case 3 * effectively only rejects (an existing entry would have been listed), so Linux * behavior is unchanged. An 8.3 alias of a symlink/junction entry may be rejected * because `realpath` follows the link to a differently-named target; this is a * pragmatic 8.3 fix, not a general alias verifier, and errs toward rejection. */ export declare function pathHasExactCasing(absTarget: string, cache: Map): Promise; /** * Compile a suite-eval pattern into a picomatch matcher, or return null when it * can't be used: empty/whitespace input, or an uncompilable glob. Centralizes * the normalize → compile dance so resolution and validation can't drift. * * Uses `dot: true` so a bare `**`/`*` also matches paths that contain * dot-segments (e.g. `.github/…`). Suite `evals` are an explicit opt-in * selection, so this leans toward inclusion; structural noise is bounded by * {@link SUITE_EXCLUDED_DIRS} during the walk. * * INVARIANT: the matcher must stay **case-sensitive** (picomatch's default — * do NOT set `nocase`). The glob remainder is matched against real on-disk * names, and {@link pathHasExactCasing} case-checks the static base and exact * paths, so together they make matching case-sensitive on every platform. If a * `nocase` matcher is introduced here (or a replacement library defaults to * case-insensitive), glob segments would silently go case-insensitive while the * base/exact stay strict. Keep case-sensitivity consistent across both mechanisms. */ export declare function tryCompileSuiteMatcher(pattern: string): picomatch.Matcher | null; /** * Report whether a suite-eval pattern is usable. {@link resolveSuiteEvals} * tolerates and skips bad patterns because the experiment file's `evals:` block * is vetted, so callers passing raw user input (e.g. CLI `--eval-filter`) * should validate up front to surface a clear error instead of a confusing * zero-match result. * * Negated patterns (`!foo/**`) compile cleanly but {@link resolveSuiteEvals} * refuses them, so they're reported invalid here too — otherwise a caller would * pass validation and then hit a misleading "matched nothing" error. */ export declare function isValidSuitePattern(pattern: string): boolean; /** * Report whether a suite-eval pattern contains glob syntax. Used to gate the * absolute-path fast path in eval-filter resolution: a non-glob filter can be * canonicalized with {@link realpath} and compared against the declared set * directly, whereas a glob must go through the directory walk. */ export declare function isGlobPattern(pattern: string): boolean; //# sourceMappingURL=suite-filter.d.ts.map