diff --git a/CHANGELOG.md b/CHANGELOG.md index d023c77..ace767e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,16 @@ ### Fixed +- **REQ-142 / #381 — s-expr filter parse error now shows the field-equality + form.** The filter dialect (shared by `query`, `list --filter`, + `export --filter`, `modify --where`) puts an operator in head position, but + the common first attempt `(status "draft")` (field name in head) failed with + only a list of head forms and no example. The `unknown head symbol` note now + states the head is an operator and shows `(= status "draft")` / + `(and (= type "requirement") (has-tag "safety"))` inline — reaching every + command that parses a filter, since the note is generated once in + `sexpr_eval`. `export --filter` help also gained an example. + - **REQ-139 — directory import no longer warns on legitimate non-artifact YAML.** The generic-YAML directory load warned `[WARN] skipping ` for *every* file it declined to load — including expected non-artifact YAML diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 1e77a90..5856093 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -3909,3 +3909,39 @@ artifacts: links: - type: traces-to target: REQ-007 + + - id: REQ-142 + type: requirement + title: "s-expr filter parse error shows the common field-equality example" + status: implemented + description: | + Self-reported via GitHub issue #381 (dogfooding). The s-expression + filter dialect (shared by `query`, `list --filter`, `export --filter`, + `modify --where`) puts an OPERATOR in head position, but the single most + common first attempt is `(status "draft")` — field name in head — which + fails with `unknown form 'status'`. The error listed every supported + head form but showed no example of the correct `(= field "value")` + shape, costing every first-time author (human or agent) a failed + round-trip + a grep through the test files to discover it. + + The `unknown head symbol` note now states the head is an operator (not a + field name) and shows the field-equality form inline: + `(= status "draft")` / `(and (= type "requirement") (has-tag "safety"))`. + Because the note is generated once in `sexpr_eval`, the improvement + reaches every command that parses a filter. The `export --filter` help + also gained an example (it previously had none, unlike `list`/`query`). + + Acceptance: + - `rivet list --filter '(status "draft")'` (or any command) prints a + note containing `(= status "draft")`. + - `cargo test -p rivet-core --lib + sexpr_eval::tests::parse_error_unknown_head_surfaces_note` passes + (asserts the example is present). + tags: [dx, agent-ux, discoverability, sexpr, user-reported, issue-381] + fields: + priority: should + category: functional + baseline: v0.15.0-track + links: + - type: traces-to + target: REQ-007 diff --git a/rivet-cli/src/main.rs b/rivet-cli/src/main.rs index 3338092..552349d 100644 --- a/rivet-cli/src/main.rs +++ b/rivet-cli/src/main.rs @@ -615,7 +615,8 @@ enum Command { #[arg(long, default_value = "rivet")] prefix: String, - /// S-expression filter to select artifact subset for export + /// S-expression filter to select artifact subset for export, + /// e.g. '(= type "requirement")' or '(and (= type "requirement") (= status "implemented"))' #[arg(long)] filter: Option, diff --git a/rivet-core/src/sexpr_eval.rs b/rivet-core/src/sexpr_eval.rs index 903ca4c..0e1a488 100644 --- a/rivet-core/src/sexpr_eval.rs +++ b/rivet-core/src/sexpr_eval.rs @@ -794,8 +794,11 @@ fn classify_filter_error(source: &str, message: &str) -> Option { let mut heads: Vec<&str> = HEADS.to_vec(); heads.sort_unstable(); return Some(format!( - "unknown head symbol; see docs/getting-started.md \ - for the supported forms ({})", + "unknown head symbol — the head is an operator, not a field name. \ + For field equality use `(= \"\")`, e.g. \ + `(= status \"draft\")` or `(and (= type \"requirement\") \ + (has-tag \"safety\"))`. See docs/getting-started.md for all \ + supported forms ({})", heads.join("/") )); } @@ -2090,6 +2093,12 @@ mod tests { for op in ["and", "or", "not", "has-tag", "linked-via"] { assert!(note.contains(op), "note should list `{op}`; got: {note}"); } + // #381: the note must show the common field-equality form so an + // author who guessed `(status "draft")` is corrected to `(= status …)`. + assert!( + note.contains("(= status \"draft\")"), + "note should show the `(= field \"value\")` example; got: {note}" + ); } /// Valid s-expression input must not carry a note — classification