Files
HarborFork/harbor/util.py
T
KeelrLeek dbfd1720c5 Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
Messages, threads, acknowledgements, claims, bands, tasks, review items and
stop flags for a team of AI agents; the chat page and board; host hooks and
MCP server; autopilot; tools/watch.py. Put under version control after the
weekend's changes so they have history and can be rolled back. Local only.
2026-10-05 10:44:23 -05:00

123 lines
4.3 KiB
Python

"""Small shared helpers: times, scopes, the compressed-text detector and Harbor's error type."""
import datetime
import fnmatch
import re
import time
# Run-together words such as "full222", "stub23HarnessBug" or "correctOffsetsWereWrong". Hex tokens (commit hashes,
# addresses) and ordinary identifiers (PascalCase names, g_snake_case globals) are not counted.
SQUASHED = re.compile(r"[A-Za-z]{3,}\d{2,}[A-Za-z]*|\b[a-z]{3,}[A-Z][a-z]{2,}[A-Z][a-z]{2,}[A-Za-z]*\b")
HEXLIKE = re.compile(r"(?:0x)?[0-9A-Fa-f]+")
PRIORITIES = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
PRIORITY_NAMES = {v: k for k, v in PRIORITIES.items()}
class HarborError(Exception):
"""A refused operation; the message says why and what to do instead."""
def squashed_tokens(text):
found = []
for token in re.split(r"[\s,;:()\[\]{}<>\"'`/|=+]+", text):
token = token.strip(".!?-")
if not token or HEXLIKE.fullmatch(token) or "_" in token:
continue
if SQUASHED.search(token):
found.append(token)
return found
def utc(ts=None):
return datetime.datetime.fromtimestamp(time.time() if ts is None else ts, datetime.timezone.utc)
def iso(dt):
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
def parse_iso(text):
return datetime.datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc)
def _span(text):
m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*([mhd])\s*", str(text))
if not m:
return None
return datetime.timedelta(seconds=float(m.group(1)) * {"m": 60, "h": 3600, "d": 86400}[m.group(2)])
def parse_since(text, now):
"""'8h', '90m', '2d' or an ISO time -> ISO time in the past."""
span = _span(text)
if span is not None:
return iso(now - span)
try:
return iso(parse_iso(str(text).strip()))
except ValueError:
raise HarborError(f"since {text!r}: use 8h, 90m, 2d or 2026-10-02T13:25:00Z")
def parse_until(text, now):
"""'30m', '2h', '1d' or an ISO time -> ISO time in the future (due dates, 'away until')."""
span = _span(text)
if span is not None:
return iso(now + span)
try:
return iso(parse_iso(str(text).strip()))
except ValueError:
raise HarborError(f"time {text!r}: use 30m, 2h, 1d or 2026-10-03T13:00:00Z")
def parse_priority(value):
if isinstance(value, int) and value in PRIORITY_NAMES:
return value
key = str(value or "normal").lower()
if key not in PRIORITIES:
raise HarborError(f"priority must be one of {', '.join(PRIORITIES)}")
return PRIORITIES[key]
def parse_scope(scope):
"""'fn:0x529350' / '0x529350' / 'range:0x440000-0x47FFFF' -> (canonical, lo, hi); others keep lo/hi None."""
text = scope.strip()
m = re.fullmatch(r"(?:fn:)?(0x[0-9A-Fa-f]+)", text)
if m:
value = int(m.group(1), 16)
return f"fn:0x{value:06X}", value, value
m = re.fullmatch(r"range:(0x[0-9A-Fa-f]+)-(0x[0-9A-Fa-f]+)", text)
if m:
lo, hi = int(m.group(1), 16), int(m.group(2), 16)
if lo > hi:
raise HarborError(f"range {text}: start is after end")
return f"range:0x{lo:06X}-0x{hi:06X}", lo, hi
m = re.fullmatch(r"(file|name|topic|task):(.+)", text)
if m:
value = m.group(2).strip()
f = re.match(r"(?:.*[/\\])?([0-9A-Fa-f]{6})_[^/\\]*\.c$", value) if m.group(1) == "file" else None
if f: # a function's file: it stands for that address
address = int(f.group(1), 16)
return f"file:{value}", address, address
return f"{m.group(1)}:{value}", None, None
raise HarborError(f"scope {scope!r}: use fn:0xADDR, range:0xLO-0xHI, name:Symbol, file:path, task:text or topic:text")
def scopes_overlap(a, b):
"""a, b: (scope, lo, hi)."""
if a[1] is not None and b[1] is not None:
return a[1] <= b[2] and b[1] <= a[2]
if a[1] is not None or b[1] is not None:
return False
ka, va = a[0].split(":", 1)
kb, vb = b[0].split(":", 1)
if ka != kb:
return False
if ka == "file":
va, vb = va.replace("\\", "/").lower(), vb.replace("\\", "/").lower()
return va == vb or fnmatch.fnmatch(va, vb) or fnmatch.fnmatch(vb, va)
return va.lower() == vb.lower()
def first_line(text, limit=160):
line = (text or "").strip().split("\n", 1)[0]
return line if len(line) <= limit else line[:limit - 1] + "…"