fix(client): TagExtractor tolerates paren-less moves + scrubs misformatted move tags (live-review LF1/LF2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,9 @@ extends RefCounted
|
||||
|
||||
static var _fact_re: RegEx = RegEx.create_from_string("\\[FACT:\\s*(.*?)\\]")
|
||||
static var _dispo_re: RegEx = RegEx.create_from_string("\\[ADJUST_DISPOSITION:\\s*([+-]?\\d+)\\]")
|
||||
static var _move_re: RegEx = RegEx.create_from_string("\\[MOVE:\\s*(\\w+)\\(([^)]*)\\)\\]")
|
||||
static var _move_re: RegEx = RegEx.create_from_string("\\[MOVE:\\s*(\\w+)(?:\\(([^)]*)\\))?\\]")
|
||||
static var _any_re: RegEx = RegEx.create_from_string("\\[[A-Z_]+:[^\\]]*\\]")
|
||||
static var _bare_move_re: RegEx = RegEx.create_from_string("\\[[a-z_]+\\([^\\]]*\\)\\]")
|
||||
|
||||
|
||||
static func extract(prose: String) -> Dictionary:
|
||||
@@ -30,6 +31,7 @@ static func extract(prose: String) -> Dictionary:
|
||||
moves.append({"name": m.get_string(1), "args": args})
|
||||
|
||||
var clean := _any_re.sub(prose, "", true)
|
||||
clean = _bare_move_re.sub(clean, "", true)
|
||||
# Collapse whitespace left where inline tags were removed.
|
||||
while clean.contains(" "):
|
||||
clean = clean.replace(" ", " ")
|
||||
|
||||
@@ -38,3 +38,45 @@ func test_no_tags_is_clean_passthrough():
|
||||
assert_eq(r["dispositions"], [])
|
||||
assert_eq(r["moves"], [])
|
||||
assert_eq(r["clean_text"], "no tags here")
|
||||
|
||||
|
||||
# LF1 — a live model emitted [MOVE: refuse] with no parens at all. The move
|
||||
# must still be recognized so the legal move isn't silently dropped.
|
||||
func test_move_without_parens_is_parsed():
|
||||
var r := TagExtractor.extract("[MOVE: refuse]")
|
||||
assert_eq(r["moves"][0]["name"], "refuse")
|
||||
assert_eq(r["moves"][0]["args"], [])
|
||||
|
||||
|
||||
# LF1 regression guard — explicit empty parens must still work as before.
|
||||
func test_move_with_explicit_empty_parens_still_parsed():
|
||||
var r := TagExtractor.extract("[MOVE: end_conversation()]")
|
||||
assert_eq(r["moves"][0]["name"], "end_conversation")
|
||||
assert_eq(r["moves"][0]["args"], [])
|
||||
|
||||
|
||||
# LF1 regression guard — well-formed args must still parse unchanged.
|
||||
func test_move_with_args_still_parsed():
|
||||
var r := TagExtractor.extract("[MOVE: reveal(varrell_twins)]")
|
||||
assert_eq(r["moves"][0]["name"], "reveal")
|
||||
assert_eq(r["moves"][0]["args"], ["varrell_twins"])
|
||||
|
||||
|
||||
# LF2 — a live model emitted [adjust_disposition(-5)] without the MOVE:
|
||||
# prefix. It must not be parsed/applied (untrusted, malformed), but it must
|
||||
# also not leak into player-visible prose as literal tag text.
|
||||
func test_bare_lowercase_move_shape_is_scrubbed_not_parsed():
|
||||
var r := TagExtractor.extract("He glares. [adjust_disposition(-5)] Fine.")
|
||||
assert_false(r["clean_text"].contains("[adjust_disposition(-5)]"))
|
||||
assert_eq(r["moves"], [])
|
||||
assert_eq(r["dispositions"], [])
|
||||
|
||||
|
||||
# LF2 regression guard — a well-formed MOVE tag alongside a bare lowercase
|
||||
# shape in the same prose must still be parsed and stripped correctly.
|
||||
func test_wellformed_move_still_parsed_alongside_bare_lowercase_shape():
|
||||
var r := TagExtractor.extract("[MOVE: reveal(x)] He glares. [adjust_disposition(-5)] Fine.")
|
||||
assert_eq(r["moves"][0]["name"], "reveal")
|
||||
assert_eq(r["moves"][0]["args"], ["x"])
|
||||
assert_false(r["clean_text"].contains("[MOVE:"))
|
||||
assert_false(r["clean_text"].contains("[adjust_disposition(-5)]"))
|
||||
|
||||
Reference in New Issue
Block a user