Add the command parse differential harness

A Java oracle builds vanilla's real Brigadier command tree with the loaded registries and data-pack tags, exports the tree, and parses a corpus of command strings. The corpus is generated by walking that tree with valid and invalid samples for every argument type plus hand-written edge cases. A Rust example parses the same corpus with Horizon's dispatcher and the Python driver compares success, exact error text and cursor, and end-of-input completion suggestions, with tree-diff and mismatch summary helpers. Currently 79 percent of 39685 inputs match at permission level 4. Entity selector errors now rewind to the start of the input like vanilla.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-10 03:43:22 +02:00
parent 4652e983c5
commit 6bde74957a
7 changed files with 1310 additions and 2 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Structural diff between vanilla's exported command tree and Horizon's.
usage: command_tree_diff.py VANILLA_TREE.json RUST_TREE.json [ROOT]
Reports, for every shared node, the children Horizon lacks or adds, differing
argument parsers and executable/redirect flags. Missing subtrees are reported
once at their root.
"""
import json
import sys
LOOSE = {
# Horizon's coarse argument kinds that stand in for several vanilla parsers.
'brigadier:string': {'brigadier:string', 'minecraft:message', 'minecraft:component'},
}
def same_parser(vanilla, rust):
if vanilla == rust:
return True
return False
def keyed(children):
"""Literals by name; arguments by position (their names are internal)."""
out = {}
index = 0
for name, child in children.items():
if child['type'] == 'literal':
out[('l', name)] = (name, child)
else:
out[('a', index)] = (name, child)
index += 1
return out
def diff(path, v, r, out):
vc, rc = keyed(v.get('children', {})), keyed(r.get('children', {}))
for key, (name, node) in vc.items():
if key not in rc:
out.append(('missing', path + [name], node.get('parser') or 'literal', count(node)))
for key, (name, node) in rc.items():
if key not in vc:
out.append(('extra', path + [name], node.get('parser') or 'literal', count(node)))
for key, (name, a) in vc.items():
if key not in rc:
continue
_, b = rc[key]
if a['type'] == 'argument' and not same_parser(a['parser'], b['parser']):
out.append(('parser', path + [name], f"{a['parser']} vs {b['parser']}", 0))
if bool(a.get('executable')) != bool(b.get('executable')):
out.append(('executable', path + [name], f"vanilla={bool(a.get('executable'))}", 0))
if bool(a.get('redirect')) != bool(b.get('redirect')):
out.append(('redirect', path + [name], f"vanilla={bool(a.get('redirect'))}", 0))
diff(path + [name], a, b, out)
def count(node):
return 1 + sum(count(c) for c in node.get('children', {}).values())
def main():
vanilla = json.load(open(sys.argv[1]))
rust = json.load(open(sys.argv[2]))
only = sys.argv[3] if len(sys.argv) > 3 else None
if only:
vanilla = {'children': {only: vanilla['children'][only]}, 'type': 'root'}
rust = {'children': {only: rust['children'].get(only, {'children': {}, 'type': 'literal'})}, 'type': 'root'}
out = []
diff([], vanilla, rust, out)
for kind, path, detail, size in out:
print(f'{kind:10} {" ".join(path)} [{detail}]' + (f' ({size} nodes)' if size else ''))
missing = sum(size for kind, _, _, size in out if kind == 'missing')
print(f'-- {len(out)} differences, {missing} vanilla nodes missing in Horizon')
if __name__ == '__main__':
main()