Mega-Tracker/scripts/gen_baseline.py
Dave 471cf6fb3c Project tracker seeded from the Mega Infrastructure Candy barchart
App for the Mega Infrastructure AI Integration programme (A000) and Mega
Plant (B110). 100 activities across 2 programmes, 06 Oct 2026 - 31 Aug 2027,
parsed from the client PDF and frozen as an immutable baseline.

Views: Dashboard, Timeline (month grid + week grid matching the PDF's
Friday-anchored week numbering), Todo list, WBS, Data.

Notes on the source data:
- Hierarchy is derived from the PDF's indentation, not the activity code.
  Codes do not sort structurally (A490 sits under Workforce, A510 under
  Financials), so a code-ordered import would produce a wrong tree.
- The schedule is built from the dates, not the PDF's "Durn" column, which
  disagrees with its own dates for 13 of the 100 activities.
- Spec issues found in the PDF are surfaced in-app under Data: 2 unresolved
  "?" placeholders, 6 activities sharing one stretched 6-week window while
  declared 15 days, 13 duration mismatches, 17 spelling corrections
  (original wording preserved and viewable).

Progress (status, % complete, note) is stored behind a pluggable adapter so
the site can be static today and point at a shared JSON store later without
touching the UI.
2026-10-01 08:48:51 +02:00

147 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Generate src/data/baseline.ts from the parsed Candy barchart dump."""
import json, datetime, re, textwrap
from collections import OrderedDict
acts = json.load(open("/tmp/mega_acts.json"))
by = {a["code"]: a for a in acts}
# ---- ordering: roots in document order, children in document order -------
roots = [a["code"] for a in acts if a["parent"] is None]
def walk(code, depth, path):
a = by[code]
a["level"] = depth
a["path"] = path
for c in a["children"]:
walk(c, depth + 1, path + [code])
for r in roots:
walk(r, 0, [])
# ---- typo corrections (display only; originalName is preserved) ----------
FIX = [
("Mega Infrastructure AI Intergration", "Mega Infrastructure AI Integration"),
("CCS Intergration", "CCS Integration"),
("Training & Cources", "Training & Courses"),
("Drivers license & PDP", "Drivers licence & PDP"),
("All sites income & Expences", "All sites income & Expenses"),
("All sites combined retetion", "All sites combined retention"),
("All sites combined Expences", "All sites combined Expenses"),
("All sites combined material expences", "All sites combined material expenses"),
("Combined supplier expences", "Combined supplier expenses"),
("Head office fixed expences", "Head office fixed expenses"),
("Expences per week", "Expenses per week"),
("Expences per month total to date", "Expenses per month total to date"),
("Opperator certificate", "Operator certificate"),
("Opperator", "Operator"),
("Expences", "Expenses"),
("VaT allocation", "VAT allocation"),
]
for a in acts:
a["originalName"] = a["name"]
n = a["name"]
for bad, good in FIX:
if n == bad:
n = good
break
if bad in n:
n = n.replace(bad, good)
a["displayName"] = n
# ---- quality analysis ----------------------------------------------------
def wd(a, b):
d, n = a, 0
while d <= b:
if d.weekday() < 5:
n += 1
d += datetime.timedelta(days=1)
return n
def dt(s): return datetime.date.fromisoformat(s)
for a in acts:
s, f = dt(a["start"]), dt(a["finish"])
a["spanDays"] = wd(s, f)
a["durMismatch"] = (a["duration"] != a["spanDays"])
# stretched-window cluster: leaves sharing an identical window that is ~2x their declared duration
from collections import defaultdict
win = defaultdict(list)
for a in acts:
win[(a["start"], a["finish"])].append(a["code"])
STRETCHED = set()
for k, v in win.items():
if len(v) < 2:
continue
for c in v:
a = by[c]
if a["durMismatch"] and a["duration"] > 0 and a["spanDays"] > a["duration"] * 1.5:
STRETCHED.add(c)
for a in acts:
a["unconfirmed"] = "?" in a["name"]
a["stretched"] = a["code"] in STRETCHED
a["typoFixed"] = a["displayName"] != a["originalName"]
a["workstream"] = a["path"][1] if len(a["path"]) >= 2 else None
PROG = {
"A000": "Mega Infrastructure AI Integration",
"B110": "Mega Plant",
}
def q(s):
return json.dumps(s).replace("\\", "\\\\") if False else json.dumps(s)
out = []
out.append("// AUTO-GENERATED from 'Mega Infrastructure AI intergration system.pdf'")
out.append("// (Candy 2.05p / P6 barchart export, printed 30 Sep 2026).")
out.append("// 100 activities, 2 programmes, 06 Oct 2026 -> 31 Aug 2027.")
out.append("// Hierarchy comes from PDF indentation -- NOT from the activity code.")
out.append("// Regenerate with scripts/gen_baseline.py if the source PDF changes.")
out.append("")
out.append("import type { BaselineActivity } from '../types'")
out.append("")
out.append("export const SOURCE_PDF = 'Mega Infrastructure AI intergration system.pdf'")
out.append("export const SOURCE_PRINTED = '30 September 2026 13:00'")
out.append("")
out.append("export const PROGRAMMES = [")
for r in roots:
a = by[r]
out.append(" {")
out.append(f" code: {q(a['code'])},")
out.append(f" name: {q(PROG[a['code']])},")
out.append(f" originalName: {q(a['originalName'])},")
out.append(f" start: {q(a['start'])},")
out.append(f" finish: {q(a['finish'])},")
out.append(" },")
out.append("] as const")
out.append("")
out.append("export const BASELINE: BaselineActivity[] = [")
for a in acts:
flags = []
if a["unconfirmed"]: flags.append("unconfirmed")
if a["stretched"]: flags.append("stretched")
if a["durMismatch"]: flags.append("duration-mismatch")
if a["typoFixed"]: flags.append("name-corrected")
out.append(" {")
out.append(f" code: {q(a['code'])},")
out.append(f" name: {q(a['displayName'])},")
out.append(f" originalName: {q(a['originalName'])},")
out.append(f" parent: {q(a['parent'])},")
out.append(f" level: {a['level']},")
out.append(f" plannedDuration: {a['duration']},")
out.append(f" spanDays: {a['spanDays']},")
out.append(f" start: {q(a['start'])},")
out.append(f" finish: {q(a['finish'])},")
out.append(f" flags: {json.dumps(flags)},")
out.append(" },")
out.append("]")
out.append("")
src = "\n".join(out)
open("/tmp/baseline.ts", "w").write(src)
print(f"generated {len(acts)} activities, {len(src)} bytes")
print(f"flags: unconfirmed={sum(1 for a in acts if a['unconfirmed'])} "
f"stretched={sum(1 for a in acts if a['stretched'])} "
f"durMismatch={sum(1 for a in acts if a['durMismatch'])} "
f"nameCorrected={sum(1 for a in acts if a['typoFixed'])}")