Kernel: the facts say whether the list of Runs was read in full

Merged
DO
5d ago
+178-24

Closes the fifth story of the epic "Epic: nine gaps in the Kernel, found in review" (#29): the facts did not say whether the list of Runs was read in full. Every ceiling is counted from Runs, and that list stops at 1,000 for a query (L-53). A list cut short could hide a Run in progress, or one that counts toward a ceiling, and an item would be admitted.

What changes

  • schemas/facts.schema.json requires complete.runs. The port sets it on each item from its one read of the list each pass. examples/facts.json, facts() in tests/support.py and facts() in sim/world.py set it. The counter-example examples/invalid/facts.no-completeness-for-runs.json is rejected. The two other facts counter-examples gain the flag, so each stays wrong in one way only.

  • Following the architect's ruling on the issue, the existing rule I-20 covers it, and no second rule is added.

  • incomplete() fails closed, as the review asked. It used to judge only the flags present, by truthiness, so a missing flag, "false", "no" or 1 counted as a whole list and could admit past the run ceiling. It now reads every name in terms.READ_IN_FULL, the flags the schema requires, and anything that is not exactly true is incomplete. This is how mergeable is read, and it follows kernel/AGENTS.md: a fact read from outside is never given a default. The tests/support.py builder now matters: without runs: True, every test defers.

  • kernel/derive.py reads incompleteness after a merged or closed item, and such an item keeps its State. The list of Runs is read once a pass for every item. Read first, a short list would leave every merged and closed item deferring. This is the same reasoning as the merge state in #68. It applies to every list, because nothing about a merged or closed item is decided from a list.

  • tests/test_decide.py:

    • test_incomplete_facts_never_converge_or_admit covers runs and asserts both sides of the boundary. With the facts whole the outcome is act (reviewed) or admit (not reviewed). With any one list short it is defer, reason facts_incomplete, and that list is the evidence.
    • New test_a_flag_that_is_not_exactly_true_is_incomplete: a missing key, "false", "no", "true", 1, 0 and None each defer, for each list. So does a missing or malformed complete.
    • New test_the_kernel_reads_every_flag_the_facts_schema_requires holds READ_IN_FULL to the schema.
    • New test_incomplete_facts_are_read_after_a_merged_or_closed_item pins the order.
  • sim/world.py gains a fault, a_run_list_stops_short, listed in sim/grounding.py. It cuts the list of Runs short, oldest first, and marks it. The invariants hold on 300 seeds.

  • tools/mutants.py gains three mutants:

    • "a list of Runs read short is trusted" (incomplete() skips runs)
    • "a flag that is not exactly true counts as whole"
    • "incomplete facts are read before a merged or closed item"

    36 of 36 killed.

  • Documents:

    • The design's §3.3 row for complete names runs. The paragraph after it says a flag is whole only if exactly true, why a short list of Runs defers (I-13, I-20), and why a merged or closed item does not.
    • L-53 names the test and the counter-example for the list of Runs. The list of items still waits on adapters/.
    • Decision 0002 gains the counter-example, the new test and a row for the order.
    • kernel/AGENTS.md says a new flag in complete goes in terms.py too.

Acceptance criteria (from "Kernel: two smaller gaps in what it is given")

  • complete.runs is required in the facts schema, the example, tests/support.py and sim/world.py. Counter-example: examples/invalid/facts.no-completeness-for-runs.json.
  • test_incomplete_facts_never_converge_or_admit also covers runs: outcome defer, reason facts_incomplete. Its mutant makes incomplete() skip runs. The simulator gains a fault that cuts the list short.
  • Criterion 3, minItems: 1 on required_lenses.deterministic, landed in #70.
  • Design §3.3 complete; L-53 names the test.

Left open, on purpose

  • The order now covers every list, not only runs. A special case for runs alone was possible; one rule for every list seemed simpler. The review accepted it.
  • The simulator's lane asks the Kernel again, with the full list, before a Run starts. So the new fault shows the rule, a deferral on a short list, but not the harm, a Run admitted past a ceiling. The boundary cases and the mutants hold the harm.
  • a_run_list_stops_short names no trial in section 6, so it is listed as unverified. Whether GitHub's list says it was cut short, or the port must work that out, is for adapters/. L-53 records a query that came back as a success with no data.
  • The list of items saying whether it is whole (the second half of L-53) waits on adapters/.

Checks and reviews

  • make verify and python3 -m tools.mutants pass on a git archive copy, which avoids the nested worktrees of #64.
  • The tests were written first.
    • The order test failed until derive.py changed.
    • The counter-example was accepted, and so failed check_examples, until the schema changed.
    • Both fail-closed tests failed until incomplete() changed: a missing reviews flag with a standing approval was enqueued.
    • The runs case of the I-20 test passed throughout, because incomplete() already read every flag present; its mutant shows the test now holds that.
  • The second model family's read is the Gemini code review and the other bot reviewers on this pull request, as the Maintainer directed. Not merged or enqueued; the Maintainer merges.

Closes #39

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Open items now remain undecided when the run list is missing, malformed, or marked incomplete, preventing incomplete information from being treated as a final state.
    • Merged and closed items retain their existing state even when other fact lists are incomplete.
  • Documentation
    • Clarified how completeness of run information affects state decisions.