Files
2026-07-13 12:20:01 +08:00

794 lines
28 KiB
Python

#!/usr/bin/env python3
"""Deterministic state engine for the feedback sweep (ce-sweep).
This helper owns ALL reads and writes of the sweep state file. Peer agents
(source connectors, the analyzer, the orchestrator) never edit the file by
hand — they go through these subcommands so the schema contract, the id-keyed
merge, the single-writer lease, and the closed-item evidence rule are enforced
in exactly one place. See `references/state-schema.md` for the cross-agent
contract this script implements.
Design rules (mirrors the repo's repo-profile-cache helper):
- Pure Python 3 stdlib. No third-party dependencies.
- Every OPERATIONAL failure path prints a parseable STATUS WORD on line 1 and
exits 0 — it never raises a traceback to the caller. Only genuine CLI
misuse (bad/missing subcommand args) exits non-zero via argparse.
- Writes are atomic: a temp file in the state dir + os.replace (atomic on
POSIX), so a concurrent reader never sees a torn file.
- The script never calls the wall clock for the values it stores EXCEPT the
lease timestamp (staleness needs "now"). Tests pin it with --now / stamp
values with --timestamp so behavior is reproducible.
STATUS WORDS (line 1 of stdout for every subcommand):
OK success (an optional JSON payload follows on line 2)
NO-STATE read: the state file does not exist yet
CORRUPT the state file exists but does not parse as our schema
LOCKED lease-acquire: a live lease is held by another writer
STALE-RECLAIMED lease-acquire: an expired lease was taken over (JSON payload)
LEASE-LOST a mutating call was made by a writer that does not own the
lease (or a release of another writer's lease); no write
REFUSED cursor-advance: unknown past-item, or non-monotonic cursor
ERROR an unexpected internal error (defensive; never a traceback)
The state file is genuine YAML restricted to a small, deliberate subset so a
hand-written stdlib serializer/parser round-trips it deterministically. See
`references/state-schema.md` (section "YAML subset") for the exact grammar:
non-empty dict values become block mappings (any depth); scalars are emitted as
JSON tokens (strings always double-quoted on one line); lists and empty dicts
are emitted as inline JSON flow on a single line — itself valid YAML.
"""
import argparse
import json
import os
import sys
import tempfile
from datetime import datetime, timezone
try:
import fcntl # POSIX advisory locks (macOS, Linux — this repo's Unix targets)
_HAS_FCNTL = True
except ImportError: # non-POSIX; degrade to unlocked (single-writer by convention)
_HAS_FCNTL = False
SCHEMA_VERSION = 1
# The closed lifecycle status enum is documented in references/state-schema.md.
# Unknown statuses are preserved on write-back, never dropped — nothing here
# whitelists values.
# A `closed` item MUST carry all three evidence fields; `validate` downgrades
# any closed item missing any of them back to `fix_pending`.
EVIDENCE_FIELDS = ("fix_ref", "verified_merge_sha", "verified_at")
DEFAULT_TTL_MINUTES = 60
# --------------------------------------------------------------------------- #
# Minimal YAML subset: serializer + parser
# --------------------------------------------------------------------------- #
# Grammar (2-space indent, no tabs):
# - Block mappings: `<key>: <value>` or `<key>:` (nested mapping follows).
# - Keys: a bare token matching _SAFE_KEY, else a JSON double-quoted string.
# - Scalars: null / true / false / integers / floats emitted bare; strings
# always emitted as JSON double-quoted (single line, fully escaped).
# - Nested containers (dict/list) that are NOT part of the known block
# structure are emitted as inline JSON flow on one line — valid YAML.
# The emitter only produces block MAPPINGS (never block sequences); the parser
# tolerates arbitrary-depth block mappings plus scalar / inline-JSON values.
import re
_SAFE_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")
# Preferred key orders for deterministic, human-legible output. Any keys not
# listed are appended in sorted order so unknown/preserved fields stay stable.
_DOC_ORDER = ("schema_version", "lease", "sources", "items", "last_run")
_LEASE_ORDER = ("writer", "timestamp", "ttl_minutes")
_ITEM_ORDER = (
"source", "status", "sensitive", "title", "url", "body", "quote",
"fix_ref", "verified_merge_sha", "verified_at",
)
_LAST_RUN_ORDER = ("timestamp", "outcome", "writer", "counts")
def _ordered_keys(d, preferred):
seen = [k for k in preferred if k in d]
rest = sorted(k for k in d if k not in preferred)
return seen + rest
def _emit_key(key):
key = str(key)
if _SAFE_KEY.match(key):
return key
return json.dumps(key, ensure_ascii=False)
def _emit_scalar(v):
if v is None:
return "null"
if v is True:
return "true"
if v is False:
return "false"
if isinstance(v, int):
return str(v)
if isinstance(v, float):
return repr(v)
if isinstance(v, str):
return json.dumps(v, ensure_ascii=False)
# dict / list -> inline JSON flow (valid YAML), sorted for determinism.
return json.dumps(v, ensure_ascii=False, sort_keys=True)
def _emit_mapping(d, indent, preferred):
"""Emit a dict as block YAML. Only nests block-style for child dicts;
lists and any other container leaf are emitted inline via _emit_scalar."""
pad = " " * indent
lines = []
for key in _ordered_keys(d, preferred):
val = d[key]
ks = _emit_key(key)
if isinstance(val, dict) and val:
lines.append(f"{pad}{ks}:")
# Nested item/source/lease/last_run maps: no special preferred order
# for arbitrary nested dicts; only the top structural maps below get
# a preferred order (handled by their callers).
child_pref = _child_preferred(indent, key)
lines.extend(_emit_mapping(val, indent + 1, child_pref))
else:
lines.append(f"{pad}{ks}: {_emit_scalar(val)}")
return lines
def _child_preferred(parent_indent, key):
"""Preferred child-key order for the known structural containers."""
if parent_indent == 0:
if key == "lease":
return _LEASE_ORDER
if key == "last_run":
return _LAST_RUN_ORDER
return () # sources / items: children are id-keyed, no field order
# A source entry or item entry: apply the item field order (harmless for
# source entries, which only carry `cursor`/`sensitive`).
return _ITEM_ORDER
def emit_document(state):
lines = _emit_mapping(state, 0, _DOC_ORDER)
return "\n".join(lines) + "\n"
def _split_key(content):
"""Split a mapping line into (key, rest-after-colon). Raises ValueError on
a line with no colon so a malformed file surfaces as CORRUPT."""
if content.startswith('"'):
key, end = json.JSONDecoder().raw_decode(content)
rest = content[end:]
if not rest.startswith(":"):
raise ValueError("expected ':' after quoted key")
return key, rest[1:]
idx = content.find(":")
if idx == -1:
raise ValueError("expected ':' in mapping line")
return content[:idx], content[idx + 1:]
_BLOCK = object() # sentinel: value is a nested block, parse deeper lines
def _parse_value(rest):
rest = rest.strip()
if rest == "":
return _BLOCK
if rest == "null":
return None
if rest == "true":
return True
if rest == "false":
return False
first = rest[0]
if first in '"{[':
return json.loads(rest) # JSON string, object, or array (flow style)
if first == "-" or first.isdigit():
try:
if "." in rest or "e" in rest or "E" in rest:
return float(rest)
return int(rest)
except ValueError:
pass
# Emitter never produces bare unquoted strings; a bare token here is a
# hand-edit — keep it verbatim rather than failing.
return rest
def _parse_mapping(rows, cursor, indent):
result = {}
while cursor["i"] < len(rows):
cur_indent, content = rows[cursor["i"]]
if cur_indent < indent:
break
if cur_indent > indent:
# Orphan deeper line with no parent key; skip defensively.
cursor["i"] += 1
continue
cursor["i"] += 1
key, rest = _split_key(content)
val = _parse_value(rest)
if val is _BLOCK:
if cursor["i"] < len(rows) and rows[cursor["i"]][0] > indent:
val = _parse_mapping(rows, cursor, rows[cursor["i"]][0])
else:
val = {}
result[key] = val
return result
def parse_document(text):
rows = []
for raw in text.split("\n"):
stripped = raw.strip()
if not stripped or stripped.startswith("#"):
continue
indent = len(raw) - len(raw.lstrip(" "))
rows.append((indent, stripped))
if not rows:
return {}
return _parse_mapping(rows, {"i": 0}, rows[0][0])
# --------------------------------------------------------------------------- #
# State load / save
# --------------------------------------------------------------------------- #
def _item_key(source, item_id):
"""Storage key for an item. The items keyspace is flat, so a bare id can
collide across sources (two Slack channels emitting the same ts, or a
source that reuses numeric ids). Namespacing by source keeps each source's
id space independent, matching the composite keys documented in
references/state-schema.md."""
return "{}:{}".format(source, item_id)
def load_state(path):
"""Return (status, data): ('absent', None), ('corrupt', None), or
('ok', dict). A file that parses but lacks schema_version is corrupt."""
try:
with open(path) as f:
# A machine-local state file can live under world-shared /tmp, and
# it is a correctness dependency (lease, cursors, closed status) as
# well as an injection sink (item bodies re-read into agent
# context). Reject a file not owned by us so a co-tenant cannot
# plant a forged lease/cursor or attacker-authored item text. Skip
# where geteuid is unavailable (non-POSIX), where the threat does
# not apply.
geteuid = getattr(os, "geteuid", None)
if geteuid is not None and os.fstat(f.fileno()).st_uid != geteuid():
return ("corrupt", None)
text = f.read()
except FileNotFoundError:
return ("absent", None)
except OSError:
return ("corrupt", None)
if not text.strip():
return ("absent", None)
try:
data = parse_document(text)
except Exception:
return ("corrupt", None)
if not isinstance(data, dict) or "schema_version" not in data:
return ("corrupt", None)
data.setdefault("sources", {})
data.setdefault("items", {})
return ("ok", data)
def new_state():
return {"schema_version": SCHEMA_VERSION, "sources": {}, "items": {}}
def write_state(path, state):
"""Atomic write of the state file. Returns True on success."""
state["schema_version"] = SCHEMA_VERSION
text = emit_document(state)
d = os.path.dirname(os.path.abspath(path))
os.makedirs(d, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=d, prefix=".tmp-sweep-", suffix=".yml")
try:
with os.fdopen(fd, "w") as f:
f.write(text)
os.replace(tmp, path)
except BaseException:
try:
os.unlink(tmp)
except OSError:
pass
raise
return True
# --------------------------------------------------------------------------- #
# Output + time helpers
# --------------------------------------------------------------------------- #
def emit(status_word, payload=None):
print(status_word)
if payload is not None:
print(json.dumps(payload))
return 0
def resolve_now(args):
"""The 'current time' for lease staleness + lease re-stamping. Pinned by
--now in tests; otherwise the real UTC clock."""
now = getattr(args, "now", None)
if now:
return now
return datetime.now(timezone.utc).isoformat()
def _parse_iso(s):
if not isinstance(s, str) or not s:
return None
try:
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
except ValueError:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def current_lease(state):
"""The active lease dict (with a non-empty writer), or None."""
lease = state.get("lease")
if isinstance(lease, dict) and lease.get("writer"):
return lease
return None
def lease_is_stale(lease, now_iso):
"""True only when we can PROVE the lease is older than its TTL. If either
timestamp is unparseable we cannot prove staleness -> treat as live (do not
reclaim). Conservative: never stomp a lease we cannot show is expired."""
ts = _parse_iso(lease.get("timestamp", ""))
now = _parse_iso(now_iso)
if ts is None or now is None:
return False
try:
ttl = int(lease.get("ttl_minutes", DEFAULT_TTL_MINUTES))
except (TypeError, ValueError):
ttl = DEFAULT_TTL_MINUTES
return (now - ts).total_seconds() > ttl * 60
def restamp_lease(state, writer, now_iso):
"""Refresh the owning writer's lease timestamp so a long sweep keeps it
alive across many writes. Only called after ownership is confirmed."""
lease = state.get("lease")
if isinstance(lease, dict) and lease.get("writer") == writer:
lease["timestamp"] = now_iso
def owns_lease(state, writer):
lease = current_lease(state)
return lease is not None and lease.get("writer") == writer
# --------------------------------------------------------------------------- #
# Subcommands
# --------------------------------------------------------------------------- #
def cmd_read(args):
st, data = load_state(args.state)
if st == "absent":
return emit("NO-STATE")
if st == "corrupt":
return emit("CORRUPT")
return emit("OK", data)
def cmd_validate(args):
st, data = load_state(args.state)
if st == "corrupt":
return emit("CORRUPT")
if st == "absent":
return emit("OK", {"downgraded": []})
downgraded = []
for item_id, item in data.get("items", {}).items():
if not isinstance(item, dict):
continue
if item.get("status") == "closed" and any(
not item.get(f) for f in EVIDENCE_FIELDS
):
item["status"] = "fix_pending"
downgraded.append(item_id)
if downgraded:
write_state(args.state, data)
return emit("OK", {"downgraded": sorted(downgraded)})
def _load_owned_state(args):
"""Load state for a lease-gated mutation. Returns (data, None) when
args.writer holds the lease, else (None, status_word) for the caller to
emit. Absent state means no lease to own -> the caller is not the owner."""
st, data = load_state(args.state)
if st == "corrupt":
return None, "CORRUPT"
if st == "absent" or not owns_lease(data, args.writer):
return None, "LEASE-LOST"
return data, None
def _commit_owned(args, data):
"""Shared tail for lease-gated mutations: re-stamp the lease, persist."""
restamp_lease(data, args.writer, resolve_now(args))
write_state(args.state, data)
return emit("OK")
def cmd_upsert_item(args):
data, err = _load_owned_state(args)
if err:
return emit(err)
try:
incoming = json.loads(args.json)
except (ValueError, TypeError):
return emit("ERROR")
if not isinstance(incoming, dict):
return emit("ERROR")
items = data.setdefault("items", {})
key = _item_key(args.source, args.id)
existing = items.get(key)
merged = dict(existing) if isinstance(existing, dict) else {}
# id-keyed merge: only the keys present in the incoming item are replaced;
# unknown fields already on the item survive untouched.
merged.update(incoming)
merged["source"] = args.source
merged["id"] = args.id
source_entry = data.get("sources", {}).get(args.source, {})
is_sensitive = (
merged.get("sensitive") is True
or (isinstance(source_entry, dict) and source_entry.get("sensitive") is True)
)
if is_sensitive:
for f in ("body", "quote"):
merged.pop(f, None)
items[key] = merged
return _commit_owned(args, data)
def cmd_cursor_get(args):
st, data = load_state(args.state)
if st == "corrupt":
return emit("CORRUPT")
if st == "absent":
return emit("OK", {"cursor": None})
entry = data.get("sources", {}).get(args.source, {})
cursor = entry.get("cursor") if isinstance(entry, dict) else None
return emit("OK", {"cursor": cursor})
def cmd_cursor_advance(args):
data, err = _load_owned_state(args)
if err:
return emit(err)
# The cursor may only advance past an item that actually exists in state,
# so a resume never skips unrecorded items.
if _item_key(args.source, args.past_item) not in data.get("items", {}):
return emit("REFUSED")
entry = data.setdefault("sources", {}).setdefault(args.source, {})
current = entry.get("cursor")
# Monotonic guard. A new cursor sorting strictly before the current one is
# refused; equal is allowed (idempotent re-advance).
if current is not None and _cursor_lt(str(args.to), str(current)):
return emit("REFUSED")
entry["cursor"] = args.to
return _commit_owned(args, data)
def _cursor_lt(a, b):
"""True when cursor `a` precedes `b`. Pure-digit cursors compare
numerically so an unpadded id like '9' correctly precedes '10'; everything
else (Slack fixed-width ts, ISO timestamps) already sorts correctly as a
string, so fall back to lexical order."""
if a.isdigit() and b.isdigit():
return int(a) < int(b)
return a < b
def cmd_lease_acquire(args):
st, data = load_state(args.state)
if st == "corrupt":
return emit("CORRUPT")
if st == "absent":
data = new_state()
now = resolve_now(args)
ttl = args.ttl_minutes if args.ttl_minutes is not None else DEFAULT_TTL_MINUTES
lease = current_lease(data)
if lease is None or lease.get("writer") == args.writer:
# Free, or re-entrant acquire by the same writer: (re)stamp and take it.
data["lease"] = {"writer": args.writer, "timestamp": now, "ttl_minutes": ttl}
write_state(args.state, data)
return emit("OK")
if lease_is_stale(lease, now):
prev = {
"previous_writer": lease.get("writer"),
"previous_timestamp": lease.get("timestamp"),
}
data["lease"] = {"writer": args.writer, "timestamp": now, "ttl_minutes": ttl}
write_state(args.state, data)
return emit("STALE-RECLAIMED", prev)
return emit("LOCKED")
def cmd_lease_release(args):
st, data = load_state(args.state)
if st == "corrupt":
return emit("CORRUPT")
if st == "absent":
return emit("OK") # nothing to release
lease = current_lease(data)
if lease is None:
return emit("OK")
if lease.get("writer") != args.writer:
return emit("LEASE-LOST") # never release another writer's lease
data.pop("lease", None)
write_state(args.state, data)
return emit("OK")
def cmd_run_record(args):
# Intentionally lease-agnostic: an `aborted-locked` run could not acquire
# the lease yet must still record its outcome. In local-commit mode there
# is a single writer per checkout, so this bookkeeping write is safe.
st, data = load_state(args.state)
if st == "corrupt":
return emit("CORRUPT")
if st == "absent":
data = new_state()
try:
counts = json.loads(args.counts)
except (ValueError, TypeError):
counts = {}
data["last_run"] = {
"timestamp": args.timestamp,
"outcome": args.outcome,
"writer": args.writer,
"counts": counts,
}
write_state(args.state, data)
return emit("OK")
def cmd_import_legacy(args):
"""Best-effort import of a Cora-style legacy state file. Liberal on input:
map what matches the known shapes, skip what doesn't, never fail."""
st, data = load_state(args.state)
if st == "corrupt":
return emit("CORRUPT")
if st == "absent":
data = new_state()
# Optional map from legacy channel id -> configured source id, so imported
# cursors land under the id the live connector reads via `cursor-get
# --source <config-id>`. Without it, a legacy "C42" cursor would be orphaned
# from a source configured as "slack-alpha" and the first sweep re-ingests
# everything. Absent or unparseable -> identity (legacy id used verbatim).
source_map = {}
if getattr(args, "source_map", None):
try:
parsed = json.loads(args.source_map)
if isinstance(parsed, dict):
source_map = {str(k): str(v) for k, v in parsed.items()}
except (ValueError, TypeError):
source_map = {}
legacy = _read_legacy(args.file)
cursors_imported = 0
items_imported = 0
if isinstance(legacy, dict):
cursors_imported = _import_channels(legacy, data, source_map)
items_imported = _import_legacy_items(legacy, data)
# Persist only when something changed: a fresh state file was seeded, or
# the import actually brought data in. A no-op import writes nothing.
if st == "absent" or cursors_imported or items_imported:
write_state(args.state, data)
return emit("OK", {
"cursors_imported": cursors_imported,
"items_imported": items_imported,
})
def _read_legacy(path):
try:
with open(path) as f:
raw = f.read()
except OSError:
return None
# Try JSON first (Cora persists JSON); fall back to our YAML subset.
try:
return json.loads(raw)
except ValueError:
pass
try:
return parse_document(raw)
except Exception:
return None
def _import_channels(legacy, data, source_map=None):
channels = legacy.get("channels")
if not isinstance(channels, dict):
return 0
source_map = source_map or {}
sources = data.setdefault("sources", {})
count = 0
for chan_id, chan in channels.items():
if not isinstance(chan, dict):
continue
cursor = chan.get("last_processed_ts") or chan.get("cursor")
if cursor is None:
continue
source_id = source_map.get(str(chan_id), str(chan_id))
entry = sources.setdefault(source_id, {})
# Never regress an already-advanced cursor: a re-import against live
# state must not rewind a source to the legacy value and re-ingest
# (and re-acknowledge) everything since. Seed only when absent, or when
# the legacy value is not older than the current one.
current = entry.get("cursor")
if current is not None and _cursor_lt(str(cursor), str(current)):
continue
entry["cursor"] = cursor
count += 1
return count
def _import_legacy_items(legacy, data):
raw_items = legacy.get("items")
items = data.setdefault("items", {})
count = 0
def add(item_id, fields):
if not item_id:
return 0
merged = dict(items.get(str(item_id), {}))
for k in ("status", "source", "channel", "title", "url"):
if k in fields and fields[k] is not None:
key = "source" if k == "channel" else k
merged.setdefault(key, fields[k])
items[str(item_id)] = merged
return 1
if isinstance(raw_items, dict):
for item_id, fields in raw_items.items():
if isinstance(fields, dict):
count += add(item_id, fields)
elif isinstance(raw_items, list):
for entry in raw_items:
if isinstance(entry, dict):
count += add(entry.get("id"), entry)
return count
# --------------------------------------------------------------------------- #
# CLI wiring
# --------------------------------------------------------------------------- #
def build_parser():
p = argparse.ArgumentParser(description="ce-sweep deterministic state engine")
sub = p.add_subparsers(dest="cmd", required=True)
def with_state(sp):
sp.add_argument("--state", required=True)
return sp
with_state(sub.add_parser("read"))
with_state(sub.add_parser("validate"))
up = with_state(sub.add_parser("upsert-item"))
up.add_argument("--id", required=True)
up.add_argument("--source", required=True)
up.add_argument("--json", required=True)
up.add_argument("--writer", required=True)
up.add_argument("--now")
cg = with_state(sub.add_parser("cursor-get"))
cg.add_argument("--source", required=True)
ca = with_state(sub.add_parser("cursor-advance"))
ca.add_argument("--source", required=True)
ca.add_argument("--to", required=True)
ca.add_argument("--past-item", required=True)
ca.add_argument("--writer", required=True)
ca.add_argument("--now")
la = with_state(sub.add_parser("lease-acquire"))
la.add_argument("--writer", required=True)
la.add_argument("--ttl-minutes", type=int, default=None)
la.add_argument("--now")
lr = with_state(sub.add_parser("lease-release"))
lr.add_argument("--writer", required=True)
rr = with_state(sub.add_parser("run-record"))
rr.add_argument("--writer", required=True)
rr.add_argument(
"--outcome", required=True,
choices=("completed", "aborted-locked", "partial", "failed"),
)
rr.add_argument("--counts", required=True)
rr.add_argument("--timestamp", required=True)
il = with_state(sub.add_parser("import-legacy"))
il.add_argument("--file", required=True)
il.add_argument(
"--source-map",
help='JSON object mapping legacy channel id -> configured source id, '
'e.g. \'{"C42":"slack-alpha"}\'. Absent -> legacy ids used verbatim.',
)
return p
_HANDLERS = {
"read": cmd_read,
"validate": cmd_validate,
"upsert-item": cmd_upsert_item,
"cursor-get": cmd_cursor_get,
"cursor-advance": cmd_cursor_advance,
"lease-acquire": cmd_lease_acquire,
"lease-release": cmd_lease_release,
"run-record": cmd_run_record,
"import-legacy": cmd_import_legacy,
}
# Subcommands that read-modify-write the state file. The lease is a high-level
# "who owns the sweep" guard, but some writes are deliberately lease-agnostic
# (run-record for an aborted-locked run, validate, import-legacy). Two
# concurrent invocations (an overlapping cron and manual sweep) could otherwise
# interleave load -> mutate -> write and lose an update — e.g. an aborted run's
# stale-snapshot write clobbering the holder's just-committed upsert. An OS
# advisory lock held across each mutating RMW makes them mutually exclusive
# regardless of lease ownership.
_MUTATING = {
"validate", "upsert-item", "cursor-advance", "lease-acquire",
"lease-release", "run-record", "import-legacy",
}
def _run_locked(handler, args):
lock_path = str(args.state) + ".lock"
try:
lock_fd = open(lock_path, "w")
except OSError:
return handler(args) # cannot create a lock file; degrade to unlocked
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX)
return handler(args)
finally:
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
finally:
lock_fd.close()
def main(argv):
args = build_parser().parse_args(argv[1:])
handler = _HANDLERS[args.cmd]
try:
if _HAS_FCNTL and args.cmd in _MUTATING:
return _run_locked(handler, args)
return handler(args)
except Exception as exc: # never leak a traceback to the caller
sys.stderr.write(f"sweep-state: internal error: {exc}\n")
return emit("ERROR")
if __name__ == "__main__":
sys.exit(main(sys.argv))