'''LinenBalance engine: deterministic double-entry linen movement journal.
Local only. Standard library only. Runs under CPython 3.11+ and Pyodide.
No network, no file access outside the CLI OUTPUT_DIR, no execution of user
supplied strings. Counts are integers; state is produced from transactions,
never optimised or guessed.
'''
from __future__ import annotations
import base64
import csv
import hashlib
import io
import json
import math
import os
import sys
import tempfile
import zipfile
from datetime import datetime, timedelta, timezone
ENGINE = 'linenbalance'
VERSION = '1.0.0'
ENGINE_ID = ENGINE + '/' + VERSION
MAX_ROWS = 200000
MAX_STR = 4096
MAX_ABS_QTY = 10 ** 12
MAX_FILE_BYTES = 8 * 1024 * 1024
MAX_INPUT_BYTES = 32 * 1024 * 1024
MAX_DEPTH = 16
SINK_DISPOSED = '__DISPOSED__'
MOVEMENT_TYPES = ('dispatch', 'receipt', 'disposal', 'correction', 'adjustment', 'reversal')
FIXED_ZIP_DATE = (1980, 1, 1, 0, 0, 0)
SUPPORTED_FILES = ('opening.csv', 'movements.csv', 'policy.json', 'input.json')
REQUIRED_FILES = ('opening.csv', 'movements.csv')
NL = chr(10)
BACKSLASH = chr(92)
DQUOTE = chr(34)
class _Err(ValueError):
pass
def _reject_const(x):
raise ValueError('non-finite JSON constant not allowed: ' + repr(x))
def _canonical(obj):
return json.dumps(obj, sort_keys=True, ensure_ascii=True, allow_nan=False, separators=(',', ':'))
def _bounded(obj):
stack = [(obj, 0)]
visited = 0
while stack:
value, depth = stack.pop()
visited += 1
if depth > MAX_DEPTH or visited > 2500000:
raise _Err('input nesting or item count exceeds supported bounds')
if isinstance(value, dict):
stack.extend((v, depth + 1) for v in value.values())
elif isinstance(value, list):
stack.extend((v, depth + 1) for v in value)
elif isinstance(value, float) and not math.isfinite(value):
raise _Err('input contains a non-finite number')
encoded = _canonical(obj)
if len(encoded.encode('utf-8')) > MAX_INPUT_BYTES:
raise _Err('input exceeds the supported byte bound')
return obj
def _strict_loads(text):
if not isinstance(text, str) or len(text.encode('utf-8')) > MAX_INPUT_BYTES:
raise _Err('input must be bounded UTF-8 JSON text')
def pairs(items):
out = {}
for key, value in items:
if key in out:
raise _Err('duplicate JSON key: ' + key)
out[key] = value
return out
return _bounded(json.loads(text, object_pairs_hook=pairs, parse_constant=_reject_const))
def _jsonb(obj):
return json.dumps(obj, ensure_ascii=True, allow_nan=False, sort_keys=True, indent=2).encode('utf-8')
def _as_str(value, field, allow_empty=False):
if not isinstance(value, str):
raise _Err(field + ' must be a string')
if len(value) > MAX_STR:
raise _Err(field + ' is too long')
if not allow_empty and value.strip() == '':
raise _Err(field + ' must not be empty')
return value
def _as_count(value, field):
if isinstance(value, bool):
raise _Err(field + ' must be an integer count, not a boolean')
if isinstance(value, int):
n = value
elif isinstance(value, float):
if not math.isfinite(value):
raise _Err(field + ' must be a finite number')
if value != int(value):
raise _Err(field + ' must be a whole count, got fractional ' + repr(value))
n = int(value)
else:
raise _Err(field + ' must be a number, got ' + type(value).__name__)
if abs(n) > MAX_ABS_QTY:
raise _Err(field + ' magnitude exceeds the supported bound')
return n
def _parse_ts(value, field):
s = _as_str(value, field)
txt = s.strip()
if txt[-1:] in ('Z', 'z'):
txt = txt[:-1] + '+00:00'
try:
dt = datetime.fromisoformat(txt)
except ValueError:
raise _Err(field + ' is not a valid ISO-8601 timestamp: ' + repr(s))
if dt.tzinfo is None:
raise _Err(field + ' must include an explicit UTC offset, e.g. 2026-01-05T08:00:00-05:00')
try:
return dt.astimezone(timezone.utc)
except (OverflowError, ValueError):
raise _Err(field + ' lies outside the supported datetime range')
def _utc_iso(dt):
return dt.isoformat().replace('+00:00', 'Z')
def _fail(errors):
return {'status': 'FAIL',
'summary': {'engine': ENGINE_ID, 'status': 'FAIL', 'error_count': len(errors)},
'tables': {},
'findings': errors,
'provenance': {'engine': ENGINE_ID, 'note': 'validation failed; no balance was computed'}}
def _resolve_period(policy_in, all_ts, errors):
pr = policy_in.get('period')
if pr is None:
if all_ts:
start = min(all_ts)
try:
end = max(all_ts) + timedelta(microseconds=1)
except OverflowError:
errors.append({'level': 'error', 'code': 'bad_period', 'message': 'latest timestamp leaves no representable exclusive end'})
end = max(all_ts)
else:
start = None
end = None
return {'start': start, 'end': end, 'explicit': False}
if not isinstance(pr, dict):
errors.append({'level': 'error', 'code': 'bad_period', 'message': 'policy.period must be an object with start and end'})
return {'start': None, 'end': None, 'explicit': True}
try:
start = _parse_ts(pr.get('start'), 'policy.period.start')
end = _parse_ts(pr.get('end'), 'policy.period.end')
except _Err as e:
errors.append({'level': 'error', 'code': 'bad_period', 'message': str(e)})
return {'start': None, 'end': None, 'explicit': True}
if start >= end:
errors.append({'level': 'error', 'code': 'bad_period', 'message': 'policy.period.start must be before policy.period.end'})
return {'start': start, 'end': end, 'explicit': True}
def run(payload):
errors = []
findings = []
def err(code, msg, **extra):
d = {'level': 'error', 'code': code, 'message': msg}
d.update(extra)
errors.append(d)
def note(level, code, msg, **extra):
d = {'level': level, 'code': code, 'message': msg}
d.update(extra)
findings.append(d)
if not isinstance(payload, dict):
return _fail([{'level': 'error', 'code': 'bad_payload', 'message': 'payload must be a JSON object'}])
try:
_bounded(payload)
except (ValueError, TypeError, OverflowError, RecursionError) as exc:
return _fail([{'level': 'error', 'code': 'input_bound', 'message': str(exc)}])
policy_in = payload.get('policy', {})
if not isinstance(policy_in, dict):
err('bad_policy', 'policy must be an object')
policy_in = {}
openings_in = payload.get('openings', [])
movements_in = payload.get('movements', [])
if not isinstance(openings_in, list):
err('bad_openings', 'openings must be a list')
openings_in = []
if not isinstance(movements_in, list):
err('bad_movements', 'movements must be a list')
movements_in = []
if len(openings_in) > MAX_ROWS or len(movements_in) > MAX_ROWS:
err('too_large', 'input exceeds the supported row bound')
return _fail(errors)
opening_map = {}
opening_accounts = set()
for i, row in enumerate(openings_in):
pre = 'openings[' + str(i) + ']'
if not isinstance(row, dict):
err('bad_opening_row', pre + ' must be an object')
continue
try:
acct = _as_str(row.get('account'), pre + '.account')
sku = _as_str(row.get('sku'), pre + '.sku')
unit = _as_str(row.get('unit'), pre + '.unit')
qty = _as_count(row.get('quantity'), pre + '.quantity')
except _Err as e:
err('bad_opening_row', str(e))
continue
if qty < 0:
err('negative_opening', pre + '.quantity must be >= 0')
continue
key = (acct, sku, unit)
if key in opening_map and opening_map[key] != qty:
err('conflicting_opening', 'conflicting opening balances for ' + repr(list(key)))
continue
opening_map[key] = qty
opening_accounts.add(acct)
tracked_in = policy_in.get('tracked_accounts')
tracked_explicit = tracked_in is not None
tracked = set()
if tracked_in is None:
tracked = set(opening_accounts)
elif not isinstance(tracked_in, list):
err('bad_tracked', 'policy.tracked_accounts must be a list')
else:
for j, a in enumerate(tracked_in):
try:
tracked.add(_as_str(a, 'policy.tracked_accounts[' + str(j) + ']'))
except _Err as e:
err('bad_tracked', str(e))
tz_label = policy_in.get('timezone', 'UTC')
if not isinstance(tz_label, str) or tz_label.strip() == '':
err('bad_timezone', 'policy.timezone must be a non-empty string')
tz_label = 'UTC'
seen = {}
parsed = []
for i, row in enumerate(movements_in):
pre = 'movements[' + str(i) + ']'
if not isinstance(row, dict):
err('bad_movement_row', pre + ' must be an object')
continue
try:
mid = _as_str(row.get('movement_id'), pre + '.movement_id')
mtype = _as_str(row.get('type'), pre + '.type')
if mtype not in MOVEMENT_TYPES:
raise _Err(pre + '.type ' + repr(mtype) + ' is not supported')
ts = _parse_ts(row.get('timestamp'), pre + '.timestamp')
except _Err as e:
err('bad_movement_row', str(e))
continue
canon = _canonical(row)
if mid in seen:
if seen[mid] != canon:
err('conflicting_duplicate_id', 'movement_id ' + repr(mid) + ' appears with two different payloads')
else:
note('info', 'duplicate_id_ignored', 'movement_id ' + repr(mid) + ' repeated identically; counted once', movement_id=mid)
continue
seen[mid] = canon
m = {'movement_id': mid, 'type': mtype, 'ts': ts, 'ts_utc': _utc_iso(ts)}
ok = True
if mtype == 'reversal':
for forbidden in ('source', 'destination', 'quantity', 'sku', 'unit'):
if forbidden in row and row.get(forbidden) not in (None, ''):
err('reversal_extra_fields', 'reversal ' + repr(mid) + ' must not carry ' + forbidden + '; it is derived from the original movement')
ok = False
try:
m['reverses'] = _as_str(row.get('reverses'), pre + '.reverses')
except _Err as e:
err('bad_movement_row', str(e))
ok = False
else:
try:
m['sku'] = _as_str(row.get('sku'), pre + '.sku')
m['unit'] = _as_str(row.get('unit'), pre + '.unit')
m['quantity'] = _as_count(row.get('quantity'), pre + '.quantity')
src = row.get('source')
dst = row.get('destination')
m['source'] = _as_str(src, pre + '.source')
if mtype == 'disposal' and dst is None:
m['destination'] = SINK_DISPOSED
else:
m['destination'] = _as_str(dst, pre + '.destination')
except _Err as e:
err('bad_movement_row', str(e))
ok = False
if ok and m['quantity'] <= 0:
err('nonpositive_quantity', pre + '.quantity must be > 0; direction comes from source/destination')
ok = False
if ok and m['source'] == m['destination']:
err('self_transfer', 'movement ' + repr(mid) + ' has identical source and destination')
ok = False
if ok:
parsed.append(m)
if errors:
return _fail(errors)
if not tracked_explicit:
tracked.update(m[k] for m in parsed if m['type'] != 'reversal' for k in ('source', 'destination'))
tracked.discard(SINK_DISPOSED)
all_ts = [m['ts'] for m in parsed]
period = _resolve_period(policy_in, all_ts, errors)
if errors:
return _fail(errors)
parsed.sort(key=lambda mm: (mm['ts'], mm['movement_id']))
transfers = {m['movement_id']: m for m in parsed if m['type'] != 'reversal'}
reversed_targets = {}
balances = {}
lineage = []
unresolved = []
def ensure(key):
b = balances.get(key)
if b is None:
b = {'in': 0, 'out': 0}
balances[key] = b
return b
def in_period(ts):
if period['start'] is None:
return True
return period['start'] <= ts < period['end']
for m in parsed:
if m['type'] == 'reversal':
tgt = transfers.get(m['reverses'])
posted = False
if tgt is None:
unresolved.append({'movement_id': m['movement_id'], 'type': 'reversal', 'reverses': m['reverses'], 'reason': 'reversal references an unknown or non-transfer movement'})
elif m['ts'] < tgt['ts']:
unresolved.append({'movement_id': m['movement_id'], 'type': 'reversal', 'reverses': m['reverses'], 'reason': 'reversal predates its original movement'})
elif m['reverses'] in reversed_targets:
unresolved.append({'movement_id': m['movement_id'], 'type': 'reversal', 'reverses': m['reverses'], 'reason': 'original already reversed by ' + reversed_targets[m['reverses']]})
else:
reversed_targets[m['reverses']] = m['movement_id']
if in_period(m['ts']):
ensure((tgt['destination'], tgt['sku'], tgt['unit']))['out'] += tgt['quantity']
ensure((tgt['source'], tgt['sku'], tgt['unit']))['in'] += tgt['quantity']
posted = True
if tgt is None:
lineage.append({'movement_id': m['movement_id'], 'type': 'reversal', 'timestamp_utc': m['ts_utc'], 'source': '', 'destination': '', 'sku': '', 'unit': '', 'quantity': '', 'reverses': m['reverses'], 'in_period': in_period(m['ts']), 'posted': posted})
else:
lineage.append({'movement_id': m['movement_id'], 'type': 'reversal', 'timestamp_utc': m['ts_utc'], 'source': tgt['destination'], 'destination': tgt['source'], 'sku': tgt['sku'], 'unit': tgt['unit'], 'quantity': tgt['quantity'], 'reverses': m['reverses'], 'in_period': in_period(m['ts']), 'posted': posted})
continue
posted = in_period(m['ts'])
if posted:
ensure((m['source'], m['sku'], m['unit']))['out'] += m['quantity']
ensure((m['destination'], m['sku'], m['unit']))['in'] += m['quantity']
lineage.append({'movement_id': m['movement_id'], 'type': m['type'], 'timestamp_utc': m['ts_utc'], 'source': m['source'], 'destination': m['destination'], 'sku': m['sku'], 'unit': m['unit'], 'quantity': m['quantity'], 'reverses': '', 'in_period': posted, 'posted': posted})
for key in opening_map:
ensure(key)
accounts_rows = []
unknown_keys = []
for key in sorted(balances.keys()):
acct, sku, unit = key
b = balances[key]
opening = opening_map.get(key)
has_open = opening is not None
net = b['in'] - b['out']
closing = (opening + net) if has_open else None
is_tracked = acct in tracked
if is_tracked and not has_open and (b['in'] or b['out']):
unknown_keys.append(key)
accounts_rows.append({'account': acct, 'sku': sku, 'unit': unit, 'opening': opening if has_open else 'UNKNOWN', 'incoming': b['in'], 'outgoing': b['out'], 'net': net, 'closing': closing if has_open else 'UNKNOWN', 'closing_known': has_open, 'tracked': is_tracked})
if has_open and closing is not None and closing < 0:
note('warn', 'negative_closing', 'closing balance for ' + repr(list(key)) + ' is negative (' + str(closing) + '); dispatches exceed recorded holdings', account=acct, sku=sku, unit=unit)
for u in unresolved:
note('warn', 'unresolved_movement', u['reason'], movement_id=u['movement_id'], reverses=u.get('reverses', ''))
for key in unknown_keys:
note('warn', 'unknown_opening', 'no opening balance for tracked line ' + repr(list(key)) + '; absolute closing is UNKNOWN', account=key[0], sku=key[1], unit=key[2])
per_su = {}
for key in balances:
b = balances[key]
su = (key[1], key[2])
per_su[su] = per_su.get(su, 0) + (b['in'] - b['out'])
conservation_ok = all(v == 0 for v in per_su.values())
no_data = (not accounts_rows) and (not lineage)
if no_data:
note('warn', 'no_data', 'no openings or movements were provided')
if no_data or unresolved or unknown_keys:
status = 'UNKNOWN'
else:
status = 'PASS'
posted_ct = sum(1 for l in lineage if l['posted'])
summary = {'engine': ENGINE_ID, 'status': status, 'timezone': tz_label,
'period': {'start': _utc_iso(period['start']) if period['start'] else None,
'end': _utc_iso(period['end']) if period['end'] else None,
'explicit': period['explicit']},
'counts': {'openings': len(opening_map), 'movements_input': len(movements_in),
'movements_posted': posted_ct,
'movements_out_of_period_or_unresolved': len(lineage) - posted_ct},
'accounts': len(accounts_rows), 'tracked_accounts': sorted(tracked),
'skus': sorted({k[1] for k in balances}), 'unresolved': len(unresolved),
'unknown_lines': len(unknown_keys), 'conservation_ok': conservation_ok}
provenance = {'engine': ENGINE_ID,
'algorithm': 'double-entry replay: each movement posts equal +/- integer entries (source out, destination in); reversals cancel one specific original; closing = opening + incoming - outgoing per (account, sku, unit); a missing opening yields UNKNOWN absolute closing while movement totals stay known.',
'determinism': 'movements sorted by (utc_timestamp, movement_id); integer counts only; no wall-clock',
'input_sha256': hashlib.sha256(_canonical(payload).encode('utf-8')).hexdigest(),
'policy_echo': {'timezone': tz_label, 'tracked_accounts': sorted(tracked), 'tracked_explicit': tracked_explicit},
'limits': ['exact user-controlled matching; no inference of wash yield or unequal-count matching', 'no theft attribution, invoicing, billing, or integration', 'counts are integers; each unit is a separate balance line']}
return {'status': status, 'summary': summary,
'tables': {'accounts.csv': accounts_rows,
'unresolved.csv': [{'movement_id': u['movement_id'], 'type': u['type'], 'reverses': u.get('reverses', ''), 'reason': u['reason']} for u in unresolved],
'movement-lineage.csv': lineage},
'findings': findings, 'provenance': provenance}
def run_json(text):
if not isinstance(text, str):
raise TypeError('run_json expects a string')
payload = _strict_loads(text)
result = run(payload)
return json.dumps(result, ensure_ascii=True, allow_nan=False, sort_keys=True, separators=(',', ':'))
def demo():
return {'policy': {'timezone': 'America/New_York', 'tracked_accounts': ['CLIENT_A'],
'period': {'start': '2026-01-01T00:00:00-05:00', 'end': '2026-02-01T00:00:00-05:00'}},
'openings': [{'account': 'CLIENT_A', 'sku': 'BATH_TOWEL', 'unit': 'pieces', 'quantity': 100}],
'movements': [{'movement_id': 'D-0001', 'type': 'dispatch', 'timestamp': '2026-01-05T08:00:00-05:00', 'source': 'CLIENT_A', 'destination': 'PLANT', 'sku': 'BATH_TOWEL', 'unit': 'pieces', 'quantity': 35},
{'movement_id': 'R-0001', 'type': 'receipt', 'timestamp': '2026-01-06T08:00:00-05:00', 'source': 'PLANT', 'destination': 'CLIENT_A', 'sku': 'BATH_TOWEL', 'unit': 'pieces', 'quantity': 20}],
'_note': 'synthetic example: CLIENT_A closing = 100 + 20 - 35 = 85'}
def _csv_rows(text, required, name):
reader = csv.DictReader(io.StringIO(text), strict=True)
names = reader.fieldnames or []
if len(set(names)) != len(names) or any(not f or f != f.strip() for f in names):
raise _Err(name + ' has duplicate, empty or padded column names')
fields = set(names)
missing = [c for c in required if c not in fields]
if missing:
raise _Err(name + ' is missing required column(s): ' + ', '.join(missing))
rows = []
for r in reader:
if None in r or any(v is None for v in r.values()):
raise _Err(name + ' contains a ragged row')
if all((v is None or str(v).strip() == '') for v in r.values()):
continue
rows.append(r)
if len(rows) > MAX_ROWS:
raise _Err(name + ' exceeds supported row bound')
return rows
def _parse_opening_csv(text):
out = []
for r in _csv_rows(text, ('account', 'sku', 'unit', 'quantity'), 'opening.csv'):
q = (r.get('quantity') or '').strip()
try:
qty = int(q)
except ValueError:
raise _Err('opening.csv quantity must be an integer, got ' + repr(q))
out.append({'account': (r.get('account') or '').strip(), 'sku': (r.get('sku') or '').strip(), 'unit': (r.get('unit') or '').strip(), 'quantity': qty})
return out
def _parse_movements_csv(text):
req = ('movement_id', 'type', 'timestamp', 'source', 'destination', 'sku', 'unit', 'quantity')
out = []
for r in _csv_rows(text, req, 'movements.csv'):
mtype = (r.get('type') or '').strip()
m = {'movement_id': (r.get('movement_id') or '').strip(), 'type': mtype, 'timestamp': (r.get('timestamp') or '').strip()}
if mtype == 'reversal':
if any((r.get(k) or '').strip() for k in ('source', 'destination', 'quantity', 'sku', 'unit')):
raise _Err('reversal CSV row must leave transfer fields empty')
m['reverses'] = (r.get('reverses') or '').strip()
else:
q = (r.get('quantity') or '').strip()
try:
m['quantity'] = int(q)
except ValueError:
raise _Err('movements.csv quantity must be an integer, got ' + repr(q))
m['sku'] = (r.get('sku') or '').strip()
m['unit'] = (r.get('unit') or '').strip()
m['source'] = (r.get('source') or '').strip()
m['destination'] = (r.get('destination') or '').strip()
out.append(m)
return out
def files_to_payload(files):
if not isinstance(files, dict):
raise _Err('files must be a mapping of filename -> {encoding, content}')
decoded = {}
for name, spec in files.items():
if not isinstance(name, str):
raise _Err('file name must be a string')
if ('/' in name) or (BACKSLASH in name) or ('..' in name) or name in ('', '.', '..'):
raise _Err('file name must be a bare basename: ' + repr(name))
if name not in SUPPORTED_FILES:
raise _Err('unsupported file ' + repr(name) + '; supported: ' + ', '.join(SUPPORTED_FILES))
if not isinstance(spec, dict):
raise _Err('file ' + repr(name) + ' must be an object {encoding, content}')
enc = spec.get('encoding')
content = spec.get('content')
if enc not in ('utf8', 'utf-8', 'base64'):
raise _Err('file ' + repr(name) + ' encoding must be utf8 or base64')
if not isinstance(content, str):
raise _Err('file ' + repr(name) + ' content must be a string')
if len(content.encode('utf-8')) > MAX_FILE_BYTES:
raise _Err('file ' + repr(name) + ' is too large')
if enc == 'base64':
try:
raw = base64.b64decode(content, validate=True)
except Exception:
raise _Err('file ' + repr(name) + ' has invalid base64 content')
try:
text = raw.decode('utf-8')
except Exception:
raise _Err('file ' + repr(name) + ' is not valid UTF-8 text')
else:
text = content
decoded[name] = text
if 'input.json' in decoded:
if len(decoded) != 1:
raise _Err('input.json cannot be combined with CSV files')
return _strict_loads(decoded['input.json'])
missing = [f for f in REQUIRED_FILES if f not in decoded]
if missing:
raise _Err('missing required file(s): ' + ', '.join(missing))
payload = {'openings': _parse_opening_csv(decoded['opening.csv']), 'movements': _parse_movements_csv(decoded['movements.csv'])}
if 'policy.json' in decoded:
try:
pol = _strict_loads(decoded['policy.json'])
except Exception as e:
raise _Err('policy.json is not valid JSON: ' + str(e))
if not isinstance(pol, dict):
raise _Err('policy.json must be a JSON object')
payload['policy'] = pol
return payload
def _csv_cell(v):
if isinstance(v, bool):
return 'true' if v else 'false'
if isinstance(v, int):
return v
if isinstance(v, float):
return v
s = '' if v is None else str(v)
if s.lstrip(' \t\r\n')[:1] in ('=', '+', '-', '@') or s[:1] in ('\t', '\r', '\n'):
return chr(39) + s
return s
def _csv_bytes(rows, columns):
out = io.StringIO()
w = csv.writer(out, lineterminator=NL)
w.writerow(columns)
for r in rows:
w.writerow([_csv_cell(r.get(c, '')) for c in columns])
return out.getvalue().encode('utf-8')
def _html_escape(s):
return (str(s).replace('&', '&').replace('<', '<').replace('>', '>').replace(DQUOTE, '"'))
def _drilldown_html(result):
rows = result['tables']['accounts.csv']
parts = ['
' for c in ('account', 'sku', 'unit', 'opening', 'incoming', 'outgoing', 'net', 'closing', 'closing_known'))
parts.append('
' + cells + '
')
parts.append('
Scope
Known movements remain visible when an opening balance is missing. UNKNOWN is not a zero balance. No wash-yield inference, theft attribution or invoicing.
')
parts.append('')
parts.append('')
return NL.join(parts).encode('utf-8')
OFFLINE_CSS = '/* ==========================================================================\n Dated change feeds — aligned to the ustechautomations.com design system.\n Tokens, type scale, radius and accent are lifted from the live main site.\n Take them from assets/WebsiteStyleProvider-*.css, NOT assets/root-*.css: the\n root sheet is the app\'s palette and the website overrides it. Reading the\n wrong one is how these pages ended up with a near-black button where the\n marketing site has a blue one.\n\n Look only. Prices, sample rows, event ids and honesty lines live in the\n HTML and are frozen: never restyle a fact out of existence.\n Each family page sets data-family on . Nothing in this file reads it:\n there is one accent and every family shares it. The attribute is kept for\n the page\'s own scripts and for anyone grepping the built site by family.\n ========================================================================== */\n\n@font-face {\n font-family: "Satoshi";\n src: url("https://ustechautomations.com/fonts/Satoshi-Variable.woff2") format("woff2-variations"),\n url("https://ustechautomations.com/fonts/Satoshi-Variable.woff2") format("woff2");\n font-weight: 300 900;\n font-display: swap;\n font-style: normal;\n}\n\n:root {\n /* type — same stack the main site resolves --font-sans to */\n --sans: "Satoshi", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n --serif: "PT Serif", ui-serif, Georgia, "Times New Roman", serif;\n --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;\n\n /* main-site tokens, light */\n --background: 220 10% 99%;\n --foreground: 220 20% 10%;\n --card: 0 0% 100%;\n --card-foreground: 220 20% 10%;\n --secondary: 220 12% 97%;\n --muted: 220 12% 97%;\n --muted-foreground: 220 10% 42%;\n --border: 220 13% 94%;\n --primary: 207 100% 50%;\n --primary-foreground: 0 0% 100%;\n --primary-hover: 207 100% 44%;\n --accent-blue: 207 100% 50%;\n --accent-emerald: 149 80% 90%;\n --accent-emerald-fg: 161 94% 30%;\n --accent-amber: 48 96% 89%;\n --accent-amber-fg: 26 90% 37%;\n /* The estate\'s one "this is wrong / do not do this" colour, added 2026-09-08.\n Four in-page tools were each carrying their own red -- #b42318, #c0392b and\n two more -- because there was no token to reach for. One red, defined here\n in both themes, is the whole reason this token exists: see BRAND.md §1. */\n --accent-rose: 0 86% 94%;\n --accent-rose-fg: 0 72% 41%;\n --radius: .5rem;\n --shadow-color: 220 15% 60%;\n\n /* resolved */\n --bg: hsl(var(--background));\n --surface: hsl(var(--card));\n --surface-2: hsl(var(--secondary));\n --fg: hsl(var(--foreground));\n --muted-fg: hsl(var(--muted-foreground));\n --faint: hsl(var(--muted-foreground) / .78);\n --line: hsl(var(--border));\n --line-strong: hsl(var(--border) / 1.6);\n --accent: hsl(var(--accent-blue));\n --shadow-sm: 0 1px 2px hsl(var(--shadow-color) / .08);\n --shadow-md: 0 4px 16px -4px hsl(var(--shadow-color) / .16);\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme="light"]) {\n --background: 220 15% 8%;\n --foreground: 220 15% 96%;\n --card: 220 12% 10%;\n --card-foreground: 220 15% 96%;\n --secondary: 220 10% 16%;\n --muted: 220 10% 16%;\n --muted-foreground: 220 8% 62%;\n --border: 220 10% 18%;\n --primary: 207 100% 50%;\n --primary-foreground: 0 0% 100%;\n --primary-hover: 207 100% 44%;\n --accent-blue: 207 100% 60%;\n --accent-emerald: 164 86% 16%;\n --accent-emerald-fg: 158 64% 52%;\n --accent-amber: 22 78% 26%;\n --accent-amber-fg: 45.9 96.7% 64.5%;\n --accent-rose: 0 63% 18%;\n --accent-rose-fg: 0 91% 71%;\n --shadow-color: 220 20% 4%;\n }\n}\n:root[data-theme="dark"] {\n --background: 220 15% 8%;\n --foreground: 220 15% 96%;\n --card: 220 12% 10%;\n --card-foreground: 220 15% 96%;\n --secondary: 220 10% 16%;\n --muted: 220 10% 16%;\n --muted-foreground: 220 8% 62%;\n --border: 220 10% 18%;\n --primary: 207 100% 50%;\n --primary-foreground: 0 0% 100%;\n --primary-hover: 207 100% 44%;\n --accent-blue: 207 100% 60%;\n --accent-emerald: 164 86% 16%;\n --accent-emerald-fg: 158 64% 52%;\n --accent-amber: 22 78% 26%;\n --accent-amber-fg: 45.9 96.7% 64.5%;\n --accent-rose: 0 63% 18%;\n --accent-rose-fg: 0 91% 71%;\n --shadow-color: 220 20% 4%;\n}\n\n*, *::before, *::after { box-sizing: border-box; }\n\nhtml { -webkit-text-size-adjust: 100%; background: hsl(var(--background)); }\n\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--fg);\n font-family: var(--sans);\n font-size: 16px;\n font-weight: 500;\n line-height: 1.65;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n}\n\n.wrap { width: 100%; max-width: 1280px; margin: 0 auto; padding: 0 1.5rem; }\n@media (min-width: 768px) { .wrap { padding: 0 2rem; } }\n\na { color: var(--accent); text-decoration: none; }\na:hover { text-decoration: underline; }\n\n.skip {\n position: absolute; left: -9999px; top: 0;\n background: var(--surface); color: var(--fg);\n padding: .75rem 1rem; border: 1px solid var(--line);\n border-radius: var(--radius); z-index: 100;\n}\n.skip:focus { left: 1rem; top: 1rem; }\n\n/* One visible focus ring for everything keyboard-reachable. BRAND.md §5 and §9\n ask for visible focus and never removing an outline; until now the estate\n relied on whatever ring the browser drew, which on the filled blue buttons and\n the dark surfaces was close to invisible. Nothing here removes an outline. */\na:focus-visible,\nbutton:focus-visible,\ninput:focus-visible,\nselect:focus-visible,\ntextarea:focus-visible,\nsummary:focus-visible,\n[tabindex]:focus-visible {\n outline: 2px solid hsl(var(--primary));\n outline-offset: 2px;\n border-radius: var(--radius);\n}\n\n/* ---------- text inputs in the in-page tools ---------- */\n/* The pre-check, the diagram tool, the hazmat search and the label forge each\n drew their own box: four borders, four greys, four corner radii, and one of\n them was written into a style="" attribute. This is that box, once. */\n.field {\n width: 100%; max-width: 32rem;\n font: inherit;\n padding: .5rem .6rem;\n color: var(--fg);\n background: var(--surface);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n}\n.field::placeholder { color: var(--muted-fg); opacity: 1; }\n\n/* ---------- masthead: mirrors the main-site sticky header ---------- */\n.masthead {\n position: sticky; top: 0; z-index: 40;\n width: 100%;\n border-bottom: 1px solid var(--line);\n background: hsl(var(--background) / .8);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n}\n.masthead .wrap {\n display: flex; align-items: center; gap: 1.25rem;\n min-height: 64px; flex-wrap: wrap;\n}\n@media (min-width: 1024px) { .masthead .wrap { flex-wrap: nowrap; } }\n.wordmark {\n display: inline-flex; align-items: center; gap: .5rem;\n font-weight: 900; font-size: 1rem; letter-spacing: normal;\n text-transform: uppercase;\n color: var(--fg); text-decoration: none;\n /* On a phone the family name and the "/ US Tech Automations" part may sit on\n two lines; from 640px up they stay on one. Found on a 375px screenshot. */\n flex-wrap: wrap; white-space: normal; row-gap: 0;\n}\n@media (min-width: 640px) { .wordmark { font-size: 1.25rem; letter-spacing: .025em; white-space: nowrap; } }\n.usta-logo { width: 24px; height: 24px; display: block; flex: none; }\n.wordmark:hover { text-decoration: none; opacity: .8; }\n.wordmark span { font-weight: 500; color: var(--muted-fg); }\n\n.mast-nav { display: none; gap: 2rem; margin-left: 2rem; align-items: center; flex: 1; }\n/* main site keeps the links beside the wordmark and the CTA hard right */\n.mast-cta { margin-left: auto; }\n@media (min-width: 1024px) { .mast-nav { display: flex; } }\n.mast-nav a {\n font-size: .875rem; font-weight: 500; color: var(--fg); text-decoration: none;\n}\n.mast-nav a:hover { color: hsl(var(--primary)); text-decoration: none; }\n.mast-cta {\n display: inline-flex; align-items: center;\n background: hsl(var(--primary)); color: hsl(var(--primary-foreground));\n font-size: .875rem; font-weight: 600;\n padding: .5rem 1rem; border-radius: var(--radius);\n text-decoration: none; white-space: nowrap;\n}\n.mast-cta:hover { background: hsl(var(--primary-hover)); text-decoration: none; }\n\n.crumbs {\n width: 100%; margin: 0 0 .75rem; font-size: .8125rem; color: var(--muted-fg);\n}\n.crumbbar { border-bottom: 1px solid var(--line); background: hsl(var(--background)); }\n.crumbbar .crumbs {\n width: auto; margin: 0; padding: .625rem 0;\n white-space: nowrap; overflow-x: auto;\n}\n.crumbs a { color: var(--muted-fg); }\n.crumbs .sep { padding: 0 .5rem; opacity: .5; }\n\n/* ---------- hero ---------- */\n.hero { padding: 3.5rem 0 2rem; }\n.hero h1 {\n font-size: 3rem;\n line-height: 1.15; letter-spacing: -.025em; font-weight: 500;\n margin: 0 0 1rem; max-width: 20ch;\n}\n@media (min-width: 768px) { .hero h1 { font-size: 3.75rem; } }\n@media (min-width: 1024px) { .hero h1 { font-size: 4.5rem; letter-spacing: -.03em; } }\n.eyebrow {\n display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;\n margin: 0 0 1rem;\n font-size: .8125rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;\n color: var(--muted-fg);\n}\n.dot { width: 3px; height: 3px; border-radius: 50%; background: currentColor; opacity: .55; display: inline-block; }\n.lede {\n font-size: clamp(1.0625rem, 1rem + .35vw, 1.25rem);\n line-height: 1.6; color: var(--muted-fg); max-width: 68ch; margin: 0 0 2rem;\n}\n.lede strong { color: var(--fg); font-weight: 600; }\n\n/* ---------- fact rail ---------- */\n.rail {\n display: grid; gap: 1px; margin: 0;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n background: var(--line);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n overflow: hidden;\n}\n.rail > div { background: var(--surface); padding: 1rem 1.25rem; }\n.rail dt {\n font-size: .75rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;\n color: var(--muted-fg); margin: 0 0 .375rem;\n}\n.rail dd { margin: 0; font-size: .9375rem; font-weight: 500; }\n.rail dd.price, .amount {\n font-size: 1.25rem; font-weight: 700; letter-spacing: -.02em;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---------- main ---------- */\nmain { padding: 1rem 0 4rem; }\nmain section { margin: 0 0 2.75rem; max-width: 76ch; }\n/* Evidence tables use the full 1280px container the main site\'s sections use,\n while the prose around them keeps its reading measure. */\nmain section:has(.evidence) { max-width: none; }\nmain section:has(.evidence) > p,\nmain section:has(.evidence) > h2,\nmain section:has(.evidence) > h3 { max-width: 76ch; }\n/* Main-site heading character (weight/tracking/leading are its h1-h6 rule).\n Sizes step one rung down its ladder: a feed page\'s h2 is a subsection of a\n record, not a marketing section header, so it takes the site\'s h3 numbers. */\nh2 {\n font-size: 1.875rem; font-weight: 500; letter-spacing: -.025em; line-height: 1.15;\n margin: 0 0 .875rem;\n}\nh3 {\n font-size: 1.5rem; font-weight: 500; letter-spacing: -.025em; line-height: 1.15;\n margin: 0 0 .5rem;\n}\n@media (min-width: 768px) {\n h2 { font-size: 2.25rem; }\n h3 { font-size: 1.875rem; }\n}\np { margin: 0 0 1rem; line-height: 1.625; }\nmain p { color: hsl(var(--foreground) / .86); }\nstrong { font-weight: 650; color: var(--fg); }\n\n/* ---------- state line ---------- */\n/* What replaced the status pills on 2026-09-08 (BRAND.md §7). The old\n .pill/.pill-ready/.pill-hold trio was a filled, 999px-radius badge in amber or\n emerald: a second radius, a second border weight and two accent colours that\n existed nowhere else on the page, carrying a fact that the words beside it\n already carried. It is now muted text with a muted icon and no container.\n\n The two states are told apart by the SHAPE of the icon and by the words, never\n by colour -- both draw in currentColor, which is --muted-fg here. A reader who\n cannot see the difference between amber and emerald loses nothing, because\n there is no longer any difference to see.\n\n align-items: baseline, not center, so a long label that wraps onto a second\n line keeps its icon on the first line next to the words rather than floating\n in the middle of the block. The nudge puts the icon\'s optical centre on the\n x-height. */\n.state {\n display: inline-flex; align-items: baseline; gap: .375rem;\n color: var(--muted-fg); font-weight: 500;\n}\n.state > svg {\n width: .8125rem; height: .8125rem; flex: none;\n transform: translateY(.09em);\n}\n\n/* ---------- honesty panel ---------- */\n.honest {\n border: 1px solid hsl(var(--accent-amber-fg) / .3);\n background: hsl(var(--accent-amber) / .5);\n border-radius: var(--radius);\n padding: 1.125rem 1.25rem;\n}\n.honest p:last-child { margin-bottom: 0; }\n.honest strong { color: hsl(var(--accent-amber-fg)); }\n\n/* The same disclosure, sitting in a hero rather than in the body. Five family\n pages used this class with nothing defined for it, so their "not for sale\n yet" line rendered as ordinary body text while the identical sentence on\n dc-buildout got a panel. One shop, one look. */\n.hero-note {\n border: 1px solid hsl(var(--accent-amber-fg) / .3);\n background: hsl(var(--accent-amber) / .5);\n border-radius: var(--radius);\n padding: 1rem 1.125rem;\n margin: 0 0 1.25rem;\n max-width: 62ch;\n}\n.hero-note strong { color: hsl(var(--accent-amber-fg)); }\n\n/* ---------- spec list ---------- */\n.spec { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .75rem; }\n.spec li {\n display: grid; gap: .125rem;\n border-left: 2px solid var(--line-strong);\n padding: .125rem 0 .125rem 1rem;\n}\n.spec li strong { font-weight: 650; }\n.sub { color: var(--muted-fg); font-size: .9375rem; }\n\n/* ---------- cards (hub) ---------- */\n.cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }\n.hub-groups { display: grid; gap: 2.5rem; }\n.group { max-width: none; }\n\n/* The list of a feed\'s own pages, printed on the feed\'s page. Columns, not one\n long line: twenty-seven states stacked vertically is a wall, and a reader\n scanning for one name reads across a column far faster than down a list.\n break-inside keeps a name from being split over a column boundary. */\n.slice-index { margin-top: 2.75rem; border-top: 1px solid var(--line); padding-top: 1.75rem; }\n.slice-index h2 { margin-bottom: .9rem; }\n.slice-list { columns: 3 190px; column-gap: 1.75rem; list-style: none; padding: 0; margin: 0 0 .9rem; }\n.slice-list li { break-inside: avoid; margin: 0 0 .5rem; }\n.slice-list a { text-decoration: none; }\n.slice-list a:hover { text-decoration: underline; }\n.card {\n display: block; background: var(--surface);\n border: 1px solid var(--line); border-radius: var(--radius);\n padding: 1.25rem; color: inherit; text-decoration: none;\n box-shadow: var(--shadow-sm);\n transition: box-shadow .15s ease, border-color .15s ease, transform .15s ease;\n}\na.card:hover {\n text-decoration: none; border-color: hsl(var(--accent-blue) / .5);\n box-shadow: var(--shadow-md); transform: translateY(-1px);\n}\n.card h3 { margin: 0 0 .375rem; }\n.card .who { color: var(--muted-fg); font-size: .875rem; margin: 0 0 .875rem; }\n.card .meta { display: flex; align-items: center; gap: .625rem; flex-wrap: wrap; font-size: .8125rem; color: var(--muted-fg); }\n\n/* ---------- evidence / sample panels ---------- */\n.evidence {\n border: 1px solid var(--line); border-radius: var(--radius);\n overflow: hidden; background: var(--surface); box-shadow: var(--shadow-sm);\n}\n.evidence-head {\n display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;\n padding: .75rem 1.125rem; background: var(--surface-2);\n border-bottom: 1px solid var(--line);\n font-size: .8125rem; font-weight: 600; color: var(--muted-fg);\n}\n.evidence-body { padding: 1.125rem; }\n.blank { color: var(--muted-fg); font-style: italic; }\n.scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }\n\ntable { width: 100%; border-collapse: collapse; font-size: .875rem; }\nth, td { text-align: left; padding: .625rem .75rem; border-bottom: 1px solid var(--line); }\nth {\n font-size: .6875rem; font-weight: 650; letter-spacing: .05em; text-transform: uppercase;\n color: var(--muted-fg);\n}\ntd.num, .num { font-variant-numeric: tabular-nums; }\ntbody tr:last-child td { border-bottom: 0; }\n\n.stamp, .seal, .fact, .change, .moved {\n font-family: var(--mono); font-size: .8125rem;\n}\nh1 > .seal, h1 > .stamp, h2 > .seal, h2 > .stamp, h3 > .seal, h3 > .stamp {\n display: block; margin-top: .375rem;\n font-weight: 400; letter-spacing: 0; color: var(--muted-fg);\n font-size: .8125rem; line-height: 1.5;\n}\n.seal { color: var(--muted-fg); word-break: break-all; }\n.stamp { color: var(--muted-fg); }\n.note { font-size: .875rem; color: var(--muted-fg); }\n\n.steps { display: grid; gap: 1rem; padding: 0; margin: 0 0 1rem; list-style: none; counter-reset: step; }\n.steps li { display: grid; grid-template-columns: auto 1fr; gap: .875rem; align-items: start; }\n\n/* ---------- contact ---------- */\n.contact {\n border: 1px solid var(--line); border-radius: var(--radius);\n background: var(--surface); padding: 1.75rem; box-shadow: var(--shadow-sm);\n max-width: none;\n}\n.contact h2 { margin-top: 0; }\n.mail {\n display: inline-flex; align-items: center;\n background: hsl(var(--primary)); color: hsl(var(--primary-foreground));\n font-weight: 600; font-size: .9375rem;\n padding: .625rem 1.25rem; border-radius: var(--radius); text-decoration: none;\n}\n.mail:hover { background: hsl(var(--primary-hover)); text-decoration: none; }\n.mail-note { margin: .875rem 0 0; font-size: .8125rem; color: var(--muted-fg); }\n\n/* ---------- footer: mirrors the main-site footer ---------- */\nfooter.site {\n border-top: 1px solid var(--line);\n background: var(--bg);\n padding: 4rem 0 2rem;\n font-size: .875rem;\n}\n@media (min-width: 768px) { footer.site { padding: 5rem 0 2rem; } }\n.foot-grid {\n display: grid; gap: 3rem; margin-bottom: 3rem;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n}\n.foot-brand { grid-column: 1 / -1; }\n@media (min-width: 1024px) { .foot-brand { grid-column: span 2; max-width: 320px; } }\n.foot-brand .wordmark {\n font-size: 1.125rem; font-weight: 600; letter-spacing: -.025em; text-transform: none;\n}\n.foot-brand p { color: var(--muted-fg); margin: .75rem 0 1rem; }\n.foot-col h4 {\n font-size: 1.875rem; font-weight: 500; letter-spacing: -.025em; line-height: 1.15;\n color: var(--fg); margin: 0 0 1rem;\n}\n@media (min-width: 768px) { .foot-col h4 { font-size: 2.25rem; } }\n.foot-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }\n.foot-col a { color: var(--muted-fg); }\n.foot-col a:hover { color: hsl(var(--primary)); }\n.foot-bottom {\n border-top: 1px solid var(--line); padding-top: 1.5rem;\n display: grid; gap: .75rem; color: var(--muted-fg);\n}\nfooter.site p { color: var(--muted-fg); }\nfooter.site .addr { font-size: .8125rem; }\n.foot-honest { max-width: 76ch; }\n\n/* ---------- offer: the pay path ----------\n A page shows btn-buy only when catalog.json carries a checkout that was\n fetched and found working. btn-ghost is the email fallback. Both are the\n same shape so a page does not look broken when the pay link is missing. */\n/* The browser\'s own "hidden means display:none" is an author-weight rule of the\n same specificity as a class, so any class that sets display -- .btn does, and\n .state does -- silently beats it and shows the element. schemahand\'s paid\n download button carries hidden until the key is entered, so that would have\n put a paid file on screen for everyone. Say it once, loudly, for the sheet. */\n[hidden] { display: none !important; }\n\n.hero-cta {\n display: flex; flex-wrap: wrap; align-items: center; gap: .875rem;\n margin: 1.5rem 0 0;\n}\n.btn {\n display: inline-flex; align-items: center; justify-content: center;\n font-weight: 650; font-size: .9375rem; line-height: 1;\n padding: .75rem 1.375rem; border-radius: var(--radius);\n text-decoration: none; border: 1px solid transparent;\n transition: background .15s ease, border-color .15s ease;\n}\n.btn:hover { text-decoration: none; }\n.btn-buy {\n background: hsl(var(--primary)); color: hsl(var(--primary-foreground));\n box-shadow: 0 1px 2px hsl(var(--foreground) / .08);\n}\n.btn-buy:hover { background: hsl(var(--primary-hover)); }\n.btn-ghost {\n background: transparent; color: hsl(var(--primary));\n border-color: hsl(var(--primary) / .35);\n}\n.btn-ghost:hover { background: hsl(var(--primary) / .06); border-color: hsl(var(--primary) / .55); }\n.btn-lg { padding: .9375rem 1.75rem; font-size: 1rem; }\n.btn-note { font-size: .8125rem; color: var(--muted-fg); }\n.contact.buy .buy-price {\n margin: 0 0 1.125rem; font-size: 1.125rem; color: var(--fg);\n}\n.contact.buy .buy-price strong { font-size: 1.5rem; letter-spacing: -.01em; }\n.contact.buy .btn-lg { margin-bottom: .25rem; }\n@media (max-width: 34rem) {\n .hero-cta { flex-direction: column; align-items: flex-start; }\n .btn { width: 100%; }\n}\n\nbody[data-family="domain-tools"] {--primary-surface:207 100% 42%;--primary-surface-hover:207 100% 36%;}\nbody[data-family="domain-tools"] .btn-buy{background:hsl(var(--primary-surface));color:hsl(var(--primary-foreground));}\nbody[data-family="domain-tools"] .wordmark {white-space:normal;flex-wrap:wrap}\nbody[data-family="domain-tools"] .table-scroll{overflow:auto;max-width:100%}\n'
def _readme_txt(result):
lines = ['LinenBalance result package',
'Engine: ' + ENGINE_ID,
'Status: ' + result['status'],
'',
'Contents:',
' report.json full run result (status, summary, tables, findings, provenance)',
' input.json the exact input this package was built from',
' accounts.csv per (account, sku, unit): opening, in, out, net, closing, known',
' unresolved.csv movements that could not be matched (e.g. reversal to unknown id)',
' movement-lineage.csv every movement, its posting and whether it fell in the period',
' opening-close.json known-only opening and closing snapshot',
' policy.json timezone, period and tracked accounts used',
' manifest.json sha256 content hash and byte size of every other file',
' hashes.json filename -> sha256 map',
' drilldown.html portable offline viewer',
'',
'Input schema (JSON):',
' policy.timezone display label (string). Timestamps carry explicit UTC offsets.',
' policy.period.start/end ISO-8601 with offset; half-open [start, end). Optional.',
' policy.tracked_accounts accounts whose absolute closing must be known for PASS. Optional.',
' openings[] {account, sku, unit, quantity(int>=0)}',
' movements[] {movement_id, type, timestamp(ISO+offset), source, destination, sku, unit, quantity(int>0)}; reversal = {movement_id, type, timestamp, reverses}',
' types dispatch, receipt, disposal, correction, adjustment, reversal',
'',
'Meaning of status:',
' PASS every tracked line reconstructed; bounded computation passed (not a professional judgment)',
' UNKNOWN a tracked line has no opening, or a movement is unresolved; totals kept, absolutes withheld',
' FAIL input rejected; see findings for the exact issue',
'',
'Scope and limits: counts only; no wash-yield inference, unequal-count matching, theft attribution, invoicing or integration. Matching is exact and user-controlled.']
return (NL.join(lines) + NL).encode('utf-8')
def _build_bundle_files(payload, result):
files = {}
files['report.json'] = _jsonb(result)
files['input.json'] = _jsonb(payload)
files['accounts.csv'] = _csv_bytes(result['tables']['accounts.csv'], ['account', 'sku', 'unit', 'opening', 'incoming', 'outgoing', 'net', 'closing', 'closing_known', 'tracked'])
files['unresolved.csv'] = _csv_bytes(result['tables']['unresolved.csv'], ['movement_id', 'type', 'reverses', 'reason'])
files['movement-lineage.csv'] = _csv_bytes(result['tables']['movement-lineage.csv'], ['movement_id', 'type', 'timestamp_utc', 'source', 'destination', 'sku', 'unit', 'quantity', 'reverses', 'in_period', 'posted'])
oc = {'engine': ENGINE_ID, 'status': result['status'], 'lines': [{k: r[k] for k in ('account', 'sku', 'unit', 'opening', 'incoming', 'outgoing', 'net', 'closing', 'closing_known', 'tracked')} for r in result['tables']['accounts.csv']]}
files['opening-close.json'] = _jsonb(oc)
files['policy.json'] = _jsonb({'engine': ENGINE_ID, 'timezone': result['summary']['timezone'], 'period': result['summary']['period'], 'tracked_accounts': result['summary']['tracked_accounts']})
files['README.txt'] = _readme_txt(result)
files['drilldown.html'] = _drilldown_html(result)
files['styles.css'] = OFFLINE_CSS.encode('utf-8')
return files
def bundle(payload):
result = run(payload)
if result['status'] == 'FAIL':
raise ValueError('cannot bundle invalid input: ' + str(len(result['findings'])) + ' issue(s)')
files = _build_bundle_files(payload, result)
hashes = {}
manifest_files = []
for name in sorted(files):
digest = hashlib.sha256(files[name]).hexdigest()
hashes[name] = digest
manifest_files.append({'name': name, 'sha256': digest, 'bytes': len(files[name])})
files['hashes.json'] = _jsonb(hashes)
files['manifest.json'] = _jsonb({'engine': ENGINE_ID, 'status': result['status'], 'files': manifest_files})
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
for name in sorted(files):
zi = zipfile.ZipInfo(filename=name, date_time=FIXED_ZIP_DATE)
zi.external_attr = (0o644 & 0xFFFF) << 16
zi.compress_type = zipfile.ZIP_DEFLATED
zf.writestr(zi, files[name])
return buf.getvalue()
def _atomic_write(path, data):
fd, tmp = tempfile.mkstemp(prefix='.linen-', dir=os.path.dirname(os.path.abspath(path)))
try:
with os.fdopen(fd, 'wb') as fh:
fh.write(data if isinstance(data, (bytes, bytearray)) else data.encode('utf-8'))
fh.flush()
os.fsync(fh.fileno())
os.replace(tmp, path)
finally:
if os.path.exists(tmp):
os.unlink(tmp)
def _write_report(output_dir, result):
_atomic_write(os.path.join(output_dir, 'report.json'), json.dumps(result, ensure_ascii=True, allow_nan=False, sort_keys=True, indent=2))
def _cli(input_path, output_dir):
if os.path.islink(output_dir):
return 2
if not os.path.isdir(output_dir):
os.makedirs(output_dir, exist_ok=True)
try:
with open(input_path, 'r', encoding='utf-8') as fh:
text = fh.read(MAX_INPUT_BYTES + 1)
except OSError as e:
sys.stderr.write('cannot read input: ' + str(e) + NL)
return 2
try:
payload = _strict_loads(text)
except Exception as e:
result = {'status': 'FAIL', 'summary': {'engine': ENGINE_ID, 'status': 'FAIL'}, 'tables': {}, 'findings': [{'level': 'error', 'code': 'bad_json', 'message': 'input is not valid JSON: ' + str(e)}], 'provenance': {'engine': ENGINE_ID}}
_write_report(output_dir, result)
stale = os.path.join(output_dir, 'result.zip')
if os.path.isfile(stale) or os.path.islink(stale):
os.unlink(stale)
return 2
result = run(payload)
_write_report(output_dir, result)
if result['status'] == 'FAIL':
stale = os.path.join(output_dir, 'result.zip')
if os.path.isfile(stale) or os.path.islink(stale):
os.unlink(stale)
return 2
_atomic_write(os.path.join(output_dir, 'result.zip'), bundle(payload))
return 0 if result['status'] == 'PASS' else 3
if __name__ == '__main__':
argv = sys.argv[1:]
if len(argv) == 2:
sys.exit(_cli(argv[0], argv[1]))
if len(argv) == 0 or argv[0] == '--selfcheck':
r = run(demo())
sys.stdout.write('selfcheck status ' + r['status'] + NL)
sys.exit(0 if r['status'] == 'PASS' else 1)
sys.stderr.write('usage: product.py INPUT.json OUTPUT_DIR' + NL)
sys.exit(64)