diff --git a/README.md b/README.md index 2cc984eb..a98bd1b3 100644 --- a/README.md +++ b/README.md @@ -641,7 +641,10 @@ new candidates permanently. ### Permuter budgets For a longer search of one function, open its drawer and expand **Deep permuter · this function**. -Choose a time limit and maximum candidate compiles, then select **Run deep permuter**. **Stop** ends the job +Choose a time limit and maximum candidate compiles, then select **Run deep permuter**. +The scorecard shows the freshly measured starting score and percentage, the current best, and how many +verified score reductions this run found. Local repairs and random rewrites share that improvement count. +Expand **Technical output** for compiler diagnostics; the progress bar stays visible when output is closed. **Stop** ends the job after its current compiler/generator finishes. Better candidates and exact matches are saved immediately. This bypasses the background queue’s instruction and turn limits for the selected function only. It visits the current local candidate list, then uses random rewrites; it cannot exhaust every possible C program. No AI tokens diff --git a/src/decompnet/app/deep_permuter.py b/src/decompnet/app/deep_permuter.py index 096ca351..f879fc52 100644 --- a/src/decompnet/app/deep_permuter.py +++ b/src/decompnet/app/deep_permuter.py @@ -7,6 +7,7 @@ import json import time +from .. import scoring from ..network import NetworkError from ..project import compiler_for from . import agent_tokens, draft_repairs, permuter_local, stack_padding_repairs @@ -34,14 +35,18 @@ def validate(net, slug, name, minutes, max_variants): class Search(Permuter): - def __init__(self, net, minutes, max_variants, log): + def __init__(self, net, minutes, max_variants, log, fn=None): super().__init__(net) self.deadline = time.monotonic()+minutes*60 self.seconds = minutes*60 self.limit = max_variants self.log = log self.tested = self.skipped = self.visited = self.rejected = 0 - self.best = None + self.starting_score = fn['best_score'] if fn is not None else None + self.best = self.starting_score + self.max_score = max(1, fn['n_insns'] or 0) * scoring.PENALTY_INSERTION if fn is not None else None + self.improvements = 0 + self.baseline_measured = False self.phase = 'local repairs' self.measuring_baseline = False @@ -52,9 +57,18 @@ def check(self): if self.tested >= self.limit or self.visited >= self.limit*20: raise SearchLimit('Variant limit reached') + def metrics(self): + """Use the ordinary score scale; a near miss must never round up to a displayed 100%.""" + def percent(score): + if score is None or self.max_score is None: + return None + return 100.0 if score == 0 else min(99.9, int(max(0, 100*(1-score/self.max_score))*10)/10) + return {'starting_score': self.starting_score, 'starting_percent': percent(self.starting_score), + 'best_score': self.best, 'best_percent': percent(self.best), 'improvements': self.improvements} + def report(self): self.log.progress({'phase': self.phase, 'tested': self.tested, 'skipped': self.skipped, - 'best_score': self.best, 'variant_limit': self.limit, 'rejected': self.rejected, + **self.metrics(), 'variant_limit': self.limit, 'rejected': self.rejected, 'seconds_left': max(0, round(self.deadline-time.monotonic()))}) def _baseline(self, *args): @@ -82,7 +96,8 @@ def _score(self, agent, proj, fn, source, best, fork): key = 'deep-v1:'+hashlib.sha256((scope+source).encode()).hexdigest() cached = self.net.store.one('SELECT feedback FROM reuse_trials WHERE function_id=? AND signature=?', fn['id'], key) score = json.loads(cached['feedback']) if cached and cached['feedback'] else None - if not self.measuring_baseline and isinstance(score, int) and score >= best: + reused = not self.measuring_baseline and isinstance(score, int) and score >= best + if reused: self.skipped += 1 else: self.tested += 1 @@ -92,7 +107,13 @@ def _score(self, agent, proj, fn, source, best, fork): db.execute('INSERT OR REPLACE INTO reuse_trials(function_id,signature,outcome,reason,feedback) VALUES(?,?,?,?,?)', (fn['id'], key, 'miss', 'deep function search', json.dumps(score))) if score is not None: - self.best = score if self.best is None else min(self.best, score) + if self.measuring_baseline and not self.baseline_measured: + self.starting_score = self.best = score + self.baseline_measured = True + elif not reused and not self.measuring_baseline and (self.best is None or score < self.best): + if self.best is not None: + self.improvements += 1 + self.best = score self.report() return score @@ -100,7 +121,8 @@ def _score(self, agent, proj, fn, source, best, fork): def run(net, slug, name, minutes, max_variants, log): """Visit the supported local repairs, then spend the remaining budget on random permutations.""" proj, fn = validate(net, slug, name, minutes, max_variants) - search = Search(net, minutes, max_variants, log) + search = Search(net, minutes, max_variants, log, fn) + search.report() state = State() agent, _ = net.local_agent('permuter') reason = 'Current candidate families finished' @@ -158,4 +180,4 @@ def run(net, slug, name, minutes, max_variants, log): search.report() log(f'{reason}: {search.tested} candidates tested, {search.skipped} cached misses skipped, {search.rejected} invalid calls skipped. Verified improvements are saved immediately.') return {'function': name, 'reason': reason, 'tested': search.tested, 'skipped': search.skipped, - 'rejected': search.rejected, 'matched': net._function(name, slug)['status'] == 'matched', 'best_score': search.best} + 'rejected': search.rejected, 'matched': net._function(name, slug)['status'] == 'matched', **search.metrics()} diff --git a/src/decompnet/app/static/app.css b/src/decompnet/app/static/app.css index 9a0a4c9a..327966cd 100644 --- a/src/decompnet/app/static/app.css +++ b/src/decompnet/app/static/app.css @@ -1118,3 +1118,36 @@ details.more[open] > summary::before { content: "− "; } .agent-results-dialog header p { margin: 0; font: 12px var(--mono); overflow-wrap: anywhere; } .agent-result-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 12px 0; border-bottom: 1px solid var(--line); } .agent-result-row button { text-align: left; overflow-wrap: anywhere; font-family: var(--mono); } + +/* Deep search keeps its scores visible while optional technical output grows separately. */ +.deep-search { padding: 16px; border: 1px solid var(--line); border-radius: var(--r); background: var(--raise); } +.deep-search > summary { font-weight: 600; } +.deep-free { display: inline-block; margin-left: 8px; padding: 3px 7px; border: 1px solid var(--line); border-radius: 4px; color: var(--muted); font-size: 11px; font-weight: 400; } +.deep-controls { display: grid; grid-template-columns: minmax(100px, 1fr) minmax(140px, 1fr) auto; gap: 12px; align-items: end; margin: 18px 0; } +.deep-controls .field { margin: 0; min-width: 0; } +.deep-controls input { width: 100%; box-sizing: border-box; } +.deep-actions { display: flex; gap: 8px; flex-wrap: wrap; padding-bottom: 1px; } +.deep-dashboard { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; } +.deep-dashboard[hidden] { display: none; } +.deep-scores { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; } +.deep-scores > div { padding: 16px; min-width: 0; } +.deep-scores > div + div { border-left: 1px solid var(--line); } +.deep-scores dt, .deep-scores span { color: var(--muted); font-size: 11px; } +.deep-scores dd { margin: 8px 0 5px; font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; } +.deep-scores span { display: block; overflow-wrap: anywhere; } +.deep-best { background: color-mix(in oklch, var(--hl-soft) 16%, transparent); } +.deep-dashboard[data-matched="true"] .deep-best dd { color: var(--match); } +.deep-dashboard[data-improved="true"] { border-color: var(--hl); } +.deep-counters { display: flex; flex-wrap: wrap; gap: 8px 20px; padding: 10px 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; } +.deep-counters b { color: var(--ink); font-weight: 600; } +.deep-counters > :last-child { margin-left: auto; } +.deep-output { margin-top: 14px; font-size: 12px; } +.deep-output summary { cursor: pointer; color: var(--muted); } +.deep-output .joblog { max-height: 180px; margin-top: 10px; } +.deep-note { margin-bottom: 0; font-size: 11px; } +@media (max-width: 650px) { + .deep-controls { grid-template-columns: 1fr 1fr; } + .deep-actions { grid-column: 1 / -1; } + .deep-scores > div { padding: 12px 8px; } + .deep-scores dd { font-size: 22px; } +} diff --git a/src/decompnet/app/static/index.html b/src/decompnet/app/static/index.html index 7cfa2b90..f5b24477 100644 --- a/src/decompnet/app/static/index.html +++ b/src/decompnet/app/static/index.html @@ -572,17 +572,27 @@

Comparison details
-