Files
learning-planner/scripts/build_evidence_docs.py

305 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from pathlib import Path
from typing import Iterable
from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "evidence-documents"
SHOTS = ROOT / "docs" / "evidence" / "screenshots"
INK = RGBColor(33, 32, 55)
BLUE = RGBColor(46, 116, 181)
DARK_BLUE = RGBColor(31, 77, 120)
MUTED = RGBColor(103, 101, 119)
PURPLE = RGBColor(98, 87, 217)
GREEN = RGBColor(11, 143, 116)
PALE = "F2F4F7"
def set_font(run, size: float, color=INK, bold: bool | None = None, italic: bool | None = None):
run.font.name = "Calibri"
run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), "Calibri")
run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), "Calibri")
run.font.size = Pt(size)
run.font.color.rgb = color
if bold is not None:
run.bold = bold
if italic is not None:
run.italic = italic
def shade_paragraph(paragraph, fill: str, border: str = "D9DCE4"):
p_pr = paragraph._p.get_or_add_pPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:fill"), fill)
p_pr.append(shd)
borders = OxmlElement("w:pBdr")
for side in ("top", "left", "bottom", "right"):
edge = OxmlElement(f"w:{side}")
edge.set(qn("w:val"), "single")
edge.set(qn("w:sz"), "6")
edge.set(qn("w:color"), border)
edge.set(qn("w:space"), "6")
borders.append(edge)
p_pr.append(borders)
def add_page_number(paragraph):
paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
run = paragraph.add_run("Learning Planner · Competency evidence · ")
set_font(run, 8.5, MUTED)
fld = OxmlElement("w:fldSimple")
fld.set(qn("w:instr"), "PAGE")
paragraph._p.append(fld)
def configure_document(doc: Document, title: str, competency: str):
section = doc.sections[0]
section.top_margin = Inches(0.82)
section.bottom_margin = Inches(0.78)
section.left_margin = Inches(1)
section.right_margin = Inches(1)
section.header_distance = Inches(0.42)
section.footer_distance = Inches(0.42)
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
normal._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
normal.font.size = Pt(11)
normal.font.color.rgb = INK
normal.paragraph_format.space_before = Pt(0)
normal.paragraph_format.space_after = Pt(6)
normal.paragraph_format.line_spacing = 1.1
for style_name, size, color, before, after in (
("Heading 1", 16, BLUE, 16, 8),
("Heading 2", 13, BLUE, 12, 6),
("Heading 3", 12, DARK_BLUE, 8, 4),
):
style = doc.styles[style_name]
style.font.name = "Calibri"
style._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
style._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
style.font.size = Pt(size)
style.font.bold = True
style.font.color.rgb = color
style.paragraph_format.space_before = Pt(before)
style.paragraph_format.space_after = Pt(after)
style.paragraph_format.keep_with_next = True
header = section.header.paragraphs[0]
header.alignment = WD_ALIGN_PARAGRAPH.LEFT
header_run = header.add_run(f"ENGINEERING COMPETENCY · {competency.upper()}")
set_font(header_run, 8.5, MUTED, bold=True)
add_page_number(section.footer.paragraphs[0])
doc.core_properties.title = title
doc.core_properties.subject = competency
doc.core_properties.author = "Learning Planner project"
def add_masthead(doc: Document, title: str, subtitle: str, status: str):
kicker = doc.add_paragraph()
kicker.paragraph_format.space_after = Pt(2)
run = kicker.add_run("COMPETENCY EVIDENCE BRIEF")
set_font(run, 9, PURPLE, bold=True)
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(4)
run = p.add_run(title)
set_font(run, 23, INK, bold=True)
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(12)
run = p.add_run(subtitle)
set_font(run, 11.5, MUTED)
meta = doc.add_paragraph()
meta.paragraph_format.space_before = Pt(0)
meta.paragraph_format.space_after = Pt(12)
meta.add_run("Project: ").bold = True
meta.add_run("Learning Planner ")
meta.add_run("Date: ").bold = True
meta.add_run("14 August 2026 ")
meta.add_run("Status: ").bold = True
status_run = meta.add_run(status)
status_run.font.color.rgb = GREEN if status == "Implemented" else RGBColor(151, 99, 12)
def add_section(doc: Document, heading: str, paragraphs: Iterable[tuple[str | None, str]]):
doc.add_heading(heading, level=2)
for label, text in paragraphs:
p = doc.add_paragraph()
if label:
lead = p.add_run(f"{label}: ")
lead.bold = True
lead.font.color.rgb = DARK_BLUE
p.add_run(text)
def add_callout(doc: Document, label: str, text: str, fill: str = PALE):
p = doc.add_paragraph()
p.paragraph_format.left_indent = Inches(0.12)
p.paragraph_format.right_indent = Inches(0.12)
p.paragraph_format.space_before = Pt(5)
p.paragraph_format.space_after = Pt(9)
shade_paragraph(p, fill)
run = p.add_run(f"{label} ")
set_font(run, 10.5, DARK_BLUE, bold=True)
run = p.add_run(text)
set_font(run, 10.5, INK)
def add_appendix_page(doc: Document, evidence_id: str, caption: str, image_name: str):
doc.add_page_break()
kicker = doc.add_paragraph()
kicker.paragraph_format.space_after = Pt(2)
run = kicker.add_run("SCREENSHOT APPENDIX · EXCLUDED FROM TWO-PAGE NARRATIVE LIMIT")
set_font(run, 8.5, PURPLE, bold=True)
title = doc.add_paragraph()
title.paragraph_format.space_after = Pt(5)
run = title.add_run(f"{evidence_id} {caption}")
set_font(run, 15, INK, bold=True)
image = SHOTS / image_name
if image.exists():
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_after = Pt(6)
shape = p.add_run().add_picture(str(image), width=Inches(6.25))
shape._inline.docPr.set("descr", caption)
shape._inline.docPr.set("title", evidence_id)
note = doc.add_paragraph()
note.paragraph_format.space_before = Pt(0)
note.paragraph_format.space_after = Pt(0)
note.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = note.add_run(f"Figure {evidence_id}. {caption}. Generated from non-sensitive sample data.")
set_font(run, 9, MUTED, italic=True)
def build_project_management():
doc = Document()
configure_document(doc, "Managing a Personal Engineering Project", "Create and manage personal projects")
add_masthead(doc, "Managing a Personal Engineering Project", "Planning, delivering and evidencing the Learning Planner web application.", "Implemented")
add_section(doc, "Objective", [(None, "Plan and deliver a realistic solo web application while making scope, dependencies, checkpoints, decisions and progress auditable to an engineering assessor.")])
add_section(doc, "Approach", [
("Scope", "I selected a local-first learning planner: substantial enough for typed state, CRUD, validation, responsive design and test automation, but bounded by excluding accounts, cloud sync and a backend."),
("Control", "I converted the work into P01P14, linked dependencies, identified the critical path and defined six milestone exit checks. A versioned board snapshot, roadmap, test log and decision log preserve the evidence."),
])
add_section(doc, "Work completed", [
("Foundation", "Created the React/TypeScript/Vite project, quality scripts, stable domain interfaces, a versioned storage adapter and realistic sample data."),
("Product", "Delivered goal/task CRUD, dashboard summaries, search/filter/sort, validation, confirmation, JSON backup/recovery and accessible responsive interactions."),
("Governance", "Recorded acceptance criteria, dependency mapping, milestone status, risks and the RWD-01 decision. Manual Edge/device items remain visibly pending rather than being reported as complete."),
])
add_callout(doc, "Critical path", "P01 → P02/P03 → P04 → P05 → P06 → P07 → P10 → P11 → P12 → P13 → P14. Search/filter and backup proceeded as parallel branches before accessibility and final testing.")
doc.add_page_break()
add_section(doc, "Testing performed", [
("Quality gate", "Lint, nine unit/component tests, TypeScript compilation and the production build passed."),
("Browser workflows", "Playwright verified persisted goal creation, task search/status changes and horizontal-overflow protection across Google Chrome, Chromium, Firefox and all required automated viewports."),
("Checkpoint review", "I compared implementation against each backlog acceptance criterion and retained named test/evidence IDs. This exposed one visual defect at 1680×1050, which was fixed and retested."),
])
add_section(doc, "Outcome", [(None, "The project is runnable from a clean clone, the automated delivery scope is complete and the repository contains a portable audit trail. The assessor-ready release remains gated by genuine Edge and two-physical-device sessions, for which a signed checklist and screenshot naming protocol are supplied.")])
add_section(doc, "Lessons learned", [
("Make dependencies explicit", "The data model and responsive shell could progress in parallel, but CRUD depended on both; documenting that convergence kept sequencing clear."),
("Treat evidence as a deliverable", "Test IDs, seeded data and screenshot names must be designed early. Retrofitting them would have produced weaker, less repeatable evidence."),
("Report status truthfully", "A viewport emulator is not a physical device and Chromium is not Edge. Visible pending checkpoints are stronger engineering evidence than unsupported completion claims."),
])
add_callout(doc, "Evidence map", "PM-01 overview and acceptance criteria · PM-02 backlog/dependencies/roadmap · PM-03 test and checkpoint logs · Appendix figures show the delivered application and validated goal workflow.", "EEF0FF")
add_appendix_page(doc, "PM-04", "Evidence-ready application at 1920×1080", "responsive-1920x1080-dashboard.png")
add_appendix_page(doc, "PM-05", "Goal creation workflow at the 1680×1050 checkpoint", "responsive-1680x1050-goal-dialog.png")
return doc
def build_devices():
doc = Document()
configure_document(doc, "Testing Across Multiple Devices", "Test against multiple devices")
add_masthead(doc, "Testing Across Multiple Devices", "Responsive verification at three desktop resolutions with a reproducible physical-device protocol.", "Manual completion pending")
add_section(doc, "Objective", [(None, "Demonstrate that Learning Planner remains readable, operable and visually coherent at 1920×1080, 1680×1050 and 1920×1200, then repeat the principal workflow on two genuine computers.")])
add_section(doc, "Approach", [
("Repeatability", "Playwright set exact viewport dimensions and used the same non-sensitive dataset and core journey at every size. Each capture includes environment metadata."),
("Physical proof", "The manual protocol records manufacturer/model, OS, native resolution, scaling, browser version, inner viewport, tester and date for two computers. These fields cannot be completed by emulation."),
])
add_section(doc, "Work completed", [
("Responsive system", "Used a fixed desktop navigation rail, max-width content, Grid/Flexbox, wrapping controls, viewport-safe dialogs, visible focus, long-text protection and reduced-motion support."),
("Breakpoint intent", "At 1920 px the summary uses one four-card row; at 1680 px it becomes a balanced 2×2 grid; additional 1200 px height exposes more content without stretching controls."),
("Robustness", "A 768×900 check verifies the top-navigation and single-column fallback even though it is outside the formal evidence matrix."),
])
add_callout(doc, "Responsive elements", "Navigation, summary cards, progress and task panels, goal cards, filters, dates, long titles, validation, focus, dialogs, empty/error states and scrolling were included in the matrix.")
doc.add_page_break()
add_section(doc, "Testing performed", [
("Automated", "At all three required viewports the persisted CRUD journey, task interaction and page-level overflow assertion passed. The narrow robustness project also passed."),
("Visual", "Full-page captures were inspected for clipping, overlap, font wrapping, spacing, card balance and modal fit. `scrollWidth` equalled `clientWidth` at 1680 after the fix."),
("Zoom and keyboard", "The checklist covers 100%/125% zoom, Tab/Shift+Tab/Enter/Escape, focus visibility, long content and a clean application console."),
])
add_section(doc, "Outcome", [(None, "Automated responsive evidence passes for 1920×1080, 1680×1050 and 1920×1200. The two physical sessions are deliberately marked pending; this document is a working competency draft until their metadata and captures replace that status.")])
add_section(doc, "Lessons learned", [
("Functional pass is not visual quality", "At 1680×1050 the original three-column summary left one orphaned card. Automated overflow checks passed, but visual review exposed weak hierarchy."),
("Breakpoints should protect composition", "Changing to a 2×2 arrangement below 1800 px created balance without smaller text or cramped controls."),
("Device evidence needs context", "Resolution alone is ambiguous when OS scaling and browser chrome change the usable viewport; recording both native and inner dimensions makes results reproducible."),
])
add_callout(doc, "Remaining checkpoint", "Run DV-01 and DV-02 on real computers, attach the named system/application captures, update the test log and regenerate this package.", "FFF5E6")
add_appendix_page(doc, "RWD-01", "Dashboard at 1920×1080", "responsive-1920x1080-dashboard.png")
add_appendix_page(doc, "RWD-02", "Dashboard at 1680×1050 after correction", "responsive-1680x1050-dashboard.png")
add_appendix_page(doc, "RWD-03", "Dashboard at 1920×1200", "responsive-1920x1200-dashboard.png")
add_appendix_page(doc, "RWD-04A", "Reproduced 1680×1050 card-orphan defect", "responsive-defect-before-1680x1050.png")
add_appendix_page(doc, "RWD-04B", "Corrected balanced 2×2 summary grid", "responsive-fix-after-1680x1050.png")
return doc
def build_browsers():
doc = Document()
configure_document(doc, "Testing Across Multiple Browsers", "Test using multiple browsers")
add_masthead(doc, "Testing Across Multiple Browsers", "A consistent browser strategy for Google Chrome, Mozilla Firefox and Microsoft Edge.", "Edge session pending")
add_section(doc, "Objective", [(None, "Show that the same Learning Planner workflows and responsive layouts behave consistently in Google Chrome, Microsoft Edge and Mozilla Firefox, with reproducible developer-tools evidence and compatibility remediation.")])
add_section(doc, "Approach", [
("Common scenario", "Each browser starts from the same seeded dataset and executes persisted goal creation, task search/status, storage reload and overflow checks. This separates browser differences from test-data differences."),
("Coverage", "Playwright runs the installed Google Chrome channel and a Mozilla Firefox build. A Windows Edge protocol repeats the scenario and captures version, Console, Local Storage and computed Grid evidence."),
])
add_section(doc, "Work completed", [
("Compatibility design", "Used semantic HTML, system fonts, standards-based Grid/Flexbox, visible focus, reduced motion, a `crypto.randomUUID` fallback and validated browser storage."),
("Developer tools", "Console review checks runtime errors; Application/Storage verifies persistence; computed styles expose breakpoint differences; the accessibility tree verifies useful control names and structure."),
("Regression path", "A reproduced issue must record environment and expected/actual behaviour, use the smallest standards-based fix and gain an automated or documented manual retest."),
])
add_callout(doc, "Compatibility concerns", "Date inputs, font metrics/wrapping, sticky positioning, focus outlines, Grid sizing, downloads/uploads, localStorage and UUID support are explicit checklist items.")
doc.add_page_break()
add_section(doc, "Testing performed", [
("Google Chrome", "The installed Chrome channel passed the core planning journey, task interaction, overflow check and evidence capture."),
("Mozilla Firefox", "Firefox passed the same functional and overflow checks; its seeded dashboard was captured for direct comparison."),
("Microsoft Edge", "The Windows/manual session remains pending because Edge is not installed on this Linux host. The checklist requires the stable version page, app, Console and Local Storage captures."),
])
add_section(doc, "Outcome", [(None, "Chrome and Firefox provide completed, matching automated evidence with no application console errors. Edge is not claimed complete: the document becomes assessor-ready only after the genuine Edge session is logged and its appendix screenshots are added.")])
add_section(doc, "Lessons learned", [
("Name browsers precisely", "A Chromium engine run helps compatibility, but it is not evidence of the branded Chrome or Edge products. Launching the installed Chrome channel strengthened the result."),
("Use tools to explain differences", "Computed styles, console messages and stored data are more diagnostic than screenshots alone; together they show whether a problem is layout, runtime or persistence."),
("Prefer progressive enhancement", "Feature detection and narrow fallbacks are safer than broad polyfills. Add a prefix/polyfill only for a reproduced support gap and retain a regression test."),
])
add_callout(doc, "Backwards-compatibility response", "Confirm the support baseline, reproduce in the affected browser, prefer standards and feature detection, add Autoprefixer or a narrowly scoped fallback/polyfill only when needed, then rerun the full scenario.", "EEF0FF")
add_appendix_page(doc, "BR-01", "Google Chrome seeded dashboard", "browser-chrome-dashboard.png")
add_appendix_page(doc, "BR-02", "Mozilla Firefox seeded dashboard", "browser-firefox-dashboard.png")
return doc
def main():
OUT.mkdir(parents=True, exist_ok=True)
builds = {
"01-personal-project-management.docx": build_project_management(),
"02-multiple-device-testing.docx": build_devices(),
"03-multiple-browser-testing.docx": build_browsers(),
}
for name, doc in builds.items():
doc.save(OUT / name)
if __name__ == "__main__":
main()