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.