Ship happy-hour quota-preference windows and default user config (issue #69) #70

Merged
adrian.tkacz merged 2 commits from feature/happy-hours-default-config into develop 2026-09-12 17:47:51 +00:00
Member

Summary

Implements issue #69: happy-hour quota-preference windows and a single default user configuration location, reviewed as one bounded owner-workflow change.

1. Happy hours (D-035)

WeeklyHappyHourRule mirrors the blackout rule mechanics exactly — exact AvailabilityTarget identity matching, explicit IANA zone, canonical mon–sun weekday vocabulary, strict HH:MM wall times, half-open [start, end) local intervals, cross-midnight support — plus optional inclusive start_date/end_date bounds because vendor campaigns are limited-time. Both schedule kinds share one matcher implementation, so boundaries can never drift.

Semantics (never an eligibility bypass):

  • During an active window, matching candidates form a preferred ranking group after the capacity knowledge class and before the scarcity penalty: a preferred candidate outranks a healthier full-price one, because its marginal quota cost in the window is zero/discounted.
  • Blackout, hard constraints, capability sufficiency, exhaustion/unknown policy and reservations all still gate first; unknown-capacity candidates stay separated by the knowledge class; exhaustion still excludes (campaign small print is why "free" never fabricates capacity); telemetry, scarcity and capability are never rewritten.
  • First matching rule in canonical rule_id order decides; blackout wins over an overlapping happy hour by stage order; the decision and --explain output name the governing rule and reason code.

Serialized compatibility: UserPolicy gains the additive optional member happy_hours; documents without it round-trip byte-identically, and an empty set is never serialized. Envelopes and the SelectionDecision field set are unchanged.

examples/selector-policy.json now also carries the GLM-5.3-Flash campaign rule (zai-flash-campaign-night-sgt, daily 23:00–09:00 Asia/Singapore, 2026-09-03 → 2026-09-20, target zai/glm-5.3-flash), matching the owner-provided campaign text.

2. Default configuration location (D-036)

Exactly one config location: $(XDG_CONFIG_HOME or ~/.config)/scarcity-router/selector-policy.json.

  • Provisioned from the audited example on first use by every surface (uv tool install has no post-install hook, so first use is the effective installation step) and via the new scarcity-router install-config [--force] command; an existing file is never silently overwritten; dir 0o700, file 0o600, no credentials ever.
  • Precedence — CLI: explicit --selector-policy > default config > neutral (--neutral-policy ignores the config for one run). MCP/REST: a request that supplies selector_policy wins; an omitted one runs under the server-configured default (ApplicationDependencies.default_policy), neutral when none resolves. No flags need to be added to the MCP command.
  • Provisioning/loading failures degrade to neutral with one concise stderr warning; a config file that exists but fails to load is a loud configuration error.
  • New module scarcity_router/config.py; the wheel packages the example via the D-034 force-include mechanism as scarcity_router/default-selector-policy.json (no committed duplicate; sdist gains /examples/selector-policy.json; the package-check examples/ tripwire gains exactly this one allowlisted exception, recorded in D-036).

Validation

  • uv sync --only-dev, make check (972 tests OK), uv run basedpyright (0 errors / 0 warnings / 0 notes), git diff --check — all clean.
  • make package-check passes end to end (wheel/sdist contents, isolated uv tool install, installed resource loading, MCP discovery, REST smoke).
  • New tests: tests/test_happy_hours.py (schedule semantics, date bounds, ranking integration incl. blackout-overlap, gating and serialization), tests/test_config.py (XDG resolution, idempotent provisioning, permissions, precedence end-to-end, install-config). CLI-touching test modules run against a throwaway XDG_CONFIG_HOME so the suite never reads or provisions the host config.

Governance

  • Decisions recorded: D-035 (happy-hour windows; alternatives rejected: inverse-blackout hard block, telemetry-side quota discounts, weak soft bonus) and D-036 (config location, provisioning, write-access security note, machine-interface default refinement).
  • Docs updated: selection-policy.md (new happy-hour section + amended ranking order), README.md (default configuration + availability policy), machine-interfaces.md, security.md, architecture.md, AGENTS.md untouched.
  • No catalog ratings change, no provider change, no envelope schema-version change, no new network exposure.

Workers/reviewers never merge: PR left open for human review.

## Summary Implements issue #69: **happy-hour quota-preference windows** and a **single default user configuration location**, reviewed as one bounded owner-workflow change. ## 1. Happy hours (D-035) `WeeklyHappyHourRule` mirrors the blackout rule mechanics exactly — exact `AvailabilityTarget` identity matching, explicit IANA zone, canonical `mon`–`sun` weekday vocabulary, strict `HH:MM` wall times, half-open `[start, end)` local intervals, cross-midnight support — plus **optional inclusive `start_date`/`end_date` bounds** because vendor campaigns are limited-time. Both schedule kinds share one matcher implementation, so boundaries can never drift. Semantics (never an eligibility bypass): - During an active window, matching candidates form a **preferred ranking group after the capacity knowledge class and before the scarcity penalty**: a preferred candidate outranks a healthier full-price one, because its marginal quota cost in the window is zero/discounted. - Blackout, hard constraints, capability sufficiency, exhaustion/unknown policy and reservations all still gate first; unknown-capacity candidates stay separated by the knowledge class; exhaustion still excludes (campaign small print is why "free" never fabricates capacity); telemetry, scarcity and capability are never rewritten. - First matching rule in canonical `rule_id` order decides; blackout wins over an overlapping happy hour by stage order; the decision and `--explain` output name the governing rule and reason code. Serialized compatibility: `UserPolicy` gains the additive optional member `happy_hours`; documents without it round-trip byte-identically, and an empty set is never serialized. Envelopes and the `SelectionDecision` field set are unchanged. `examples/selector-policy.json` now also carries the GLM-5.3-Flash campaign rule (`zai-flash-campaign-night-sgt`, daily 23:00–09:00 Asia/Singapore, 2026-09-03 → 2026-09-20, target `zai/glm-5.3-flash`), matching the owner-provided campaign text. ## 2. Default configuration location (D-036) Exactly one config location: `$(XDG_CONFIG_HOME or ~/.config)/scarcity-router/selector-policy.json`. - Provisioned from the audited example on first use by every surface (`uv tool install` has no post-install hook, so first use is the effective installation step) and via the new **`scarcity-router install-config [--force]`** command; an existing file is never silently overwritten; dir `0o700`, file `0o600`, no credentials ever. - Precedence — CLI: explicit `--selector-policy` > default config > neutral (`--neutral-policy` ignores the config for one run). MCP/REST: a request that supplies `selector_policy` wins; an omitted one runs under the server-configured default (`ApplicationDependencies.default_policy`), neutral when none resolves. **No flags need to be added to the MCP command.** - Provisioning/loading failures degrade to neutral with one concise stderr warning; a config file that exists but fails to load is a loud configuration error. - New module `scarcity_router/config.py`; the wheel packages the example via the D-034 force-include mechanism as `scarcity_router/default-selector-policy.json` (no committed duplicate; sdist gains `/examples/selector-policy.json`; the package-check `examples/` tripwire gains exactly this one allowlisted exception, recorded in D-036). ## Validation - `uv sync --only-dev`, `make check` (972 tests OK), `uv run basedpyright` (0 errors / 0 warnings / 0 notes), `git diff --check` — all clean. - `make package-check` passes end to end (wheel/sdist contents, isolated `uv tool install`, installed resource loading, MCP discovery, REST smoke). - New tests: `tests/test_happy_hours.py` (schedule semantics, date bounds, ranking integration incl. blackout-overlap, gating and serialization), `tests/test_config.py` (XDG resolution, idempotent provisioning, permissions, precedence end-to-end, `install-config`). CLI-touching test modules run against a throwaway `XDG_CONFIG_HOME` so the suite never reads or provisions the host config. ## Governance - Decisions recorded: **D-035** (happy-hour windows; alternatives rejected: inverse-blackout hard block, telemetry-side quota discounts, weak soft bonus) and **D-036** (config location, provisioning, write-access security note, machine-interface default refinement). - Docs updated: `selection-policy.md` (new happy-hour section + amended ranking order), `README.md` (default configuration + availability policy), `machine-interfaces.md`, `security.md`, `architecture.md`, `AGENTS.md` untouched. - No catalog ratings change, no provider change, no envelope schema-version change, no new network exposure. Workers/reviewers never merge: PR left open for human review.
Author
Member

Self-review remediation pushed (aee0c24) — the end-user self-review found no merge blockers; per owner go-ahead, the three highest-value findings are folded into this PR as the one remediation round:

  1. First provisioning is no longer silent. The run that implicitly creates ~/.config/scarcity-router/selector-policy.json prints note: provisioned default user config: <path> once on stderr (JSON stdout stays clean; MCP/REST announce through the same channel at process start; existing files are never announced).
  2. Compact output now explains a happy-hour win. render_select_human prints one Happy hour: rule … (reason) line for the selected candidate, so the "why did it pick this?" question no longer requires --explain/--json.
  3. Expired campaigns are visible. SelectionDecision gains the additive optional member expired_happy_hour_rules (serialized only when non-empty): rules whose weekly window would cover the evaluated instant but whose date bounds exclude it. --explain lists them under "Expired happy-hour rules" — after 2026-09-20 the campaign rule announces its own expiry instead of silently going inert. Explanation-only; no ranking or eligibility effect.

Remaining self-review items stay DEFER (target-typo validation would change policy-failure behavior; provisioning race degrades one run with a warning; campaign end-boundary nuance is documented).

Validation re-run: 978 tests OK, basedpyright 0/0/0, git diff --check clean, make package-check green end to end. D-035/D-036 amended in place (still unmerged, same PR).

**Self-review remediation pushed (`aee0c24`)** — the end-user self-review found no merge blockers; per owner go-ahead, the three highest-value findings are folded into this PR as the one remediation round: 1. **First provisioning is no longer silent.** The run that implicitly creates `~/.config/scarcity-router/selector-policy.json` prints `note: provisioned default user config: <path>` once on stderr (JSON stdout stays clean; MCP/REST announce through the same channel at process start; existing files are never announced). 2. **Compact output now explains a happy-hour win.** `render_select_human` prints one `Happy hour: rule … (reason)` line for the selected candidate, so the "why did it pick this?" question no longer requires `--explain`/`--json`. 3. **Expired campaigns are visible.** `SelectionDecision` gains the additive optional member `expired_happy_hour_rules` (serialized only when non-empty): rules whose weekly window would cover the evaluated instant but whose date bounds exclude it. `--explain` lists them under "Expired happy-hour rules" — after 2026-09-20 the campaign rule announces its own expiry instead of silently going inert. Explanation-only; no ranking or eligibility effect. Remaining self-review items stay **DEFER** (target-typo validation would change policy-failure behavior; provisioning race degrades one run with a warning; campaign end-boundary nuance is documented). Validation re-run: 978 tests OK, basedpyright 0/0/0, `git diff --check` clean, `make package-check` green end to end. D-035/D-036 amended in place (still unmerged, same PR).
adrian.tkacz deleted branch feature/happy-hours-default-config 2026-09-12 17:47:51 +00:00
Sign in to join this conversation.
No description provided.