Files
stack/src/sem/plan.py
kert 96b8ccf6df
Some checks failed
CI / skinny-install (aco) (push) Successful in 53s
CI / lint-test (push) Successful in 1m15s
CI / skinny-install (api) (push) Successful in 26s
CI / skinny-install (bcda) (push) Successful in 29s
CI / skinny-install (bib) (push) Successful in 31s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 30s
CI / skinny-install (cli) (push) Successful in 30s
CI / skinny-install (cms) (push) Successful in 26s
CI / skinny-install (conf) (push) Successful in 26s
CI / skinny-install (opps) (push) Successful in 28s
CI / skinny-install (perf) (push) Successful in 29s
CI / skinny-install (pfs) (push) Successful in 27s
Infra CI / notebooks (push) Successful in 7s
Infra CI / zotero (push) Successful in 7s
CI / skinny-install (rex) (push) Successful in 27s
Infra CI / docs (push) Failing after 9s
Infra CI / api (push) Successful in 8s
Infra CI / mc (push) Successful in 6s
Package Supply Chain / pkg-supply-chain (push) Successful in 52s
CI / lint-test (pull_request) Successful in 1m18s
CI / skinny-install (aco) (pull_request) Successful in 44s
CI / skinny-install (api) (pull_request) Successful in 24s
CI / skinny-install (bcda) (pull_request) Successful in 32s
CI / skinny-install (bib) (pull_request) Successful in 30s
CI / skinny-install (bls) (pull_request) Successful in 25s
CI / skinny-install (ccw) (pull_request) Successful in 23s
CI / skinny-install (cli) (pull_request) Successful in 30s
CI / skinny-install (cms) (pull_request) Successful in 27s
CI / skinny-install (conf) (pull_request) Successful in 30s
CI / skinny-install (opps) (pull_request) Successful in 29s
CI / skinny-install (perf) (pull_request) Successful in 32s
CI / skinny-install (pfs) (pull_request) Successful in 33s
Infra CI / notebooks (pull_request) Successful in 7s
Infra CI / zotero (pull_request) Successful in 6s
Infra CI / docs (pull_request) Failing after 6s
CI / skinny-install (rex) (pull_request) Successful in 27s
Infra CI / api (pull_request) Successful in 6s
Infra CI / mc (pull_request) Successful in 6s
feat: sem — semantic coverage orchestration (AST + Ruff + ty + coverage)
4-layer architecture for tracking coverage as semantic AST nodes rather
than raw line numbers: parse → enrich → runtime → plan.  Stable node
identities, Ruff/ty diagnostic attachment, coverage mapping, priority
scoring heuristic, and drift-resistant rolling state persistence.

39 tests, ruff clean.
2026-03-26 14:37:54 -04:00

109 lines
2.9 KiB
Python

"""Layer 4 — Planner: prioritise uncovered nodes and suggest test targets.
Implements the priority heuristic from the design doc::
priority =
uncovered_branch * 5
+ ty_related * 4
+ ruff_warning * 2
+ inside_partially_tested_function * 3
- prior_failures * 2
- likely_unreachable * 5
"""
from __future__ import annotations
from sem.nodes import NodeKind, SemanticNode
# Branch-like node kinds that carry higher weight when uncovered.
_BRANCH_KINDS = {
NodeKind.BRANCH_IF,
NodeKind.BRANCH_ELSE,
NodeKind.BRANCH_ELIF,
NodeKind.MATCH_CASE,
}
_EXCEPTION_KINDS = {
NodeKind.EXCEPT_HANDLER,
}
def _is_branch(node: SemanticNode) -> bool:
return node.kind in _BRANCH_KINDS
def _is_exception_path(node: SemanticNode) -> bool:
return node.kind in _EXCEPTION_KINDS
def _function_coverage_ratio(
node: SemanticNode, all_nodes: list[SemanticNode]
) -> float:
"""Return fraction of nodes hit in the same function context."""
if not node.symbol_context:
return 0.0
fn_ctx = node.symbol_context[0] if node.symbol_context else ""
siblings = [
n
for n in all_nodes
if n.module == node.module
and n.symbol_context
and n.symbol_context[0] == fn_ctx
]
if not siblings:
return 0.0
hit = sum(1 for s in siblings if s.runtime.hit)
return hit / len(siblings)
def score_node(node: SemanticNode, all_nodes: list[SemanticNode]) -> float:
"""Compute priority score for a single node."""
score = 0.0
# Uncovered branch
if not node.runtime.hit and _is_branch(node):
score += 5.0
# ty diagnostics present
if node.ty_codes:
score += 4.0
# Ruff warnings present
if node.ruff_codes:
score += 2.0
# Inside a partially tested function (some siblings hit, this one not)
fn_ratio = _function_coverage_ratio(node, all_nodes)
if 0.0 < fn_ratio < 1.0 and not node.runtime.hit:
score += 3.0
# Exception paths — valuable targets
if not node.runtime.hit and _is_exception_path(node):
score += 3.0
# Penalise prior failures
score -= node.attempts * 2.0
# Penalise already-covered nodes
if node.runtime.hit:
score -= 5.0
return score
def rank_nodes(nodes: list[SemanticNode]) -> list[SemanticNode]:
"""Score and sort nodes by descending priority.
Mutates ``node.priority`` in place, then returns nodes sorted
highest-priority-first.
"""
for node in nodes:
node.priority = score_node(node, nodes)
return sorted(nodes, key=lambda n: n.priority, reverse=True)
def next_targets(nodes: list[SemanticNode], *, limit: int = 10) -> list[SemanticNode]:
"""Return the top-*limit* uncovered nodes worth testing next."""
ranked = rank_nodes(nodes)
return [n for n in ranked if not n.runtime.hit and n.priority > 0][:limit]