Kernel: a dependency update needs no model review

Merged
DO
5d ago
+185-12

Closes the fourth gap of the epic "Epic: nine gaps in the Kernel, found in review": a pull request of class dependency with green checks waited for a model review, and one was admitted.

Stacks on "Kernel: an unknown merge state defers" (#68), which stacks on "Kernel: a Pause stops approvals and merges, not only Runs" (#67). It will be rebased onto main once they merge.

What changes

  • kernel/derive.py: one function, required_model_lenses(facts, config), says which model Lenses an item needs, and the Kernel uses it where it waits for reviews. A dependency update needs none.

  • A dependency update is a pull request of a class named in required_lenses.model_not_required_for whose head was pushed by the identity that opened it. The class is worked out from who opened it, and the last pusher is GitHub's record of the push, not a commit's free-text author. Anyone else's push, including the author Role's fix after a failed check, puts their content in it, and it needs pr-review again. No new fact is needed.

  • Following the architect's ruling on the issue, a Finding a model Lens has already raised gates whatever the class: gating_threads reads every configured model Lens. A bot that recreates its branch after someone else's push therefore cannot drop a thread pr-review opened, a security Finding among them.

  • config/defaults.json and schemas/config.schema.json: the new key, as data (§4.2). The schema allows only dependency; examples/invalid/config.a-change-by-a-person-skips-the-model-review.json lists maintainer and is rejected.

  • required_lenses.deterministic now needs at least one entry, with the counter-example examples/invalid/config.no-deterministic-lens.json. This story makes an empty list dangerous: it would approve a dependency update with no check at all. This is criterion 3 of "Kernel: two smaller gaps in what it is given" (#39), and it lands here.

  • tests/test_decide.py: test_a_dependency_update_needs_no_model_review loses @pending, leaves NotYetHeld and asserts both sides. Seven tests join it:

    • every other class needs a model review;
    • a push by someone else needs one too;
    • each deterministic Lens failing sends the update back, and pending makes it wait; secret-scan and code-scanning judge security;
    • a neutral deterministic check does not pass it;
    • a writer's thread still gates, and a stranger's does not;
    • a model Lens's thread gates, including after the bot recreates the branch, with the content changed or unchanged;
    • the order: a Hold, an unknown merge state, a conflict, a Pause and a standing approval still bind.
  • tools/mutants.py: seven mutants.

    • "a dependency update waits for a model review"
    • "a model Lens's thread does not gate a dependency update"
    • "a dependency bot's pull request someone else pushed to skips the model review"
    • "a class the configuration does not name skips the model review"
    • "a dependency update converges before its checks are read"
    • "a dependency update converges before its threads are read"
    • "a neutral deterministic check passes"

    33 of 33 killed.

  • Documents:

    • Decision 0012 loses "not held yet" and gains rows for each boundary.
    • One of those rows, waiting on adapters/, says last_pusher must be the identity GitHub records as pushing the head, never a commit's author or committer. It also says class dependency is given only for an exact App identity, and a fork is always outside.
    • The design's §3.3 says what last_pusher means. §4.2, the dependency-update row of §4.6, the glossary's Class entry and config/AGENTS.md say how a dependency update is recognised.

Acceptance criteria (from "Kernel: a dependency update needs no model review")

  • test_a_dependency_update_needs_no_model_review passes with @pending removed.
  • One function says which model Lenses an item's class requires, and is used where the Kernel waits for reviews. A Finding a model Lens has raised gates whatever the class. Each has a test and a mutant.
  • A person's thread still gates a dependency update.
  • No document says not held yet beside this test.

Left open, on purpose

  • Code the bot writes itself (Renovate post-upgrade tasks, vendored code) is still a dependency update. Catching it needs the changed_paths fact of §3.3 and a set of manifest paths in the configuration.
  • That a bot never becomes the last pusher over someone else's surviving commits is bot behaviour, to settle by trial (§6).
  • A same-named check from another App, or a re-run, can hide a failure, because the last entry wins. That belongs to "Kernel: a Lens is read only from the App that reports it" (#60).
  • _wanted_run reads model[0], which is safe today with one model Lens.
  • What a neutral conclusion means is not changed here; it is pinned only as not passing a dependency update. That is "What each check conclusion means" (#35).
  • The simulator's world is always of class maintainer, so the tests carry this rule. sim/world.py is shared with other stories in the epic.

Checks and reviews

  • make verify and python3 -m tools.mutants pass on a git archive copy, which avoids the nested worktrees of "Verify passes in a checkout that holds other worktrees" (#64).
  • The new tests were written before the code. The ones that depend on the rule failed first: needing no model review, a writer's thread, the order, and a model Lens's thread gating after the ruling. The others passed throughout, because they pin existing behaviour: the other classes, someone else's push, each deterministic Lens, and neutral. The neutral mutant survived every test until its test was added.
  • 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 #32

🤖 Generated with Claude Code