Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add a not comparator modifier and the file.contents metric#68

Merged
PeterKneale merged 2 commits into
mainfrom
feat/negated-comparators-file-contents
Jun 26, 2026
Merged

feat: add a not comparator modifier and the file.contents metric#68
PeterKneale merged 2 commits into
mainfrom
feat/negated-comparators-file-contents

Conversation

@PeterKneale

Copy link
Copy Markdown
Owner

What

Closes the long-standing gap that bioassert cannot ask "does this file not contain X". Two additions, designed together:

  1. A not comparator modifier. Any comparator can be prefixed with not (not contains, not matches, not starts, not ends). The string comparators previously had no negation (eq had ne, but starts/ends/contains/matches had nothing), which is why absence checks had to be pre-greped outside the tool.

  2. A file.contents metric. The file-backed twin of text.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 a grep -v pre-step.

out.log       file.contents not contains 'Exception'   # PASS when the log has no Exception
reads.tsv     tsv.column.3.data.all not matches '^ERR'  # PASS when no data cell starts with ERR
ref.fasta     fasta.seq.0.name not starts 'chrUn'       # PASS when the first record is placed
output.bam    file.size not gt 1MB                      # accepted, equals lte 1MB

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:

  • Negating the final result of column.N.all contains X gives the De Morgan dual "some cell lacks X".
  • Negating the per-cell comparison gives "no cell contains X", the natural reading of column.N.all not contains X.

Baking negate into StringMatcher::is_match gets the per-cell form for free: the existing streaming check_column yields the correct "none" semantics with correct first-offender reporting (got line 3 = "NDA"), with no change to that code.

Blast radius

Comparator becomes an Operator plus a negate flag. Because every executor already calls compare / string_matcher / compare_string as 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 new src/file/contents/ module, dispatch registration, and docs.

Out of scope (deferred, recorded in the spec)

  • Streaming file.lines.{any,none,all} for multi-gigabyte files (file.contents reads the body into memory, so it is scoped to log-sized text).
  • Full boolean composition (and, or, grouped not).
  • Comparator/metric coherence checking (not inherits the existing mis-pairing footgun).

Tests

  • Unit: Comparator parse/compare/display under negation, StringMatcher negation for streaming, file.contents read and bounded summary, non-UTF-8 and missing-file errors.
  • Integration (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 numeric not, and the binary-file ERROR. Plus an all-passing run snapshot.
  • Full suite: 355 passing, cargo fmt --check clean, cargo clippy --all-targets clean.

Design doc: specs/negated-comparators.md.

🤖 Generated with Claude Code

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]>
Copilot AI review requested due to automatic review settings June 26, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 not as an optional comparator modifier in the grammar and implement negation in Comparator/StringMatcher.
  • Add file.contents metric 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.

Comment thread src/core/comparisons/comparator.rs Outdated
Comment on lines +51 to +53
_ => 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]>
@PeterKneale PeterKneale merged commit 3cfd430 into main Jun 26, 2026
3 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants