"""Overlap check between a new training task and the eval tasks (spec text and object names). A training task is rejected when its spec or its object names are too close to an eval task. """ import glob import json import math import os import re from collections import Counter ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STOP = set("the a an of to in is are be and or for with that this it as on by at from not no if then each " "all any must shall can will when which its into than one two use used using value values " "object objects method methods class classes table tables field fields return returns".split()) SPEC_LIMIT = 0.55 # cosine of word counts of the whole spec GOAL_LIMIT = 0.60 # cosine of the Goal and Business rules sections NAME_LIMIT = 0.60 # Jaccard of name tokens (placeholder and Z prefix removed) def words(text): text = re.sub(r"\{\{[Pp]\}\}", " ", text) return [w for w in re.findall(r"[a-z][a-z0-9]{2,}", text.lower()) if w not in STOP] def cosine(a, b): ca, cb = Counter(a), Counter(b) dot = sum(ca[k] * cb[k] for k in ca if k in cb) na = math.sqrt(sum(v * v for v in ca.values())) nb = math.sqrt(sum(v * v for v in cb.values())) return dot / (na * nb) if na and nb else 0.0 def section(spec, start, stop=None): m = re.search(rf"(?:^|\n)#*\s*\d*\.?\s*{start}.*?\n(.*?)(?=\n#*\s*\d*\.?\s*(?:{stop})|\Z)" if stop else rf"(?:^|\n)#*\s*\d*\.?\s*{start}.*?\n(.*)", spec, re.S | re.I) return m.group(1) if m else "" def core_text(spec): return section(spec, "Goal", "Open questions|Context") + "\n" + section(spec, "Business rules", "Constraints") def name_tokens(name): n = re.sub(r"\{\{[Pp]\}\}", "", name).lower() n = re.sub(r"^(?:zcl_|zif_|zcx_|z|/\w+/)", "", n) return {t for t in re.split(r"[_\W]+", n) if len(t) > 2 and t not in ("test", "tab", "cls", "intf")} def task_names(task): return [o["name"] for k in ("contract", "seed", "reference") for o in task.get(k, []) if o.get("name")] def load_task(d): task = json.load(open(os.path.join(d, "task.json"))) spec = open(os.path.join(d, "spec.md")).read() return {"id": task.get("id", os.path.basename(d)), "spec": words(spec), "core": words(core_text(spec)), "names": [name_tokens(n) for n in task_names(task)], "main": [name_tokens(o["name"]) for o in task.get("contract", []) if o.get("name")]} def load_pool(pool): out = [] for d in sorted(glob.glob(os.path.join(ROOT, "tasks_gen", pool, "G*")) + glob.glob(os.path.join(ROOT, "tasks", "T*"))): try: out.append(load_task(d)) except (OSError, ValueError): pass return out def jaccard(a, b): return len(a & b) / len(a | b) if a and b else 0.0 def compare(x, y): """Scores of two loaded tasks: spec cosine, core cosine, best name Jaccard (contract names).""" name = max([jaccard(a, b) for a in x["main"] for b in y["main"]] or [0.0]) return {"spec": cosine(x["spec"], y["spec"]), "core": cosine(x["core"], y["core"]), "name": name} def check(task_dir_or_loaded, pool_tasks): """Return a list of problems (empty = no overlap) against a list of loaded tasks.""" x = task_dir_or_loaded if isinstance(task_dir_or_loaded, dict) else load_task(task_dir_or_loaded) problems = [] for y in pool_tasks: if y["id"] == x["id"]: continue s = compare(x, y) if s["spec"] >= SPEC_LIMIT or s["core"] >= GOAL_LIMIT or s["name"] >= NAME_LIMIT: problems.append((y["id"], s)) return problems def load_bundle(b): """Loaded form of a generated bundle ({"task", "files"}) before it is written to disk.""" task, spec = b["task"], b["files"].get("spec.md", "") return {"id": task.get("id", "(new)"), "spec": words(spec), "core": words(core_text(spec)), "names": [name_tokens(n) for n in task_names(task)], "main": [name_tokens(o["name"]) for o in task.get("contract", []) if o.get("name")]}