feat(client): TagExtractor — pure regex parse/strip of §12 tags

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 14:46:11 -05:00
parent 7c5eaf3ba3
commit 5388979bf1
4 changed files with 80 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
class_name TagExtractor
extends RefCounted
## Pure §12 tag parsing. Extracts [FACT:], [ADJUST_DISPOSITION:], [MOVE:] tags
## and returns the prose with all tags stripped. It never applies anything and
## never validates a move (that needs NPC context from /npc/speak — a later
## plan). Its outputs are exactly the inputs CanonLog's mutators consume.
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 _any_re: RegEx = RegEx.create_from_string("\\[[A-Z_]+:[^\\]]*\\]")
static func extract(prose: String) -> Dictionary:
var facts: Array = []
for m in _fact_re.search_all(prose):
facts.append(m.get_string(1).strip_edges())
var dispositions: Array = []
for m in _dispo_re.search_all(prose):
dispositions.append(int(m.get_string(1)))
var moves: Array = []
for m in _move_re.search_all(prose):
var raw_args := m.get_string(2).strip_edges()
var args: Array = []
if raw_args != "":
for a in raw_args.split(","):
args.append(a.strip_edges())
moves.append({"name": m.get_string(1), "args": args})
var clean := _any_re.sub(prose, "", true)
# Collapse whitespace left where inline tags were removed.
while clean.contains(" "):
clean = clean.replace(" ", " ")
clean = clean.strip_edges()
return {"clean_text": clean, "facts": facts, "dispositions": dispositions, "moves": moves}

View File

@@ -0,0 +1 @@
uid://cib5yw5k0kip4