Skip to content

v0.6.0 — Receivables, a narrative that can't lie, and a guard that keeps its word

Choose a tag to compare

@gulmezeren2-byte gulmezeren2-byte released this 17 Jul 12:25
· 45 commits to main since this release

First release on PyPI: pipx install erp-report-engine

The headline of this release is not the new features. A five-way audit of this repository found the honesty discipline holding in the Python core and not travelling to the newer surfaces — and for a project whose entire claim is measurement honesty, a place where it says more than it knows is not an ordinary bug. Everything under Fixed is one of those places.

Added

  • Receivables aging (cari yaşlandırma) as an optional canonical entity — current / 1-30 / 31-60 / 61-90 / 91+, the overdue share, and who owes the most. A profile that can't reach an AR ledger omits it and everything downstream degrades gracefully.
  • Real-ERP AR mappings for Logo Tiger (PAYTRANS), Netsis (TBLCAHAR) and Mikro (CARI_HESAP_HAREKETLERI) — each with its weak points flagged inline rather than smoothed over.
  • Mikro profile — the third Turkish ERP. With Logo Tiger and Netsis, the bundled profiles now cover most of the Turkish SME ERP market.
  • Optional LLM narrative (run --narrate) built only from audited aggregates, with the exact payload printed in the report as a "what the model saw" appendix. Works with any OpenAI-compatible endpoint, including a local keyless one.
  • Revenue concentration — top-3 share + Herfindahl index (HHI), across all three surfaces.
  • Agent skill pack + a sixth MCP tool (aging).
  • Power BI — a receivables Aging page, a dark theme validated against Microsoft's official theme schema (0 errors), DAX SVG micro-charts, and the SPC control band.
  • Packaging — PyPI Trusted Publishing (OIDC, no stored token) and a lean non-root Docker image.

Fixed — where this project said more than it knew

  • "Read-only by construction" did not hold at the guard level. The guard checked a statement's shape and never asked what it called, so pg_read_file, lo_export (which writes a file), dblink (which dials out), OPENROWSET, LOAD_FILE, load_extension, query_to_xml, SLEEP — and worst, set_config('default_transaction_read_only','off') — all passed. It was read-only by configuration: it held because the docs tell you to use a least-privilege login. Functions are now checked by AST and lexically (OPENROWSET is precisely what sqlglot cannot parse), the parser fails closed rather than waving through what it cannot read, and agent SQL runs in a strict mode that default-denies every function the guard cannot name. Pinned by name and per dialect in tests/test_guard.py.
  • Power BI plotted the current partial week — the exact thing the README promises never happens. The trend visuals now carry a locked filter built from the engine's own window constant, shipped as data.
  • The agent could read any table the login could reach. "The agent talks to orders, never LG_001_01_ORFICHE" was true of every tool except query, which passed raw SQL through — so the semantic layer, the entire product, was optional. It isn't now.
  • A duplicated item_code crashed the whole run. The three real profiles GROUP BY and hid it; generic.yaml doesn't — the "swap the profile, keep the report" path.
  • Stock cover was overstated on short history, suppressing the low-stock alert on exactly the first run, when a new deployment has the least history.
  • On-time % can rise as fulfilment collapses — a late, unshipped order is in neither the numerator nor the denominator. The engine now counts what the percentage cannot see.
  • Attribution could claim 999% of a move, exactly when attribution mattered most.
  • Every SPC signal was labelled provisional, forever — the limits promised to stabilise at a threshold the architecture forbade reaching.
  • Power BI had no SPC at all, and its alert layer flagged the ordinary variation the method exists to ignore.
  • The dashboard claimed a validated palette while using brightened approximations of it — two of its colours were below the normal-vision ΔE floor.
  • The narrative's "aggregates only" understated what left the building — an aggregate can still name a party. Names are pseudonymised by default now.
  • The 90+ aging bucket actually held 91+; MySQL had no read-only session or statement timeout; the credential check missed ?passwd= and ?sslpassword=; and SELECT 'please delete this note' was refused because the keyword scan read string literals as code.

Changed

  • report.lookback_weeks now defaults to 26 (was 13). The chart still shows 13; the extra history is what the control limits are computed from, and they only settle around n≥15. The demo already generated 26 weeks — half of it was being discarded.

91 → 155 tests. CI green on Linux and Windows (3.10–3.13), with a coverage floor.

Full detail: CHANGELOG.md · Security model: SECURITY.md