Repository navigation
[DRAFT] Add validation module to serve as backend for submission checker and policies enforcement - #544
[DRAFT] Add validation module to serve as backend for submission checker and policies enforcement#544nv-alicheng wants to merge 54 commits into
Conversation
W1 condition row semantics (Compare/Verdict/Ref with abs/rel tolerance and the absent-vs-null grid) and W2 submission tree walk (kind-registered Node subclasses, hardened artifact load, MISSING sentinel reads, scopes for Ref resolution), with their unit suites.
Op family (self-registering subclasses, __new__ dispatch), Predicate(subject, target) with MISSING-typed subjects, Interval with __contains__, Aggregate family (count/sum/mean/min/max/any/all) with __new__ dispatch, and the shared MISSING sentinel.
Check/Cohort/Row/Severity (YAML check blocks with when/assert row decode), the catalog view (ruleset root of Ref), the unified Interval (closed/open-upper/half-open/scalar) with the accuracy-gate band (Endpoints Policy Section 2.9.8) and the Section 5.5 concurrency-regions reference algorithm across its three worked examples.
covers returns which RegionKind covers a concurrency value (Rules-compute_regions key names as values) and None outside every region; truthiness keeps its predicate use. Includes removal of swap-file artifacts swept into f7c3a80 and two whitespace-only import-gap touches.
Regions.for_submission (C_max from system_desc.max_supported_concurrency, C_min from the lone [1,32] point), Regions.coverage (the tiled Interval(1, margin.hi)), and a regions Ref root fed by SubmissionNode.from_directory, matching the catalog hook.
Point/Submission carries (offline, concurrency, system_tps per Endpoints Policy Section 8.3/4.1), those fields terminology sweep through compare.py/tree.py docstrings, Submission.offline_ordering (Section 5.7.2 pair of clauses with bool|None where elected/agentic is not-applicable) and Submission.point_cap (Section 5.6 <=32 cap, machine-verified boundaries).
Finding (audit-trail record per evaluated assert row, holds and failures alike), Finding.collect with 3 phases (collect candidates / needs gate with recursive memo + cycle ValueError / evaluate rows via should_run filtering), Check.scope rename plus needs/optional_needs pair, optional Check.fix (fix: str | None per the oracle's Message.fix where-declared semantics), vocab/YAML-key clean cuts (root->scope across fixtures and stubs), the tests/assets/cohorts/stub cohort with PR_CHARTER and devdocs notes.
Submission.point_count_min (Section 5.3: 8 dedicated-offline else 7) and Submission.accuracy_run_count_requirement (Section 4.3: 5 non-agentic else 4), each with its 3-case declaration-shape grid.
Point.point_power_kw (Endpoints Policy Section 4.5.2/4.5.3 formula with 2dp-round-once per :897) with both :1906/:1907 golden rows pinned as a 2-case grid, machine-verified before landing.
…ndary, tolerance-exact Tree._read_artifact single bounded symlink-safe byte channel (O_NOFOLLOW, cap-before-decode, UnicodeDecodeError funnel, dead-handler cleanup); Verdict.evaluate boundary converts incommensurable pairs to failing verdicts naming both sides + exc-class, retargeting test_incommensurable_ordering_pair_fails_with_why; Op.tolerant ClassVar + Eq/Ne opt-in + Compare._check_shape's tolerance-capability-rejection + _close_to's int-exact-short-circuit (2^53-int-false-pass fixed) + 2 new test-pins.
docs/validation/README.md = the 4-section how-to-use (what the submodule is for / cohort YAML organization / how files are written with worked examples / full decoded-key vocabulary table). docs/validation/DESIGN.md = the what-happens-under-the-hood counterpart (module map, row-operator mechanics, runner cascade, derivation formulas table with Appendix C goldens, notable engineering calls). .agents/devdocs/ de-tracked from git + gitignored per policy (files kept locally for my reference).
…unification DerivedFields lazy mapping per key (a row naming no derivation runs none); _evaluate_rows restored (Finding per assert row per when-applicable node); Finding.collect 2-arg with test call-sites unified and two regression pins.
- refuse-loud load contracts: FIFO/special-file and empty/non-mapping artifact reads, duplicate keys in both decoders, Point domain plus tree↔points bijection and system_desc agreement, empty/stray cohort directories, ruleset folds, unjudgeable when verdicts (Verdict.broken) - cohort layout: strict scope files (system/submission/curve/point.yaml, id-keyed blocks); Submission.from_directory builds the typed view from the same artifacts; normalize_offline moves to submission - CLI: python -m inference_endpoint.validation (--ruleset, --audit) - docs: README author how-to (predicate forms, queued/passed gating, Reference forms, refusals, keys) and DESIGN module map + data-flow diagram; 3 template_full.yaml regenerated by the templates hook
Wave 1 - strict `Reference` decode (`_decode_operand`): a dotted scalar
must name a scope root or use `{"literal": ...}`; a one-edit near-miss
of a root keeps its misspelled refusal. Non-dotted scalars stay bare
literals. `exists`/`absent`/`not_null` refuse a non-`Reference` `left`
at load: `Compare._outcome` previously returned `subject is not MISSING`
for any literal, a silently always-true `exists` on `left: blah.foo`.
Wave 2 - one-use leaf-helper inlines: `_same_multiset` -> `SameMultiset.holds`,
`_one_edit_apart` -> `_near_root_misspelling`, `_point_from_node` ->
`Submission.from_directory`, `_check_from_entry` + `_load_scope_checks` ->
`Cohort.from_directory`, `_raise_on_needs_cycles` -> `_collect_candidates`,
`_parser` -> `main`. Behavior-neutral (287 passed + CLI smoke unchanged).
Wave 3 - `matches` patterns are always form-literal:
`Compare._matches_right_is_explicit_literal` (a `mode=before` validator -
the {}-literal pocket in `_decode_operand` unwraps to the same bare string
downstream) refuses any non-`{"literal": <str>}` `right`. A pattern is
never a `Reference`, and no ruleset field can carry one anyway, so the
constraint is unambiguous instead of collided with by the missed-reference
guard. `vet_pattern` still compile-checks at load.
Docs mirror each rule (README's References list, unary-operator lead,
`matches` clause, Refusals bullet, closing decode claims; DESIGN.md's
operand-decode bullet names `Compare._matches_right_is_explicit_literal`).
Tests pin the new refusals in `test_compare.py` and drop superseded
literal-reasoning pins (stale-repin policy).
One commit carrying all three waves: `docs/validation/DESIGN.md`,
`tests/unit/validation/test_compare.py` and `docs/validation/README.md`
interleave the waves' hunks at block granularity, so per-wave commits would
have required unverifiable line-level patch surgery. The body order above
is the wave history.
- `_StrictLoader` -> `StrictLoader`: it has three importers beyond `tree.py` (`catalog.py`, `check.py`), so the underscore misread as private. All 7 sites (`tree.py`, `catalog.py`, `check.py`, `docs/validation/DESIGN.md`) renamed in lockstep. - `Ref.ROOTS` carries `ClassVar[tuple[str, ...]]` explicitly: typed rather than an unannotated class attribute, matching its documented class-constant (non-field) role. - `Verdict`'s docstring stops using `row` and says `assertion`, per the standing terminology rule (whitespace-vocabulary cleanup only -- no behavior change: 287 passed + CLI smoke unchanged).
- `matches` example swaps `submission.system_desc.model_name` plus an
invented slug pattern for a genuine shape check: `point.config.target_cohort`
against the documented `YYYY-MM-C0`/`YYYY-MM-C1` form
`20[0-9]{2}-(0[1-9]|1[0-2])-C[01]`.
- `model_naming` is now the membership check it should always have been:
`in` over `ruleset.models`, scanned by keys (each supported model name).
`left` corrected to `point.config.model_name` (per `FILE_TO_KEY_MAP`,
`point.yaml` lands under `point.config`), and the `rule` field now cites
`endpoints_rules.md` §8.3, Measurement Point (point.yaml).
Policy ground truth: `mlcommons/endpoints_policies/endpoints_rules.md`
§8.2/§8.3 (verified `d2d9da6`, 2026-10-02). The retired sample text
("The model name follows the system.model naming convention") traced to
the AGENTS placeholder-token convention (`system.model`), not policy.
Reformat the docstrings to the default format: summary sentence, `Example:` block, `Args:`/`Returns:`/`Raises:` with continuations aligned under the description. Split semicolon-chained clauses into sentences, and document `Op` ClassVars on the fields themselves. Docstring and comment changes only: the code AST is unchanged from HEAD in all three files.
A stray edit in this branch had removed the `steady_state` block, the `metrics_isl` flag and the extended `dataloader_random_seed` note from all three `*_template_full.yaml` files (flagged in review). Restored to the schema-derived template output.
Continuation lines sit one indent past their item instead of aligning under the description text. Reindents catalog/operations/ranges; also rejoins sentinels.py's split `value is MISSING` span.
Cohort directories now require valid_refs.yaml beside the scope files (thread A4): RefTable parses the declared names and required/default/allowlist notes, every row Ref is validated at cohort load (misspellings get a one-edit near_match hint, below-leaf paths are refused), and Ref moves out of compare.py into refs.py with a dependency-free typo_checks.near_match. compare's operand resolution collapses to one _operand/_missing_outcome with a single mapping-only path (no getattr fallback); map_refs lets Cohort.from_directory annotate every row.
Pareto is one results/<system>/<model>/ (threads A5/A16): point cap, C_min/regions, offline rules and point/accuracy counts compute per pareto; each r<N>/ reads its own system_desc.json (A2/A3); c_min_override is honored and validated (A6); elected must sit at C_max, dedicated is exempt from out_of_range_points, and thresholds become named constants (A10/A11/B15). Regions gain for_pareto/for_bounds(*, c_min, c_max), ULTRA_LOW_CONCURRENCY naming, and pinned margin/override constants (A18/A19/A20).
RuleScopes.PARETO and ParetoNode replace CURVE/CurveNode, the cohort file is pareto.yaml, the Ref root is pareto. (thread A26); scopes() folds and node docstrings follow the per-pareto contract.
… in runner runner validates declared values per node against the cohort's valid_refs.yaml (refusal names the offending run directory) and resolves derived.<name> through the owning Pareto; fold requirements narrow to ruleset; needs/gate docs updated.
Exit 1 counts failing fail findings only; a failing warn reports and passes through with a WARN mark; the summary reads N finding(s), F failing (B blocking); per-check ran counts plus NEVER-RAN on stderr (A9/A22).
DESIGN.md/README.md: rewritten runner cascade, accuracy-gate bounds, out-of-range anchor, when/assert provenance, check-field Type column, measure/aggregate naming (A12/A13/A15/A25/A27/A28).
compare.py: `_finite` guards every resolved operand slot, so a NaN/Inf out of a fact fails the row with a sentence instead of poisoning the comparison; `between` refuses null bounds; `Aggregate` guards its measured value and declared threshold. operations.py: `Matches` caps its subject at 64 KiB, `SameMultiset`'s unhashable fallback caps its scan at 1024 elements, and `In` refuses scalar targets. Each refusal names the row and the limit.
tree.py: `Node` compares by identity (`eq=False`) so two nodes with equal payloads stay distinct; `SubmissionNode.from_directory` refuses a symlinked root with the remedy in the message; `_read_artifact` spends a shared `_ArtifactBudget` (8x the single-artifact cap) so a bloated upload cannot grow the walk unboundedly. submission.py: `system_desc` is excluded from `Point`/`Pareto` hashing.
…s disavow runner.py: `evaluate_at` returns one `_Evaluation` (state plus the row verdicts) that gating and emission both consume, so a dependent check can never gate on a different evaluation than the one reported; the state folds a broken row into FAILS as `Verdict.evaluate`'s fail-not-abort policy dictates. A SILENCED claim now disavows a `needs` dependent like FAILS (a check whose results were withheld did not vouch); NOT_APPLICABLE stays neutral. Findings carry the row's own label and the check's `rule`/`fix` for the report side.
catalog.py: `_load_mapping` wraps the YAML parse in a ValueError naming the file, so a malformed catalog reports which catalog broke and refuses the load instead of raising a bare parser traceback.
…ames __main__.py: a report line reads `<mark> <id> [<severity>] <dir>: <row>` with ` -> <why>` when the row states one, the rules citation on failures, and an indented `fix:` line when the check declares one; directory names are escaped so upload-controlled control characters cannot forge lines. README.md: the report paragraph and check-table rows match, and a malformed catalog is documented as a load error.
…eal owners Committed text stops citing session-local shorthand: internal slice/wave labels in DESIGN and four test module docstrings, finding-code suffixes in two test docstrings, the stub cohort's planning-directive reference, and submission.py's "the charter". References to helpers that no longer exist point at their real owners instead: the `needs`-cycle DFS in `_collect_candidates`, and `Cohort.from_directory`'s per-file wrapper (was `_load_check`). DESIGN's reserved-keys caveat and `refs._entry`'s docstring now state the actual rule (a mapping is the annotated leaf only when `type` is a type name and every key is reserved), the stale `right`/`lo`/`hi` field-presence line drops the removed `lo`/`hi`, and the catalog docstring's example no longer names an unshipped `config/rulesets/<name>/` path.
The checker read its `ruleset` catalog from a directory passed on the command line (`--ruleset DIR`), a second home for data the benchmark side already pins in Python (`config/rulesets/mlcommons/models.py` / `datasets.py`). The catalog now derives from the registered round itself: `Catalog.from_ruleset` reads `config/ruleset_registry.py`, and a cohort links to one through a new reserved `cohort.yaml` manifest whose one key, `ruleset`, names the round (e.g. `mlperf-inference-v6.1`). `--ruleset` is gone; a cohort without a manifest has no ruleset fold, and `Catalog.from_directory` stays for hand-built development catalogs. What a cohort's submissions are valid against is a packaged, versioned ruleset; the manifest is where a cohort pins it. Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py (375 passed); the `--ruleset` end-to-end test is replaced by a manifest-driven CLI test, and a rule row reads `ruleset.models` of `mlperf-inference-v6.1`.
The validation package is a new top-level module, so AGENTS.md's own rules put it in the Key Components table and the Code Organization tree in the same PR. Also corrects the module map's count in docs/validation/DESIGN.md.
…ecker roots "fold" named the three roots whose namespaces the checker owns, but the term was never defined in the docs and its reduce sense fits only `derived` (computed over a pareto's points); `ruleset` is static packaged data and `regions` a per-pareto computation. `Ref.CHECKER_ROOTS` and the docs now say what the class is: roots the checker owns - the cohort's `valid_refs.yaml` may not declare them and no table annotates them. `_validate_fold_needs` becomes `_validate_ruleset_needs`: it refuses a cohort naming the `ruleset` root the tree was built without, nothing else. Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py (375 passed).
…ore either pass `_read_artifact` now spends the tree budget before the size refusal: an oversized artifact is still a read, so a crafted upload of many oversized files is refused at load instead of multiplying the per-file cap by node count. `same_multiset` refuses a pair longer than `_MULTISET_SCAN_LIMIT` before either pass: `Counter` degrades quadratically on same-hash collisions, so the cap could not live in the unhashable fallback alone. Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py (394 passed).
…ent, empty systems Cohort load cross-checks a ref table's notes: a `default` and every allowlist value must match the declared type, and `required` cannot carry a `default` (a default answers the fact's absence). Upload load refuses numeric facts no arithmetic can carry (a non-finite `system_tps`, or a `system_tps`/`concurrency`/`max_supported_concurrency` outside `float` range) and a `<system>/` directory with no model directories, so an empty system cannot hide from system-scoped checks. `_shared_facts` compares by presence and typed value, so a later run's `c_min_override: null` can no longer pass as equal to a first run's omission. The stub cohort's range row gates on the dedicated Offline exemption (Section 5.7.2), the same exemption `Pareto.out_of_range_points` applies, so a conforming dedicated run draws no spurious warn. Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py (394 passed).
`Finding.collect` returns a `Collected` record: the row findings plus one `CheckRun` per check, counting the nodes it ran at, the nodes its `when` gated off, and the nodes a `needs` chain silenced. The report words its `ran at N instance(s)`/`NEVER-RAN` lines from those counts, so a `when`-only check that ran everywhere is no longer reported as never-run with the wrong cause, and a silenced check says so. The CLI's refusal net also takes `OverflowError`, belt for numbers the load-side guards do not reach. Verified: tests/unit/validation + tests/unit/config/test_regenerate_templates.py (394 passed).
Module-map notes for the tree-budget charge, the ref-note cross-validation, the numeric guards, the typed descriptor agreement, the empty-system refusal, the `Collected`/`CheckRun` records, and the stub's gated range row.
"root" carried three senses in the module's prose: a ref's first segment, the tree's top node, and an upload directory. The docs now call the ref sense a namespace — a ref's first segment (`point.config.concurrency`'s namespace is `point`). Code identifiers keep their names (`Ref.ROOTS`, `Ref.SCOPE_ROOTS`), except the private compare helper, renamed `_near_namespace_misspelling`. Two refusal strings follow the vocabulary: "is dotted but 'x' is no scope root" becomes "... is no ref namespace" (the gate was never scope-only — it is `Ref.ROOTS`), and "fix the root" becomes "fix the namespace".
The user-facing page catches up with the checker as it stands: - the cohort artifact: `cohort.yaml` is the optional manifest naming the packaged ruleset its `ruleset.*` rows read, and a submission may carry several `<system>/<model>` paretos - the stray-file rule, the ruleset paragraph and an instance note follow - the report: findings are one per evaluated `assert` predicate (holds included), and NEVER-RAN reasons name the gating/silencing state exactly - the ref vocabulary: the ref-head sense of "root" becomes "namespace", with the quoted refusal strings matching what the checker emits - prose converted to lists through Sections 3 and 4: the valid_refs entry forms, predicate kinds and instance cadence, the aggregate lead-in and the Possible-keys close
The finer-scope sentence said a dependent runs only if the listed check passes for every point in the pareto. That overstates it: the chain vouches over the nodes where the listed check holds, fails or was silenced, while a node its `when` skipped makes no claim at all -- so a gated-off point neither vouches nor disavows.
A wrong `cohort`/`submission` argument printed the path alone (`refused: /tmp/no-such-cohort`), because three `NotADirectoryError(root)` guards carried the path as their whole message. Each now says which role the path belongs to and whether it is missing or not a directory -- `the cohort directory <path> does not exist` / `the submission path <path> is not a directory` -- so a swapped or mistyped argument reads as a statement instead of a bare path. Exit 2 and the `refused:` prefix are unchanged.
A directory that is not an `r<N>` point (`r32_rerun`), a symlinked directory, a point-named entry that is not a directory, and a directory that resolves outside the upload all escaped every check while the rest of the upload passed. `_walk` now collects them per level and refuses once, naming each with its reason -- `<dir> holds entries no check can reach: r32_rerun (not an `r<N>` point directory)` -- the upload-side counterpart of the cohort's stray-file refusal. Plain files with other names stay ignored: uploads carry them.
Two present values read as if they were fine while they were not: - an aggregate's `over` resolving to a non-collection (`17`, a stored `null`) returned a plain false verdict -- which as a `when` gate reads as "not applicable" and switched the check off by omission. It raises now, the `_finite` treatment: `Verdict.evaluate` maps it to a broken verdict, so a gate over it refuses loudly and names the row. - a `dict` leaf's own value was never checked (`_Fact.matches` has no `dict` branch and `validate_values` skipped subtrees), so `steady_state: 17` resolved nothing while claiming the document shape. The leaf's own value must be a block; the values below it stay unchecked.
The checker required `result_summary.system_tps` at load while the benchmark's report writes the same aggregate as `tps` (`metrics/report.py`; None on a run that recorded no OSL), so a self-produced upload was refused with exit 2 and rows over `point.result_summary.system_tps` could only ever read absent. `Node.read` now surfaces the policy name when the upload carries only the client's spelling: a shallow copy gains `system_tps` beside the document's own `tps`, an explicit `system_tps` is returned untouched, and anything that is not a mapping is left for its own refusal to name. The parsed document stays as written, and a `tps: null` (no OSL) still refuses -- `needs a numeric system_tps` -- since the metric is not there to read. Which spelling an upload must carry is a policy question for MLCommons.
The row side of the client-`tps` bridge: a cohort table declaring `result_summary.system_tps` plus an upload whose summary carries only the client's spelling still evaluates the row, where the fact would have read absent before.
`pareto_at` scanned `self.paretos` per lookup, and `Submission.points`
rebuilt its tuple on every access (the runner reads it while resolving
`needs` and enumerating nodes). Both are computed once in `__post_init__`
now: a `{id(pareto.node): pareto}` map behind `pareto_at` -- a dict hit after
the parent walk -- and a `points` field in place of the property. The API is
unchanged for callers and hand-built `Submission`s, and the multi-pareto
lookup is pinned by the test.
A reader sees two checkers in one repo and no sentence telling them apart: `compliance/checker.py` validates one completed Edge-Agentic (BFCL) run's report directory (resolved `config.yaml` plus scorer output) against its ruleset -- config lock, accuracy gate, dropped-turn validity -- while this module checks an assembled Endpoints submission against a cohort's declared checks. Neither imports the other.
…s declared A declared `int`/`float` fact now reads a numeric string as the number it spells, and a numeric path component reads a key spelling the same number, so an artifact that stringifies its numbers or keys a percentile block `"90.0"` is read the way the submission checker reads it, instead of being refused or read as absent. - `decode_number` is the one rule: exact numeric spellings only (`"512"`, and the integral `"512.0"` for an `int`; whatever `float` reads as finite). Units, separators, non-finite text and non-numeric declarations stay as they were. - The parser-enforced facts (`_int_fact`, `_finite_fact`) decode through it too, so a stringified `concurrency` is read once and identically by the parser and by the ref table. - `Ref.type` carries the leaf's declared type from `RefTable.annotate`, so `Ref.resolve` decodes only where a declaration exists: an un-annotated ref (a checker-owned namespace, a hand-built row) still reads the artifact raw. - `_key_for`: the exact key always wins, and equivalent spellings break by sorted key, so one artifact always reads the same way. - `RefTable.validate_values` returns one note per decoding, threaded through `Collected.coercions` to the CLI's `coerced:` lines on stderr, so the leniency is reported rather than silent.
|
MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅ |
| } | ||
|
|
||
| # The `derived.<name>` names a row may name (single owner: the table above). | ||
| DERIVATION_NAMES: tuple[str, ...] = tuple(_DERIVATION_METHODS) |
There was a problem hiding this comment.
DERIVATION_NAMES is used: check.py:74 imports it and check.py:112 checks every
derived.<name> ref against it at cohort load, and
tests/unit/validation/test_submission.py asserts the registry matches it. Leaving the constant
where it is.
| """ | ||
| return self.type == "dict" | ||
|
|
||
| def matches(self, value: Any) -> bool: |
…alyzer nits The full suite collects every test file by module basename, so `tests/unit/validation/test_runner.py` collided with the pre-existing `tests/unit/evaluation/swebench_service/test_runner.py` (and `test_cli.py` with `tests/integration/commands/test_cli.py`): the CI test job died at collection with an `import file mismatch`. Both files are renamed to `test_validation_*`, which is unique in the tree; the module's own suite never caught it because it collects one directory at a time. Two analyzer notes settled while here: - `_Fact.matches` spells out a `dict` arm beside the `_` catch-all, so every declared type name has a branch and no path can look like a fall-through. - `derived["nope"]` in `test_unknown_derivation_is_absent` binds its lookup, so the statement is not a bare expression with no effect. The `LoudDerived` stub keeps raising `ValueError` (not a `LookupError`): `Verdict.evaluate` turns a `ValueError` into the fail-loud verdict the test pins, while a `KeyError` would escape the verdict entirely, with a comment saying so.
Review findings on the decoding change, all five addressed:
- `Point.from_node` discarded the decoded `max_supported_concurrency`, so a
stringified maximum passed the parser and was refused later by the pareto
shape check as "must declare one integer max_supported_concurrency".
`_descriptor_facts` now carries the decoded maximum, and decodes a
stringified `c_min_override` too, while the artifact document itself stays as
written for the ref table (`coerced:` still reports the decoding). The shape
check and the region bounds read the carried descriptor.
- `_check_values` matched declared names by exact membership while
`Ref.resolve` reads a numeric-equivalent key, so a declared `90` skipped an
artifact key `"90.0"` and left a reachable value unjudged. Both now look the
key up through `_key_for`.
- The `int` decode tested integrality on a `float` pass, so
`"512.9999999999999999"` rounded to a whole number and
`"9007199254740993.0"` lost a digit. `_spelled_integer` reads integrality
from the spelling with `Decimal`, exactly.
- The `TestResolve` class statement was lost in an earlier edit: its docstring
sat as a bare string inside `TestMapRefs` and its tests collected there.
- DESIGN's contract-validation paragraph had lost the declaration-refusal
clause ("a present value its `valid_refs.yaml` declaration will not carry").
| def __contains__(self, name): | ||
| return True | ||
|
|
||
| def __getitem__(self, name): |
There was a problem hiding this comment.
Kept as ValueError, with a comment. I ran the suggested KeyError recipe: __contains__ false +
__getitem__ raising KeyError + a get that raises ValueError makes the verdict
expected derived.x to exist, got absent (broken=False) and the test fails —
Verdict.evaluate maps ValueError/OverflowError to the fail-loud verdict this test pins, and
nothing calls Mapping.get on the way there. The stub is deliberately non-standard.
|
Thanks — both the CI break and the four code-quality notes are settled, plus five review-council CI test job (fixed, now green). The full suite imports every test file by module basename, so Code-quality notes, triaged:
Review-council findings on the decoding commit (all five addressed):
Verification: module suite 439 passed; whole tree collects with 0 errors; the cohort still reports |
What does this PR do?
Adds a cohort-driven validator for submission bundles:
src/inference_endpoint/validation/(fourteen modules), run asA cohort is a directory that declares what one benchmark round requires: a
cohort.yamlmanifest (hint, ruleset or cohort-bundled catalog), one YAML file per scope (system.yaml,point.yaml,pareto.yaml), and avalid_refs.yamlnaming the artifact fields a row may address. Checks are declarative rows — scope, dotted reference into the submission's artifacts, operator (comparison, membership, regex, aggregate over lists/regions), severity, and the policy rule it enforces. Each row prints one finding per instance with the rule citation and the fix, and the run exits 0 (pass), 1 (afailrow failed) or 2 (refused before any check ran).warnrows are reported but do not fail the run.What the module covers:
valid_refs.yamlgives every addressable field its expected type. A present value that contradicts its declaration, a name no declaration covers, and an artifact the upload walk would silently skip are all refused up front, so rows only judge facts that actually exist.results/<system>/<model>/r<N>/with the scope files (point.yaml,system_desc.json,result_summary.json,accuracy_results.json,system_power.json); directories that do not match the point shape, symlinks, and entries resolving outside the submission are refused once, naming each.whengated every instance off printsNEVER-RANinstead of looking like a pass, so a cohort cannot quietly stop checking things.Docs:
docs/validation/README.mdis the usage contract (how to write a cohort, run it, and read findings);docs/validation/DESIGN.mdholds the internals, invariants, and the engineering calls with their rationale.AGENTS.mdmaps the new package.The change is additive: nothing outside the new package changes except the
AGENTS.mdcomponent map and one.gitignoreentry.Type of change
Related issues
None.
Testing
tests/unit/validation/(~4.4k lines) cover ref resolution and refusals, comparison and aggregate operands, region arithmetic, the upload walk, runner reporting, and the CLI, with stub cohort assets undertests/assets/cohorts/stub/.uv run pytest tests/unit/validation -q→ 407 passed.Checklist
ruffandruff-formatclean.SKIP=mypy: the mypy hook trips on three pre-existing macOS-typeshed errors unrelated to this branch.