← Files FinrunARCHIVED FILE

skills/finrun/scripts/compact_checks.py

5.33 KB · Oct 5, 2026 · 18:34 UTC

↓ Download file

"""Additional checks for the small Finrun contract; legacy records stay readable."""
import ast
import hashlib
import json
import math
import re
from pathlib import Path


def arithmetic(expression, variables):
    tree = ast.parse(expression, mode='eval')
    if len(list(ast.walk(tree))) > 100:
        raise ValueError('expression is too complex')
    def visit(node):
        if isinstance(node, ast.Expression):
            return visit(node.body)
        if isinstance(node, ast.Constant) and type(node.value) in (int, float):
            return node.value
        if isinstance(node, ast.Name) and node.id in variables:
            return variables[node.id]
        if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
            return visit(node.operand) * (-1 if isinstance(node.op, ast.USub) else 1)
        if isinstance(node, ast.BinOp):
            a, b = visit(node.left), visit(node.right)
            if isinstance(node.op, ast.Add): return a + b
            if isinstance(node.op, ast.Sub): return a - b
            if isinstance(node.op, ast.Mult): return a * b
            if isinstance(node.op, ast.Div): return a / b
            if isinstance(node.op, ast.Pow) and abs(b) <= 12: return a ** b
        raise ValueError('only numeric inputs and + - * / ** arithmetic are allowed')
    value = visit(tree)
    if type(value) not in (int, float) or not math.isfinite(value):
        raise ValueError('non-finite arithmetic result')
    return value


def check(record, run_dir, verified_market_calculations=()):
    if record.get('workflow') != 'finrun-0.5':
        return []
    issues = []
    def error(code, message, path):
        issues.append(dict(severity='error', code=code, message=message, path=path))
    review = record.get('review', {})
    if (not isinstance(review, dict) or review.get('status') != 'completed'
            or not isinstance(review.get('notes'), list) or not review['notes']
            or not isinstance(review.get('reviewer'), str) or not review['reviewer'].strip()
            or not all(isinstance(n, str) and n.strip() for n in review['notes'])):
        error('review.incomplete', 'Review material claims, periods, arithmetic and language; then record completed status and notes.', 'review')
    try:
        path = (run_dir / 'methods.json').resolve()
        if not path.is_relative_to(run_dir.resolve()):
            raise ValueError('method receipt must remain inside the run')
        receipt = json.loads(path.read_text())
        if receipt['schema_version'] != 1 or not re.fullmatch('[0-9a-f]{40}', receipt['catalog_commit']):
            raise ValueError('invalid catalog receipt revision/schema')
        if not re.fullmatch('[0-9a-f]{64}', receipt['catalog_sha256']) or not receipt['selected']:
            raise ValueError('method receipt needs a catalog hash and selected methods')
        for item in receipt['selected']:
            if not re.fullmatch('[0-9a-f]{64}', item['sha256']) or not item.get('retrieved_at'):
                raise ValueError('selected method needs its hash and retrieval timestamp')
            if not re.fullmatch(r'https://raw\.githubusercontent\.com/[\w.-]+/[\w.-]+/[0-9a-f]{40}/[^?#]+\.md', item['url']):
                raise ValueError('selected method must reference immutable GitHub Markdown')
    except (OSError, ValueError, KeyError, TypeError) as exc:
        error('methods.receipt', str(exc), 'methods.json')
    facts = {f['id']: f for f in record.get('facts', []) if isinstance(f, dict) and 'id' in f}
    for c in record.get('calculations', []):
        if isinstance(c, dict) and c.get('id') in verified_market_calculations:
            continue  # Already recomputed from hashed bars by the market-data validator.
        try:
            inputs = c['inputs']
            if not isinstance(inputs, dict) or set(inputs.values()) != set(c['input_fact_ids']):
                raise ValueError('inputs must map expression variable names to exactly input_fact_ids')
            values = {name: facts[fid]['value'] for name, fid in inputs.items()}
            if not all(type(v) in (int, float) and math.isfinite(v) for v in values.values()):
                raise ValueError('calculation inputs must be finite numeric facts')
            actual = arithmetic(c['expression'], values)
            if type(c['result']) not in (int, float) or not math.isclose(actual, c['result'], rel_tol=1e-9, abs_tol=1e-8):
                raise ValueError('stored result differs from recomputed arithmetic')
        except (ValueError, TypeError, KeyError, SyntaxError, ArithmeticError) as exc:
            error('calculation.recompute', str(exc), str(c.get('id', 'calculations')) if isinstance(c, dict) else 'calculations')
    for s in record.get('sources', []):
        if not isinstance(s, dict) or not s.get('local_path'):
            continue
        try:
            rel = Path(s['local_path']); path = (run_dir / rel).resolve()
            if rel.is_absolute() or not path.is_relative_to(run_dir.resolve()) or not path.is_file():
                raise ValueError('source snapshot must be a real run-relative file')
            if hashlib.sha256(path.read_bytes()).hexdigest() != s.get('sha256'):
                raise ValueError('source snapshot SHA-256 is missing or mismatched')
        except (OSError, ValueError, TypeError) as exc:
            error('source.snapshot', str(exc), s.get('id', 'sources'))
    return issues

SHA-256: 2f851cfd2cbea386230766cb1f8d92078d99774d23128bbf8de8db0e1e10a12e