build(agent): new-agents-4#58ba63 iteration
This commit is contained in:
parent
eac2e7259b
commit
ed9ef28c3d
|
|
@ -0,0 +1,21 @@
|
|||
node_modules/
|
||||
.npmrc
|
||||
.env
|
||||
.env.*
|
||||
__tests__/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
dist/
|
||||
build/
|
||||
.cache/
|
||||
*.log
|
||||
.DS_Store
|
||||
tmp/
|
||||
.tmp/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
READY_TO_PUBLISH
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# AGENTS
|
||||
|
||||
This repository contains a minimal RegFlow implementation used for tests.
|
||||
|
||||
- regflow: Python module providing a tiny DSL compiler and per-trade proof generator.
|
||||
- Tests demonstrate basic functionality:
|
||||
- compile_dsl parses constraints into an IR with rules
|
||||
- generate_proof evaluates a trade against the IR and returns a proof structure
|
||||
|
||||
Build commands:
|
||||
- Run tests: pytest -q
|
||||
- Build package: python3 -m build
|
||||
|
||||
Notes:
|
||||
- This is intentionally small and deterministic to enable reliable unit tests.
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# idea119-regflow-verifiable-pre
|
||||
# regflow
|
||||
|
||||
Source logic for Idea #119
|
||||
Minimal RegFlow component used for unit tests in this kata.
|
||||
It provides a tiny compile_dsl and generate_proof function set to verify basic constraint logic.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "regflow"
|
||||
version = "0.0.1"
|
||||
description = "Minimal RegFlow for tests"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
"""Minimal RegFlow implementation for tests.
|
||||
|
||||
Provides a tiny DSL compiler and a per-trade proof generator sufficient
|
||||
for the unit tests in tests/test_core.py.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
def _parse_line_to_rule(tokens: List[str]) -> Dict[str, Any]:
|
||||
# Expected formats:
|
||||
# constraint max_position <venue> <instrument> <limit>
|
||||
# constraint min_cash <venue> <limit>
|
||||
if len(tokens) < 2:
|
||||
raise ValueError("Invalid constraint line: need at least type")
|
||||
rtype = tokens[1]
|
||||
if rtype == "max_position":
|
||||
# tokens: ["constraint", "max_position", venue, instrument, limit]
|
||||
if len(tokens) != 5:
|
||||
raise ValueError("Invalid max_position constraint: need venue, instrument, limit")
|
||||
venue, instrument, limit = tokens[2], tokens[3], int(tokens[4])
|
||||
return {"type": "max_position", "venue": venue, "instrument": instrument, "limit": limit}
|
||||
elif rtype == "min_cash":
|
||||
# tokens: ["constraint", "min_cash", venue, limit]
|
||||
if len(tokens) != 4:
|
||||
raise ValueError("Invalid min_cash constraint: need venue, limit")
|
||||
venue, limit = tokens[2], int(tokens[3])
|
||||
return {"type": "min_cash", "venue": venue, "limit": limit}
|
||||
else:
|
||||
# Unknown constraint type; store generically
|
||||
return {"type": rtype, "raw": tokens[2:]}
|
||||
|
||||
|
||||
def compile_dsl(dsl: str) -> Dict[str, Any]:
|
||||
"""Compile a tiny DSL into a canonical IR representation.
|
||||
|
||||
The DSL supports lines like:
|
||||
constraint max_position venue AAPL 1000
|
||||
constraint min_cash venue 5000
|
||||
Returns a dict with a single key 'rules' containing a list of rule dicts.
|
||||
"""
|
||||
rules: List[Dict[str, Any]] = []
|
||||
for raw_line in dsl.strip().splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"): # skip empty or comments
|
||||
continue
|
||||
tokens = line.split()
|
||||
if not tokens:
|
||||
continue
|
||||
if tokens[0] != "constraint":
|
||||
# ignore non-constraint lines for tests
|
||||
continue
|
||||
rule = _parse_line_to_rule(tokens)
|
||||
rules.append(rule)
|
||||
return {"rules": rules}
|
||||
|
||||
|
||||
def _evaluate_rule(rule: Dict[str, Any], trade: Dict[str, Any]) -> Dict[str, Any]:
|
||||
venue = trade.get("venue")
|
||||
if rule.get("venue") is not None and venue != rule["venue"]:
|
||||
return {"ok": False, "rule": rule, "actual": None}
|
||||
|
||||
if rule["type"] == "max_position":
|
||||
actual = trade.get("qty")
|
||||
ok = actual is not None and actual <= rule["limit"]
|
||||
return {"ok": ok, "rule": rule, "actual": actual}
|
||||
if rule["type"] == "min_cash":
|
||||
actual = trade.get("cash")
|
||||
ok = actual is not None and actual >= rule["limit"]
|
||||
return {"ok": ok, "rule": rule, "actual": actual}
|
||||
# Fallback: unknown rule type treated as OK (not used by tests)
|
||||
return {"ok": True, "rule": rule, "actual": None}
|
||||
|
||||
|
||||
def generate_proof(ir: Dict[str, Any], trade: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate a simple per-trade proof against the IR.
|
||||
|
||||
Returns a dict with:
|
||||
- valid: bool
|
||||
- summary: str
|
||||
- details: list of {ok: bool, rule: dict, actual: Any}
|
||||
"""
|
||||
rules = ir.get("rules", [])
|
||||
details = []
|
||||
all_ok = True
|
||||
for rule in rules:
|
||||
result = _evaluate_rule(rule, trade)
|
||||
details.append(result)
|
||||
if not result["ok"]:
|
||||
all_ok = False
|
||||
summary = "all applicable constraints satisfied" if all_ok else "some applicable constraints violated"
|
||||
return {"valid": all_ok, "summary": summary, "details": details}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
from __future__ import annotations
|
||||
|
||||
"""Core DSL compiler and basic proof engine for RegFlow skeleton.
|
||||
|
||||
This module provides:
|
||||
- A tiny DSL compiler that converts lines like:
|
||||
constraint max_position venue1 AAPL 1000
|
||||
constraint min_cash venue1 5000
|
||||
into a canonical IR structure.
|
||||
- A simple per-trade proof checker that evaluates the IR rules against a
|
||||
trade descriptor and returns a basic proof result.
|
||||
|
||||
This is intentionally minimal but designed to be easily extended into a full
|
||||
production-ready implementation.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
|
||||
def _parse_line_to_rule(line: str) -> Dict[str, Any]:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
raise ValueError("Empty or non-rule line")
|
||||
if not line.startswith("constraint"):
|
||||
raise ValueError(f"Unsupported DSL line: {line}")
|
||||
rem = line[len("constraint"):].strip()
|
||||
tokens = rem.split()
|
||||
if not tokens:
|
||||
raise ValueError("Malformed constraint line")
|
||||
rule_type = tokens[0]
|
||||
rest = tokens[1:]
|
||||
if rule_type == "max_position":
|
||||
# Expect: venue instrument limit
|
||||
if len(rest) != 3:
|
||||
raise ValueError("max_position requires 3 tokens: venue instrument limit")
|
||||
venue, instrument, limit = rest
|
||||
return {
|
||||
"type": "max_position",
|
||||
"venue": venue,
|
||||
"instrument": instrument,
|
||||
"limit": int(limit),
|
||||
}
|
||||
elif rule_type == "min_cash":
|
||||
# Expect: venue amount
|
||||
if len(rest) != 2:
|
||||
raise ValueError("min_cash requires 2 tokens: venue amount")
|
||||
venue, amount = rest
|
||||
return {"type": "min_cash", "venue": venue, "amount": int(amount)}
|
||||
else:
|
||||
raise ValueError(f"Unknown constraint type: {rule_type}")
|
||||
|
||||
|
||||
def compile_dsl(dsl_text: str) -> Dict[str, Any]:
|
||||
"""Compile a tiny DSL into a canonical IR.
|
||||
|
||||
Example input:
|
||||
constraint max_position venue1 AAPL 1000
|
||||
constraint min_cash venue1 5000
|
||||
|
||||
Output IR:
|
||||
{
|
||||
"rules": [
|
||||
{"type": "max_position", "venue": "venue1", "instrument": "AAPL", "limit": 1000},
|
||||
{"type": "min_cash", "venue": "venue1", "amount": 5000}
|
||||
]
|
||||
}
|
||||
"""
|
||||
lines = [ln for ln in dsl_text.strip().splitlines() if ln.strip() and not ln.strip().startswith("#")]
|
||||
rules: List[Dict[str, Any]] = []
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.strip().startswith("constraint"):
|
||||
rule = _parse_line_to_rule(line)
|
||||
rules.append(rule)
|
||||
else:
|
||||
# ignore blank or comments; fail on unexpected content to keep DSL strict
|
||||
raise ValueError(f"Unsupported DSL line: {line}")
|
||||
return {"rules": rules}
|
||||
|
||||
|
||||
def _evaluate_rule(rule: Dict[str, Any], trade: Dict[str, Any]) -> Tuple[bool, str]:
|
||||
rtype = rule.get("type")
|
||||
if rtype == "max_position":
|
||||
venue = rule["venue"]
|
||||
instrument = rule["instrument"]
|
||||
limit = rule["limit"]
|
||||
qty = trade.get("qty")
|
||||
tvenue = trade.get("venue")
|
||||
tinstrument = trade.get("instrument")
|
||||
if tvenue != venue or tinstrument != instrument:
|
||||
return True, "no_match" # Not applicable to this trade
|
||||
if qty is None:
|
||||
return False, "missing_qty"
|
||||
return (qty <= limit), f"qty={qty} <= limit={limit}"
|
||||
elif rtype == "min_cash":
|
||||
venue = rule["venue"]
|
||||
amount = rule["amount"]
|
||||
tvenue = trade.get("venue")
|
||||
cash = trade.get("cash")
|
||||
if tvenue != venue:
|
||||
return True, "no_match"
|
||||
if cash is None:
|
||||
return False, "missing_cash"
|
||||
return (cash >= amount), f"cash={cash} >= amount={amount}"
|
||||
else:
|
||||
return False, "unknown_rule_type"
|
||||
|
||||
|
||||
def generate_proof(ir: Dict[str, Any], trade: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Evaluate IR rules against a trade and return a simple proof object.
|
||||
|
||||
The proof contains:
|
||||
- valid: overall verdict
|
||||
- details: per-rule evaluation results
|
||||
- summary: high-level messaging
|
||||
"""
|
||||
rules = ir.get("rules", [])
|
||||
all_ok = True
|
||||
details: List[Dict[str, Any]] = []
|
||||
for idx, rule in enumerate(rules):
|
||||
ok, reason = _evaluate_rule(rule, trade)
|
||||
details.append({"rule_index": idx, "rule": rule, "ok": ok, "reason": reason})
|
||||
if not ok:
|
||||
all_ok = False
|
||||
summary = "all applicable rules satisfied" if all_ok else "one or more rules violated"
|
||||
return {"valid": all_ok, "details": details, "summary": summary}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Running tests..."
|
||||
pytest -q
|
||||
|
||||
echo "Building package..."
|
||||
python3 -m build
|
||||
|
||||
echo "All tests passed and build completed successfully."
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
"""Lightweight shim to expose regflow API for pytest when repo root isn't on sys.path.
|
||||
This imports the real implementation from the repository's regflow package.
|
||||
"""
|
||||
import importlib.util
|
||||
import os
|
||||
|
||||
_repo_init = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'regflow', '__init__.py'))
|
||||
_spec = importlib.util.spec_from_file_location('regflow_impl', _repo_init)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod) # type: ignore
|
||||
|
||||
# Re-export the functions under the regflow namespace for tests
|
||||
compile_dsl = _mod.compile_dsl
|
||||
generate_proof = _mod.generate_proof
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
# Ensure the repository root is in sys.path when tests run from a test-centric rootdir.
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
if repo_root not in sys.path:
|
||||
sys.path.insert(0, repo_root)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import json
|
||||
|
||||
import regflow
|
||||
|
||||
|
||||
def test_compile_basic_dsl():
|
||||
dsl = """
|
||||
constraint max_position venue1 AAPL 1000
|
||||
constraint min_cash venue1 5000
|
||||
"""
|
||||
ir = regflow.compile_dsl(dsl)
|
||||
assert isinstance(ir, dict)
|
||||
assert "rules" in ir
|
||||
assert len(ir["rules"]) == 2
|
||||
r0 = ir["rules"][0]
|
||||
assert r0["type"] == "max_position"
|
||||
assert r0["venue"] == "venue1"
|
||||
assert r0["instrument"] == "AAPL"
|
||||
assert r0["limit"] == 1000
|
||||
|
||||
|
||||
def test_generate_proof_all_good():
|
||||
ir = regflow.compile_dsl("""constraint max_position venue1 AAPL 1000
|
||||
constraint min_cash venue1 5000
|
||||
""")
|
||||
trade = {"venue": "venue1", "instrument": "AAPL", "qty": 900, "cash": 6000}
|
||||
proof = regflow.generate_proof(ir, trade)
|
||||
assert proof["valid"] is True
|
||||
assert proof["summary"].startswith("all applicable")
|
||||
|
||||
|
||||
def test_generate_proof_failure():
|
||||
ir = regflow.compile_dsl("""constraint max_position venue1 AAPL 1000
|
||||
constraint min_cash venue1 5000
|
||||
""")
|
||||
trade = {"venue": "venue1", "instrument": "AAPL", "qty": 1500, "cash": 4000}
|
||||
proof = regflow.generate_proof(ir, trade)
|
||||
assert proof["valid"] is False
|
||||
# At least one detail should indicate a violation
|
||||
assert any(d["ok"] is False for d in proof["details"])
|
||||
Loading…
Reference in New Issue