148 lines
5.4 KiB
Python
148 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'])}")
|