feat: D24 fact intervals (--as-of) and bin/stack assistant helpers. (#37)
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (push) Skipped
Tests / OCR (tesseract fixture) (push) Skipped
Tests / Release (semver) (push) Skipped
Store valid_from/valid_to on leafs and filter search by calendar day without overloading D16 source staleness; stack start/start-assistant wires brain + PicoClaw.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package facts
|
||||
|
||||
// Interval of truth for a fact leaf (D24 / OQ5). Not D16 source staleness.
|
||||
//
|
||||
// Empty valid_from and valid_to means "always" (legacy leafs). Empty asOf
|
||||
// means "do not filter". Dates compare as YYYY-MM-DD (lexicographic).
|
||||
|
||||
// NormalizeDay keeps the calendar day from ISO-8601 or bare dates.
|
||||
func NormalizeDay(s string) string {
|
||||
s = trimSpace(s)
|
||||
if len(s) >= 10 && s[4] == '-' && s[7] == '-' {
|
||||
return s[:10]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func trimSpace(s string) string {
|
||||
i, j := 0, len(s)
|
||||
for i < j && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
|
||||
i++
|
||||
}
|
||||
for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') {
|
||||
j--
|
||||
}
|
||||
return s[i:j]
|
||||
}
|
||||
|
||||
// ActiveAt reports whether a fact with [validFrom, validTo] holds at asOf.
|
||||
// validTo empty = open-ended. Both ends inclusive.
|
||||
func ActiveAt(validFrom, validTo, asOf string) bool {
|
||||
asOf = NormalizeDay(asOf)
|
||||
if asOf == "" {
|
||||
return true
|
||||
}
|
||||
from := NormalizeDay(validFrom)
|
||||
to := NormalizeDay(validTo)
|
||||
if from != "" && asOf < from {
|
||||
return false
|
||||
}
|
||||
if to != "" && asOf > to {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package facts
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestActiveAtOpenEnded(t *testing.T) {
|
||||
// works at Y from 2025-07-16, no end
|
||||
if !ActiveAt("2025-07-16", "", "2025-07-16") {
|
||||
t.Fatal("inclusive valid_from")
|
||||
}
|
||||
if !ActiveAt("2025-07-16", "", "2026-01-01") {
|
||||
t.Fatal("open-ended valid_to")
|
||||
}
|
||||
if ActiveAt("2025-07-16", "", "2025-07-15") {
|
||||
t.Fatal("before valid_from must be inactive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveAtClosedInterval(t *testing.T) {
|
||||
// works at X 2024-03-01 .. 2025-07-15
|
||||
if !ActiveAt("2024-03-01", "2025-07-15", "2025-01-01") {
|
||||
t.Fatal("mid interval")
|
||||
}
|
||||
if !ActiveAt("2024-03-01", "2025-07-15", "2024-03-01") {
|
||||
t.Fatal("inclusive start")
|
||||
}
|
||||
if !ActiveAt("2024-03-01", "2025-07-15", "2025-07-15") {
|
||||
t.Fatal("inclusive end")
|
||||
}
|
||||
if ActiveAt("2024-03-01", "2025-07-15", "2025-07-16") {
|
||||
t.Fatal("day after end")
|
||||
}
|
||||
if ActiveAt("2024-03-01", "2025-07-15", "2024-02-28") {
|
||||
t.Fatal("day before start")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveAtEmptyIntervalAlwaysTrue(t *testing.T) {
|
||||
// legacy leafs without intervals stay visible for any as-of
|
||||
if !ActiveAt("", "", "2025-01-01") {
|
||||
t.Fatal("empty interval must remain active")
|
||||
}
|
||||
if !ActiveAt("", "", "") {
|
||||
t.Fatal("no as-of means all active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActiveAtEmptyAsOfKeepsAll(t *testing.T) {
|
||||
if !ActiveAt("2099-01-01", "2099-12-31", "") {
|
||||
t.Fatal("empty as-of must not filter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsOfPickXNotY(t *testing.T) {
|
||||
// Acceptance from #36: as of 2025-01-01 → X, not Y
|
||||
xFrom, xTo := "2024-03-01", "2025-07-15"
|
||||
yFrom, yTo := "2025-07-16", ""
|
||||
asOf := "2025-01-01"
|
||||
if !ActiveAt(xFrom, xTo, asOf) {
|
||||
t.Fatal("X must be active as of 2025-01-01")
|
||||
}
|
||||
if ActiveAt(yFrom, yTo, asOf) {
|
||||
t.Fatal("Y must be inactive as of 2025-01-01")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDayTrimsTime(t *testing.T) {
|
||||
if NormalizeDay("2025-01-01T12:00:00Z") != "2025-01-01" {
|
||||
t.Fatalf("got %q", NormalizeDay("2025-01-01T12:00:00Z"))
|
||||
}
|
||||
if NormalizeDay("2025-01-01") != "2025-01-01" {
|
||||
t.Fatalf("got %q", NormalizeDay("2025-01-01"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user