Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions extensions/loom/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,37 @@ unverified. Do **not** mark the step \`- [x]\` and do **not** say "done"
or "complete" for that artifact. Say "created but not verified" and ask
for the missing input or approval to change scope.

### Filtering significance tables — never coerce missing values

When you derive a filtered subset or a count from a statistics table by
thresholding a p-value / \`padj\` / FDR column (or any column where a
missing value is not the same as \`0\`), do not let a bare numeric
coercion turn a missing or non-numeric value into a number. In awk,
\`"NA"+0\` is \`0\`, so \`$7+0 < 0.05\` counts every \`NA\` row as
passing — and the header row, blank fields, and \`.\` coerce to \`0\` the
same way. A DESeq2 results table writes \`NA\` in \`padj\` for
independent-filtered and outlier genes, so this trap silently inflates
the "significant" count with no error to catch; the wrong number looks
completely plausible and flows into downstream figures and tables.

Exclude non-numeric/missing rows explicitly before comparing:

- Prefer Python/R with real NA handling (pandas
\`dropna(subset=["padj"])\`, R \`!is.na()\` / \`na.rm\`) — the reliable
default for anything past a trivial one-column filter.
- In awk, guard the column so non-numeric values can't reach the
comparison. \`awk '$7 != "NA" && $7+0 < 0.05'\` only excludes the
literal \`NA\`; the header, blanks, \`.\`, and \`NaN\` still coerce to
\`0\`, so validate the field is actually numeric (or skip the header
and the known sentinels) rather than trusting \`$7+0\` alone.

Legitimate numeric filtering is fine, and a column with a deliberate
zero-imputation convention is the user's call — the rule is only that a
*missing* value must be dropped, not silently coerced to \`0\`, unless
the user has defined how to impute it. After filtering, sanity-check the
surviving row count against expectation (the row/object-count check
above); an implausible jump usually means a sentinel slipped through.

### Notebook requirement

Every new plan step should include a concrete \`Verification:\` sub-bullet
Expand Down
18 changes: 18 additions & 0 deletions tests/verification-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ describe("buildVerificationDisciplineBlock", () => {
expect(ctx).toContain("element count");
});

it("warns against coercing missing values when filtering significance tables", () => {
const ctx = buildVerificationDisciplineBlock();
// Normalize whitespace so these load-bearing phrases survive prose reflow.
const flat = ctx.replace(/\s+/g, " ");

expect(flat).toContain("Filtering significance tables — never coerce missing values");
expect(flat).toContain('`"NA"+0` is `0`');
expect(flat).toContain("$7+0 < 0.05");
expect(flat).toContain('writes `NA` in `padj`');
expect(flat).toContain('awk \'$7 != "NA" && $7+0 < 0.05\'');
// The awk example must flag its own limitation (it only catches literal NA).
expect(flat).toContain("only excludes the literal `NA`");
expect(flat).toContain('dropna(subset=["padj"])');
expect(flat).toContain("Legitimate numeric filtering is fine");
expect(flat).toContain("zero-imputation convention is the user's call");
expect(flat).toContain("sanity-check the surviving row count");
});

it("is wired into the assembled system prompt", async () => {
const handlers = new Map<string, (event: unknown, ctx: unknown) => Promise<unknown>>();
const pi = {
Expand Down
Loading