feat: add a not comparator modifier and the file.contents metric#68
Conversation
The string comparators (starts, ends, contains, matches) had no negation, so "this file does not contain X" was inexpressible and pipelines had to pre-grep. Add a `not` prefix that negates any comparator. Negation is applied per comparison rather than on the assertion's final result, so a whole-column `tsv.column.N.all not contains 'X'` means "no cell contains X" with correct first-offender reporting, not the De Morgan dual. Add file.contents, the file-backed twin of text.value: it reads the body as a UTF-8 string and compares it with the string comparators. With the modifier, `out.log file.contents not contains 'Exception'` is the native replacement for a `grep -v` pre-step. It is for log-sized text, reports a bounded byte count as its actual rather than the body, and errors on a non-UTF-8 file. `Comparator` becomes an `Operator` plus a `negate` flag, so every executor call site is untouched and the report message renders the modifier verbatim. Spec in specs/negated-comparators.md. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR extends bioassert’s assertion language to support negated comparisons (not <comparator>) and introduces a new file.contents metric for applying string comparators over an entire UTF-8 file body (enabling native “absence” checks like file.contents not contains 'Exception'). It refactors comparator handling by splitting the comparison keyword into an Operator plus a negate flag, and threads that through string matching so whole-column aggregations get the intended per-cell “none” semantics.
Changes:
- Add
notas an optional comparator modifier in the grammar and implement negation inComparator/StringMatcher. - Add
file.contentsmetric executor + functions to read a file as UTF-8 and report a bounded “got” summary. - Add unit + integration tests, fixtures, snapshots, and update specs/docs to describe the new behavior.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/snapshots/negated_subcommand_test__run_negated_stdout.snap | Snapshot for the all-passing negation/file.contents integration run output. |
| tests/negated_subcommand_test.rs | Integration tests covering negated comparator semantics and file.contents behavior/errors. |
| tests/data/negated_comparators.txt | All-pass assertion batch used by run snapshot test. |
| tests/data/clean_log.txt | New text fixture for file.contents absence/presence checks. |
| src/text/value/functions.rs | Update tests to construct comparators via the new Operator API. |
| src/file/mod.rs | Register and re-export the new file.contents module/executor. |
| src/file/contents/mod.rs | file.contents module entry point. |
| src/file/contents/functions.rs | Implement UTF-8 file read + bounded “got” summary helpers. |
| src/file/contents/executor.rs | Implement file.contents assertion executor. |
| src/engine/executor.rs | Register FileContentsExecutor in metric dispatch. |
| src/engine/assertions.pest | Extend grammar to accept optional not prefix as part of comparator token. |
| src/delimited/column_all/functions.rs | Update tests to use Operator + Comparator::new(...). |
| src/core/mod.rs | Re-export Operator from core. |
| src/core/comparisons/mod.rs | Re-export Operator alongside Comparator and StringMatcher. |
| src/core/comparisons/comparator.rs | Refactor comparator into Operator + negate and apply negation in comparison/matcher semantics. |
| specs/negated-comparators.md | New design/spec doc describing semantics, constraints, and test plan. |
| skills/bioassert/references/metrics.md | Document file.contents and the not modifier, including aggregate semantics. |
| CLAUDE.md | Update contributor-facing architecture notes for negated comparators and file.contents. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _ => false, | ||
| } | ||
| }; | ||
| base ^ self.negate |
Document the `not` modifier and the file.contents metric across the user-facing docs and the skill: README.md (file metrics table, a file checks example, a comparators note) and skills/bioassert/SKILL.md (the assertion model, a logs output-type entry). The skill's metrics.md was updated in the prior commit. Extend the exhaustive fixture tests/data/assertions.txt with a file.contents section and a dedicated negated-comparators section that exercises `not` across the string, numeric, boolean and whole-column-aggregate families, and regenerate the run_all_passing snapshot. Address review feedback on Comparator::compare: negation is now applied only to the numeric operators. A string comparator on a numeric or boolean metric (file.size not contains 1MB) returns false regardless of negate, so it stays an always-FAIL rather than flipping to an always-PASS that would silently hide a mis-pairing in a validation gate. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
What
Closes the long-standing gap that bioassert cannot ask "does this file not contain X". Two additions, designed together:
A
notcomparator modifier. Any comparator can be prefixed withnot(not contains,not matches,not starts,not ends). The string comparators previously had no negation (eqhadne, butstarts/ends/contains/matcheshad nothing), which is why absence checks had to be pre-greped outside the tool.A
file.contentsmetric. The file-backed twin oftext.value: it reads the whole body as a UTF-8 string and compares it with the string comparators. With the modifier,out.log file.contents not contains 'Exception'is the native replacement for agrep -vpre-step.Key design decision
Negation is applied per comparison, not on the assertion's final result. For a scalar metric the two are identical, but for the whole-column aggregates they diverge:
column.N.all contains Xgives the De Morgan dual "some cell lacks X".column.N.all not contains X.Baking
negateintoStringMatcher::is_matchgets the per-cell form for free: the existing streamingcheck_columnyields the correct "none" semantics with correct first-offender reporting (got line 3 = "NDA"), with no change to that code.Blast radius
Comparatorbecomes anOperatorplus anegateflag. Because every executor already callscompare/string_matcher/compare_stringas methods, no executor call site changes, and because the comparator stays a single source-slice token on the AST (not contains), the report message renders the modifier verbatim with no formatting change. The real edits are the grammar's comparator rule,comparator.rs, the newsrc/file/contents/module, dispatch registration, and docs.Out of scope (deferred, recorded in the spec)
file.lines.{any,none,all}for multi-gigabyte files (file.contentsreads the body into memory, so it is scoped to log-sized text).and,or, groupednot).notinherits the existing mis-pairing footgun).Tests
Comparatorparse/compare/display under negation,StringMatchernegation for streaming,file.contentsread and bounded summary, non-UTF-8 and missing-file errors.tests/negated_subcommand_test.rs): the grep-replacement PASS path, presence FAIL, negation-of-present-word FAIL, the per-cell "none" FAIL with first-offender reporting, the redundant numericnot, and the binary-file ERROR. Plus an all-passing run snapshot.cargo fmt --checkclean,cargo clippy --all-targetsclean.Design doc:
specs/negated-comparators.md.🤖 Generated with Claude Code