From 9d8d6519f80d2af56bcf4ee9e3797ce949845817 Mon Sep 17 00:00:00 2001 From: Terastar-Paperclip <279373186+Terastar-Paperclip@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:40:45 -0500 Subject: [PATCH 1/2] feat(compat): check resolved local date windows --- CONTRACT.md | 13 ++++++++++++- compat/README.md | 16 ++++++++++++++-- compat/dates/dates.go | 28 ++++++++++++++++++++++++++++ compat/dates/dates_test.go | 6 ++++++ compat/internal/stubcli/main.go | 17 +++++++++++++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/CONTRACT.md b/CONTRACT.md index 9fed9b8..a233c93 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -63,7 +63,18 @@ Every subcommand that selects a window of data accepts: | Absolute | `2026-04-15` | Local midnight that calendar day | | Relative | `7d`, `4w`, `6m`, `1y` | N units before today's local midnight | -Window semantics are half-open `[since, until_exclusive)` internally, but `--until` is inclusive of the named day from the user's point of view: the CLI adds 24h to the parsed value before filtering, so `--until 2026-04-15` includes all 24 hours of April 15. +Window semantics are half-open `[since, until_exclusive)` internally, but `--until` is inclusive of the named day from the user's point of view: the exclusive bound is the next local calendar midnight. A local day may be 23 or 25 hours when daylight saving time changes. + +### Resolved window diagnostic + +Every window-selecting subcommand accepts `--print-resolved-window` (default +false). When set, it resolves its normal date arguments and defaults, prints +exactly `since=VALUE until_exclusive=VALUE` followed by a newline, and exits +without credentials or network requests. Each finite VALUE is an RFC3339 +timestamp with its local offset; an absent bound is `unbounded`. The reported +half-open window must describe the same dates used by the export path. For an +inclusive calendar-day API, the exclusive end represents the following local +midnight. Invalid dates fail with a nonzero exit and no diagnostic on stdout. Relative durations snap to local midnight, **not** "this many days before the current instant". `--since 30d` returns the same window whether you run it at 9am or 11pm. diff --git a/compat/README.md b/compat/README.md index f6c8a90..9f85d47 100644 --- a/compat/README.md +++ b/compat/README.md @@ -154,12 +154,12 @@ formats.RunContract(t, compat.Runner{ ## What it does NOT cover yet -The actual local-midnight semantics of `--since 2026-04-15` (the harmonization that just landed across crono/liftoff/withings) is still **human-attested** in the status table. Asserting it black-box requires either: +The actual local-midnight semantics of `--since 2026-04-15` remains **human-attested** until exporters adopt the resolved-window check in CI. Black-box coverage can use either: - a `--print-resolved-window`-style affordance on every CLI (a substantive contract change, intentionally out of scope of the first compat-test cut), or - per-upstream recorded HTTP fixtures (heavy, and pinned to specific API shapes). -When that affordance lands, the test belongs here as `dates.LocalMidnightSemantics`. +The opt-in `dates.RunResolvedWindow` check below supports the first approach. ## Adding a new contract test @@ -175,3 +175,15 @@ This module has its own tests that run each bundle against a stub CLI in `intern The stub has two modes (`STUBCLI_MODE=flat` and `STUBCLI_MODE=cobra`). The flat-mode self-tests exercise the original Runner shape; the cobra-mode self-tests exercise `Subcommands`-based dispatch. In cobra mode, the stub's root `--help` deliberately omits `--since/--until/--format`, so the cobra-mode self-tests fail fast if `compat.Runner` ever stops prepending the subcommand. The stub emits an empty data set per `--format` codec (`[]` for json, a single header row for csv, nothing for markdown) so the formats bundle's data-path subtests run hermetically. There is also a focused unit test for `Runner.WithSubcommand` using an `argecho` helper that just prints `os.Args`. The stub additionally honors `STUBCLI_FORMATS` (comma-separated) to restrict which codecs it will accept at parse time — used by the formats bundle's `TestRunContract_PartialCodec_SkipsCSV` self-test. Setting `STUBCLI_FORMATS=markdown,json` makes the stub reject `--format csv`, so the partial-codec self-test proves the `SupportedFormats` skip is actually load-bearing: without it, the test would fail at `CSVHasHeader` because the stub rejects csv. + +## Proposed resolved-window diagnostic + +Exporters adopting the proposed `--print-resolved-window` flag can call +`dates.RunResolvedWindow(t, runner)` separately from `RunContract` while +rollout is in progress. The command prints exactly one line: +`since=VALUE until_exclusive=VALUE`, followed by a newline. Finite bounds are +RFC3339 timestamps in local time; absent bounds are `unbounded`. The the upper bound is the next local calendar midnight for an +explicit inclusive `--until`. It exits successfully without credentials or +network access. The diagnostic must use the same resolved dates as the +export path. These checks cover parsing and the reported window, not API +requests or data filtering, which need exporter-specific tests. diff --git a/compat/dates/dates.go b/compat/dates/dates.go index 394e6de..8991300 100644 --- a/compat/dates/dates.go +++ b/compat/dates/dates.go @@ -216,3 +216,31 @@ func noNetworkEnv() []string { "TZ=UTC", } } + +// RunResolvedWindow checks the optional offline window diagnostic on each +// declared command. Call it separately while exporters adopt the proposal. +func RunResolvedWindow(t *testing.T, r compat.Runner) { + t.Helper() + subs := r.Subcommands + if len(subs) == 0 { + subs = []string{""} + } + cases := []struct{ zone, day, start, end string }{ + {"Pacific/Honolulu", "2026-04-15", "2026-04-15T00:00:00-10:00", "2026-04-16T00:00:00-10:00"}, + {"Pacific/Kiritimati", "2026-04-15", "2026-04-15T00:00:00+14:00", "2026-04-16T00:00:00+14:00"}, + {"America/New_York", "2026-03-08", "2026-03-08T00:00:00-05:00", "2026-03-09T00:00:00-04:00"}, + {"America/New_York", "2026-11-01", "2026-11-01T00:00:00-04:00", "2026-11-02T00:00:00-05:00"}, + } + for _, sub := range subs { + for _, tc := range cases { + t.Run(sub+"/"+tc.zone+"/"+tc.day, func(t *testing.T) { + runner := r.WithSubcommand(sub).WithEnv(noNetworkEnv()...).WithEnv("TZ=" + tc.zone) + res := runner.MustRun(t, "--since", tc.day, "--until", tc.day, "--print-resolved-window") + want := "since=" + tc.start + " until_exclusive=" + tc.end + "\n" + if res.ExitCode != 0 || res.StdoutString() != want || res.StderrString() != "" { + t.Errorf("exit=%d stdout=%q stderr=%q; want exit 0 stdout=%q and empty stderr", res.ExitCode, res.StdoutString(), res.StderrString(), want) + } + }) + } + } +} diff --git a/compat/dates/dates_test.go b/compat/dates/dates_test.go index e0ab7f1..8dabff9 100644 --- a/compat/dates/dates_test.go +++ b/compat/dates/dates_test.go @@ -54,3 +54,9 @@ func buildStub(t *testing.T) string { } return out } + +func TestResolvedWindowAgainstStub(t *testing.T) { + bin := buildStub(t) + dates.RunResolvedWindow(t, compat.Runner{Binary: bin}) + dates.RunResolvedWindow(t, compat.Runner{Binary: bin, Env: []string{"STUBCLI_MODE=cobra"}, Subcommands: []string{"biometrics"}}) +} diff --git a/compat/internal/stubcli/main.go b/compat/internal/stubcli/main.go index e23b0be..ce8df0a 100644 --- a/compat/internal/stubcli/main.go +++ b/compat/internal/stubcli/main.go @@ -60,6 +60,7 @@ import ( "fmt" "os" "strings" + "time" ) const cobraSubcommand = "biometrics" @@ -155,6 +156,7 @@ func parseAndEmit(progName string, args []string) { fs.SetOutput(os.Stderr) since := fs.String("since", "", "inclusive lower bound") until := fs.String("until", "", "inclusive upper bound") + window := fs.Bool("print-resolved-window", false, "print resolved window") format := fs.String("format", "markdown", "output format (markdown|json|csv)") if err := fs.Parse(args); err != nil { os.Exit(2) @@ -173,6 +175,21 @@ func parseAndEmit(progName string, args []string) { os.Exit(2) } + if *window { + start, err := time.ParseInLocation("2006-01-02", *since, time.Local) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + end, err := time.ParseInLocation("2006-01-02", *until, time.Local) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + fmt.Printf("since=%s until_exclusive=%s\n", start.Format(time.RFC3339), end.AddDate(0, 0, 1).Format(time.RFC3339)) + return + } + // STUBCLI_NEEDS_TOKEN=1 simulates a credentialless-CI failure: // flags parsed cleanly, but the data call fails because no token // is available. Mirrors the shape real credentialed exporters From c37ee46fc8ff475a6fdf1abb4121524113137544 Mon Sep 17 00:00:00 2001 From: Terastar-Paperclip <279373186+Terastar-Paperclip@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:43:00 -0500 Subject: [PATCH 2/2] docs: correct resolved window description --- compat/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compat/README.md b/compat/README.md index 9f85d47..be5d7c3 100644 --- a/compat/README.md +++ b/compat/README.md @@ -182,7 +182,7 @@ Exporters adopting the proposed `--print-resolved-window` flag can call `dates.RunResolvedWindow(t, runner)` separately from `RunContract` while rollout is in progress. The command prints exactly one line: `since=VALUE until_exclusive=VALUE`, followed by a newline. Finite bounds are -RFC3339 timestamps in local time; absent bounds are `unbounded`. The the upper bound is the next local calendar midnight for an +RFC3339 timestamps in local time; absent bounds are `unbounded`. The upper bound is the next local calendar midnight for an explicit inclusive `--until`. It exits successfully without credentials or network access. The diagnostic must use the same resolved dates as the export path. These checks cover parsing and the reported window, not API