Skip to content
Open
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
1 change: 1 addition & 0 deletions .kiro/specs/natural-language-time/.config.kiro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"specId": "0b3eeac1-e28d-4bed-a76f-0c3388610c53", "workflowType": "requirements-first", "specType": "feature"}
170 changes: 170 additions & 0 deletions .kiro/specs/natural-language-time/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Requirements Document

## Introduction

The `meeting-alerter` CLI in this repository accepts a calendar window via the `--minutes`, `--day`, `--from`, and `--to` flags. The accepted inputs are strict: integer minutes, `today` / `tomorrow` / `YYYY-MM-DD` for `--day`, and RFC3339-ish stamps for `--from` / `--to`. The user wants to be able to express a window in plain English from the terminal — phrases like `3 days from now`, `this week`, `monday through friday`, `next 4 hours`, and `rest of the week`.

This spec adds a hand-rolled natural-language time parser at `internal/whenparse`, plumbs it through the existing `meeting-alerter list` flags, and introduces a new `--when` flag that accepts a range phrase as a single argument. The parser is intentionally bounded — a closed grammar of phrases the user actually types — and lives behind a small interface so a smarter implementation (e.g. an LLM-backed fallback) can be slotted in later without changing callers. Initial scope is `meeting-alerter`; `work-cal-sync` is a candidate consumer once the package is stable.

The parser must be hand-rolled with no new module dependencies (we evaluated `github.com/markusmobius/go-dateparser` and concluded the binary cost and lack of range support did not justify it). All parsing happens in `time.Local`. Week boundaries are Sunday-anchored to match the user's iCloud configuration.

## Glossary

- **whenparse**: The new Go package at `internal/whenparse` that exposes `ParseInstant` and `ParseRange`.
- **Instant**: A single point in time, returned as `time.Time` in `time.Local`.
- **Range**: A half-open `[start, end)` interval expressed as two `time.Time` values in `time.Local`.
- **Day_Range**: A range whose start is local midnight of day D and whose end is local midnight of day D+1.
- **Week_Range**: A range that begins at local midnight on a Sunday and ends at local midnight on the following Sunday (seven full local days).
- **ParseInstant**: The function `whenparse.ParseInstant(s string, now time.Time) (time.Time, error)`.
- **ParseRange**: The function `whenparse.ParseRange(s string, now time.Time) (start, end time.Time, error)`.
- **Range_Provider**: The interface satisfied by `ParseRange` (`func(s string, now time.Time) (time.Time, time.Time, error)`) that downstream callers depend on. The hand-rolled parser is the default implementation.
- **meeting-alerter**: The CLI at `cmd/meeting-alerter` that consumes the parser via its `list` subcommand flags.
- **list_subcommand**: The `meeting-alerter list` subcommand and its flag parser (currently `parseListWindowAt` in `cmd/meeting-alerter/main.go`).
- **Window_Flags**: The flag set on `list_subcommand` covering `--minutes`, `--day`, `--from`, `--to`, and (new) `--when`.
- **Weekday_Name**: A case-insensitive English weekday word: `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`.
- **Unit_Word**: A case-insensitive duration unit: `minute`/`minutes`, `hour`/`hours`, `day`/`days`, `week`/`weeks`.
- **Number_Word**: A small English number: `one` through `ten`, treated as the integers 1 through 10. Numeric digits are also accepted.

## Requirements

### Requirement 1: Instant Parser

**User Story:** As a CLI user, I want to type a single point in time as plain English, so that I can pass it to `--from` or `--to` without remembering RFC3339.

#### Acceptance Criteria

1. WHEN `ParseInstant("now", now)` is called, THE whenparse SHALL return a time equal to `now`.
2. WHEN `ParseInstant("today", now)` is called, THE whenparse SHALL return local midnight at the start of `now`'s calendar day.
3. WHEN `ParseInstant("tomorrow", now)` is called, THE whenparse SHALL return local midnight at the start of the day after `now`.
4. WHEN `ParseInstant("yesterday", now)` is called, THE whenparse SHALL return local midnight at the start of the day before `now`.
5. WHEN the input matches `in N <Unit_Word>` or `N <Unit_Word> from now` (where `N` is a positive integer or `Number_Word`), THE whenparse SHALL return `now` plus N units.
6. WHEN the input matches `N <Unit_Word> ago`, THE whenparse SHALL return `now` minus N units.
7. WHEN the input is a `Weekday_Name`, THE whenparse SHALL return local midnight of the next occurrence of that weekday strictly after `now`'s calendar day.
8. WHEN the input matches `today at <time-of-day>` or `tomorrow at <time-of-day>` (where time-of-day is `HH:MM` 24-hour or `H[:MM][am|pm]` 12-hour), THE whenparse SHALL return that wall-clock time on the corresponding local day.
9. WHEN the input is `YYYY-MM-DD`, THE whenparse SHALL return local midnight at the start of that date.
10. WHEN the input is `YYYY-MM-DDTHH:MM` or `YYYY-MM-DD HH:MM` without a zone, THE whenparse SHALL return that wall-clock time interpreted in `time.Local`.
11. WHEN the input is a full RFC3339 timestamp, THE whenparse SHALL return the time it represents converted to `time.Local`.
12. THE whenparse SHALL accept input with surrounding whitespace and SHALL match keywords case-insensitively.
13. IF the input does not match any supported instant phrase, THEN THE whenparse SHALL return a non-nil error whose message names the input and lists at least one alternative phrasing.

### Requirement 2: Range Parser

**User Story:** As a CLI user, I want to express a calendar window as a single phrase, so that I can ask `meeting-alerter list --when "this week"` instead of computing two timestamps.

#### Acceptance Criteria

1. WHEN the input is a phrase that `ParseInstant` resolves to a calendar day (`today`, `tomorrow`, `yesterday`, a `Weekday_Name`, or `YYYY-MM-DD`), THE whenparse SHALL return a `Day_Range` covering that local day.
2. WHEN the input matches `next N <Unit_Word>` (with N a positive integer or `Number_Word`), THE whenparse SHALL return `[now, now + N units)` and SHALL NOT round to a day boundary.
3. WHEN the input is `this week`, THE whenparse SHALL return the `Week_Range` containing `now`.
4. WHEN the input is `next week`, THE whenparse SHALL return the `Week_Range` immediately after the one containing `now`.
5. WHEN the input is `last week`, THE whenparse SHALL return the `Week_Range` immediately before the one containing `now`.
6. WHEN the input is `rest of the week`, THE whenparse SHALL return `[now, end_of_saturday)` where `end_of_saturday` is local midnight at the start of the Sunday that ends `now`'s `Week_Range`.
7. WHEN the input matches `<Weekday_A> through <Weekday_B>`, THE whenparse SHALL return `[start_of_A, end_of_B)` where both endpoints are anchored to the `Week_Range` containing `now` (with `start_of_A` snapped to the Sunday-anchored start and `end_of_B` snapped to the Saturday-anchored end of that week).
8. WHEN the input matches `until <Weekday_Name>` or `until <YYYY-MM-DD>`, THE whenparse SHALL return `[now, end_of_that_day)` where `end_of_that_day` is local midnight at the start of the day after the named day.
9. WHEN the input matches `<instant_A> through <instant_B>`, THE whenparse SHALL parse each side via `ParseInstant` and return `[A, B)`.
10. IF a successfully parsed range has `end <= start`, THEN THE whenparse SHALL return a non-nil error identifying the offending phrase.
11. IF the input does not match any supported range phrase, THEN THE whenparse SHALL return a non-nil error whose message names the input and lists at least one alternative phrasing.
12. THE whenparse SHALL accept input with surrounding whitespace and SHALL match keywords case-insensitively.

### Requirement 3: List Subcommand Flag Plumbing

**User Story:** As a `meeting-alerter` user, I want the existing `--from` / `--to` / `--day` flags to accept the new phrases plus a new `--when` flag for full ranges, so that I have one consistent vocabulary for time windows.

#### Acceptance Criteria

1. WHEN `--from <value>` is supplied to `list_subcommand`, THE list_subcommand SHALL resolve `<value>` via `ParseInstant` and use the result as the window start.
2. WHEN `--to <value>` is supplied to `list_subcommand`, THE list_subcommand SHALL resolve `<value>` via `ParseInstant` and use the result as the window end.
3. WHEN `--day <value>` is supplied to `list_subcommand`, THE list_subcommand SHALL resolve `<value>` via `ParseRange` and use the resulting `[start, end)` as the window.
4. WHEN `--when <value>` is supplied to `list_subcommand`, THE list_subcommand SHALL resolve `<value>` via `ParseRange` and use the resulting `[start, end)` as the window.
5. WHEN `--minutes N` is supplied to `list_subcommand`, THE list_subcommand SHALL require `N` to be a non-negative integer literal and SHALL reject any non-integer value with an error.
6. WHEN no `Window_Flags` are supplied, THE list_subcommand SHALL use `[now, now + cfg.LookaheadMinutes)` as the window (preserving existing default behavior).
7. IF the resolved window has `end <= start` after resolving any combination of `Window_Flags`, THEN THE list_subcommand SHALL exit with a non-zero status and SHALL print an error identifying the offending flag values.

### Requirement 4: Mutual Exclusion Between Window Flags

**User Story:** As a `meeting-alerter` user, I want clear feedback when I combine flags that disagree, so that I don't get a silently wrong window.

#### Acceptance Criteria

1. IF `--minutes` is supplied together with any of `--from`, `--to`, `--day`, or `--when`, THEN THE list_subcommand SHALL exit with a non-zero status and SHALL print an error naming the conflicting flags.
2. IF `--day` is supplied together with `--from`, `--to`, or `--when`, THEN THE list_subcommand SHALL exit with a non-zero status and SHALL print an error naming the conflicting flags.
3. IF `--when` is supplied together with `--from` or `--to`, THEN THE list_subcommand SHALL exit with a non-zero status and SHALL print an error naming the conflicting flags.
4. IF exactly one of `--from` and `--to` is supplied (without `--day`, `--when`, or `--minutes`), THEN THE list_subcommand SHALL exit with a non-zero status and SHALL print an error stating that `--from` and `--to` must be supplied together.
5. WHEN `--from` and `--to` are supplied together (without other `Window_Flags`), THE list_subcommand SHALL accept the combination.

### Requirement 5: Actionable Error Messages

**User Story:** As a CLI user, I want a parse failure to tell me what I typed and suggest a phrase that would have worked, so that I can fix my command without consulting docs.

#### Acceptance Criteria

1. WHEN `ParseInstant` returns an error, THE error message SHALL include the raw input string in quotes.
2. WHEN `ParseRange` returns an error, THE error message SHALL include the raw input string in quotes.
3. WHEN `ParseInstant` returns an error, THE error message SHALL include at least one example of a supported instant phrase.
4. WHEN `ParseRange` returns an error, THE error message SHALL include at least one example of a supported range phrase.
5. WHEN `list_subcommand` rejects a `Window_Flags` value, THE error message printed to stderr SHALL include the flag name, the offending value, and an example of valid input.

### Requirement 6: Test Coverage

**User Story:** As a maintainer, I want a single table-driven test suite that exercises every documented phrase, so that regressions in the parser are caught immediately.

#### Acceptance Criteria

1. THE whenparse SHALL include a table-driven test that pins `now` to a fixed local timestamp and SHALL contain at least 30 distinct phrase cases covering each acceptance criterion in Requirements 1 and 2.
2. WHEN a test case in the table specifies an expected `[start, end)`, THE test SHALL assert that both bounds match exactly.
3. WHEN a test case in the table specifies an expected error, THE test SHALL assert that an error is returned and that the error message contains the expected substring.
4. THE whenparse SHALL include cases that exercise every Requirement 1 acceptance criterion (instants) and every Requirement 2 acceptance criterion (ranges).
5. THE list_subcommand tests SHALL include at least one case per mutual-exclusion rule in Requirement 4.

### Requirement 7: Documentation Updates

**User Story:** As a new user, I want the README and user guide to show natural-language examples first, so that I learn the friendly syntax before the strict one.

#### Acceptance Criteria

1. THE `docs/user-guide.md` SHALL include a section showing at least the eight phrase examples called out in the spec request (`3 days from now`, `two days from now through the end of the week`, `tomorrow` paired with `three days from now`, `this week`, `monday through friday`, `next 4 hours`, `rest of the week`, `until friday`).
2. THE `cmd/meeting-alerter/main.go` `printUsage` text and `listUsage` constant SHALL document `--when` and SHALL describe `--from`, `--to`, and `--day` as accepting natural-language phrases.
3. THE updated documentation SHALL state that `--minutes` remains integer-only.
4. THE updated documentation SHALL state that all parsing happens in the local time zone and that week boundaries are Sunday-anchored.

### Requirement 8: Dependency and Platform Constraints

**User Story:** As the project maintainer, I want this feature implemented without growing the dependency surface, so that the binary stays small and the build stays simple.

#### Acceptance Criteria

1. THE whenparse SHALL be implemented using only the Go standard library.
2. THE `go.mod` and `go.sum` SHALL NOT gain any new direct or transitive dependencies as a result of this change.
3. THE whenparse SHALL build and pass tests on macOS (the only supported platform for this repository).
4. THE whenparse SHALL perform all time arithmetic in `time.Local` and SHALL anchor week boundaries to Sunday.

### Requirement 9: Range_Provider Interface for Future Extension

**User Story:** As the future-me adding an LLM-backed fallback, I want callers to depend on an interface rather than the concrete parser, so that I can swap in a smarter implementation without touching call sites.

#### Acceptance Criteria

1. THE whenparse SHALL expose a `Range_Provider` function type whose signature matches `ParseRange`.
2. THE whenparse SHALL expose an `Instant_Provider` function type whose signature matches `ParseInstant`.
3. THE list_subcommand SHALL accept its parser dependencies as values of these function types (defaulting to `whenparse.ParseRange` and `whenparse.ParseInstant`) so tests and future callers can substitute alternative implementations.

## TODO / Future Direction

The following items are explicitly out of scope for this spec and are listed here so they are not lost when the implementation lands.

### TODO-1: Reusable Kiro Skill

Package the natural-language parser as a reusable Kiro skill (or equivalent shared module) so any other tool in this workspace can call it without copying code or duplicating the grammar. The skill would expose `ParseInstant` and `ParseRange` over whatever transport the host provides, and would carry the same documentation and test fixtures as the Go package.

### TODO-2: LLM-Backed Higher-Tier Fallback

Add an LLM-backed implementation of `Range_Provider` (and `Instant_Provider`) that runs only when the hand-rolled parser returns an error. The LLM would interpret novel phrasings such as "the day before my anniversary" or "before lunch" and return a structured `{start, end}` JSON, which the host would then validate and convert to `time.Time` values. Constraints to design toward:

- The LLM call SHALL be opt-in (config flag or env var) so the default build has no network dependency.
- The LLM call SHALL be cached by `(input, now-truncated-to-minute)` so repeated invocations do not re-query.
- The LLM response SHALL be validated against the same `end > start` invariant the hand-rolled parser enforces before being handed to callers.
- The LLM implementation SHALL be a drop-in `Range_Provider` so the rest of the system does not need to know whether a phrase was resolved locally or remotely.

### TODO-3: `work-cal-sync` Adoption

Once the parser is stable in `meeting-alerter`, plumb `--from` / `--to` / `--when` into `cmd/work-cal-sync` (e.g. for a future ad-hoc range-sync mode) so both CLIs share the same vocabulary.