ANW-43 CLI: add a query subcommand writing the /query JSON offline

The README claims the query engine is available offline, but only the
merge half was: no subcommand produced the path / frontmatter /
last_modified / etag / size projection that GET /query returns.

`anwesen query` takes the same --vault and --query flags as merge and
writes that document to stdout, compact, one line. src/merge.rs becomes
src/oneshot.rs and holds both: one parse-and-load path, so the two
subcommands cannot drift in grammar or in how strictly they read a
vault. cli.rs shares one VaultQueryArgs between them for the same
reason. rfc3339_z moves from http.rs to query.rs, next to the projection
whose timestamp dialect it is.

Assumed a trailing newline on stdout is wanted here even though merge
writes none: the JSON body is one line for jq and shell pipelines, and
the newline terminates it rather than joining the document. Flag if
wrong.

The hurl harness now compares `anwesen query` against GET /query on the
fixture vault for four queries before running the suite, so a drift
between the two surfaces fails CI.
This commit is contained in:
Andreas Brenner 2026-07-31 15:52:50 +03:00
parent 30bca24b5c
commit 6e07bff08a
9 changed files with 489 additions and 228 deletions

View file

@ -18,7 +18,7 @@
use std::collections::BTreeMap;
use chrono::{DateTime, NaiveDate};
use chrono::{DateTime, NaiveDate, SecondsFormat, Utc};
use regex::Regex;
use serde::Serialize;
use serde_json::Value as JsonValue;
@ -280,6 +280,16 @@ fn hex_value(b: u8) -> Option<u8> {
}
}
/// Canonical RFC 3339 form with a `Z` suffix -- the shape the User Manual
/// example uses for `last_modified`. It lives next to [`ResultEntry`] because
/// it is the projection's dialect: both the HTTP handlers and the offline
/// `anwesen query` subcommand ([ANW-43]) format timestamps with it, so the
/// two surfaces cannot drift.
#[must_use]
pub fn rfc3339_z(dt: DateTime<Utc>) -> String {
dt.to_rfc3339_opts(SecondsFormat::Secs, true)
}
/// One result row in the `/query` response. Per User Manual the body is
/// elided -- consumers fetch bodies via `/notes/<path>` if needed.
#[derive(Debug, Serialize)]