aboutsummaryrefslogtreecommitdiff
path: root/scripts/newos4-feedback-autoreview.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/newos4-feedback-autoreview.py')
-rwxr-xr-xscripts/newos4-feedback-autoreview.py125
1 files changed, 119 insertions, 6 deletions
diff --git a/scripts/newos4-feedback-autoreview.py b/scripts/newos4-feedback-autoreview.py
index 2021de3..1bb6d25 100755
--- a/scripts/newos4-feedback-autoreview.py
+++ b/scripts/newos4-feedback-autoreview.py
@@ -75,13 +75,73 @@ def approved_today():
return n
+# Generic vocabulary: JS keywords, builtins and DOM handler names. A token in here identifies the
+# LANGUAGE, not the defect, so it can never justify "still present in shipped code" (2026-11-14).
+# Before this, `cmds.indexOf is not a function` produced present_tokens ["indexOf","function"] with
+# hits {indexOf: 36, function: 74} and became a HIGH steering directive for a function that does not
+# exist anywhere in the tree (plan 2026-11-13_062052 §2).
+SKIP = {'error', 'failed', 'fetch', 'undefined', 'cannot', 'reading', 'properties', 'uncaught',
+ 'invalid', 'unexpected', 'token', 'https', 'http', 'com', 'org', 'null', 'true', 'false',
+ # language / builtins
+ 'function', 'return', 'indexof', 'lastindexof', 'tolowercase', 'touppercase', 'tostring',
+ 'length', 'push', 'pop', 'slice', 'splice', 'split', 'join', 'replace', 'concat', 'shift',
+ 'map', 'filter', 'reduce', 'foreach', 'json', 'parse', 'stringify', 'object', 'array',
+ 'promise', 'async', 'await', 'then', 'catch', 'finally', 'typeof', 'constructor',
+ 'prototype', 'valueof', 'hasownproperty', 'assign', 'keys', 'entries', 'values',
+ # host objects and event plumbing
+ 'window', 'document', 'console', 'navigator', 'localstorage', 'settimeout', 'setinterval',
+ 'addeventlistener', 'removeeventlistener', 'queryselector', 'onclick', 'onload', 'onerror',
+ 'eval', 'anonymous', 'innerhtml', 'textcontent', 'classlist', 'dispatchevent'}
+
+# `at isCommand (<anonymous>:34:17)`, `at t.executeCommand (<anonymous>:66:12)`,
+# `at HTMLButtonElement.onclick (…/main.html:954:454)` -> the function name is the only frame part
+# that can identify code, and it was never parsed before.
+STACK_FN = re.compile(r'at\s+(?:async\s+)?([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(')
+
+
def identifiers(msg):
"""Distinctive names in an error message: drop numbers/quoted parts, keep real tokens."""
cleaned = re.sub(r'["\'].*?["\']', ' ', str(msg))
cleaned = re.sub(r'0x[0-9a-f]+', ' ', cleaned, flags=re.I)
- skip = {'error', 'failed', 'fetch', 'undefined', 'cannot', 'reading', 'properties', 'uncaught',
- 'invalid', 'unexpected', 'token', 'https', 'http', 'com', 'org', 'null', 'true', 'false'}
- return [t for t in IDENT.findall(cleaned) if t.lower() not in skip][:4]
+ return [t for t in IDENT.findall(cleaned) if t.lower() not in SKIP][:4]
+
+
+def stack_functions(stack):
+ """Function names from stack frames, most specific last segment, generics removed."""
+ out = []
+ for name in STACK_FN.findall(str(stack or '')):
+ base = name.split('.')[-1]
+ if not base or base.startswith('<') or base.lower() in SKIP:
+ continue
+ if base not in out:
+ out.append(base)
+ return out[:4]
+
+
+ANON_FRAME = re.compile(r'at\s+[^\n]*\(<anonymous>(?::\d+:\d+)?\)')
+REAL_FRAME = re.compile(r'at\s+[^\n]*\((?:https?:)?//?[^)\s]+\.(?:js|mjs|html)(?:[:?][^)\s]*)?\)')
+
+
+def stack_generated(stack):
+ """True when the stack describes runtime-generated code rather than a shipped file.
+
+ `<anonymous>` frames come from `new Function(...)` module loading and the `js`/`js-run` eval
+ path — a snippet someone pasted, not our source. A function name from such a frame says nothing
+ about shipped code, which is exactly how `cmds.indexOf` became a HIGH directive (2026-11-12).
+ Rule: anonymous frames present AND no frame pointing at a real .js/.html file.
+ """
+ frames = [ln.strip() for ln in str(stack or '').splitlines() if ln.strip().startswith('at ')]
+ if not frames:
+ return False
+ # The ORIGIN is the top frame. The bottom `HTMLButtonElement.onclick (…/main.html)` frame proves
+ # only that a click started it, so deciding on "does any frame name a real file" got this wrong.
+ top = frames[0]
+ return '<anonymous>' in top or not REAL_FRAME.search(top)
+
+
+def distinctive(tokens, hits):
+ """Tokens specific enough to justify 'still present': >=6 chars, in modules/, not generic."""
+ return [t for t in tokens if len(t) >= 6 and hits.get(t) and t.lower() not in SKIP]
def code_hits(tokens):
@@ -181,6 +241,9 @@ def classify(rep, dec, fps, cap_left):
return 'reject', 'noise: test artefact or client-side network failure', {'match': noise_hit.group(0)}
tokens = identifiers(msg)
+ for fn in stack_functions(stack): # the frame names are the most specific identifiers we get
+ if fn not in tokens:
+ tokens.append(fn)
hits = code_hits(tokens) if tokens else {}
present = [t for t in tokens if hits.get(t)]
absent = [t for t in tokens if not hits.get(t)]
@@ -197,11 +260,21 @@ def classify(rep, dec, fps, cap_left):
'absent_tokens': absent}
# R4 still-present: real stack error whose identifiers still exist in shipped code.
- if stack and present and cap_left > 0:
+ distinct = distinctive(present, hits)
+ if stack and stack_generated(stack):
+ return None, 'stack frames are <anonymous> (generated code — js-run / new Function), not shipped source — needs a human', {
+ 'present_tokens': present, 'generated_stack': True}
+ if stack and distinct and cap_left > 0:
return 'approve', 'still present in shipped code (stack + identifiers found)', {
+ 'present_tokens': present, 'distinctive_tokens': distinct,
+ 'hits': {t: hits[t] for t in present}}
+ if stack and distinct and cap_left <= 0:
+ return None, 'daily auto-approve cap reached — left for the operator', {'present_tokens': distinct}
+ if stack and present and not distinct:
+ # Generic-only match (indexOf/function/length...): a human must decide, because "the word
+ # appears in 74 files" is not evidence the defect is in shipped code.
+ return None, 'only generic identifiers in message — needs a human', {
'present_tokens': present, 'hits': {t: hits[t] for t in present}}
- if stack and present and cap_left <= 0:
- return None, 'daily auto-approve cap reached — left for the operator', {'present_tokens': present}
# R5 stale artefact: one occurrence that never came back, and no stack to act on.
# Distinct from R2 (identifier removed) — here the emitter is still there but the symptom is not.
@@ -223,6 +296,43 @@ def classify(rep, dec, fps, cap_left):
'tokens': tokens, 'present': present, 'stack': stack}
+SELF_CASES = [
+ ("identifiers drop generic words",
+ identifiers('cmds.indexOf is not a function'), []),
+ ("stack names are the specific identifiers",
+ stack_functions("at isCommand (<anonymous>:34:17)\n"
+ " at t.executeCommand (<anonymous>:66:12)\n"
+ " at HTMLButtonElement.onclick (https://x/main.html:954:454)"),
+ ['isCommand', 'executeCommand']),
+ ("generics cannot justify 'still present'",
+ distinctive(['indexOf', 'function'], {'indexOf': 36, 'function': 74}), []),
+ ("a >=6-char shipped name still can",
+ distinctive(['indexOf', 'executeCommand'], {'indexOf': 36, 'executeCommand': 12}),
+ ['executeCommand']),
+ ("anonymous-origin stack is generated code",
+ stack_generated("TypeError: x\n at isCommand (<anonymous>:34:17)\n"
+ " at HTMLButtonElement.onclick (https://x/main.html:954:454)"), True),
+ ("shipped-origin stack is not generated",
+ stack_generated("TypeError: x\n at Terminal.dispatchCommand (https://x/core-bundle.js:1:2)"),
+ False),
+]
+
+
+def self_test():
+ """Regression cover for the 2026-11-12 false positive (plan 2026-11-13_062052 §2).
+
+ `--self-test` — run it after touching the identifier or stack parsers. A false "still present"
+ turns a non-defect into a HIGH steering directive, which is worse than a missed report.
+ """
+ bad = 0
+ for label, got, want in SELF_CASES:
+ ok = got == want
+ bad += 0 if ok else 1
+ print(('PASS ' if ok else 'FAIL ') + label + ('' if ok else ' got=%r want=%r' % (got, want)))
+ print('%d/%d self-tests pass' % (len(SELF_CASES) - bad, len(SELF_CASES)))
+ return 0 if not bad else 1
+
+
def main():
global REPORTS, MODULES, DECISIONS, ROOTDIR, ERRORS
ap = argparse.ArgumentParser()
@@ -234,7 +344,10 @@ def main():
ap.add_argument('--cap', type=int, default=None)
ap.add_argument('--errors', default=ERRORS, help='debug error store (fixtures)')
ap.add_argument('--include-plans', action='store_true', help='never used in production')
+ ap.add_argument('--self-test', action='store_true', help='run identifier/stack regression checks and exit')
a = ap.parse_args()
+ if a.self_test:
+ return self_test()
ROOTDIR = a.root
REPORTS, MODULES, DECISIONS = a.reports_dir, a.modules, a.decisions
ERRORS = a.errors