runcard 0.1.1

An append-only record of every run: samples, evals, checkpoints, params, tags and aliases over one eventsdb log, with a Teal policy layer and a CLI.
Documentation
-- What `cards.prune` decides before any of it reaches a store.
--
-- `htl test` runs the Teal alone, so there is no host here: `plan_prune` takes the two
-- facts about the world outside a card — which cards carry an alias, which cards are
-- somebody's parent — as plain sets, precisely so that the rule can be argued about
-- without opening a log. Everything below is therefore also an assertion that the rule is
-- total: it decides every candidate from the three inputs and asks nothing.
--
-- The property the refusals are for: **a prune never removes something another thing is
-- still pointing at, and never removes everything by accident.** The first is the three
-- skip buckets; the second is the refusal of a spec that bounds nothing.

local t = require("htl.test")
local cards = require("cardbox.cards")
local type find = require("cardbox.find")
local type prune = require("cardbox.prune")

local function card(id: string, state?: string): find.CardSummary
   return { id = id, pkg = "cot", scenario = "arith", state = state or "closed_ok" }
end

local function ids(list: {string}): string
   return table.concat(list, ",")
end

-- The smallest spec that is bounded and has a reason, which is what every bucket test
-- wants: the refusals it is about are per card, not per spec.
local function spec(over: prune.PruneSpec): prune.PruneSpec
   local out: prune.PruneSpec = { pkg = "cot", reason = "test debris" }
   if over ~= nil then
      out.pkg = over.pkg or out.pkg
      out.pkg_like = over.pkg_like
      out.older_than_ms = over.older_than_ms
      out.states = over.states
      out.ids = over.ids
      out.reason = over.reason or out.reason
      out.dry_run = over.dry_run
   end
   return out
end

t.describe("cards.plan_prune", function()
   t.it("selects a closed card nothing points at", function()
      local plan, err = cards.plan_prune({ card("a"), card("b") }, {}, {}, spec(nil))
      t.expect(err):to_be_nil()
      t.expect(ids(plan.selected)):to_equal("a,b")
      t.expect(#plan.skipped_aliased):to_equal(0)
      t.expect(#plan.skipped_parent):to_equal(0)
      t.expect(#plan.skipped_open):to_equal(0)
   end)

   t.it("skips a card that carries an alias", function()
      local plan = cards.plan_prune({ card("a"), card("b") }, { b = true }, {}, spec(nil))
      t.expect(ids(plan.selected)):to_equal("a")
      t.expect(ids(plan.skipped_aliased)):to_equal("b")
   end)

   t.it("skips a card that is somebody's parent", function()
      local plan = cards.plan_prune({ card("a"), card("b") }, {}, { a = true }, spec(nil))
      t.expect(ids(plan.selected)):to_equal("b")
      t.expect(ids(plan.skipped_parent)):to_equal("a")
   end)

   t.it("skips an open card unless the spec names the state", function()
      local plan = cards.plan_prune({ card("a", "open"), card("b") }, {}, {}, spec(nil))
      t.expect(ids(plan.selected)):to_equal("b")
      t.expect(ids(plan.skipped_open)):to_equal("a")
   end)

   t.it("takes an open card when states names it", function()
      local allowed = spec({ states = { "open", "closed_failed" } })
      local plan = cards.plan_prune({ card("a", "open"), card("b") }, {}, {}, allowed)
      t.expect(ids(plan.selected)):to_equal("a,b")
      t.expect(#plan.skipped_open):to_equal(0)
   end)

   -- A card can fail more than one refusal, and the report has to file it once. The
   -- alias is first because it is the first thing a reader would have to undo.
   t.it("files a card that is both aliased and a parent under the alias", function()
      local plan = cards.plan_prune({ card("a") }, { a = true }, { a = true }, spec(nil))
      t.expect(#plan.selected):to_equal(0)
      t.expect(ids(plan.skipped_aliased)):to_equal("a")
      t.expect(#plan.skipped_parent):to_equal(0)
   end)
end)

t.describe("cards.plan_prune refusals", function()
   t.it("refuses a spec that bounds nothing, naming the four ways to bound one", function()
      local plan, err = cards.plan_prune({ card("a") }, {}, {}, { reason = "spring cleaning" })
      t.expect(plan):to_be_nil()
      t.expect(err):to_contain("every card")
      t.expect(err):to_contain("pkg")
      t.expect(err):to_contain("pkg_like")
      t.expect(err):to_contain("older_than_ms")
      t.expect(err):to_contain("ids")
   end)

   t.it("refuses a spec with no reason: the journal outlives the cards", function()
      local plan, err = cards.plan_prune({ card("a") }, {}, {}, { pkg = "cot" })
      t.expect(plan):to_be_nil()
      t.expect(err):to_contain("reason")

      local blank, berr = cards.plan_prune({ card("a") }, {}, {}, { pkg = "cot", reason = "" })
      t.expect(blank):to_be_nil()
      t.expect(berr):to_contain("reason")
   end)

   t.it("refuses ids and states that are not arrays of strings", function()
      local _, ierr = cards.plan_prune({}, {}, {}, { ids = {}, reason = "why" })
      t.expect(ierr):to_contain("ids")

      local _, serr = cards.plan_prune({}, {}, {}, { pkg = "cot", states = {}, reason = "why" })
      t.expect(serr):to_contain("states")
   end)

   t.it("refuses an older_than_ms that is not a positive number", function()
      local _, err = cards.plan_prune({}, {}, {}, { older_than_ms = 0, reason = "why" })
      t.expect(err):to_contain("older_than_ms")
   end)
end)

t.describe("cards.like_escape", function()
   -- `_` and `%` are LIKE wildcards and `\` is the escape the query names, so all three
   -- come back quoted. The pattern a caller wants is the escaped literal plus the
   -- wildcards they meant: `_test_` is a pkg prefix, `%` is "and anything after it".
   t.it("quotes the three characters LIKE does not read literally", function()
      t.expect(cards.like_escape("_test_")):to_equal("\\_test\\_")
      t.expect(cards.like_escape("_test_") .. "%"):to_equal("\\_test\\_%")
      t.expect(cards.like_escape("100%")):to_equal("100\\%")
      t.expect(cards.like_escape("a\\b")):to_equal("a\\\\b")
   end)

   t.it("leaves a string with none of them alone", function()
      t.expect(cards.like_escape("cot")):to_equal("cot")
   end)

   -- Escaping the whole of a pattern is the other reading, and it is the wrong one: it
   -- would turn "every pkg starting with _test_" into "the pkg literally called _test_%".
   t.it("escapes every wildcard, including one the caller meant as a wildcard", function()
      t.expect(cards.like_escape("_test_%")):to_equal("\\_test\\_\\%")
   end)
end)