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:
@@ -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()
|
||||
Reference in New Issue
Block a user