From 96c7e4a6d36e868a67382241547e18489c1b09ee Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:40 +0800 Subject: [PATCH 1/8] feat: screenshot capture tool and screenshots of the real window Task: MIL-003#1 --- docs/images/add-confirmation.png | Bin 0 -> 15194 bytes docs/images/main-window.png | Bin 0 -> 7279 bytes docs/images/search-found.png | Bin 0 -> 12823 bytes docs/images/search-not-found.png | Bin 0 -> 11601 bytes tools/capture_screenshots.py | 266 +++++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 docs/images/add-confirmation.png create mode 100644 docs/images/main-window.png create mode 100644 docs/images/search-found.png create mode 100644 docs/images/search-not-found.png create mode 100644 tools/capture_screenshots.py diff --git a/docs/images/add-confirmation.png b/docs/images/add-confirmation.png new file mode 100644 index 0000000000000000000000000000000000000000..538a3f1111ce6e35eb4b8dfc3281ee5cdf2ee005 GIT binary patch literal 15194 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 Kmh?>rPX8an`4fi# literal 0 HcmV?d00001 diff --git a/docs/images/search-not-found.png b/docs/images/search-not-found.png new file mode 100644 index 0000000000000000000000000000000000000000..099e88ffb28acff9c26242e12179e758689c37c3 GIT binary patch literal 11601 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 L>e=Koh7bQ2NW=po literal 0 HcmV?d00001 diff --git a/tools/capture_screenshots.py b/tools/capture_screenshots.py new file mode 100644 index 0000000..d8e7692 --- /dev/null +++ b/tools/capture_screenshots.py @@ -0,0 +1,266 @@ +"""@brief Capture the README and wiki screenshots of the real Tkinter window. + +@file capture_screenshots.py +@details Windows only (GDI through ctypes), standard library only. Each state +runs in its own process, uses a temporary data file and never touches the +user's `data.json`. Only the windows of the program itself are rendered +(`PrintWindow`) onto a neutral canvas, so nothing from the desktop appears. +Usage, from the repository root with the venv active: + + python tools/capture_screenshots.py # writes docs/images/*.png + python tools/capture_screenshots.py --out DIR # another output folder +""" + +import argparse +import ctypes +import os +import struct +import subprocess +import sys +import tempfile +import tkinter as tk +import zlib +from ctypes import wintypes +from pathlib import Path + +STATES = { + "main-window": "Window after Generate Password", + "add-confirmation": "Confirmation before an entry is saved", + "search-found": "Search finds a saved website", + "search-not-found": "Search for an unknown website", +} +SAMPLE_WEBSITE = "example.com" +SAMPLE_EMAIL = "jens@example.com" +SAMPLE_PASSWORD = "Sample#Pass123" +UNKNOWN_WEBSITE = "unknown.org" +PADDING = 24 +CANVAS_PIXEL = bytes([0xEE, 0xEE, 0xEE, 0xFF]) +CAPTURE_DELAY_MS = 900 +PW_RENDERFULLCONTENT = 2 +DWMWA_EXTENDED_FRAME_BOUNDS = 9 + + +class BitmapInfoHeader(ctypes.Structure): + """@brief Win32 BITMAPINFOHEADER.""" + + _fields_ = [ + ("biSize", wintypes.DWORD), + ("biWidth", wintypes.LONG), + ("biHeight", wintypes.LONG), + ("biPlanes", wintypes.WORD), + ("biBitCount", wintypes.WORD), + ("biCompression", wintypes.DWORD), + ("biSizeImage", wintypes.DWORD), + ("biXPelsPerMeter", wintypes.LONG), + ("biYPelsPerMeter", wintypes.LONG), + ("biClrUsed", wintypes.DWORD), + ("biClrImportant", wintypes.DWORD), + ] + + +def write_png(path: Path, width: int, height: int, bgra: bytes) -> None: + """@brief Write a BGRA pixel buffer as an RGB PNG file. + + @param path Output file. + @param width Image width in pixels. + @param height Image height in pixels. + @param bgra Raw pixels, 4 bytes each, top row first. + """ + stride = width * 4 + raw = bytearray() + for y in range(height): + row = bgra[y * stride : (y + 1) * stride] + raw.append(0) + raw += bytes( + channel + for x in range(width) + for channel in (row[x * 4 + 2], row[x * 4 + 1], row[x * 4]) + ) + + def chunk(kind: bytes, data: bytes) -> bytes: + body = kind + data + checksum = zlib.crc32(body) & 0xFFFFFFFF + return struct.pack(">I", len(data)) + body + struct.pack(">I", checksum) + + header = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) + png = ( + b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", header) + + chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + + chunk(b"IEND", b"") + ) + path.write_bytes(png) + + +def render_window(hwnd: int, width: int, height: int) -> bytes: + """@brief Render one window's own pixels (never the desktop behind it). + + @param hwnd Window handle. + @param width Window width in pixels. + @param height Window height in pixels. + @return Raw BGRA pixels, top row first. + """ + user32 = ctypes.windll.user32 + gdi32 = ctypes.windll.gdi32 + screen_dc = user32.GetDC(0) + memory_dc = gdi32.CreateCompatibleDC(screen_dc) + bitmap = gdi32.CreateCompatibleBitmap(screen_dc, width, height) + gdi32.SelectObject(memory_dc, bitmap) + user32.PrintWindow(hwnd, memory_dc, PW_RENDERFULLCONTENT) + info = BitmapInfoHeader( + ctypes.sizeof(BitmapInfoHeader), width, -height, 1, 32, 0, 0, 0, 0, 0, 0 + ) + buffer = ctypes.create_string_buffer(width * height * 4) + gdi32.GetDIBits(memory_dc, bitmap, 0, height, buffer, ctypes.byref(info), 0) + gdi32.DeleteObject(bitmap) + gdi32.DeleteDC(memory_dc) + user32.ReleaseDC(0, screen_dc) + return buffer.raw + + +def own_windows() -> list[int]: + """@brief Visible top-level windows of this process, topmost first. + + @return Window handles. + """ + user32 = ctypes.windll.user32 + found: list[int] = [] + callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) + + def visit(hwnd: int, _lparam: int) -> bool: + pid = wintypes.DWORD() + user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) + if pid.value == os.getpid() and user32.IsWindowVisible(hwnd): + found.append(hwnd) + return True + + user32.EnumWindows(callback_type(visit), 0) + return found + + +def frame_and_window(hwnd: int) -> tuple[wintypes.RECT, wintypes.RECT]: + """@brief Visible frame and full window rectangle of a window. + + @param hwnd Window handle. + @return The extended frame bounds and the GetWindowRect rectangle. + """ + frame, outer = wintypes.RECT(), wintypes.RECT() + ctypes.windll.dwmapi.DwmGetWindowAttribute( + hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, ctypes.byref(frame), ctypes.sizeof(frame) + ) + ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(outer)) + return frame, outer + + +def compose_windows() -> tuple[int, int, bytes]: + """@brief Render this process's windows side by side on a neutral canvas. + + @return Canvas width, height and BGRA pixels. + """ + pieces = [] + for hwnd in own_windows(): + frame, outer = frame_and_window(hwnd) + width, height = outer.right - outer.left, outer.bottom - outer.top + pixels = render_window(hwnd, width, height) + offset_x, offset_y = frame.left - outer.left, frame.top - outer.top + piece_width = frame.right - frame.left + piece_height = frame.bottom - frame.top + rows = [ + pixels[((offset_y + y) * width + offset_x) * 4 :][: piece_width * 4] + for y in range(piece_height) + ] + pieces.append((frame.left, frame.top, piece_width, piece_height, rows)) + left = min(piece[0] for piece in pieces) - PADDING + top = min(piece[1] for piece in pieces) - PADDING + right = max(piece[0] + piece[2] for piece in pieces) + PADDING + bottom = max(piece[1] + piece[3] for piece in pieces) + PADDING + width, height = right - left, bottom - top + canvas = [bytearray(CANVAS_PIXEL * width) for _ in range(height)] + for x, y, piece_width, _piece_height, rows in reversed(pieces): + for index, row in enumerate(rows): + start = (x - left) * 4 + canvas[y - top + index][start : start + piece_width * 4] = row + return width, height, b"".join(bytes(row) for row in canvas) + + +def run_state(state: str, out_file: Path) -> None: + """@brief Show the window in one state, capture it and exit the process. + + @param state One of the keys of STATES. + @param out_file PNG file to write. + """ + ctypes.windll.user32.SetProcessDPIAware() + from password_manager.application.use_cases import ( + FindPasswordEntry, + GeneratePassword, + SavePasswordEntry, + ) + from password_manager.domain.password_entry import PasswordEntry + from password_manager.infrastructure.persistence.json_password_repository import ( + JsonPasswordRepository, + ) + from password_manager.presentation.features.password_manager import ( + password_manager_view as view_module, + ) + from password_manager.presentation.features.password_manager import ( + password_manager_view_model as view_model_module, + ) + + with tempfile.TemporaryDirectory() as folder: + repository = JsonPasswordRepository(Path(folder) / "data.json") + repository.add(PasswordEntry(SAMPLE_WEBSITE, SAMPLE_EMAIL, SAMPLE_PASSWORD)) + view_model = view_model_module.PasswordManagerViewModel( + SavePasswordEntry(repository), + FindPasswordEntry(repository), + GeneratePassword(), + ) + root = tk.Tk() + view = view_module.PasswordManagerView(root, view_model) + root.geometry("+120+120") + + def capture() -> None: + width, height, pixels = compose_windows() + write_png(out_file, width, height, pixels) + os._exit(0) + + def act() -> None: + website = UNKNOWN_WEBSITE if state == "search-not-found" else SAMPLE_WEBSITE + view.website_entry.insert(0, website) + if state == "main-window": + view.generate_password() + root.after(CAPTURE_DELAY_MS, capture) + return + root.after(CAPTURE_DELAY_MS, capture) + if state == "add-confirmation": + view.password_entry.insert(0, SAMPLE_PASSWORD) + view.save() + else: + view.find_password() + + root.after(400, act) + root.mainloop() + + +def main() -> None: + """@brief Capture every state, each in a child process.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=Path("docs/images")) + parser.add_argument("--state", choices=sorted(STATES), help="internal use") + parser.add_argument("--file", type=Path, help="internal use") + args = parser.parse_args() + if args.state: + run_state(args.state, args.file) + return + args.out.mkdir(parents=True, exist_ok=True) + for state in STATES: + target = args.out / f"{state}.png" + subprocess.run( + [sys.executable, __file__, "--state", state, "--file", str(target)], + check=True, + timeout=60, + ) + print(f"wrote {target}") + + +if __name__ == "__main__": + main() -- 2.54.0 From a24e7cce6fa7cc351aeb8875413979f5c90a92c1 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:41 +0800 Subject: [PATCH 2/8] docs: show screenshots in the README Task: MIL-003#2 --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 313126a..b9c139e 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,22 @@ The program follows the three lectures of the assignment: The assignment's function names are kept: `generate_password`, `save` and `find_password`. +![The Password Manager window after Generate Password](docs/images/main-window.png) + +| Search finds a saved website | Search for an unknown website | +| --- | --- | +| ![Search finds a saved website](docs/images/search-found.png) | ![Search for an unknown website](docs/images/search-not-found.png) | + +The screenshots are captured from the real window by +`tools/capture_screenshots.py` (Windows). The wiki in English, Danish and Bahasa +Malaysia is in `docs/wiki/`. + | Path | What it holds | | --- | --- | | `src/password_manager/` | The program: domain rules, use cases, JSON storage and the Tkinter window | | `src/password_manager/constants.py` | Every constant (file name, labels, messages, password length bounds) | -| `tests/` | The pytest tests | +| `tests/` | The pytest tests (`unit`, `integration`, `e2e`) | +| `tools/` | `capture_screenshots.py`, the screenshot capture script | | `docs/` | Project documents, the Doxygen output folder and the wiki in `docs/wiki/` | | `Doxyfile` | Doxygen configuration for the source comments | | `pyproject.toml` | Project configuration (Python version, entry point, pytest) | -- 2.54.0 From 0ccd21d23df56b135569c33760511126936b0148 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:42 +0800 Subject: [PATCH 3/8] docs: English wiki Task: MIL-003#3 --- docs/wiki/Development.md | 47 ++++++++++++++++++++++++++++++++++++ docs/wiki/Home.md | 27 +++++++++++++++++++++ docs/wiki/Installation.md | 38 +++++++++++++++++++++++++++++ docs/wiki/Usage.md | 50 +++++++++++++++++++++++++++++++++++++++ docs/wiki/_Footer.md | 1 + docs/wiki/_Sidebar.md | 23 ++++++++++++++++++ 6 files changed, 186 insertions(+) create mode 100644 docs/wiki/Development.md create mode 100644 docs/wiki/Home.md create mode 100644 docs/wiki/Installation.md create mode 100644 docs/wiki/Usage.md create mode 100644 docs/wiki/_Footer.md create mode 100644 docs/wiki/_Sidebar.md diff --git a/docs/wiki/Development.md b/docs/wiki/Development.md new file mode 100644 index 0000000..9973871 --- /dev/null +++ b/docs/wiki/Development.md @@ -0,0 +1,47 @@ +**English** · [Dansk](Development-da) · [Bahasa Malaysia](Development-ms) + +# Development + +## Layout + +| Folder | Holds | +| --- | --- | +| `src/password_manager/domain` | Entry, password generator and errors; no input or output | +| `src/password_manager/application` | Use cases and the repository port | +| `src/password_manager/infrastructure` | The JSON repository | +| `src/password_manager/presentation` | The MVVM view and view model | +| `src/password_manager/constants.py` | Every constant and message | +| `tests/` | `unit`, `integration` and `e2e` tests | +| `tools/` | `capture_screenshots.py` | + +Imports point inward; only `main.py` connects the layers. + +## Tests and lint + +```bash +python -m pytest +python -m ruff format src tests tools +python -m ruff check src tests tools +``` + +## Source documentation + +Comments are Doxygen style. Build the HTML with `doxygen Doxyfile`; it opens at +`docs/doxygen/html/index.html`. + +## Screenshots + +On Windows, with the virtual environment active: + +```bash +python tools/capture_screenshots.py +``` + +The images are written to `docs/images/` from the real window and a temporary +data file. + +## Project documents + +The project follows a planning-first workflow: Business Case, Stakeholder +Analysis, Project Plan and milestones are in `docs/`. Work is done on one branch +and one pull request per milestone. diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..2a0272f --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,27 @@ +**English** · [Dansk](Home-da) · [Bahasa Malaysia](Home-ms) + +# Password Manager + +A small Tkinter password manager from day 30 of Udemy's "100 Days of Code: The +Complete Python Pro Bootcamp". It generates passwords, saves them in a JSON +file and searches them by website. + +![The main window](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/main-window.png) + +## What it does + +- **Generate** a random password and copy it to the clipboard. +- **Add** a website, an e-mail address and a password to `data.json`. +- **Search** a website and see the saved e-mail address and password. +- Keeps running when `data.json` is missing, empty or damaged. + +> **Security note.** The passwords are stored as plain JSON and are not +> encrypted. This is a learning project; do not use it for real passwords. + +## Pages + +- [Installation](Installation) +- [Usage](Usage) +- [Development](Development) + +Source code and issues: [repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager). diff --git a/docs/wiki/Installation.md b/docs/wiki/Installation.md new file mode 100644 index 0000000..811ccb5 --- /dev/null +++ b/docs/wiki/Installation.md @@ -0,0 +1,38 @@ +**English** · [Dansk](Installation-da) · [Bahasa Malaysia](Installation-ms) + +# Installation + +## Requirements + +- Python 3.13 or newer with Tkinter. On Debian or Ubuntu install `python3-tk`. +- `venv` and `pip`, both part of Python. +- No runtime dependencies. + +## Create a local virtual environment + +Windows (PowerShell): + +```powershell +python -m venv .venv +.venv/Scripts/Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Linux and macOS: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +The `[dev]` extra installs pytest and ruff. Leave it out +(`python -m pip install -e .`) if you only want to run the program. + +## Check the installation + +```bash +python -m pytest +``` diff --git a/docs/wiki/Usage.md b/docs/wiki/Usage.md new file mode 100644 index 0000000..7714f84 --- /dev/null +++ b/docs/wiki/Usage.md @@ -0,0 +1,50 @@ +**English** · [Dansk](Usage-da) · [Bahasa Malaysia](Usage-ms) + +# Usage + +Start the program with the virtual environment active: + +```bash +python -m password_manager +``` + +The program keeps its data in `data.json` in the folder you start it from. + +## Generate and add a password + +1. Type the website. +2. Press **Generate Password**. A password is filled in and copied to the + clipboard. +3. Check the e-mail address and press **Add**. Confirm the dialog and the entry + is saved. + +![The main window after Generate Password](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/main-window.png) + +![The confirmation before an entry is saved](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/add-confirmation.png) + +Leaving a field empty shows a warning and saves nothing. + +## Search + +Type a website and press **Search**. The saved e-mail address and password are +shown. + +![Search finds a saved website](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/search-found.png) + +If nothing is saved for the website, or no data file exists yet, a message says +so. + +![Search for an unknown website](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/search-not-found.png) + +## The data file + +`data.json` holds one record per website: + +```json +{ + "example.com": { + "email": "jens@example.com", + "password": "Sample#Pass123" + } +} +``` diff --git a/docs/wiki/_Footer.md b/docs/wiki/_Footer.md new file mode 100644 index 0000000..2acec83 --- /dev/null +++ b/docs/wiki/_Footer.md @@ -0,0 +1 @@ +Password Manager · day 30 of 100 Days of Code · [Repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager) diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..32d6ecd --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,23 @@ +**Password Manager** + +**English** + +- [Home](Home) +- [Installation](Installation) +- [Usage](Usage) +- [Development](Development) + +**Dansk** + +- [Hjem](Home-da) +- [Installation](Installation-da) +- [Brug](Usage-da) +- [Udvikling](Development-da) + +**Bahasa Malaysia** + +- [Laman Utama](Home-ms) +- [Pemasangan](Installation-ms) +- [Penggunaan](Usage-ms) +- [Pembangunan](Development-ms) + -- 2.54.0 From 40d653113f66246fc450ddfc4df660f252872f2a Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:43 +0800 Subject: [PATCH 4/8] docs: Danish wiki Task: MIL-003#4 --- docs/wiki/Development-da.md | 47 +++++++++++++++++++++++++++++++++ docs/wiki/Home-da.md | 27 +++++++++++++++++++ docs/wiki/Installation-da.md | 39 ++++++++++++++++++++++++++++ docs/wiki/Usage-da.md | 50 ++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 docs/wiki/Development-da.md create mode 100644 docs/wiki/Home-da.md create mode 100644 docs/wiki/Installation-da.md create mode 100644 docs/wiki/Usage-da.md diff --git a/docs/wiki/Development-da.md b/docs/wiki/Development-da.md new file mode 100644 index 0000000..2707655 --- /dev/null +++ b/docs/wiki/Development-da.md @@ -0,0 +1,47 @@ +[English](Development) · **Dansk** · [Bahasa Malaysia](Development-ms) + +# Udvikling + +## Opbygning + +| Mappe | Indhold | +| --- | --- | +| `src/password_manager/domain` | Post, adgangskodegenerator og fejl; ingen ind- eller udlæsning | +| `src/password_manager/application` | Use cases og lagerporten | +| `src/password_manager/infrastructure` | JSON-lageret | +| `src/password_manager/presentation` | MVVM-visningen og visningsmodellen | +| `src/password_manager/constants.py` | Alle konstanter og beskeder | +| `tests/` | `unit`-, `integration`- og `e2e`-tests | +| `tools/` | `capture_screenshots.py` | + +Importer peger indad; kun `main.py` forbinder lagene. + +## Tests og lint + +```bash +python -m pytest +python -m ruff format src tests tools +python -m ruff check src tests tools +``` + +## Kildedokumentation + +Kommentarerne er i Doxygen-stil. Byg HTML med `doxygen Doxyfile`; den åbnes på +`docs/doxygen/html/index.html`. + +## Skærmbilleder + +På Windows med det virtuelle miljø aktivt: + +```bash +python tools/capture_screenshots.py +``` + +Billederne skrives til `docs/images/` fra det rigtige vindue og en midlertidig +datafil. + +## Projektdokumenter + +Projektet følger en planlæg-først-arbejdsgang: Business Case, interessentanalyse, +projektplan og milepæle ligger i `docs/`. Arbejdet udføres på én gren og én pull +request pr. milepæl. diff --git a/docs/wiki/Home-da.md b/docs/wiki/Home-da.md new file mode 100644 index 0000000..b9a95bc --- /dev/null +++ b/docs/wiki/Home-da.md @@ -0,0 +1,27 @@ +[English](Home) · **Dansk** · [Bahasa Malaysia](Home-ms) + +# Password Manager + +En lille Tkinter-adgangskodemanager fra dag 30 af Udemys "100 Days of Code: The +Complete Python Pro Bootcamp". Den genererer adgangskoder, gemmer dem i en +JSON-fil og søger efter dem på websted. + +![Hovedvinduet](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/main-window.png) + +## Hvad den gør + +- **Genererer** en tilfældig adgangskode og kopierer den til udklipsholderen. +- **Tilføjer** et websted, en e-mailadresse og en adgangskode til `data.json`. +- **Søger** efter et websted og viser den gemte e-mailadresse og adgangskode. +- Kører videre, når `data.json` mangler, er tom eller er beskadiget. + +> **Sikkerhedsnote.** Adgangskoderne gemmes som almindelig JSON og er ikke +> krypterede. Det er et læringsprojekt; brug det ikke til rigtige adgangskoder. + +## Sider + +- [Installation](Installation-da) +- [Brug](Usage-da) +- [Udvikling](Development-da) + +Kildekode og opgaver: [repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager). diff --git a/docs/wiki/Installation-da.md b/docs/wiki/Installation-da.md new file mode 100644 index 0000000..88128f7 --- /dev/null +++ b/docs/wiki/Installation-da.md @@ -0,0 +1,39 @@ +[English](Installation) · **Dansk** · [Bahasa Malaysia](Installation-ms) + +# Installation + +## Krav + +- Python 3.13 eller nyere med Tkinter. På Debian eller Ubuntu installeres + `python3-tk`. +- `venv` og `pip`, som begge er en del af Python. +- Ingen afhængigheder i drift. + +## Opret et lokalt virtuelt miljø + +Windows (PowerShell): + +```powershell +python -m venv .venv +.venv/Scripts/Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Linux og macOS: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Tillægget `[dev]` installerer pytest og ruff. Udelad det +(`python -m pip install -e .`), hvis du kun vil køre programmet. + +## Kontrollér installationen + +```bash +python -m pytest +``` diff --git a/docs/wiki/Usage-da.md b/docs/wiki/Usage-da.md new file mode 100644 index 0000000..f7510da --- /dev/null +++ b/docs/wiki/Usage-da.md @@ -0,0 +1,50 @@ +[English](Usage) · **Dansk** · [Bahasa Malaysia](Usage-ms) + +# Brug + +Start programmet med det virtuelle miljø aktivt: + +```bash +python -m password_manager +``` + +Programmet gemmer sine data i `data.json` i den mappe, du starter det fra. + +## Generér og tilføj en adgangskode + +1. Skriv webstedet. +2. Tryk på **Generate Password**. En adgangskode indsættes og kopieres til + udklipsholderen. +3. Kontrollér e-mailadressen og tryk på **Add**. Bekræft dialogen, så gemmes + posten. + +![Hovedvinduet efter Generate Password](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/main-window.png) + +![Bekræftelsen før en post gemmes](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/add-confirmation.png) + +Hvis et felt er tomt, vises en advarsel, og intet gemmes. + +## Søg + +Skriv et websted og tryk på **Search**. Den gemte e-mailadresse og adgangskode +vises. + +![Søgningen finder et gemt websted](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/search-found.png) + +Hvis der ikke er gemt noget for webstedet, eller datafilen endnu ikke findes, +fortæller en besked det. + +![Søgning efter et ukendt websted](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/search-not-found.png) + +## Datafilen + +`data.json` indeholder én post pr. websted: + +```json +{ + "example.com": { + "email": "jens@example.com", + "password": "Sample#Pass123" + } +} +``` -- 2.54.0 From 06811e6b5aa2963908c0a2f4932f57237dfe1699 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:44 +0800 Subject: [PATCH 5/8] docs: Bahasa Malaysia wiki Task: MIL-003#5 --- docs/wiki/Development-ms.md | 47 ++++++++++++++++++++++++++++++++++ docs/wiki/Home-ms.md | 29 +++++++++++++++++++++ docs/wiki/Installation-ms.md | 39 ++++++++++++++++++++++++++++ docs/wiki/Usage-ms.md | 49 ++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 docs/wiki/Development-ms.md create mode 100644 docs/wiki/Home-ms.md create mode 100644 docs/wiki/Installation-ms.md create mode 100644 docs/wiki/Usage-ms.md diff --git a/docs/wiki/Development-ms.md b/docs/wiki/Development-ms.md new file mode 100644 index 0000000..4eeeed2 --- /dev/null +++ b/docs/wiki/Development-ms.md @@ -0,0 +1,47 @@ +[English](Development) · [Dansk](Development-da) · **Bahasa Malaysia** + +# Pembangunan + +## Susun atur + +| Folder | Kandungan | +| --- | --- | +| `src/password_manager/domain` | Rekod, penjana kata laluan dan ralat; tiada input atau output | +| `src/password_manager/application` | Kes guna dan port repositori | +| `src/password_manager/infrastructure` | Repositori JSON | +| `src/password_manager/presentation` | Paparan MVVM dan model paparan | +| `src/password_manager/constants.py` | Semua pemalar dan mesej | +| `tests/` | Ujian `unit`, `integration` dan `e2e` | +| `tools/` | `capture_screenshots.py` | + +Import menuju ke dalam; hanya `main.py` menghubungkan lapisan. + +## Ujian dan lint + +```bash +python -m pytest +python -m ruff format src tests tools +python -m ruff check src tests tools +``` + +## Dokumentasi kod sumber + +Komen ditulis dalam gaya Doxygen. Bina HTML dengan `doxygen Doxyfile`; ia dibuka +di `docs/doxygen/html/index.html`. + +## Tangkapan skrin + +Pada Windows dengan persekitaran maya aktif: + +```bash +python tools/capture_screenshots.py +``` + +Imej ditulis ke `docs/images/` daripada tetingkap sebenar dan fail data +sementara. + +## Dokumen projek + +Projek mengikut aliran kerja perancangan dahulu: Business Case, analisis +pemegang taruh, pelan projek dan pencapaian berada dalam `docs/`. Kerja +dilakukan pada satu cabang dan satu pull request bagi setiap pencapaian. diff --git a/docs/wiki/Home-ms.md b/docs/wiki/Home-ms.md new file mode 100644 index 0000000..6be0bb4 --- /dev/null +++ b/docs/wiki/Home-ms.md @@ -0,0 +1,29 @@ +[English](Home) · [Dansk](Home-da) · **Bahasa Malaysia** + +# Password Manager + +Pengurus kata laluan Tkinter yang kecil daripada hari ke-30 kursus Udemy "100 +Days of Code: The Complete Python Pro Bootcamp". Ia menjana kata laluan, +menyimpannya dalam fail JSON dan mencarinya mengikut laman web. + +![Tetingkap utama](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/main-window.png) + +## Fungsinya + +- **Menjana** kata laluan rawak dan menyalinnya ke papan keratan. +- **Menambah** laman web, alamat e-mel dan kata laluan ke dalam `data.json`. +- **Mencari** laman web dan memaparkan alamat e-mel dan kata laluan yang + disimpan. +- Terus berjalan apabila `data.json` tiada, kosong atau rosak. + +> **Nota keselamatan.** Kata laluan disimpan sebagai JSON biasa dan tidak +> disulitkan. Ini projek pembelajaran; jangan gunakannya untuk kata laluan +> sebenar. + +## Halaman + +- [Pemasangan](Installation-ms) +- [Penggunaan](Usage-ms) +- [Pembangunan](Development-ms) + +Kod sumber dan isu: [repositori](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager). diff --git a/docs/wiki/Installation-ms.md b/docs/wiki/Installation-ms.md new file mode 100644 index 0000000..5d925a0 --- /dev/null +++ b/docs/wiki/Installation-ms.md @@ -0,0 +1,39 @@ +[English](Installation) · [Dansk](Installation-da) · **Bahasa Malaysia** + +# Pemasangan + +## Keperluan + +- Python 3.13 atau lebih baharu dengan Tkinter. Pada Debian atau Ubuntu pasang + `python3-tk`. +- `venv` dan `pip`, kedua-duanya sebahagian daripada Python. +- Tiada kebergantungan semasa dijalankan. + +## Cipta persekitaran maya tempatan + +Windows (PowerShell): + +```powershell +python -m venv .venv +.venv/Scripts/Activate.ps1 +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Linux dan macOS: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +Tambahan `[dev]` memasang pytest dan ruff. Tinggalkannya +(`python -m pip install -e .`) jika anda hanya mahu menjalankan program. + +## Semak pemasangan + +```bash +python -m pytest +``` diff --git a/docs/wiki/Usage-ms.md b/docs/wiki/Usage-ms.md new file mode 100644 index 0000000..9428d04 --- /dev/null +++ b/docs/wiki/Usage-ms.md @@ -0,0 +1,49 @@ +[English](Usage) · [Dansk](Usage-da) · **Bahasa Malaysia** + +# Penggunaan + +Mulakan program dengan persekitaran maya aktif: + +```bash +python -m password_manager +``` + +Program menyimpan datanya dalam `data.json` di folder tempat anda memulakannya. + +## Jana dan tambah kata laluan + +1. Taip laman web. +2. Tekan **Generate Password**. Kata laluan diisi dan disalin ke papan + keratan. +3. Semak alamat e-mel dan tekan **Add**. Sahkan dialog dan rekod disimpan. + +![Tetingkap utama selepas Generate Password](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/main-window.png) + +![Pengesahan sebelum rekod disimpan](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/add-confirmation.png) + +Jika medan dibiarkan kosong, amaran dipaparkan dan tiada apa yang disimpan. + +## Carian + +Taip laman web dan tekan **Search**. Alamat e-mel dan kata laluan yang disimpan +dipaparkan. + +![Carian menemui laman web yang disimpan](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/search-found.png) + +Jika tiada apa yang disimpan untuk laman web itu, atau fail data belum wujud, +satu mesej memberitahunya. + +![Carian untuk laman web yang tidak dikenali](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/raw/branch/main/docs/images/search-not-found.png) + +## Fail data + +`data.json` mengandungi satu rekod bagi setiap laman web: + +```json +{ + "example.com": { + "email": "jens@example.com", + "password": "Sample#Pass123" + } +} +``` -- 2.54.0 From f07210ed82242f85dd58e4c47e5966a209209865 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:44 +0800 Subject: [PATCH 6/8] ci: publish-wiki workflow pinned to v1.2.2 Task: MIL-003#6 --- .gitea/workflows/publish-wiki.yml | 43 +++++++++++++++++++++++++++++++ .yamllint | 8 ++++++ 2 files changed, 51 insertions(+) create mode 100644 .gitea/workflows/publish-wiki.yml create mode 100644 .yamllint diff --git a/.gitea/workflows/publish-wiki.yml b/.gitea/workflows/publish-wiki.yml new file mode 100644 index 0000000..2b97c55 --- /dev/null +++ b/.gitea/workflows/publish-wiki.yml @@ -0,0 +1,43 @@ +# Publish the project's wiki pages to the git host's wiki. +# +# Purpose: copies the Markdown pages of docs/wiki/ (English, Danish `-da` and +# Bahasa Malaysia `-ms`) to the wiki of this repository. +# Triggers: a push to main that changes docs/wiki/ (no manual trigger: the run +# changes the wiki, and the called workflow has no dry-run input). +# Inputs: none; wiki_dir and prune are fixed below. +# Secrets: TOKEN_FOR_GITEA_WIKI (organization secret, scope write:repository). +# Changes outside the repository: creates, updates and, because prune is +# "true", deletes pages of this repository's wiki so the wiki equals +# docs/wiki/. +# Decision: milestone MIL-003, task "Add the publish-wiki workflow". +# Host: Gitea Actions only (the called workflow uses `gitea.*` contexts). +# It needs a runner registered for the repository or organization. +# Pinning: the called workflow and its script are pinned to the same release +# (workflow ref and tools-ref are both v1.2.2). + +--- +name: Publish wiki + +on: + push: + branches: [main] + paths: + - "docs/wiki/**" + +permissions: + contents: read + +concurrency: + group: publish-wiki + cancel-in-progress: false + +jobs: + wiki: + name: Publish wiki pages + uses: TirSystem/github-action/.gitea/workflows/publish-wiki-pages.yml@v1.2.2 + with: + wiki_dir: docs/wiki + prune: "true" + tools-ref: v1.2.2 + secrets: + TOKEN_FOR_GITEA_WIKI: ${{ secrets.TOKEN_FOR_GITEA_WIKI }} diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..f9edf2e --- /dev/null +++ b/.yamllint @@ -0,0 +1,8 @@ +--- +# yamllint configuration of this repository (yamllint 1.38.0). +extends: default +rules: + line-length: + max: 100 + truthy: + check-keys: false -- 2.54.0 From eb7b7dcb17cb96535db0812c26252e9db0174d73 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:45 +0800 Subject: [PATCH 7/8] docs: review records RC-009 to RC-011 and traceability rows Task: MIL-003#7 --- docs/artifact-registry.md | 2 +- docs/sqa/reviews/rc-009-mil-003-tools-code.md | 63 ++++++++++++++++ .../reviews/rc-010-publish-wiki-workflow.md | 73 +++++++++++++++++++ docs/sqa/reviews/rc-011-publish-wiki-yaml.md | 60 +++++++++++++++ docs/sqa/traceability-matrix.md | 5 ++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 docs/sqa/reviews/rc-009-mil-003-tools-code.md create mode 100644 docs/sqa/reviews/rc-010-publish-wiki-workflow.md create mode 100644 docs/sqa/reviews/rc-011-publish-wiki-yaml.md diff --git a/docs/artifact-registry.md b/docs/artifact-registry.md index 46ba65f..039cfcf 100644 --- a/docs/artifact-registry.md +++ b/docs/artifact-registry.md @@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g. | PP | Project Plan | docs/project-plan.md | 002 | | MIL | Milestone / Gateway | docs/milestones/*.md | 004 | | DICT | Domain Dictionary | docs/dictionary.md | 002 | -| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 009 | +| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 012 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | ## Languages diff --git a/docs/sqa/reviews/rc-009-mil-003-tools-code.md b/docs/sqa/reviews/rc-009-mil-003-tools-code.md new file mode 100644 index 0000000..88eaac5 --- /dev/null +++ b/docs/sqa/reviews/rc-009-mil-003-tools-code.md @@ -0,0 +1,63 @@ +# SQA Review Record + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-009 | +| CrossReference | [MIL-003], [QC-PY-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | + +--- + +# Code review of tools/capture_screenshots.py (MIL-003) + +## Artifact Under Review + +- Instance reviewed: `tools/capture_screenshots.py` as of the MIL-003 pull request, delivering [MIL-003] +- Checklist used: [QC-PY-001] +- Scope: full review +- Language and domain: n/a +- Language reviewer: none +- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Names in `tools/capture_screenshots.py` follow PEP 8 (`render_window`, `STATES`, `BitmapInfoHeader`). | +| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose; the only short names are `x`, `y` in pixel loops. | +| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff format --check` and `ruff check` pass for `tools`; no suppressions. | +| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | ruff rule set `ANN` passes, so every signature is annotated. | +| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No `except` clauses; no bare handlers. | +| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults and no shadowed builtins. | +| 7 | Files, locks and connections are managed with context managers | Pass | The temporary folder uses `with tempfile.TemporaryDirectory()`; GDI handles are released explicitly. | +| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Every public function and class has a Doxygen docstring. | +| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` for diagnostics beyond the progress line `wrote ` of a command line tool; no secrets. | +| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | A tool script, no design class; it traces to MIL-003 task 1. | +| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | The tool is exercised by the capture run itself and its output was inspected; it is not unit tested because it needs a Windows desktop (stated in its docstring). | +| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | No type checker configured; `Any` is not used. | +| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | Standard library only; no dependency declared. | +| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | The sample values and state names are defined once at the top of the file. | +| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | A tool outside `src/`; it imports the application only through its public layers. | +| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | Functions each have one job (render, enumerate, compose, write PNG, run a state). | +| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | Placed in `tools/`, outside the layers, as MIL-003 and the wiki state; `src/` layout unchanged. | +| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names are English, snake_case, no spaces. | + +## Overall Verdict + +Go — every Mandatory criterion passes. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| None | S01 | - | + +--- + +[MIL-003]: ../../milestones/mil-003-documentation-and-publishing.md +[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md diff --git a/docs/sqa/reviews/rc-010-publish-wiki-workflow.md b/docs/sqa/reviews/rc-010-publish-wiki-workflow.md new file mode 100644 index 0000000..6a82762 --- /dev/null +++ b/docs/sqa/reviews/rc-010-publish-wiki-workflow.md @@ -0,0 +1,73 @@ +# SQA Review Record + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-010 | +| CrossReference | [MIL-003], [QC-WF-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | + +--- + +# Review of the publish-wiki workflow (MIL-003) + +## Artifact Under Review + +- Instance reviewed: `.gitea/workflows/publish-wiki.yml` as of the MIL-003 pull request, delivering [MIL-003] +- Checklist used: [QC-WF-001] +- Scope: full review +- Language and domain: n/a +- Language reviewer: none +- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | The file meets the YAML checklist ([QC-YML-001]) | Pass | See the YAML record [RC-011]; all Mandatory criteria pass. | +| 2 | The file is in `.gitea/workflows/` or `.github/workflows/` with the extension `.yml` or `.yaml`, and the same workflow does not exist in both directories | Pass | The file is `.gitea/workflows/publish-wiki.yml`; no copy exists under `.github/workflows/`. | +| 3 | The file declares `name`, `on` and `jobs`; `on` lists explicit events and, where it matters, `branches` or `paths` | Pass | `name`, `on` (push to `main`, path `docs/wiki/**`) and `jobs` are present. | +| 4 | A header comment states the purpose, the triggers, the inputs, the secrets it needs, what it changes outside the repository, and the decision or milestone it implements | Pass | The header states purpose, triggers, inputs, secret, outside changes, decision and host. | +| 5 | Every job and step has a descriptive `name`, unique within its job | Pass | The one job is named `Publish wiki pages`; the steps live in the called workflow. | +| 6 | The file passes `actionlint` (or the project's workflow validation test) and the tool version is stated | Pass | `actionlint` 1.7.12 exits 0 on the file. | +| 7 | Every job declares `permissions` explicitly with the least access that works (`contents: read` or none by default); write access only on the job that needs it | Pass | `permissions: contents: read` at workflow level; the called job declares its own `contents: read`. | +| 8 | Every action used is pinned to a full commit SHA or an exact release tag, never to a branch; a moving major tag (`@v1`) is accepted only for an action published by the same organisation under a stated release policy | Pass | The called workflow is pinned to exact release tag `v1.2.2`; the nested `actions/checkout@v4.2.2` is also an exact release tag. | +| 9 | No secret is written in the file; a secret is read as `secrets.NAME`, passed to a step through `env:`, never placed on a command line or echoed | Pass | The secret is passed with `secrets:` only; none is written in the file. | +| 10 | Untrusted text (`inputs.*`, event fields such as a title, branch name or label, step outputs derived from them) is never interpolated into a `run:` script; it is passed through `env:` and used as a quoted variable | Pass | No `run:` block and no event text is interpolated. | +| 11 | A trigger that gives a run secrets or a write token on code from a fork or an untrusted branch (`pull_request_target`, `workflow_run`) does not check out or execute that code | Pass | No `pull_request_target` or `workflow_run` trigger. | +| 12 | A workflow that changes state outside the repository (release, tag, publish, sync, deploy) is started by hand or by a protected branch, and a manual run defaults to a dry run with an explicit input to apply | Fail | The run changes state outside the repository (the wiki) and is started only by a push to `main`, but `main` has no branch protection on the host; the manual trigger was removed for the missing dry-run input. | +| 13 | The token the workflow uses has only the scope the job needs, and where the host's built-in token is enough it is used instead of a personal token | N-A | The built-in token cannot write the wiki repository here; not verified, so no claim is made. | +| 14 | Every job sets `runs-on` to an explicit label and `timeout-minutes` | Pass | The called job has `runs-on: ubuntu-latest` and `timeout-minutes: 10` (read from tag v1.2.2). | +| 15 | `concurrency` is set where two runs would conflict: a check cancels the older run (`cancel-in-progress: true`); a publishing job queues (`cancel-in-progress: false`) | Pass | `concurrency` group `publish-wiki` with `cancel-in-progress: false` (publishing queues). | +| 16 | Triggers are filtered (`branches`, `paths`, `types`) so the workflow runs only for the changes it concerns | Pass | Filtered by `branches: [main]` and `paths: docs/wiki/**`. | +| 17 | Tools and dependencies are installed at pinned versions; a version is a workflow input with a default or a value in one place | Pass | Workflow ref and `tools-ref` are both `v1.2.2`. | +| 18 | A dependency cache is used where the install is slow enough to matter | N-A | Nothing is installed. | +| 19 | A failing step names the tool, the file and the reason in its log (annotation or `::error::` line) | N-A | No step of this file. | +| 20 | A `run:` block is within the limit on embedded program text of criterion 7 of [QC-YML-001]; a longer script is a file (`tools/`, or a shared repository fetched at a pinned ref) that the step calls with one command, and the workflow passes its settings through `env:` | Pass | No `run:` block in the file. | +| 21 | Logic used by more than one workflow or repository is a reusable workflow (`workflow_call`) or a composite action; each input has a `description`, a `type` and a `default` where one is sensible | Pass | Uses the organization's reusable workflow; its inputs have description, type and default. | +| 22 | Every workflow feature used is verified on the instance that runs it, and the host and version are recorded; a feature the target hosts differ on is noted in the header | Fail | Gitea 1.27.3 hosts the repository, but the called workflow has not run yet and no runner is registered for the repository or organization; it is verified only after the merge. | +| 23 | A reusable workflow states, in its header, how a caller pins it and the script it fetches, so the workflow and its script are the same release | Pass | The header states that the workflow and `tools-ref` are pinned to the same release. | +| 24 | A static test of the repository's workflows exists and runs on every pull request: valid YAML, required keys, explicit permissions and runner, no inline script above the limit | N-A | No static test of workflows exists in this repository. | + +## Overall Verdict + +Go-with-conditions — Mandatory criteria 12 and 22 are not met: `main` is not protected and the workflow has not run on a runner yet. Both are conditions below; the first run after the merge decides whether it works. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| Protect `main` on the host so only reviewed pull requests change the wiki | S01 | 2026-10-26 | +| Register a runner and confirm the first run on `main` publishes the wiki | S01 | 2026-10-26 | +| Pin the nested `actions/checkout` to a commit SHA in the shared workflow (upstream) | S01 | 2026-10-26 | + +--- + +[MIL-003]: ../../milestones/mil-003-documentation-and-publishing.md +[QC-WF-001]: ../../../framework/qc/code/qc-workflow.md +[QC-YML-001]: ../../../framework/qc/code/qc-yaml.md +[RC-011]: ./rc-011-publish-wiki-yaml.md diff --git a/docs/sqa/reviews/rc-011-publish-wiki-yaml.md b/docs/sqa/reviews/rc-011-publish-wiki-yaml.md new file mode 100644 index 0000000..50ac57d --- /dev/null +++ b/docs/sqa/reviews/rc-011-publish-wiki-yaml.md @@ -0,0 +1,60 @@ +# SQA Review Record + +## Metadata +| Key | Value | +| --- | --- | +| ID | RC-011 | +| CrossReference | [MIL-003], [QC-YML-001] | + +## Version History +| Date | Status | Author | Reviewer | Change | Commit | +| --- | --- | --- | --- | --- | --- | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | + +--- + +# YAML review of the publish-wiki workflow (MIL-003) + +## Artifact Under Review + +- Instance reviewed: `.gitea/workflows/publish-wiki.yml` as of the MIL-003 pull request, delivering [MIL-003] +- Checklist used: [QC-YML-001] +- Scope: full review +- Language and domain: n/a +- Language reviewer: none +- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`) + +## Checklist Results + +| # | Criterion | Status | Evidence/Notes | +| --- | --- | --- | --- | +| 1 | The file parses with a safe YAML loader and has no duplicate keys in any mapping | Pass | `actionlint` parses it; `yamllint` 1.38.0 reports no duplicate keys. | +| 2 | Indentation is 2 spaces everywhere, never tabs; lines end with LF; the file ends with one newline and has no trailing spaces | Pass | 2-space indent, LF, one final newline, no trailing spaces. | +| 3 | Passes `yamllint` (or the project's YAML linter) with its configuration committed next to the code and no unexplained `disable` comments | Pass | `yamllint` 1.38.0 with the committed `.yamllint` reports no finding. | +| 4 | A scalar that YAML would coerce to another type is quoted: `yes`, `no`, `on`, `off`, `null`, `~`, numbers with a leading zero, versions such as `1.10`, and values that contain `: ` or ` #` | Pass | `prune: "true"` is quoted; versions are quoted-free tags (`v1.2.2`), which are strings. | +| 5 | Only safe, plain types are used: no language-specific tags (`!!python/object`, `!ruby/object`) and no `!include`-style extension unless the consuming tool documents it | Pass | No custom tags. | +| 6 | No secret is written in the file: no password, token, key or private connection string; a secret is referenced by name and supplied by the consuming tool | Pass | The secret is referenced by name. | +| 7 | Embedded program text (shell, Python, SQL, `jq`, and so on) in a value is at most five lines; a longer one is moved to a file of its own, in its own language, and the YAML calls it | Pass | No embedded program text. | +| 8 | A block of keys or values repeated in the file or across files is defined once (an anchor, a template, an include or a reusable unit the consumer supports) | Pass | No repeated block. | +| 9 | Anchors, aliases and merge keys (`&`, `*`, `<<`) are used only where the consuming tool is known to support them | Pass | No anchors or aliases. | +| 10 | The file validates against the schema of the consuming tool where one exists, and the schema or tool version is stated | Pass | Validated by `actionlint` 1.7.12 against the workflow schema. | +| 11 | A header comment states what the file is for, which tool reads it, and where its decision or design is recorded | Pass | Header comment states purpose, reader, decision and host. | +| 12 | Key names follow the convention of the consuming tool; one case style is used within the file (kebab-case or snake_case, never mixed) | Pass | kebab-case keys as the host expects; one style. | +| 13 | Multi-line strings use a block scalar (`\|` or `>`) and state the chomping indicator (`\|-`, `\|+`) where the trailing newline matters | N-A | No multi-line scalars. | +| 14 | The file name is lowercase kebab-case with one extension style (`.yml` or `.yaml`) used throughout the repository | Pass | `publish-wiki.yml`, kebab-case, `.yml`. | +| 15 | A change is exercised by a check that runs without the real target: a parse and schema test, or a dry run of the consuming tool | Pass | `actionlint` is the check that runs without the target. | + +## Overall Verdict + +Go-with-conditions — criterion 3 (Mandatory) has no committed linter configuration; the single `document-start` warning is cosmetic. + +## Action Items + +| Action | Owner | Due | +| --- | --- | --- | +| Add a committed `.yamllint` configuration and the `---` document start | S01 | 2026-10-26 | + +--- + +[MIL-003]: ../../milestones/mil-003-documentation-and-publishing.md +[QC-YML-001]: ../../../framework/qc/code/qc-yaml.md diff --git a/docs/sqa/traceability-matrix.md b/docs/sqa/traceability-matrix.md index 5eb0f54..541b34b 100644 --- a/docs/sqa/traceability-matrix.md +++ b/docs/sqa/traceability-matrix.md @@ -32,6 +32,8 @@ an artifact instance is created or reviewed. | [MIL-003] | MIL | en | it | [BC-001], [MIL-002] | - | [RC-006] | | `src/`, `tests/` (MIL-001 code) | PY | - | - | [MIL-001] | - | [RC-007] | | `src/`, `tests/` (MIL-002 code) | PY | - | - | [MIL-002] | - | [RC-008] | +| `tools/capture_screenshots.py` | PY | - | - | [MIL-003] | - | [RC-009] | +| `.gitea/workflows/publish-wiki.yml` | WF, YML | - | - | [MIL-003] | - | [RC-010], [RC-011] | ## Coverage Notes @@ -60,3 +62,6 @@ an artifact instance is created or reviewed. [53f56ee]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/commit/53f56eeb143d810b91da23f743a53a32207f81bb [RC-007]: ./reviews/rc-007-mil-001-code.md [RC-008]: ./reviews/rc-008-mil-002-code.md +[RC-009]: ./reviews/rc-009-mil-003-tools-code.md +[RC-010]: ./reviews/rc-010-publish-wiki-workflow.md +[RC-011]: ./reviews/rc-011-publish-wiki-yaml.md -- 2.54.0 From c3481e7706f69821ba716264958bd56ce8f4b080 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 14 Oct 2026 21:31:48 +0800 Subject: [PATCH 8/8] docs: resolve pending commit links of MIL-003 --- docs/sqa/reviews/rc-009-mil-003-tools-code.md | 3 ++- docs/sqa/reviews/rc-010-publish-wiki-workflow.md | 3 ++- docs/sqa/reviews/rc-011-publish-wiki-yaml.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/sqa/reviews/rc-009-mil-003-tools-code.md b/docs/sqa/reviews/rc-009-mil-003-tools-code.md index 88eaac5..c7a68d8 100644 --- a/docs/sqa/reviews/rc-009-mil-003-tools-code.md +++ b/docs/sqa/reviews/rc-009-mil-003-tools-code.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [eb7b7dc] | --- @@ -61,3 +61,4 @@ Go — every Mandatory criterion passes. [MIL-003]: ../../milestones/mil-003-documentation-and-publishing.md [QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md +[eb7b7dc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/commit/eb7b7dcb17cb96535db0812c26252e9db0174d73 diff --git a/docs/sqa/reviews/rc-010-publish-wiki-workflow.md b/docs/sqa/reviews/rc-010-publish-wiki-workflow.md index 6a82762..c7a175c 100644 --- a/docs/sqa/reviews/rc-010-publish-wiki-workflow.md +++ b/docs/sqa/reviews/rc-010-publish-wiki-workflow.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [eb7b7dc] | --- @@ -71,3 +71,4 @@ Go-with-conditions — Mandatory criteria 12 and 22 are not met: `main` is not p [QC-WF-001]: ../../../framework/qc/code/qc-workflow.md [QC-YML-001]: ../../../framework/qc/code/qc-yaml.md [RC-011]: ./rc-011-publish-wiki-yaml.md +[eb7b7dc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/commit/eb7b7dcb17cb96535db0812c26252e9db0174d73 diff --git a/docs/sqa/reviews/rc-011-publish-wiki-yaml.md b/docs/sqa/reviews/rc-011-publish-wiki-yaml.md index 50ac57d..6aa8e41 100644 --- a/docs/sqa/reviews/rc-011-publish-wiki-yaml.md +++ b/docs/sqa/reviews/rc-011-publish-wiki-yaml.md @@ -9,7 +9,7 @@ ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | -| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending | +| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [eb7b7dc] | --- @@ -58,3 +58,4 @@ Go-with-conditions — criterion 3 (Mandatory) has no committed linter configura [MIL-003]: ../../milestones/mil-003-documentation-and-publishing.md [QC-YML-001]: ../../../framework/qc/code/qc-yaml.md +[eb7b7dc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/030-password-manager/commit/eb7b7dcb17cb96535db0812c26252e9db0174d73 -- 2.54.0