aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/plans/2026-08-13-rule-builder.md21
-rw-r--r--docs/superpowers/specs/2026-08-13-rule-builder-design.md21
2 files changed, 33 insertions, 9 deletions
diff --git a/docs/superpowers/plans/2026-08-13-rule-builder.md b/docs/superpowers/plans/2026-08-13-rule-builder.md
index 4f65f7d..96fddc6 100644
--- a/docs/superpowers/plans/2026-08-13-rule-builder.md
+++ b/docs/superpowers/plans/2026-08-13-rule-builder.md
@@ -1113,6 +1113,11 @@ void TestRuleQuery::anUnrepresentableQueryRejectsWhole()
QStringLiteral("date:2026-01-01..2026-02-01"), // two-sided range
QStringLiteral("from:a.example.org xor subject:x"),
};
+ // NOT in this list: `from:((((`. It parses, as a From row whose value is
+ // the literal text `((((`, and round-trips byte for byte. That is exactly
+ // what the query means to notmuch, which treats the parens as characters
+ // to search for rather than as grouping, so the row tells the truth and
+ // rejecting it would buy nothing. See the test below.
for (const QString &query : unrepresentable) {
const RuleQuery q = RuleQuery::parse(query);
@@ -1126,11 +1131,19 @@ void TestRuleQuery::anUnrepresentableQueryRejectsWhole()
void TestRuleQuery::aMalformedQueryIsRejectedNotDiagnosed()
{
- // notmuch accepts `from:((((` cleanly and matches nothing, so there is no
- // failure to observe and a test asserting one fails against correct code.
- // The assertion is on OUR rejection only.
+ // notmuch accepts `from:((((` cleanly and matches nothing: the parens are
+ // characters it searches for, not grouping. So there is no failure to
+ // observe, and a test asserting one fails against correct code.
+ //
+ // This parser accepts it too, as a From row whose value is that literal
+ // text, which is what the query actually means. What must hold is the
+ // round trip, not a rejection: displaying it as a row and compiling it
+ // back must not alter the stored string.
const RuleQuery q = RuleQuery::parse(QStringLiteral("from:(((("));
- QVERIFY(!q.parsed);
+ QVERIFY(q.parsed);
+ QCOMPARE(q.terms.size(), 1);
+ QCOMPARE(q.terms.at(0).value, QStringLiteral("(((("));
+ QCOMPARE(q.compile(), QStringLiteral("from:(((("));
}
```
diff --git a/docs/superpowers/specs/2026-08-13-rule-builder-design.md b/docs/superpowers/specs/2026-08-13-rule-builder-design.md
index c11ef47..a571506 100644
--- a/docs/superpowers/specs/2026-08-13-rule-builder-design.md
+++ b/docs/superpowers/specs/2026-08-13-rule-builder-design.md
@@ -221,7 +221,12 @@ an empty query and must open in the builder ready to receive a row.
Everything else sets `parsed = false`: nested parens beyond that one shape,
mixed `and`/`or` without parens, `xor`, an unrecognised prefix (`body:`, `mid:`,
-`folder:`), a bare word with no prefix, a `path:` not ending in `/**`.
+`folder:`), a bare word with no prefix, a `path:` not ending in `/**`, a
+two-sided `date:` range, a trailing operator, an unterminated quote.
+
+A parenthesis **inside a value** is not a shape at all: `from:((((` is a From
+term whose value happens to contain parens, and it parses and round-trips like
+any other. Only a parenthesis in grouping position is a shape question.
### The parser is strict, and that is the safety property
@@ -334,10 +339,16 @@ terms and tests exactly the same thing.
**Rejection tests**, which carry the safety property. Queries that must set
`parsed = false` and must not partially parse: nested `or` inside `or`, mixed
-`and`/`or` without parens, `body:foo`, a bare word, a `path:` without `/**`, and
-`from:((((`. That last asserts **our** rejection, never a provoked notmuch
-failure: `CLAUDE.md` records twice that notmuch accepts it cleanly, and a test
-expecting an error there fails against correct code.
+`and`/`or` without parens, `body:foo`, a bare word, a `path:` without `/**`, a
+two-sided `date:` range, and a trailing operator.
+
+**`from:((((` is not among them, and the reason is worth stating.** notmuch
+treats those parens as characters to search for rather than as grouping, so the
+query is meaningful, matches nothing, and reports no error. This parser accepts
+it as a From row whose value is that literal text, which is what it means. The
+assertion there is the **round trip**, never a rejection and never a provoked
+notmuch failure: `CLAUDE.md` records twice that notmuch accepts it cleanly, and
+a test expecting an error fails against correct code.
**Compile tests** for every field and operator pair including both negations,
and the parenthesisation rule at its boundary: `join == Any` with zero