funded-report-assistant

Tracker lint, source-referenced progress reports and review packs for a funded R&D project. By Pavan Yadav Annappa. Code on GitHub

All project data on these pages is fictional. "eDrive-X" and Partners A-D are generated by a seeded script; no real company, person or project data is used.

Overview

A small command line tool (fra) for the paperwork side of a publicly funded R&D project: it checks an Excel project tracker against the meeting minutes, drafts the periodic progress report with a source reference on every sentence and table row, and prepares the agenda and open-points list for a milestone review. An optional LLM step rewrites the work-package summaries as prose, behind a guard that rejects any draft with a number, date or ID that is not in the source rows.

Live demo: https://huggingface.co/spaces/pavanyadava07/funded-report-assistant (sample tracker, lint findings, generated report, LLM variant, review pack, evaluation)

All project data is fictional. "eDrive-X" (a GaN eDrive inverter and eAxle demonstrator) and Partners A-D are produced by a seeded generator (fra/generator.py). No real company, person, funding call or project data is used.

What it does

data/edrive-x/tracker.xlsx WorkPackages, Milestones, ActionItems, Risks (+ Meta) data/edrive-x/minutes/*.md 7 meetings: action status, decisions, new action items -> fra lint 13 consistency rules, findings as Markdown + JSON -> fra report --period 2026-H1 progress report (Markdown + .docx), every line cites sheet!row or minutes file:line -> fra report --llm ollama:... same report, WP summaries reworded by an LLM if they pass the guard -> fra review-pack --milestone M3 agenda, open points, earlier decisions, action log extract

Part File What it does
Loader fra/model.py reads the .xlsx with openpyxl (Excel dates, ISO and DD.MM.YYYY strings, status case and spacing), parses the minutes, keeps a source reference per row and line
Lint fra/rules.py rules FRA001-FRA013 (table below)
Report fra/report.py funder-style structure: progress per WP, milestone table (planned vs actual), deviations and corrective actions, resource use in person-months, risks, plan for the next period; Markdown and .docx (python-docx)
Review pack fra/review.py agenda, open points (linked actions, lint findings, open risks), earlier decisions, action log extract
LLM + guard fra/llm.py, fra/guard.py Ollama and Gemini backends; token guard, milestone status check, omission check, fallback to the deterministic text
Generator fra/generator.py seeded fictional projects and 13 kinds of planted defects with ground truth
Evaluation scripts/run_eval.py planted-defect campaign and LLM grounding run, writes results/*.json

Lint rules

Rule Check
FRA001 duplicate id in any sheet
FRA002 status value outside the allowed set of the sheet
FRA003 action without owner
FRA004 open action without due date
FRA005 open action past its due date
FRA006 milestone marked done while linked actions are still open
FRA007 milestone marked done without an evidence link
FRA008 milestone still "planned" / "in progress" after its due date
FRA009 actual person-months above the plan to date by more than 15 %
FRA010 action id mentioned in minutes but missing in the tracker
FRA011 tracker action cites a meeting whose minutes do not record it
FRA012 action or milestone refers to an unknown work package
FRA013 open risk without mitigation

Measured results

The block below is written by scripts/make_results.py from results/*.json; numbers are not edited by hand. Details per rule and rejected-sentence examples: docs/RESULTS.md.

Tracker lint on 20 generated trackers (seeds 1000-1019, 13 defect kinds planted per tracker where placeable, formatting noise on) with 257 planted inconsistencies, plus the same 20 trackers without defects:

Method Planted defects found False alarms (planted trackers) Findings on clean trackers
fra lint (13 rules) 257/257 (100.0 %) 0 0
Baseline: date filter only 20/257 (7.8 %) 56 45
Baseline: spreadsheet checks (date filter, blanks, duplicates) 80/257 (31.1 %) 56 45

Mean runtime per tracker on this host: lint 0.3 ms, deterministic report 0.7 ms (after loading the .xlsx).

Closed world: the generator and the checker were written together by the same author, so 100 % recall here shows that each rule fires on the defect it was written for and stays quiet on clean data. It does not show how the rules do on a real project tracker. The baseline false alarms are cancelled actions with a past due date, which a plain "not closed" filter reports as overdue.

LLM narrative + guard: llama3.1:8b via Ollama 0.33.3, temperature 0, seed 42, device: CPU only (num_gpu=0, 32 vCPU). 21 projects (the sample + 20 generated), period 2026-H1, 126 work-package summaries.

Metric Value
Sentences where every number, date and ID is in the source facts 380/393 (96.7 %)
Summaries with all sentences grounded 113/126
Ungrounded tokens caught by the guard 15 (num: 15)
Summaries with a milestone status contradiction 0/126
Summaries that dropped at least one fact (ID, date or number) 109/126 (515 of 1748 fact tokens dropped)
LLM summaries kept (pass all three checks) 17/126 (13.5 %)
Kept summaries with a meaning error found on manual reading (one reader, not blind, every kept summary compared with its facts) 6/17
Guard on the deterministic text itself 592/592 sentences grounded
Planted one-digit changes in grounded LLM sentences, new value not in facts caught 364/364
Planted one-digit changes that land on another value present in the facts caught 0/14 (not detectable by a token check)
Mean time per WP summary / per report (6 WPs) 10.29 s / 61.74 s

Rejected summaries fall back to the deterministic text, so the report never contains an ungrounded number. The guard checks tokens, not meaning: a sentence can use only true numbers and still bind them to the wrong claim. In this run the status check found no contradiction, yet manual reading of the kept summaries found 'ahead of plan' for late milestones and 'exceeds the plan by 153%' for 153 % of plan. The other way round, several rejected sentences hold a correct derived number (for example 'exceeding the plan by 8%' for 32.5 vs 30.0 PM): the guard rejects any number that is not literally in the facts. The LLM ran on CPU because the host GPU (NVIDIA L4) was in use by another job; runtimes are CPU runtimes. No time saving is claimed; only runtime was measured.

Pre-rendered by scripts/build_site.py from the files in the repository.