feat: grammar_structure_score + min_structure filter
Quantifies how structured a SORE is (0.0=flat bag, 1.0=fully structured). Flat bags are CRX over-approximations — they list symbols without ordering. With min_structure=0.2: Flask: 2 kept (was 5), 9 dropped RAGSAK: 10 kept (was 19), 114 dropped FastAPI: 47 kept (was 106), 95 dropped Total: 59 useful grammars, 218 noise removed CLI: --min-structure 0.2 (default: 0, keep all)
This commit is contained in:
parent
92af932e9d
commit
e62fffc6e0
5 changed files with 669 additions and 411 deletions
45
bex/gbnf.py
45
bex/gbnf.py
|
|
@ -383,3 +383,48 @@ def validate_sore(sore):
|
||||||
return True, None
|
return True, None
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def grammar_structure_score(sore):
|
||||||
|
"""Quantify how structured a SORE is (0.0 = flat bag, 1.0 = fully structured).
|
||||||
|
|
||||||
|
Structured grammars have ordering (concatenation, optional, repetition)
|
||||||
|
that tells you the SEQUENCE things happen. Flat bags just list symbols.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
if not sore or sore in ('∅', 'ε'):
|
||||||
|
return 0.0
|
||||||
|
depth = 0
|
||||||
|
dots = 0
|
||||||
|
pluses_outside = 0
|
||||||
|
questions = 0
|
||||||
|
stars = 0
|
||||||
|
for ch in sore:
|
||||||
|
if ch == '(':
|
||||||
|
depth += 1
|
||||||
|
elif ch == ')':
|
||||||
|
depth -= 1
|
||||||
|
elif ch == '.' and depth == 0:
|
||||||
|
dots += 1
|
||||||
|
elif ch == '+' and depth == 0:
|
||||||
|
pluses_outside += 1
|
||||||
|
elif ch == '?' and depth == 0:
|
||||||
|
questions += 1
|
||||||
|
elif ch == '*' and depth == 0:
|
||||||
|
stars += 1
|
||||||
|
disj_parts = 0
|
||||||
|
for m in re.finditer(r'\(([^)]+)\)', sore):
|
||||||
|
inner = m.group(1)
|
||||||
|
if '+' in inner:
|
||||||
|
disj_parts = max(disj_parts, inner.count('+') + 1)
|
||||||
|
symbols_only = re.sub(r'[.?*+()]', '', sore)
|
||||||
|
sym_len = len(symbols_only)
|
||||||
|
if sym_len == 0:
|
||||||
|
return 0.0
|
||||||
|
struct_ops = dots + questions + stars + pluses_outside
|
||||||
|
struct_ratio = struct_ops / max(sym_len, 1)
|
||||||
|
disj_ratio = disj_parts / max(sym_len, 1)
|
||||||
|
score = min(1.0, struct_ratio * 3)
|
||||||
|
if disj_ratio > 0.5 and dots == 0:
|
||||||
|
score *= 0.3
|
||||||
|
return score
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import pathspec
|
||||||
|
|
||||||
from .code import preprocess_by_method, extract_arg_info, _summarize_arg_info
|
from .code import preprocess_by_method, extract_arg_info, _summarize_arg_info
|
||||||
from bex.ensemble import infer_ensemble
|
from bex.ensemble import infer_ensemble
|
||||||
from bex.gbnf import validate_sore
|
from bex.gbnf import validate_sore, grammar_structure_score
|
||||||
|
|
||||||
SUPPORTED_EXTENSIONS = {
|
SUPPORTED_EXTENSIONS = {
|
||||||
".py", ".js", ".ts", ".kt", ".rb", ".go", ".rs", ".java", ".c", ".cpp",
|
".py", ".js", ".ts", ".kt", ".rb", ".go", ".rs", ".java", ".c", ".cpp",
|
||||||
|
|
@ -283,7 +283,7 @@ def analyze_clusters(file_paths, extension, project_root="", min_coverage=DEFAUL
|
||||||
return [("(all methods)", result, len(sequences), meta)]
|
return [("(all methods)", result, len(sequences), meta)]
|
||||||
|
|
||||||
|
|
||||||
def _infer_group(label, group_seqs, group_files, project_root, min_coverage, prefer, kmax, N, include_kore=False, include_idregex=False, method='langsize', min_methods=5, crx_method='standard'):
|
def _infer_group(label, group_seqs, group_files, project_root, min_coverage, prefer, kmax, N, include_kore=False, include_idregex=False, method='langsize', min_methods=5, crx_method='standard', min_structure=0.0):
|
||||||
"""Infer grammar for one package group. Module-level for ProcessPoolExecutor."""
|
"""Infer grammar for one package group. Module-level for ProcessPoolExecutor."""
|
||||||
filtered = frequency_filter(group_seqs, min_coverage=min_coverage)
|
filtered = frequency_filter(group_seqs, min_coverage=min_coverage)
|
||||||
imports = _extract_imports(group_files)
|
imports = _extract_imports(group_files)
|
||||||
|
|
@ -316,16 +316,20 @@ def _infer_group(label, group_seqs, group_files, project_root, min_coverage, pre
|
||||||
|
|
||||||
# Validate grammar is parseable
|
# Validate grammar is parseable
|
||||||
if result and result.get('best') and result['best'].get('grammar'):
|
if result and result.get('best') and result['best'].get('grammar'):
|
||||||
ok, err = validate_sore(result['best']['grammar'])
|
grammar = result['best']['grammar']
|
||||||
|
ok, err = validate_sore(grammar)
|
||||||
if not ok:
|
if not ok:
|
||||||
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages, "skip_reason": "malformed_grammar", "skip_detail": err}
|
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages, "skip_reason": "malformed_grammar", "skip_detail": err}
|
||||||
return (label, None, len(filtered), meta)
|
return (label, None, len(filtered), meta)
|
||||||
|
if min_structure > 0 and grammar_structure_score(grammar) < min_structure:
|
||||||
|
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages, "skip_reason": "low_structure", "structure_score": grammar_structure_score(grammar)}
|
||||||
|
return (label, None, len(filtered), meta)
|
||||||
|
|
||||||
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
meta = {"files": group_files, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
||||||
return (label, result, len(filtered), meta)
|
return (label, result, len(filtered), meta)
|
||||||
|
|
||||||
|
|
||||||
def analyze_by_package(file_paths, extension, project_root="", min_coverage=DEFAULT_COVERAGE, prefer=None, kmax=2, N=3, min_pkg_size=3, include_kore=False, include_idregex=False, method='langsize', min_methods=5, crx_method='standard'):
|
def analyze_by_package(file_paths, extension, project_root="", min_coverage=DEFAULT_COVERAGE, prefer=None, kmax=2, N=3, min_pkg_size=3, include_kore=False, include_idregex=False, method='langsize', min_methods=5, crx_method='standard', min_structure=0.0):
|
||||||
"""Preprocess and group by package directory, infer per group.
|
"""Preprocess and group by package directory, infer per group.
|
||||||
|
|
||||||
Groups methods by their file's relative directory path, merging
|
Groups methods by their file's relative directory path, merging
|
||||||
|
|
@ -360,7 +364,7 @@ def analyze_by_package(file_paths, extension, project_root="", min_coverage=DEFA
|
||||||
gs = [sequences[i] for i in indices]
|
gs = [sequences[i] for i in indices]
|
||||||
gf = set(seq_files[i] for i in indices)
|
gf = set(seq_files[i] for i in indices)
|
||||||
f = ex.submit(_infer_group, label, gs, gf, project_root,
|
f = ex.submit(_infer_group, label, gs, gf, project_root,
|
||||||
min_coverage, prefer, kmax, N, include_kore, include_idregex, method, min_methods, crx_method)
|
min_coverage, prefer, kmax, N, include_kore, include_idregex, method, min_methods, crx_method, min_structure)
|
||||||
futures[f] = label
|
futures[f] = label
|
||||||
|
|
||||||
for f in as_completed(futures):
|
for f in as_completed(futures):
|
||||||
|
|
@ -473,6 +477,7 @@ def analyze_directory(
|
||||||
method='langsize',
|
method='langsize',
|
||||||
min_methods=5,
|
min_methods=5,
|
||||||
crx_method='standard',
|
crx_method='standard',
|
||||||
|
min_structure=0.0,
|
||||||
):
|
):
|
||||||
"""Scan a directory and run analysis for each language found.
|
"""Scan a directory and run analysis for each language found.
|
||||||
|
|
||||||
|
|
@ -511,6 +516,7 @@ def analyze_directory(
|
||||||
method=method,
|
method=method,
|
||||||
min_methods=min_methods,
|
min_methods=min_methods,
|
||||||
crx_method=crx_method,
|
crx_method=crx_method,
|
||||||
|
min_structure=min_structure,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
results[ext] = analyze_clusters(
|
results[ext] = analyze_clusters(
|
||||||
|
|
@ -680,6 +686,10 @@ def _parse_args(argv=None):
|
||||||
"--crx-method", choices=["standard", "refined"], default="standard",
|
"--crx-method", choices=["standard", "refined"], default="standard",
|
||||||
help="CRX method: standard (default) or refined (cluster-then-infer, tighter grammars)",
|
help="CRX method: standard (default) or refined (cluster-then-infer, tighter grammars)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--min-structure", type=float, default=0.0,
|
||||||
|
help="Minimum structure score (0.0-1.0) to keep grammar. Flat bags of symbols below this are dropped (default: 0, keep all)",
|
||||||
|
)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -703,6 +713,7 @@ def main():
|
||||||
method=args.scoring_method,
|
method=args.scoring_method,
|
||||||
min_methods=args.min_methods,
|
min_methods=args.min_methods,
|
||||||
crx_method=args.crx_method,
|
crx_method=args.crx_method,
|
||||||
|
min_structure=args.min_structure,
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.json_flag or args.format == "json":
|
if args.json_flag or args.format == "json":
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,66 +3,71 @@
|
||||||
"package": "examples/celery/src/task_app",
|
"package": "examples/celery/src/task_app",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 11,
|
"methods": 11,
|
||||||
"skip": "too_diverse"
|
"skip": "too_diverse",
|
||||||
|
"structure_score": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "examples/javascript/tests",
|
"package": "examples/javascript/tests",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 5,
|
"methods": 5,
|
||||||
"skip": "too_diverse"
|
"skip": "too_diverse",
|
||||||
|
"structure_score": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "examples/tutorial/flaskr",
|
"package": "examples/tutorial/flaskr",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 18,
|
"methods": 18,
|
||||||
"skip": "too_diverse"
|
"skip": "too_diverse",
|
||||||
|
"structure_score": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "examples/tutorial/tests",
|
"package": "examples/tutorial/tests",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 25,
|
"methods": 25,
|
||||||
"skip": "too_diverse"
|
"skip": "too_diverse",
|
||||||
|
"structure_score": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "src/flask",
|
"package": "src/flask",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 216,
|
"methods": 216,
|
||||||
"skip": "too_diverse"
|
"skip": "too_diverse",
|
||||||
|
"structure_score": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "src/flask/json",
|
"package": "src/flask/json",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 50,
|
"methods": 50,
|
||||||
"sore": "(TypeError+UUID+__html__+_app+_default+_json+_prepare_response_obj+_untag_scan+and+args+current_app+dict+dumps+else+for+fp+http_date+if+in+is+isinstance+item+items+iter+json+k+key+kwargs+len+list+loads+mimetype+next+not+obj+or+order+raise+response_class+return+s+self+serializer+setdefault+str+tag+tags+tuple+v+value)+.NotImplementedError+?",
|
"skip": "low_structure",
|
||||||
"gbnf": "root ::= (\"TypeError\" | \"UUID\" | \"__html__\" | \"_app\" | \"_default\" | \"_json\" | \"_prepare_response_obj\" | \"_untag_scan\" | \"and\" | \"args\" | \"current_app\" | \"dict\" | \"dumps\" | \"else\" | \"for\" | \"fp\" | \"http_date\" | \"if\" | \"in\" | \"is\" | \"isinstance\" | \"item\" | \"items\" | \"iter\" | \"json\" | \"k\" | \"key\" | \"kwargs\" | \"len\" | \"list\" | \"loads\" | \"mimetype\" | \"next\" | \"not\" | \"obj\" | \"or\" | \"order\" | \"raise\" | \"response_class\" | \"return\" | \"s\" | \"self\" | \"serializer\" | \"setdefault\" | \"str\" | \"tag\" | \"tags\" | \"tuple\" | \"v\" | \"value\")+ \"NotImplementedError\"*",
|
"structure_score": 0.042105263157894736
|
||||||
"gbnf_ok": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "src/flask/sansio",
|
"package": "src/flask/sansio",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 102,
|
"methods": 102,
|
||||||
"sore": "(Any+BlueprintSetupState+ValueError+__name__+_method_route+add_url_rule+and+app+append+callable+code+config+decorator+def+dict+else+endpoint+f+for+func+if+import_name+in+is+is not+lambda+name+not+options+or+os+path+raise+record_once+return+root_path+rstrip+rule+s+self+setdefault+state+static_folder+str+t+url_default_functions+value+values+view_func+view_functions)+",
|
"skip": "low_structure",
|
||||||
"gbnf": "root ::= (\"Any\" | \"BlueprintSetupState\" | \"ValueError\" | \"__name__\" | \"_method_route\" | \"add_url_rule\" | \"and\" | \"app\" | \"append\" | \"callable\" | \"code\" | \"config\" | \"decorator\" | \"def\" | \"dict\" | \"else\" | \"endpoint\" | \"f\" | \"for\" | \"func\" | \"if\" | \"import_name\" | \"in\" | \"is\" | \"is not\" | \"lambda\" | \"name\" | \"not\" | \"options\" | \"or\" | \"os\" | \"path\" | \"raise\" | \"record_once\" | \"return\" | \"root_path\" | \"rstrip\" | \"rule\" | \"s\" | \"self\" | \"setdefault\" | \"state\" | \"static_folder\" | \"str\" | \"t\" | \"url_default_functions\" | \"value\" | \"values\" | \"view_func\" | \"view_functions\")+",
|
"structure_score": 0.009523809523809525
|
||||||
"gbnf_ok": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "tests",
|
"package": "tests",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 962,
|
"methods": 962,
|
||||||
"sore": "(@+Blueprint+__name__+app+append+class+client+config+data+def+e+flask+get+in+index+isinstance+not+raise+render_template+request+return+route+rv+s+self+session+value)+",
|
"skip": "low_structure",
|
||||||
"gbnf": "root ::= (\"@\" | \"Blueprint\" | \"__name__\" | \"app\" | \"append\" | \"class\" | \"client\" | \"config\" | \"data\" | \"def\" | \"e\" | \"flask\" | \"get\" | \"in\" | \"index\" | \"isinstance\" | \"not\" | \"raise\" | \"render_template\" | \"request\" | \"return\" | \"route\" | \"rv\" | \"s\" | \"self\" | \"session\" | \"value\")+",
|
"structure_score": 0.021897810218978103
|
||||||
"gbnf_ok": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "tests/test_apps",
|
"package": "tests/test_apps",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 3,
|
"methods": 3,
|
||||||
"skip": "too_diverse"
|
"skip": "too_diverse",
|
||||||
|
"structure_score": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "tests/test_apps/blueprintapp/apps",
|
"package": "tests/test_apps/blueprintapp/apps",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 4,
|
"methods": 4,
|
||||||
|
"skip": "",
|
||||||
|
"structure_score": 0.2857142857142857,
|
||||||
"sore": "return.render_template+",
|
"sore": "return.render_template+",
|
||||||
"gbnf": "root ::= \"return\" \"render_template\"+",
|
"gbnf": "root ::= \"return\" \"render_template\"+",
|
||||||
"gbnf_ok": true
|
"gbnf_ok": true
|
||||||
|
|
@ -71,13 +76,17 @@
|
||||||
"package": "tests/type_check",
|
"package": "tests/type_check",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 24,
|
"methods": 24,
|
||||||
"sore": "def?.(Generator+encode+for+in+iter+range+return+show+str+t+x+yield)+?.(Response+code+jsonify)+?.render_template+?.HTTPStatus?.stream_template+?.self?.OK?.name+?.template_name+?",
|
"skip": "",
|
||||||
"gbnf": "root ::= \"def\"? (\"Generator\" | \"encode\" | \"for\" | \"in\" | \"iter\" | \"range\" | \"return\" | \"show\" | \"str\" | \"t\" | \"x\" | \"yield\")* (\"Response\" | \"code\" | \"jsonify\")* \"render_template\"* \"HTTPStatus\"? \"stream_template\"* \"self\"? \"OK\"? \"name\"* \"template_name\"*",
|
"structure_score": 0.5597014925373135,
|
||||||
|
"sore": "def?.(Generator+encode+for+in+iter+range+return+show+str+t+x+yield)+?.stream_template+?.(Response+code+jsonify)+?.render_template+?.HTTPStatus?.name+?.self?.OK?.template_name+?",
|
||||||
|
"gbnf": "root ::= \"def\"? (\"Generator\" | \"encode\" | \"for\" | \"in\" | \"iter\" | \"range\" | \"return\" | \"show\" | \"str\" | \"t\" | \"x\" | \"yield\")* \"stream_template\"* (\"Response\" | \"code\" | \"jsonify\")* \"render_template\"* \"HTTPStatus\"? \"name\"* \"self\"? \"OK\"? \"template_name\"*",
|
||||||
"gbnf_ok": true
|
"gbnf_ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "(other)",
|
"package": "(other)",
|
||||||
"ext": ".py",
|
"ext": ".py",
|
||||||
"methods": 4
|
"methods": 4,
|
||||||
|
"skip": "",
|
||||||
|
"structure_score": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue