mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
refactor(p1): one source-facts module owns function ranges, callers, declaration tables and language aliases
src/openultrasast/source.py replaces the copies in semantic/functions.py, plane/tasks/repo_facts.py, learn/features.py, learn/excerpt.py and plane/tasks/verify.py. Two engines share one sentinel (<module>), one DECLARATION/KEYWORDS/RESERVED set and one alias map: the parser engine (ranges, enclosing, bounds, call_sites, callers_of, facts_for) for the scorer, the overlay and the hunter's annotations; the line-pattern engine (declared_functions, enclosing_at, function_span, function_bounds, the brace matcher) that labels, features, roles, excerpts and plane facts were built on. learn no longer imports plane.tasks. Frozen records that store the old sentinel are mapped on read (module_function). Gate: labels rebuilt before and after are identical, 7,921 rows, after sentinel normalisation (benchmarks/measurements/2026-10-13-source-module- label-diff/record.json). 490 targeted tests pass; 3,312 collect. Net: 657 lines deleted, 212 added. benchmarks/unseen keeps <global> for its own frozen manifests. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f63703a604
commit
062a7b780e
@@ -85,7 +85,7 @@ def php_code(text: str) -> str:
|
||||
"""`text` with comments, string contents and inline HTML blanked to spaces, newlines kept (line numbers hold).
|
||||
|
||||
Enough to find function bodies by brace matching; heredoc/nowdoc bodies are blanked too. Not a parser: a file
|
||||
that defeats it yields no function, and the site falls back to `<global>`, which the freeze record shows."""
|
||||
that defeats it yields no function, and the site falls back to `<module>`, which the freeze record shows."""
|
||||
out, i, n, php = list(text), 0, len(text), False
|
||||
|
||||
def blank(start: int, end: int) -> None:
|
||||
@@ -148,10 +148,10 @@ def php_functions(text: str) -> list[tuple[str, int, int]]:
|
||||
|
||||
|
||||
def enclosing(spans: list[tuple[str, int, int]], line: int, inserted: bool = False) -> str:
|
||||
"""The innermost named function containing `line`, or `<global>`. For a pure insertion, `line` is the line the
|
||||
"""The innermost named function containing `line`, or `<module>`. For a pure insertion, `line` is the line the
|
||||
new code follows, so a function whose closing brace is on that line does not contain it."""
|
||||
inside = [(last - first, name) for name, first, last in spans if first <= line <= last and not (inserted and line == last)]
|
||||
return min(inside)[1] if inside else "<global>"
|
||||
return min(inside)[1] if inside else "<module>"
|
||||
|
||||
|
||||
def old_hunks(repo: Path, vulnerable: str, fixed: str, path: str) -> list[tuple[int, int, bool]]:
|
||||
@@ -166,14 +166,14 @@ def old_hunks(repo: Path, vulnerable: str, fixed: str, path: str) -> list[tuple[
|
||||
|
||||
|
||||
def changed_functions(repo: Path, vulnerable: str, fixed: str, path: str) -> list[str]:
|
||||
"""The functions (or `<global>`) of `path` at the vulnerable pin that the fix's hunks touch, in hunk order."""
|
||||
"""The functions (or `<module>`) of `path` at the vulnerable pin that the fix's hunks touch, in hunk order."""
|
||||
text = (blob(repo, vulnerable, path) or b"").decode(errors="replace")
|
||||
spans, names = php_functions(text), []
|
||||
for first, last, inserted in old_hunks(repo, vulnerable, fixed, path):
|
||||
for line in range(first, last + 1):
|
||||
name = enclosing(spans, line, inserted)
|
||||
# New code added between functions (a new helper) changes no vulnerable-side code: no site.
|
||||
if name not in names and not (inserted and name == "<global>"):
|
||||
if name not in names and not (inserted and name == "<module>"):
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
@@ -198,12 +198,12 @@ def resolve_sites(repo: Path, case: dict[str, object], changed: list[str]) -> di
|
||||
|
||||
|
||||
def fix_changes_sites(repo: Path, case: dict[str, object], sites: list[str]) -> dict[str, bool]:
|
||||
"""Per declared site: does the fix's diff change a line inside that function (or the file, for `<global>`)?"""
|
||||
"""Per declared site: does the fix's diff change a line inside that function (or the file, for `<module>`)?"""
|
||||
result = {}
|
||||
for site in sites:
|
||||
path, _, name = site.partition("::")
|
||||
hunks = old_hunks(repo, str(case["vulnerable"]), str(case["fixed"]), path)
|
||||
if name == "<global>" or not hunks:
|
||||
if name == "<module>" or not hunks:
|
||||
result[site] = bool(hunks)
|
||||
continue
|
||||
spans = php_functions((blob(repo, str(case["vulnerable"]), path) or b"").decode(errors="replace"))
|
||||
@@ -289,7 +289,7 @@ def verify(case: dict[str, object], cache: Path) -> dict[str, object]:
|
||||
# Declared `path::function` sites (population v2) are what the verification reads; the first names the
|
||||
# sink file, and every function declared in that file must appear in it.
|
||||
sink_file = sites[0].partition("::")[0]
|
||||
functions = [site.partition("::")[2] for site in sites if site.partition("::")[0] == sink_file and not site.endswith("::<global>")]
|
||||
functions = [site.partition("::")[2] for site in sites if site.partition("::")[0] == sink_file and not site.endswith("::<module>")]
|
||||
else:
|
||||
sink_file, functions = sink_parts(str(case["sink"]))
|
||||
record["sink_file"], record["sink_functions"] = sink_file, functions
|
||||
|
||||
@@ -76,10 +76,10 @@ from openultrasast.plane.harvest import declares
|
||||
from openultrasast.plane.tasks.alerts import covers
|
||||
from openultrasast.plane.tasks.alerts import engine_languages as _engine_languages
|
||||
from openultrasast.plane.tasks.alerts import quick_languages as _quick_languages
|
||||
from openultrasast.plane.tasks.repo_facts import GLOBAL
|
||||
from openultrasast.preprocess import detect_language
|
||||
from openultrasast.rank import rank_targets
|
||||
from openultrasast.ruleset import DEFAULT_RULESET_DIR, load_ruleset
|
||||
from openultrasast.source import MODULE_FUNCTION
|
||||
|
||||
RESULTS = Path.home() / "ousast-results"
|
||||
MISSING = Part("failed", version="missing:not-run")
|
||||
@@ -306,7 +306,7 @@ def static_parts(root: Path, index: SourceIndex, quick: Mapping, engine: Mapping
|
||||
parts["roles"] = roles_part(function_of(lines, path, language, function), inferred, language)
|
||||
parts["delta"] = NOT_APPLICABLE
|
||||
if repository and entries is not None:
|
||||
distance = 0 if function == GLOBAL and path in entries[1] else entry_distance(function, entries[0], index.callers())
|
||||
distance = 0 if function == MODULE_FUNCTION and path in entries[1] else entry_distance(function, entries[0], index.callers())
|
||||
parts["entry_points"] = Part("ran", {"facts.entry_distance": distance})
|
||||
parts["facts"] = facts_part(_callers_of(index, index.callers(), path, function), function, "host")
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Compare the baseline labels (old modules) with the labels built on source.py, sentinel-normalised."""
|
||||
import glob, json, collections, sys
|
||||
def load(pattern):
|
||||
files = glob.glob(pattern)
|
||||
if not files:
|
||||
sys.exit(f"no labels file for {pattern}")
|
||||
rows = [json.loads(l) for l in open(files[0]) if l.strip()]
|
||||
return files[0], rows
|
||||
bfile, before = load("/home/mc/ousast-results/fossil/p1-baseline/out/labels-*.jsonl")
|
||||
afile, after = load("/home/mc/ousast-results/fossil/p1-after/out/labels-*.jsonl")
|
||||
def key(r):
|
||||
s = json.dumps(r, sort_keys=True).replace("<global>", "<module>")
|
||||
return s
|
||||
B = collections.Counter(key(r) for r in before); A = collections.Counter(key(r) for r in after)
|
||||
only_b = B - A; only_a = A - B
|
||||
print("baseline:", bfile, len(before), "rows"); print("after: ", afile, len(after), "rows")
|
||||
print("identical after sentinel normalisation:", len(only_b) == 0 and len(only_a) == 0)
|
||||
print("rows only in baseline:", sum(only_b.values()), " rows only in after:", sum(only_a.values()))
|
||||
def summarize(c, title):
|
||||
print(f"--- {title} (first 8)")
|
||||
for s, n in list(c.items())[:8]:
|
||||
r = json.loads(s); print(" ", {k: r.get(k) for k in ("candidate", "family", "label", "source", "unit", "pin_role") if k in r})
|
||||
summarize(only_b, "only baseline"); summarize(only_a, "only after")
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"what": "Label diff gate for P1 (PATHFINDER-2026-10-13/04-handoff-prompts.md): labels built by `ousast learn labels` with the modules before source.py (HEAD f63703a, exported pristine) and after, compared row by row after mapping the legacy module-level sentinel <global> to <module>.",
|
||||
"before": {
|
||||
"file": "labels-c3d9c302eadf79de.jsonl",
|
||||
"rows": 7921,
|
||||
"sha256": "c3d9c302eadf79dedd3b91b5545ee4f2d91d634dafe244eb7f7cf73e76d3f1f4",
|
||||
"code": "f63703a"
|
||||
},
|
||||
"after": {
|
||||
"file": "labels-7701cc7339a0d0f7.jsonl",
|
||||
"rows": 7921,
|
||||
"sha256": "7701cc7339a0d0f7aff036581c8417f5afc645d9e45bc067787fbf767425f02a",
|
||||
"code": "working tree before the P1 commit"
|
||||
},
|
||||
"result": "identical after sentinel normalisation (0 rows only-before, 0 rows only-after)",
|
||||
"iterations": [
|
||||
"1: Codex's parser-first rewrite of the learn and plane modules: 18 test failures (function bounds semantics), reverted those modules to the line-pattern engine moved into source.py",
|
||||
"2: 4 rows missing: KEYWORDS had been widened to the parser's reserved set; restored the 22-word set (no change in output, kept for correctness)",
|
||||
"3: 4 rows still missing: frozen population records store declared sites as path::<global>; normalised on read (module_function) in labels and in function_bounds/function_span",
|
||||
"4: identical"
|
||||
],
|
||||
"sources": "src/openultrasast/learn/sources.toml (population-v1, population-v2, dev-php, pairs, advisory-fixes, ...; v3 excluded unread)",
|
||||
"note": "The label files themselves stay outside the repo (they name functions in private repositories): ~/ousast-results/fossil/p1-{baseline,after}/out/."
|
||||
}
|
||||
@@ -106,10 +106,10 @@ def _body(lines: list[str], span: tuple[int, int]) -> str:
|
||||
|
||||
|
||||
def extract(source: str, language: str, function: str) -> tuple[list[str], tuple[int, int]] | None:
|
||||
from openultrasast.learn.excerpt import LANGUAGE_ALIASES, function_bounds
|
||||
from openultrasast.source import canonical_language, function_bounds
|
||||
|
||||
lines = source.splitlines()
|
||||
span = function_bounds(lines, LANGUAGE_ALIASES.get(language, language), function)
|
||||
span = function_bounds(lines, canonical_language(language), function)
|
||||
return (lines, span) if span else None
|
||||
|
||||
|
||||
@@ -159,14 +159,14 @@ def materialise(pair: dict[str, Any], *, force: bool = False) -> dict[str, Any]:
|
||||
|
||||
def verify(pair: dict[str, Any]) -> dict[str, Any]:
|
||||
"""The label builder's check, from the cached excerpts: declared on both sides, body changed, a real fix."""
|
||||
from openultrasast.learn.excerpt import LANGUAGE_ALIASES, function_bounds
|
||||
from openultrasast.source import canonical_language, function_bounds
|
||||
|
||||
result: dict[str, Any] = {"name": pair["name"], "ok": False, "bytes_read": 0}
|
||||
commit, parent = str(pair.get("commit", "")), str(pair.get("parent", ""))
|
||||
if not (_SHA.fullmatch(commit) and _SHA.fullmatch(parent)) or commit == parent:
|
||||
result["reason"] = "fix commit must be a full SHA distinct from its parent"
|
||||
return result
|
||||
language = LANGUAGE_ALIASES.get(str(pair["language"]), str(pair["language"]))
|
||||
language = canonical_language(str(pair["language"]))
|
||||
spans = {}
|
||||
for side, path in zip(("vuln", "fixed"), excerpt_paths(pair), strict=True):
|
||||
if not path.is_file():
|
||||
|
||||
@@ -15,7 +15,8 @@ import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from openultrasast.semantic.functions import ANON_FUNCTION, named_function_ranges
|
||||
from openultrasast.source import ANON_FUNCTION
|
||||
from openultrasast.source import ranges as source_ranges
|
||||
except ImportError as exc: # never degrade into "no named function": that path deletes corpora under --prune
|
||||
raise SystemExit(f"catalog_gen needs the openultrasast package (run it with the project venv): {exc}") from exc
|
||||
|
||||
@@ -268,7 +269,7 @@ def _language(recipe: dict[str, object]) -> str:
|
||||
|
||||
|
||||
def _ranges(path: Path, language: str) -> tuple[tuple[str, int, int], ...] | None:
|
||||
ranges = named_function_ranges(str(path), path.read_text(errors="ignore"), language)
|
||||
ranges = source_ranges(str(path), path.read_text(errors="ignore"), language)
|
||||
if ranges is None:
|
||||
return None # parser rejected the excerpt; a regex label would not scope
|
||||
return tuple(r for r in ranges if r[0] != ANON_FUNCTION)
|
||||
|
||||
Reference in New Issue
Block a user