hexcli 2.10.0__tar.gz → 2.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hexcli-2.10.0 → hexcli-2.11.0}/CHANGELOG.md +35 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/PKG-INFO +1 -1
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/__init__.py +1 -1
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/agent.py +45 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/tools.py +188 -5
- {hexcli-2.10.0 → hexcli-2.11.0}/.gitignore +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/Hex CLI.cmd +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/LICENSE +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/README.md +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/assets/hexcli.ico +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/assets/hexcli.png +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/cancel.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/chatlog.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/commands.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/compaction.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/config.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/diffview.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/distribution.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/doctor.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/escalate.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/http_client.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/launcher.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/lineedit.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/llm.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/local_escalation.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/lockfile.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/loop_v2.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/markdown_stream.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/memory.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/network.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/parsing.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/paths.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/prompts.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/protocol_v2.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/repl.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/safety.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/sessions.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/setup_wizard.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/shell_session.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/statusbar.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/stream_render.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/telemetry.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/hexcli/ui.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/install.ps1 +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/launcher.py +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/pyproject.toml +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/shellai.cmd +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/shellai.example.json +0 -0
- {hexcli-2.10.0 → hexcli-2.11.0}/shellai.py +0 -0
|
@@ -6,6 +6,41 @@ the Hexagon NPU, not single-run anecdotes.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 2.11.0 — 2026-09-14
|
|
10
|
+
|
|
11
|
+
A minor release: tool result text the model reads changed (`verify_syntax`
|
|
12
|
+
reports its rung) and a new nudge was added. Gate: extended suite at 5 runs, seed 20260914, gate PASS after a 6-run recheck (five cases missed once, all 5/6 or better on recheck; run-level 156/205 vs 165/208, p=0.48); multi-turn at 3 runs on a fresh server with 0 invalid runs, no 3/3 case lost, uc3-t7 gained (38/48 vs 33/44, p=0.80); smoke 10/10; stall probe clean; CI green on main and the tag.
|
|
13
|
+
|
|
14
|
+
- Claims need evidence. A finish that says it ran something, that it
|
|
15
|
+
works, that the buttons respond or that the tests pass, needs a run this
|
|
16
|
+
turn (run_code or run_command); reading the file back proves the bytes,
|
|
17
|
+
not the behaviour. The owner's 2026-09-13 calculator session made three
|
|
18
|
+
such claims after read_file, with nothing ever run and nothing wired.
|
|
19
|
+
One nudge names the claim and asks for a run or a plain statement that
|
|
20
|
+
it was not run; a second unbacked claim goes out with a dim "Nothing was
|
|
21
|
+
run this turn." under it. The tests-claim nudge is the same rule for one
|
|
22
|
+
phrase; this is the class.
|
|
23
|
+
- `verify_syntax` is a ladder that reports the rung it reached. A page is
|
|
24
|
+
parsed and cross-referenced: handlers the markup calls must be functions
|
|
25
|
+
a script defines, ids the scripts look up must be elements the markup
|
|
26
|
+
has, buttons must be wired to something, and a script after `</body>` is
|
|
27
|
+
noted (the calculator: no handler on any button, an id no element had, a
|
|
28
|
+
function nothing called). Python is parsed and cross-referenced for
|
|
29
|
+
names it reads but never binds (a star import turns that off). JSON and
|
|
30
|
+
PowerShell say "parsed". A file with no checker is `NOT CHECKED`, never
|
|
31
|
+
`OK: skipped`, and says so, so the claim above cannot rest on it.
|
|
32
|
+
- Two eval cases: `claims-1`, the calculator prompt graded on the page
|
|
33
|
+
being wired and the finish not claiming a run that never happened, and
|
|
34
|
+
`claims-2`, a Python edit-and-claim variant. The 2026-09-13 page is a
|
|
35
|
+
fixture.
|
|
36
|
+
- The eval runner waits for the inference slot after a client timeout.
|
|
37
|
+
An abandoned request keeps the server's one slot until it finishes; the
|
|
38
|
+
next run queued behind it, timed out too, and took a whole scenario
|
|
39
|
+
down as invalid (uc3, 2026-09-13, twice). After a timed-out run the
|
|
40
|
+
runner probes until the backend answers again, up to ten minutes, and
|
|
41
|
+
the timeout no longer counts toward the abort streak once the slot is
|
|
42
|
+
back.
|
|
43
|
+
|
|
9
44
|
## 2.10.0 — 2026-09-14
|
|
10
45
|
|
|
11
46
|
A minor release: the retry feedback the model reads changed. Gate: extended
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hexcli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
4
4
|
Summary: Local Hexagon NPU terminal agent for Snapdragon X Elite Windows ARM64
|
|
5
5
|
Project-URL: Homepage, https://github.com/NathanL15/Hex-CLI
|
|
6
6
|
Project-URL: Repository, https://github.com/NathanL15/Hex-CLI
|
|
@@ -1193,6 +1193,9 @@ def _run_autopilot_turn(
|
|
|
1193
1193
|
_tests_requested = _asks_to_run_tests(query)
|
|
1194
1194
|
_tests_nudge_used = False
|
|
1195
1195
|
_run_targets: list[str] = []
|
|
1196
|
+
_ran_anything = False # any run_code / run_command this turn: the evidence a behaviour claim needs
|
|
1197
|
+
_claim_nudge_used = False
|
|
1198
|
+
_unbacked_claim = False
|
|
1196
1199
|
# The last test run's failure output (None once a run passes), so a
|
|
1197
1200
|
# finish right after a failing run can be sent back once more.
|
|
1198
1201
|
_last_test_failure: str | None = None
|
|
@@ -1338,6 +1341,21 @@ def _run_autopilot_turn(
|
|
|
1338
1341
|
messages.append({"role": "assistant", "content": strip_thinking(raw)})
|
|
1339
1342
|
messages.append({"role": "user", "content": _retest_nudge_text(_last_test_failure)})
|
|
1340
1343
|
continue
|
|
1344
|
+
# Claims need evidence. "Ran it successfully", "the buttons now
|
|
1345
|
+
# respond", "tests pass": a behaviour claim in the finish needs
|
|
1346
|
+
# a run this turn; reading a file back proves the bytes, not the
|
|
1347
|
+
# behaviour (the calculator session, 2026-09-13: three such
|
|
1348
|
+
# claims, nothing ever run, nothing wired). One nudge: run it, or
|
|
1349
|
+
# say plainly that it was not run. A second unbacked claim goes
|
|
1350
|
+
# out with a one-line notice under it, so the user knows.
|
|
1351
|
+
claim = _behaviour_claim(msg)
|
|
1352
|
+
if (claim and not _ran_anything and config.get("require_verification", True)):
|
|
1353
|
+
if not _claim_nudge_used:
|
|
1354
|
+
_claim_nudge_used = True
|
|
1355
|
+
messages.append({"role": "assistant", "content": strip_thinking(raw)})
|
|
1356
|
+
messages.append({"role": "user", "content": _claim_nudge_text(claim)})
|
|
1357
|
+
continue
|
|
1358
|
+
_unbacked_claim = True
|
|
1341
1359
|
# Escalation trigger B — the verification nudge was ignored: the
|
|
1342
1360
|
# model finished a second time without checking its own mutation.
|
|
1343
1361
|
if (_unverified_mutation and _verify_nudge_used
|
|
@@ -1367,6 +1385,8 @@ def _run_autopilot_turn(
|
|
|
1367
1385
|
})
|
|
1368
1386
|
continue
|
|
1369
1387
|
result = msg or last_tool_output or "Done."
|
|
1388
|
+
if _unbacked_claim:
|
|
1389
|
+
ui.cprint(" Nothing was run this turn.", C.DIM)
|
|
1370
1390
|
memory.maybe_index_turn(config, query, tools_used, touched_paths, outcome="completed")
|
|
1371
1391
|
if session:
|
|
1372
1392
|
_record_undo_snapshots(session, _turn_snapshots)
|
|
@@ -1398,6 +1418,7 @@ def _run_autopilot_turn(
|
|
|
1398
1418
|
touched_paths.append(str(tool_path))
|
|
1399
1419
|
if tool_name in ("run_code", "run_command") and isinstance(action.get("args"), dict):
|
|
1400
1420
|
_run_targets.append(str(action["args"].get("path") or action["args"].get("command") or ""))
|
|
1421
|
+
_ran_anything = True
|
|
1401
1422
|
|
|
1402
1423
|
# Capture file state before first mutation so /undo can restore it.
|
|
1403
1424
|
if tool_name in {"edit_file", "write_file", "append_file"} and tool_path:
|
|
@@ -1685,6 +1706,30 @@ _RUN_TESTS_RE = re.compile(
|
|
|
1685
1706
|
)
|
|
1686
1707
|
|
|
1687
1708
|
|
|
1709
|
+
_BEHAVIOUR_CLAIM_RE = re.compile(
|
|
1710
|
+
r"\b(ran (?:it|the [a-z]+|successfully)(?: successfully)?|runs (?:correctly|fine|successfully|as expected)|"
|
|
1711
|
+
r"(?:it|this|that|everything|the [a-z]+) (?:now |all )?works\b|works (?:as expected|correctly|now|fine)|"
|
|
1712
|
+
r"(?:it|this|that|everything|the [a-z]+) is (?:now )?working(?: correctly| as expected)?|"
|
|
1713
|
+
r"(?:buttons?|it|the app|the page|the script) (?:now )?responds?|"
|
|
1714
|
+
r"calculates? correctly|opens? correctly|executed successfully|"
|
|
1715
|
+
r"tests? (?:pass|passed|passing|succeed)|all tests pass|verified (?:that )?it (?:works|runs)|"
|
|
1716
|
+
r"confirmed (?:that )?it (?:works|runs))\b", re.IGNORECASE)
|
|
1717
|
+
|
|
1718
|
+
|
|
1719
|
+
def _behaviour_claim(message: str) -> str:
|
|
1720
|
+
"""The first behaviour claim in a finish message, or ''. Deliberately a
|
|
1721
|
+
list of ways to say "I ran it and it works", not a list of verbs: "the
|
|
1722
|
+
function returns the sum" is a description, "it works" is a claim."""
|
|
1723
|
+
m = _BEHAVIOUR_CLAIM_RE.search(message or "")
|
|
1724
|
+
return m.group(0) if m else ""
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
def _claim_nudge_text(claim: str) -> str:
|
|
1728
|
+
return (f"You said \"{claim}\" but nothing was run this turn. Run it with run_code or "
|
|
1729
|
+
"run_command and report what you observed, or say plainly that it was not run. "
|
|
1730
|
+
"Respond with JSON only.")
|
|
1731
|
+
|
|
1732
|
+
|
|
1688
1733
|
def _asks_to_run_tests(query: str) -> bool:
|
|
1689
1734
|
"""True when the request itself asks for the tests to be run."""
|
|
1690
1735
|
return bool(_RUN_TESTS_RE.search(query or ""))
|
|
@@ -581,22 +581,205 @@ def _verify_node_syntax(path: Path) -> tuple[bool, str]:
|
|
|
581
581
|
return False, f"FAIL: {result.stderr.strip() or result.stdout.strip()}"
|
|
582
582
|
|
|
583
583
|
|
|
584
|
+
_PY_IMPLICIT = {"__name__", "__file__", "__doc__", "__builtins__", "__spec__", "__loader__",
|
|
585
|
+
"__package__", "__path__", "__annotations__", "__debug__"}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def undefined_python_names(source: str) -> list[str]:
|
|
589
|
+
"""Names the module reads but never binds anywhere and Python does not
|
|
590
|
+
provide: the cross-reference rung for Python. Scopes are collapsed to
|
|
591
|
+
the module (a name bound anywhere counts), so this never flags a name
|
|
592
|
+
that some function defines; it only catches the calculator-class
|
|
593
|
+
mistake of calling something that does not exist. A star import turns
|
|
594
|
+
the check off."""
|
|
595
|
+
import builtins
|
|
596
|
+
try:
|
|
597
|
+
tree = ast.parse(source)
|
|
598
|
+
except SyntaxError:
|
|
599
|
+
return []
|
|
600
|
+
bound: set[str] = set(dir(builtins)) | _PY_IMPLICIT
|
|
601
|
+
loads: dict[str, int] = {}
|
|
602
|
+
for node in ast.walk(tree):
|
|
603
|
+
if isinstance(node, ast.ImportFrom) and any(a.name == "*" for a in node.names):
|
|
604
|
+
return []
|
|
605
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
606
|
+
for a in node.names:
|
|
607
|
+
bound.add((a.asname or a.name).split(".")[0])
|
|
608
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
609
|
+
bound.add(node.name)
|
|
610
|
+
if not isinstance(node, ast.ClassDef):
|
|
611
|
+
args = node.args
|
|
612
|
+
for a in [*args.posonlyargs, *args.args, *args.kwonlyargs]:
|
|
613
|
+
bound.add(a.arg)
|
|
614
|
+
for a in (args.vararg, args.kwarg):
|
|
615
|
+
if a is not None:
|
|
616
|
+
bound.add(a.arg)
|
|
617
|
+
elif isinstance(node, ast.Lambda):
|
|
618
|
+
args = node.args
|
|
619
|
+
for a in [*args.posonlyargs, *args.args, *args.kwonlyargs]:
|
|
620
|
+
bound.add(a.arg)
|
|
621
|
+
for a in (args.vararg, args.kwarg):
|
|
622
|
+
if a is not None:
|
|
623
|
+
bound.add(a.arg)
|
|
624
|
+
elif isinstance(node, ast.ExceptHandler) and node.name:
|
|
625
|
+
bound.add(node.name)
|
|
626
|
+
elif isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
627
|
+
bound.update(node.names)
|
|
628
|
+
elif isinstance(node, ast.MatchAs) and node.name:
|
|
629
|
+
bound.add(node.name)
|
|
630
|
+
elif isinstance(node, ast.MatchStar) and node.name:
|
|
631
|
+
bound.add(node.name)
|
|
632
|
+
elif isinstance(node, ast.MatchMapping) and node.rest:
|
|
633
|
+
bound.add(node.rest)
|
|
634
|
+
elif isinstance(node, ast.Name):
|
|
635
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
636
|
+
bound.add(node.id)
|
|
637
|
+
else:
|
|
638
|
+
loads.setdefault(node.id, node.lineno)
|
|
639
|
+
return [f"{name} (line {line})" for name, line in sorted(loads.items(), key=lambda kv: kv[1])
|
|
640
|
+
if name not in bound]
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
_JS_BUILTIN_CALLS = {"alert", "confirm", "prompt", "eval", "parseInt", "parseFloat", "String",
|
|
644
|
+
"Number", "Boolean", "Math", "setTimeout", "setInterval", "console",
|
|
645
|
+
"document", "window", "event", "this", "if", "for", "while", "return",
|
|
646
|
+
"function", "new", "Array", "Object", "JSON", "isNaN", "Date", "fetch",
|
|
647
|
+
"requestAnimationFrame", "clearTimeout", "clearInterval", "Promise"}
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def html_wiring_report(path: Path) -> tuple[bool, str]:
|
|
651
|
+
"""Cross-reference rung for a page: the handlers the markup calls must be
|
|
652
|
+
functions the scripts define, the ids the scripts look up must be
|
|
653
|
+
elements the markup has, and buttons must be wired to something. Reading
|
|
654
|
+
a page back proves the bytes; this proves the parts are connected
|
|
655
|
+
(2026-09-14: a calculator with buttons that called nothing, a script that
|
|
656
|
+
looked up an id no element had, and a function nothing called was
|
|
657
|
+
"verified" three times by read_file)."""
|
|
658
|
+
from html.parser import HTMLParser
|
|
659
|
+
|
|
660
|
+
class _Walk(HTMLParser):
|
|
661
|
+
def __init__(self) -> None:
|
|
662
|
+
super().__init__()
|
|
663
|
+
self.ids: set[str] = set()
|
|
664
|
+
self.handlers: list[str] = []
|
|
665
|
+
self.buttons = 0
|
|
666
|
+
self.scripts: list[str] = []
|
|
667
|
+
self.script_srcs: list[str] = []
|
|
668
|
+
self._in_script = False
|
|
669
|
+
self._body_closed = False
|
|
670
|
+
self.script_after_body = False
|
|
671
|
+
self._buf: list[str] = []
|
|
672
|
+
|
|
673
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
674
|
+
a = {k.lower(): (v or "") for k, v in attrs}
|
|
675
|
+
if a.get("id"):
|
|
676
|
+
self.ids.add(a["id"])
|
|
677
|
+
for k, v in a.items():
|
|
678
|
+
if k.startswith("on") and v:
|
|
679
|
+
self.handlers.extend(m for m in re.findall(r"([A-Za-z_$][\w$]*)\s*\(", v))
|
|
680
|
+
if tag == "button" or (tag == "input" and a.get("type", "").lower() in {"button", "submit"}):
|
|
681
|
+
self.buttons += 1
|
|
682
|
+
if tag == "script":
|
|
683
|
+
self._in_script = True
|
|
684
|
+
self._buf = []
|
|
685
|
+
if a.get("src"):
|
|
686
|
+
self.script_srcs.append(a["src"])
|
|
687
|
+
if self._body_closed:
|
|
688
|
+
self.script_after_body = True
|
|
689
|
+
|
|
690
|
+
def handle_endtag(self, tag: str) -> None:
|
|
691
|
+
if tag == "script" and self._in_script:
|
|
692
|
+
self._in_script = False
|
|
693
|
+
self.scripts.append("".join(self._buf))
|
|
694
|
+
if tag in {"body", "html"}:
|
|
695
|
+
self._body_closed = True
|
|
696
|
+
|
|
697
|
+
def handle_data(self, data: str) -> None:
|
|
698
|
+
if self._in_script:
|
|
699
|
+
self._buf.append(data)
|
|
700
|
+
|
|
701
|
+
source = path.read_text(encoding="utf-8", errors="replace")
|
|
702
|
+
w = _Walk()
|
|
703
|
+
try:
|
|
704
|
+
w.feed(source)
|
|
705
|
+
w.close()
|
|
706
|
+
except Exception as exc: # noqa: BLE001 — html.parser is lenient; anything else is a report
|
|
707
|
+
return False, f"FAIL: could not parse the page: {exc}"
|
|
708
|
+
script = "\n".join(w.scripts)
|
|
709
|
+
for src in w.script_srcs:
|
|
710
|
+
try:
|
|
711
|
+
script += "\n" + (path.parent / src).read_text(encoding="utf-8", errors="replace")
|
|
712
|
+
except OSError:
|
|
713
|
+
pass
|
|
714
|
+
defined = set(re.findall(r"\bfunction\s+([A-Za-z_$][\w$]*)\s*\(", script))
|
|
715
|
+
defined |= set(re.findall(r"\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\(|[A-Za-z_$][\w$]*\s*=>)", script))
|
|
716
|
+
defined |= set(re.findall(r"\b(?:window\.)?([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?function\b", script))
|
|
717
|
+
looked_up = set(re.findall(r"getElementById\(\s*['\"]([^'\"]+)['\"]", script))
|
|
718
|
+
looked_up |= set(re.findall(r"querySelector(?:All)?\(\s*['\"]#([\w-]+)", script))
|
|
719
|
+
listeners = bool(re.search(r"addEventListener\s*\(|\.on[a-z]+\s*=", script))
|
|
720
|
+
problems: list[str] = []
|
|
721
|
+
undefined = sorted({h for h in w.handlers if h not in defined and h not in _JS_BUILTIN_CALLS})
|
|
722
|
+
if undefined:
|
|
723
|
+
problems.append("handlers the markup calls but no script defines: " + ", ".join(undefined))
|
|
724
|
+
missing = sorted(i for i in looked_up if i not in w.ids)
|
|
725
|
+
if missing:
|
|
726
|
+
problems.append("ids the script looks up but no element has: " + ", ".join(missing))
|
|
727
|
+
if w.buttons and not w.handlers and not listeners:
|
|
728
|
+
problems.append(f"{w.buttons} button(s) and none is wired to a script (no on* attribute, no addEventListener)")
|
|
729
|
+
notes: list[str] = []
|
|
730
|
+
if w.script_after_body:
|
|
731
|
+
notes.append("a <script> sits after </body>")
|
|
732
|
+
if defined and not w.handlers and not listeners:
|
|
733
|
+
notes.append("functions defined but nothing calls them: " + ", ".join(sorted(defined)))
|
|
734
|
+
if problems:
|
|
735
|
+
return False, "FAIL: parsed, cross-referenced: " + "; ".join(problems + notes)
|
|
736
|
+
summary = f"OK: parsed, cross-referenced ({len(w.handlers)} handler call(s), {len(w.ids)} id(s), {w.buttons} button(s))"
|
|
737
|
+
if notes:
|
|
738
|
+
summary += "; " + "; ".join(notes)
|
|
739
|
+
return True, summary
|
|
740
|
+
|
|
741
|
+
|
|
584
742
|
def verify_syntax_tool(path_text: str, language: str, shell_exe: str) -> str:
|
|
743
|
+
"""The verification ladder. Every result says which rung was reached:
|
|
744
|
+
executed (not here: run_code does that), parsed and cross-referenced,
|
|
745
|
+
parsed only, or NOT CHECKED. A file this tool cannot check is reported
|
|
746
|
+
as unchecked, never as OK: the model used to read "OK: skipped" and
|
|
747
|
+
finish with "verified"."""
|
|
585
748
|
path = resolve_path(path_text)
|
|
586
749
|
if not path.exists():
|
|
587
750
|
raise RuntimeError(f"File not found: {path}")
|
|
588
|
-
|
|
589
|
-
|
|
751
|
+
suffix = path.suffix.lower()
|
|
752
|
+
lang = (language or "").strip().lower() or _LANGUAGE_BY_EXT.get(suffix, "")
|
|
753
|
+
if suffix in {".html", ".htm"} or lang in {"html", "htm"}:
|
|
754
|
+
ok, detail = html_wiring_report(path)
|
|
755
|
+
elif lang == "python":
|
|
590
756
|
ok, detail = _verify_python_syntax(path)
|
|
757
|
+
if ok and not detail.startswith("OK: skipped"):
|
|
758
|
+
undefined = undefined_python_names(path.read_text(encoding="utf-8", errors="replace"))
|
|
759
|
+
if undefined:
|
|
760
|
+
ok, detail = False, "FAIL: parsed, cross-referenced: references undefined name(s): " + ", ".join(undefined)
|
|
761
|
+
else:
|
|
762
|
+
detail = "OK: parsed, cross-referenced: no syntax errors, every name it reads is defined"
|
|
591
763
|
elif lang == "json":
|
|
592
764
|
ok, detail = _verify_json_syntax(path)
|
|
765
|
+
if ok:
|
|
766
|
+
detail = detail.replace("OK: valid JSON", "OK: parsed: valid JSON")
|
|
593
767
|
elif lang == "powershell":
|
|
594
768
|
ok, detail = _verify_powershell_syntax(path, shell_exe)
|
|
595
|
-
|
|
769
|
+
if ok and detail.startswith("OK: skipped"):
|
|
770
|
+
ok, detail = True, "NOT CHECKED: " + detail[len("OK: skipped ("):].rstrip(")")
|
|
771
|
+
elif ok:
|
|
772
|
+
detail = "OK: parsed: no syntax errors"
|
|
773
|
+
elif lang == "node" or suffix in {".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"}:
|
|
596
774
|
ok, detail = _verify_node_syntax(path)
|
|
775
|
+
if ok and detail.startswith("OK: skipped"):
|
|
776
|
+
ok, detail = True, "NOT CHECKED: " + detail[len("OK: skipped ("):].rstrip(")")
|
|
777
|
+
elif ok:
|
|
778
|
+
detail = "OK: parsed: no syntax errors"
|
|
597
779
|
else:
|
|
598
|
-
ok, detail = True, f"
|
|
599
|
-
|
|
780
|
+
ok, detail = True, (f"NOT CHECKED: no checker for '{suffix or language or 'this file'}'. "
|
|
781
|
+
"The file was not verified; say so if you report on it.")
|
|
782
|
+
ui.tool_event("verify", f"{path} ({'fail' if not ok else ('unchecked' if detail.startswith('NOT CHECKED') else 'pass')})")
|
|
600
783
|
return detail
|
|
601
784
|
|
|
602
785
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|