Compare commits

...
31 Commits
Author SHA1 Message Date
Tirsvad e0d990a98d Merge pull request 'MIL-012: the tag v0.2.0 is annotated (RC-081)' (#109) from mil-012-tag-annotated into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 11s
2026-10-15 04:19:56 +02:00
Tirsvad 49c316f5dd Resolve pending commit link of RC-081 2026-10-15 10:19:46 +08:00
Tirsvad 8f135b94bf MIL-012: the tag v0.2.0 is annotated
At the Product Owner's request the lightweight tag on the host was replaced
by an annotated tag on the same commit. Review record RC-081 (Go).

Task: MIL-012#6
2026-10-15 10:19:45 +08:00
Tirsvad 6f7fb534d7 Merge pull request 'MIL-013: Windows Sandbox run passed (RC-080)' (#108) from mil-013-sandbox-results into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
2026-10-15 04:18:02 +02:00
Tirsvad da4a16c572 Resolve pending commit link of RC-080 2026-10-15 10:17:53 +08:00
Tirsvad 6e3fcf8aa3 MIL-013: Windows Sandbox run of the executable passed
The Windows executable passed the smoke test in Windows Sandbox: 5 of 5
runs (window, list override, known button, progress file). Review record
RC-080 (Go) closes criterion 7 of MIL-013 and the open condition of RC-072
on the smoke test.

Task: MIL-013#8
2026-10-15 10:17:52 +08:00
Tirsvad a4e55541c1 Merge pull request 'MIL-013: Debian container and Windows Sandbox checks' (#107) from mil-013-container-checks into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-15 04:10:24 +02:00
Tirsvad 1785dc9635 Resolve pending commit links of the MIL-013 results 2026-10-15 10:10:11 +08:00
Tirsvad 2f192b6936 MIL-013: results of the container and sandbox checks
Review record RC-079 (Go-with-conditions): the Debian checks and the Linux
smoke tests pass; the Windows Sandbox run is pending because it opens a
window on the host.

Task: MIL-013#8
2026-10-15 10:10:10 +08:00
Tirsvad f8ccfca02e MIL-013: Debian container and Windows Sandbox checks
A Debian image (python:3.13-slim-bookworm with Xvfb and GNOME's window
manager) runs the linters, the whole test suite, the screenshots and the
smoke test of a PyInstaller Linux binary and of the source program, so no
window opens on the development machine. The screenshot tool works on
Linux, a smoke driver presses the known button with the virtual mouse, and
Windows Sandbox scripts are written (not run, they open a window on the
host). README and the wiki Install pages describe it. Review records
RC-077 (Python) and RC-078 (shell).

Task: MIL-013#2
Task: MIL-013#3
Task: MIL-013#4
Task: MIL-013#5
Task: MIL-013#6
Task: MIL-013#7
Task: MIL-013#9
Task: MIL-013#10
2026-10-15 10:09:48 +08:00
Tirsvad a72ddb9175 Merge pull request 'MIL-013 plan and documents: container and sandbox checks' (#96) from plan-mil-013-container into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-15 03:55:36 +02:00
Tirsvad bc4395df43 Resolve pending commit links of the MIL-013 plan 2026-10-15 09:55:23 +08:00
Tirsvad f86bc9136e MIL-013 plan and documents: container and sandbox checks
Milestone MIL-013 (Debian container with GNOME, Windows Sandbox for the
executable), ADR-0005, BC-001 and PP-001 updated. Review records RC-073 to
RC-076, all Go (autonomous mode, not independent).

Task: MIL-013#1
2026-10-15 09:55:21 +08:00
Tirsvad 99858fb13b Merge pull request 'MIL-012 results: release v0.2.0' (#95) from mil-012-results into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-15 03:49:23 +02:00
Tirsvad c1a92ce34f Resolve pending commit link of RC-072 2026-10-15 09:48:58 +08:00
Tirsvad 5345fd77d4 MIL-012: release v0.2.0 results
The tag v0.2.0 and the release page exist (lightweight tag, no executable
attached: the host refuses the upload). Review record RC-072
(Go-with-conditions): the executable smoke test is incomplete and the tag
is not annotated.

Task: MIL-012#5
Task: MIL-012#6
Task: MIL-012#7
2026-10-15 09:48:57 +08:00
Tirsvad d7ca24dc69 Merge pull request 'MIL-012: version 0.2.0, CHANGELOG and links' (#94) from mil-012-version-0-2-0 into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-15 03:39:35 +02:00
Tirsvad 2d94678899 Resolve pending commit link of RC-071 2026-10-15 09:39:25 +08:00
Tirsvad 7c1f1b6423 MIL-012: version 0.2.0, CHANGELOG and links
Version 0.2.0 in pyproject.toml, flash_card.__version__ and the Doxyfile
with a test that keeps the three equal; CHANGELOG entry for 0.2.0; README
and the wiki Install pages link the release pages. The upload limit probe
refused a 1 MB file (413), so the executable is not attached. Review
record RC-071 (Go).

Task: MIL-012#2
Task: MIL-012#3
Task: MIL-012#4
Task: MIL-012#8
2026-10-15 09:39:16 +08:00
Tirsvad f9ca635a15 Merge pull request 'MIL-012 plan and documents: release v0.2.0' (#85) from plan-mil-012-release-0-2-0 into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 8s
2026-10-15 03:36:10 +02:00
Tirsvad 08c11b2108 Resolve pending commit links of the MIL-012 plan 2026-10-15 09:35:54 +08:00
Tirsvad 0264b6084f MIL-012 plan and documents: release v0.2.0
Milestone MIL-012; BC-001, US-001 (US-001.08 names the latest release)
and PP-001 updated. Review records RC-067 to RC-070, all Go (autonomous
mode, not independent).

Task: MIL-012#1
2026-10-15 09:35:52 +08:00
Tirsvad f13f32052b Merge pull request 'MIL-011: Menu bar' (#84) from mil-011-menu-bar into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-15 03:07:08 +02:00
Tirsvad 6c6fd723bf Resolve pending commit links of MIL-011 2026-10-15 09:07:04 +08:00
Tirsvad ce4aacb4bc MIL-011: Menu bar
Review record RC-066 (Go, autonomous mode, not independent).

Task: MIL-011#1
Task: MIL-011#2
Task: MIL-011#3
Task: MIL-011#4
Task: MIL-011#5
Task: MIL-011#6
Task: MIL-011#7
Task: MIL-011#8
2026-10-15 09:06:47 +08:00
Tirsvad 5d626dc890 Merge pull request 'MIL-011 plan and design: menu bar' (#75) from plan-mil-011-menu-bar into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
2026-10-15 02:53:02 +02:00
Tirsvad 9693ed6c79 Resolve pending commit links of the MIL-011 plan 2026-10-15 08:52:17 +08:00
Tirsvad 2211d0f4cb MIL-011 plan and design: menu bar
Milestone MIL-011 with stories US-001.09 to US-001.12, and the documents
updated for the menu bar: BC-001, PP-001, US-001, UC-001, SSD-001, OC-001,
SD-001, DCD-001, DICT-001 and the wireframe. Review records RC-056 to
RC-065, all Go (autonomous mode, not independent).

Task: MIL-011#1
2026-10-15 08:52:13 +08:00
Tirsvad 9ab490e520 Merge pull request 'MIL-010 results: release v0.1.0, executable not attached' (#74) from mil-010-release-results into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
2026-10-15 02:43:22 +02:00
Tirsvad 60bcd2c9fb Resolve pending commit links of the MIL-010 results 2026-10-15 08:43:13 +08:00
Tirsvad db87a6d33b MIL-010: release v0.1.0 results, the executable is not attached
The tag v0.1.0 and the release exist and the executable was rebuilt and
smoke-tested from the tagged commit, but the server's reverse proxy rejects
the upload (413). README, CHANGELOG and the wiki Install pages now say that
the executable is built from source until the limit is raised. Review
record RC-055 (Go-with-conditions).

Task: MIL-010#2
Task: MIL-010#3
Task: MIL-010#4
Task: MIL-010#6
2026-10-15 08:43:12 +08:00
91 changed files with 4946 additions and 106 deletions
+15
View File
@@ -0,0 +1,15 @@
.git
.venv
.env
.env.*
.codex
**/__pycache__
**/*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.import_linter_cache
build
dist
docs/doxygen
data/words_to_learn*.csv
+4
View File
@@ -3,3 +3,7 @@
*.toml text eol=lf *.toml text eol=lf
*.csv text eol=lf *.csv text eol=lf
*.png binary *.png binary
*.sh text eol=lf
*.Dockerfile text eol=lf
.dockerignore text eol=lf
*.ps1 text eol=crlf
+31
View File
@@ -4,6 +4,35 @@ All notable changes of the flash card program. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the version follows
[Semantic Versioning](https://semver.org/). [Semantic Versioning](https://semver.org/).
## [0.2.0] - 2026-10-15
Adds the classic menu bar and keeps the progress of each word list apart.
### Added
- A menu bar. File: Open... (another word list from a CSV file with a header
of two language codes), Change level... (the level choice without a restart),
Reset word list... (after a confirmation the progress of the open list is
removed and all its words are to learn again), Exit. About: Contact (the
author's name and title, and a link to the author's LinkedIn page).
- The progress of each word list is a file of its own: `words_to_learn.csv` for
`ms-da.csv` (unchanged) and `words_to_learn_<name>.csv` for another list.
### Fixed
- A word list that is not UTF-8 text gives a message that names the file,
instead of an error.
- The level screen has the same window title, padding and background as the
card screen.
### Known limits
- The executable is not signed; Windows SmartScreen may warn about it.
- The executable is not attached to the release: the upload limit of the git
host rejects a file of its size (HTTP 413, already at 1 MB). Build it with
`python tools/build_executable.py`.
- An opened word list is not remembered after a restart.
## [0.1.0] - 2026-10-15 ## [0.1.0] - 2026-10-15
First release. The solution of day 31 of Udemy's *100 Days of Code: The Complete First release. The solution of day 31 of Udemy's *100 Days of Code: The Complete
@@ -26,8 +55,10 @@ Python Pro Bootcamp*, for Malay words with Danish translations.
### Known limits ### Known limits
- The executable is not signed; Windows SmartScreen may warn about it. - The executable is not signed; Windows SmartScreen may warn about it.
- The executable is not attached to the release: the upload limit of the server rejects a file of its size. Build it with `python tools/build_executable.py`.
- The executable is built for Windows (x64); on Linux and macOS run the program - The executable is built for Windows (x64); on Linux and macOS run the program
from source or build it on that system. from source or build it on that system.
- The Danish column of the word list is machine translated. - The Danish column of the word list is machine translated.
[0.1.0]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0 [0.1.0]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0
[0.2.0]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.2.0
+1 -1
View File
@@ -4,7 +4,7 @@
PROJECT_NAME = "Flash Card" PROJECT_NAME = "Flash Card"
PROJECT_BRIEF = "Malay to Danish flash cards (Udemy 100 Days of Code, day 31)" PROJECT_BRIEF = "Malay to Danish flash cards (Udemy 100 Days of Code, day 31)"
PROJECT_NUMBER = 0.1.0 PROJECT_NUMBER = 0.2.0
OUTPUT_DIRECTORY = docs/doxygen OUTPUT_DIRECTORY = docs/doxygen
CREATE_SUBDIRS = NO CREATE_SUBDIRS = NO
+39 -3
View File
@@ -102,11 +102,25 @@ The program is run from the activated `.venv`. The window opens with the level s
| `--level {100,1000,all}` | Start at that level and skip the level screen | | `--level {100,1000,all}` | Start at that level and skip the level screen |
| `--no-speech` | Do not speak the Malay word | | `--no-speech` | Do not speak the Malay word |
Known words stay known at every level: the level only limits which of the words you do not know yet come up. To start again, delete `data/words_to_learn.csv`. Known words stay known at every level: the level only limits which of the words you do not know yet come up. To start again, delete `data/words_to_learn.csv` or use the menu.
### Menu bar
![The window with the menu bar][screenshot-menu]
| Menu | Command | What it does |
| --- | --- | --- |
| File | Open... | Opens another word list from a CSV file (header with two language codes, for example `en;da`). Progress is kept apart for each list |
| File | Change level... | Shows the level choice again without a restart; known words stay known |
| File | Reset word list... | After a confirmation removes the progress of the open list, so all its words are to learn again; the list file is not changed |
| File | Exit | Closes the program |
| About | Contact | The author, Jens Tirsvad Nielsen, Software engineer; opens the LinkedIn page in your browser |
The progress of the delivered `ms-da.csv` is `data/words_to_learn.csv`; another list gets `data/words_to_learn_<name>.csv`. The opened list is not remembered after a restart.
### Executable (Windows, no Python needed to run it) ### Executable (Windows, no Python needed to run it)
Download `flash-card.exe` and `flash-card.exe.sha256` from the [release page][release-0-1-0] and compare the SHA-256 hash of the file with the checksum file, or build it yourself: The release pages of [v0.2.0][release-0-2-0] and [v0.1.0][release-0-1-0] have the release notes. The executable is not attached there yet, because the server's upload limit rejects a file of this size (HTTP 413). Build it yourself:
```powershell ```powershell
python -m pip install -e ".[build,speech]" python -m pip install -e ".[build,speech]"
@@ -133,6 +147,26 @@ The window test in `tests/e2e/` is skipped when no display is available.
Build the source code documentation (Doxygen comments) with `doxygen Doxyfile`; the HTML is written to `docs/doxygen/html/`. The screenshots are made with `python tools/capture_screenshots.py` after `python -m pip install -e ".[tools]"` (Pillow, tool only). Build the source code documentation (Doxygen comments) with `doxygen Doxyfile`; the HTML is written to `docs/doxygen/html/`. The screenshots are made with `python tools/capture_screenshots.py` after `python -m pip install -e ".[tools]"` (Pillow, tool only).
### Checks without a desktop (Docker)
The tests, the linters, the screenshots and a smoke test of a Linux binary run in a Debian container on a virtual screen, so no window opens on your own desktop.
```bash
docker build -f docker/debian.Dockerfile -t flash-card-debian .
# linters and the whole test suite, including the real window tests
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh run-checks.sh
# screenshots into ./build/container-out
docker run --rm -v "$PWD:/repo:ro" -v "$PWD/build/container-out:/out" flash-card-debian bash /repo/tools/docker/in-container.sh screenshots.sh /out
# build the Linux binary and press the known button 5 times (binary and source)
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh smoke-binary.sh 5
```
On Windows PowerShell write `${PWD}` for `$PWD`. The repository is mounted read-only and copied inside the container.
- The desktop is Xvfb with GNOME's window manager (mutter). GNOME Shell is tried first and does not start in a container (it needs a login manager), so the scripts fall back to mutter and print which one ran.
- The Windows executable is checked in **Windows Sandbox** (Windows 11 Pro, feature enabled; the version 0.2.0 build passed 5 of 5 runs, see RC-080): `powershell -File tools\sandbox\run.ps1 -Runs 5` after `python tools/build_executable.py`. The sandbox shows its own window while it runs; the click is made inside the sandbox.
- Not covered: macOS (it cannot be run in Docker on Windows or Linux and Apple's licence allows it only on Apple hardware), and Windows itself in the Debian container.
## 📄 License ## 📄 License
GNU Affero General Public License v3.0 or later. See [LICENSE][license]. GNU Affero General Public License v3.0 or later. See [LICENSE][license].
@@ -142,7 +176,7 @@ GNU Affero General Public License v3.0 or later. See [LICENSE][license].
- [Repository][repository] - [Repository][repository]
- [Documentation][documentation] - [Documentation][documentation]
- [Issue tracker][issues] - [Issue tracker][issues]
- [Releases][releases] (the Windows executable `flash-card.exe` and its checksum) - [Releases][releases] (v0.2.0 and v0.1.0: release notes; the executable is built from source, see Run)
## 📎 Appendix: Project documents ## 📎 Appendix: Project documents
@@ -166,6 +200,7 @@ The documents a newcomer needs, in reading order.
[license]: ./LICENSE [license]: ./LICENSE
[screenshot-level]: ./docs/images/screenshot-level.png [screenshot-level]: ./docs/images/screenshot-level.png
[screenshot-front]: ./docs/images/screenshot-front.png [screenshot-front]: ./docs/images/screenshot-front.png
[screenshot-menu]: ./docs/images/screenshot-menu.png
[screenshot-back]: ./docs/images/screenshot-back.png [screenshot-back]: ./docs/images/screenshot-back.png
[wiki-en]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home [wiki-en]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home
[wiki-da]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home-da [wiki-da]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home-da
@@ -176,6 +211,7 @@ The documents a newcomer needs, in reading order.
[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/issues [issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/issues
[releases]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases [releases]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases
[release-0-1-0]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0 [release-0-1-0]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0
[release-0-2-0]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.2.0
[agents]: ./AGENTS.md [agents]: ./AGENTS.md
[BC-001]: ./docs/business-case.md [BC-001]: ./docs/business-case.md
[SA-001]: ./docs/stakeholder-analysis.md [SA-001]: ./docs/stakeholder-analysis.md
+45
View File
@@ -0,0 +1,45 @@
# Debian based image for the flash card checks (ADR-0005).
#
# Build (from the repository root):
# docker build -f docker/debian.Dockerfile -t flash-card-debian .
# Run the linters and the tests on a virtual desktop:
# docker run --rm -v "$PWD:/app" flash-card-debian
#
# The base image is the official Python image (Debian bookworm), because the
# program needs Python 3.13. The desktop is a virtual X screen (Xvfb) with
# GNOME Shell when it can start in a container, else GNOME's window manager.
FROM python:3.13-slim-bookworm
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
DISPLAY=:99
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
xvfb xauth x11-utils x11-xserver-utils xdotool dbus dbus-x11 \
procps ca-certificates \
libgl1-mesa-dri libegl1 libgles2 \
mutter gnome-session gnome-shell gnome-settings-daemon \
adwaita-icon-theme gsettings-desktop-schemas \
tk fonts-liberation fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
# PyInstaller on Linux needs objdump (binutils).
RUN apt-get update \
&& apt-get install -y --no-install-recommends binutils \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --shell /bin/bash flash
WORKDIR /app
# Dependencies first, so a change of the sources does not rebuild this layer.
COPY pyproject.toml README.md LICENSE ./
COPY src ./src
RUN python -m pip install --upgrade pip \
&& python -m pip install -e ".[dev,speech,tools,build]" \
&& chown -R flash:flash /app
USER flash
ENV PATH="/app/tools/docker:${PATH}"
CMD ["bash", "tools/docker/run-checks.sh"]
@@ -0,0 +1,55 @@
# ADR-0005: Container and sandbox checks without the user's desktop
## Metadata
| Key | Value |
| --- | --- |
| ID | ADR-0005 |
| CrossReference | [BC-001], [ADR-0004] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f86bc91] |
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
---
## Context
Smoke tests of the program drive a real window. Run on the Product Owner's own Windows desktop they open windows, take the focus and move the mouse, which interrupts the Product Owner's work (RC-072). The checks must run somewhere else. Options:
1. A Windows hidden desktop (a second Win32 desktop of the same session): invisible, but still the host's session and a place where a mistake can reach the host.
2. A Debian Docker container with a virtual X screen and GNOME for the Linux checks, and Windows Sandbox for the Windows executable: both are isolated from the host's input and desktop; the Debian check is not a Windows check.
3. A Wine container for the Windows executable, or a real Windows virtual machine run in Docker with KVM: Wine is an emulation of the Windows interface; the Windows virtual machine needs a Microsoft evaluation image of tens of gigabytes and licence terms the Product Owner has not accepted.
4. macOS in Docker: not possible on a Windows host, and Apple's licence allows macOS only on Apple hardware.
## Decision
Run the Linux checks (linters, tests including the real window, screenshots, a Linux binary and its smoke test) in a Debian image based on `python:3.13-slim-bookworm` with Xvfb, GNOME's window manager and, if it runs in a container, the GNOME session. Run the Windows executable in Windows Sandbox of Windows 11 Pro through a `.wsb` configuration and a script that presses the known button inside the sandbox and writes the result to a mapped folder. Do not run macOS, and state that in the documents.
## Consequences
**Positive:**
- No check takes the focus or the mouse of the host any more.
- The container checks are repeatable on any machine with Docker and need no Python installation.
- The sandbox is a real Windows, thrown away after each run.
**Negative:**
- The Debian image is large (a desktop and a Python image) and the first build takes time.
- A full GNOME session may not run in a container; the fallback (GNOME's window manager) is a weaker statement, and the record says which one ran.
- Windows Sandbox needs Windows 11 Pro with the feature enabled and shows its own window on the host while it runs.
- macOS stays untested.
## Affected Artifacts
- [BC-001] — In Scope and Out of Scope lists
- [ADR-0004] — the executable that the sandbox runs
---
[BC-001]: ../business-case.md
[ADR-0004]: ./adr-0004-pyinstaller-executable.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+3 -3
View File
@@ -14,7 +14,7 @@ document of a type. `Primary File` may contain a glob (e.g.
| BC | Business Case | docs/business-case.md | 002 | | BC | Business Case | docs/business-case.md | 002 |
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 011 | | MIL | Milestone / Gateway | docs/milestones/*.md | 014 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| US | User Story | docs/user-stories.md | 002 | | US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-*/uc.md | 002 | | UC | Use Case | docs/uc-*/uc.md | 002 |
@@ -24,8 +24,8 @@ document of a type. `Primary File` may contain a glob (e.g.
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 | | SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
| DCD | Design Class Diagram | docs/dcd.md | 002 | | DCD | Design Class Diagram | docs/dcd.md | 002 |
| DICT | Domain Dictionary | docs/dictionary.md | 002 | | DICT | Domain Dictionary | docs/dictionary.md | 002 |
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0005 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0006 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 055 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 082 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
## Languages ## Languages
+8 -5
View File
@@ -11,8 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope: executable built with PyInstaller (new request of S01) | [768ca34] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope: tagged releases v0.1.0 and v0.2.0 (new request of S01) | [0264b60] |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope: release of the executable (new request of S01) | [9c8e294] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope: container and sandbox checks (new request of S01) | [f86bc91] |
--- ---
@@ -54,8 +54,10 @@ A small, well-structured reference solution gives the Product Owner a language l
- Saving the words still to learn to `data/words_to_learn.csv` and using that file on the next start. - Saving the words still to learn to `data/words_to_learn.csv` and using that file on the next start.
- Optional spoken Malay word (text to speech) as an extra that can be installed. - Optional spoken Malay word (text to speech) as an extra that can be installed.
- A choice of level (the first 100 words, the first 1000 words or all words of the frequency ordered word list) before learning starts. - A choice of level (the first 100 words, the first 1000 words or all words of the frequency ordered word list) before learning starts.
- A menu bar with the commands to open another word list (CSV), change the level, reset a word list, exit, and the author's contact.
- Checks that do not use the user's desktop: a Debian based Docker image with a virtual GNOME desktop for the tests, headless screenshots and a Linux binary, and Windows Sandbox for the Windows executable.
- A single Windows executable file, built with PyInstaller by a script of this repository, that runs without an installed Python. - A single Windows executable file, built with PyInstaller by a script of this repository, that runs without an installed Python.
- A tagged release `v0.1.0` of the repository with the executable, its SHA-256 checksum and release notes attached. - Tagged releases of the repository (`v0.1.0`, `v0.2.0`), each with release notes and, when the git host accepts the upload, the executable and its SHA-256 checksum attached.
- `pyproject.toml`, `.gitignore`, `Doxyfile`, virtual environment instructions, `pytest` tests and `constants.py`. - `pyproject.toml`, `.gitignore`, `Doxyfile`, virtual environment instructions, `pytest` tests and `constants.py`.
- README from the framework template, screenshots, and a wiki in English, Danish and Bahasa Malaysia. - README from the framework template, screenshots, and a wiki in English, Danish and Bahasa Malaysia.
- Repository description and topics on the git host. - Repository description and topics on the git host.
@@ -65,6 +67,7 @@ A small, well-structured reference solution gives the Product Owner a language l
- Other languages: the user interface labels are Malay and Danish only; another word list works by changing the data file but is not delivered. - Other languages: the user interface labels are Malay and Danish only; another word list works by changing the data file but is not delivered.
- Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps. - Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps.
- Running macOS or a Windows virtual machine other than Windows Sandbox in the checks (macOS is not run in Docker: Apple's licence allows it only on Apple hardware).
- Packaging for distribution on PyPI, an installer and a signed executable (the executable built with PyInstaller and its release on the git host are in scope; the file itself is not committed to the repository). - Packaging for distribution on PyPI, an installer and a signed executable (the executable built with PyInstaller and its release on the git host are in scope; the file itself is not committed to the repository).
- Redistribution of the course's own image files; the card and button images are generated for this project. - Redistribution of the course's own image files; the card and button images are generated for this project.
- A translation quality review of the word list. - A translation quality review of the word list.
@@ -150,5 +153,5 @@ Proceed — the scope is small, the risks are mitigated, and the result serves t
--- ---
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54 [0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
[9c8e294]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/9c8e2940217e615f82eda2d48aca9b2fd58f3687 [f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+43 -6
View File
@@ -9,9 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope note: runtime path rules and the executable build are packaging support, not design classes | [768ca34] |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added Level, Deck.within, the level argument and the level screen | [0b983b7] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the menu classes, Application and ProgressRepository.reset | [2211d0f] |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope note: runtime path rules and the executable build are packaging support, not design classes | [768ca34] |
--- ---
@@ -56,6 +55,7 @@ package domain {
interface ProgressRepository { interface ProgressRepository {
+load() : Deck | None +load() : Deck | None
+save(deck : Deck) : None +save(deck : Deck) : None
+reset() : None
} }
class VocabularyError class VocabularyError
class VocabularyFormatError class VocabularyFormatError
@@ -102,6 +102,29 @@ package infrastructure {
} }
} }
package presentation { package presentation {
class MenuEntry <<frozen>> {
+label : str
+command : str
}
class MenuViewModel {
+file_entries : tuple[MenuEntry, ...]
+contact_label : str
+contact_url : str
}
class MenuView {
+open_word_list() : None
+change_level() : None
+reset_word_list() : None
+show_contact() : None
+exit_program() : None
}
class Application {
+start(level : Level) : None
+open_word_list(path : Path) : None
+change_level() : None
+reset_word_list() : None
+exit_application() : None
}
class LevelChoice <<frozen>> { class LevelChoice <<frozen>> {
+label : str +label : str
+level : Level +level : Level
@@ -162,6 +185,12 @@ Main ..> CsvVocabularyRepository : composes
Main ..> CsvProgressRepository : composes Main ..> CsvProgressRepository : composes
Main ..> GttsSpeech : composes Main ..> GttsSpeech : composes
Main ..> FlashCardView : composes Main ..> FlashCardView : composes
Main ..> Application : creates
Main ..> MenuView : composes
MenuView "1" --> "1" MenuViewModel
MenuViewModel "1" *-- "5" MenuEntry
MenuView "1" --> "1" Application
Application ..> ProgressRepository : resets
@enduml @enduml
``` ```
@@ -183,6 +212,9 @@ Main ..> FlashCardView : composes
| `GttsSpeech`, `NullSpeech` | (none) | Adapters for speech: the gTTS service, and silence | none | `speak` | | `GttsSpeech`, `NullSpeech` | (none) | Adapters for speech: the gTTS service, and silence | none | `speak` |
| `FlashCardViewModel` | Study Session | Exposes the side, language name and text of the card shown, and the three commands | `_session`, `side`, `language_name`, `text` | `next_card`, `flip_card`, `is_known` | | `FlashCardViewModel` | Study Session | Exposes the side, language name and text of the card shown, and the three commands | `_session`, `side`, `language_name`, `text` | `next_card`, `flip_card`, `is_known` |
| `LevelChoice`, `LevelViewModel` | Level | Plain data and view model of the level screen: the title and the three choices with their labels | `title`, `choices` | none | | `LevelChoice`, `LevelViewModel` | Level | Plain data and view model of the level screen: the title and the three choices with their labels | `title`, `choices` | none |
| `MenuEntry`, `MenuViewModel` | (none: user interface) | Plain data and view model of the menu bar: the labels and the order of the File entries, the contact label and link | `file_entries`, `contact_label`, `contact_url` | none |
| `MenuView` | (none: user interface) | Draws the menu bar, asks the dialogs (file, confirmation, messages) and forwards the command | menus | `open_word_list`, `change_level`, `reset_word_list`, `show_contact`, `exit_program` |
| `Application` | Study Session | Holds the window, the word list and the progress in use; starts, drops and replaces sessions on level choice and menu commands | `root`, `view`, `exit_code` | `start`, `open_word_list`, `change_level`, `reset_word_list`, `exit_application` |
| `LevelView` | (none: user interface) | Draws the level screen and forwards the chosen level | buttons | `choose` | | `LevelView` | (none: user interface) | Draws the level screen and forwards the chosen level | buttons | `choose` |
| `FlashCardView` | (none: user interface) | Draws the window, forwards the buttons, owns the 3000 ms timer | `flip_timer`, widgets | `next_card`, `flip_card`, `is_known` | | `FlashCardView` | (none: user interface) | Draws the window, forwards the buttons, owns the 3000 ms timer | `flip_timer`, widgets | `next_card`, `flip_card`, `is_known` |
@@ -193,6 +225,11 @@ Main ..> FlashCardView : composes
| `open_study_session(vocabulary_repository, progress_repository, speech, choose, level) -> StudySession` | [OC-001] `chooseLevel`, [SD-001] sequence chooseLevel (loads the words to learn and the level's cards) | | `open_study_session(vocabulary_repository, progress_repository, speech, choose, level) -> StudySession` | [OC-001] `chooseLevel`, [SD-001] sequence chooseLevel (loads the words to learn and the level's cards) |
| `Level.first(cards) -> tuple[Card, ...]`, `Deck.within(cards) -> tuple[Card, ...]` | [OC-001] `chooseLevel` postcondition (the level's cards); [SD-001] | | `Level.first(cards) -> tuple[Card, ...]`, `Deck.within(cards) -> tuple[Card, ...]` | [OC-001] `chooseLevel` postcondition (the level's cards); [SD-001] |
| `LevelViewModel.choices -> tuple[LevelChoice, ...]` | [OC-001] `startStudySession`; [SD-001] sequence startStudySession | | `LevelViewModel.choices -> tuple[LevelChoice, ...]` | [OC-001] `startStudySession`; [SD-001] sequence startStudySession |
| `Application.open_word_list(path) -> None` | [OC-001] `openWordList`; [SD-001] sequence openWordList |
| `Application.change_level() -> None` | [OC-001] `changeLevel` |
| `Application.reset_word_list() -> None`, `ProgressRepository.reset() -> None` | [OC-001] `resetWordList`; [SD-001] sequence resetWordList |
| `MenuView.show_contact() -> None` | [OC-001] `showContact` |
| `Application.exit_application() -> None`, `MenuView.exit_program() -> None` | [OC-001] `exit` |
| `LevelView.choose(level) -> None` | [OC-001] `chooseLevel` (forwards the level to the composition root) | | `LevelView.choose(level) -> None` | [OC-001] `chooseLevel` (forwards the level to the composition root) |
| `StudySession.next_card() -> CardDto \| None` | [OC-001] `chooseLevel` (first card), `markUnknown`; [SD-001] | | `StudySession.next_card() -> CardDto \| None` | [OC-001] `chooseLevel` (first card), `markUnknown`; [SD-001] |
| `StudySession.mark_known() -> None` | [OC-001] `markKnown` (remove and save); [SD-001] | | `StudySession.mark_known() -> None` | [OC-001] `markKnown` (remove and save); [SD-001] |
@@ -208,8 +245,9 @@ Main ..> FlashCardView : composes
| Repository | `VocabularyRepository`, `ProgressRepository`, CSV adapters | The domain does not know the CSV file | | Repository | `VocabularyRepository`, `ProgressRepository`, CSV adapters | The domain does not know the CSV file |
| Ports and adapters | `SpeechPort`, `GttsSpeech`, `NullSpeech` | Speech is an optional extra; the core runs without it | | Ports and adapters | `SpeechPort`, `GttsSpeech`, `NullSpeech` | Speech is an optional extra; the core runs without it |
| Null Object | `NullSpeech` | No `if speech:` checks in the use case | | Null Object | `NullSpeech` | No `if speech:` checks in the use case |
| MVVM | `FlashCardView`, `FlashCardViewModel`, `LevelView`, `LevelViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) | | MVVM | `FlashCardView`, `FlashCardViewModel`, `LevelView`, `LevelViewModel`, `MenuView`, `MenuViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) |
| Composition root | `app.main` | The one place that names the adapters, shows the level screen and chooses the word list file (beside the executable, else inside it) | | Composition root | `app.main` | The one place that names the adapters, shows the level screen and chooses the word list file (beside the executable, else inside it) |
| Controller (GRASP) | `Application` | One object receives the menu and level commands and coordinates repositories and sessions |
| Value object | `Level`, `Card`, `LanguagePair` | Immutable data with its own rule (`Level.first`) | | Value object | `Level`, `Card`, `LanguagePair` | Immutable data with its own rule (`Level.first`) |
## Dependency Check ## Dependency Check
@@ -225,6 +263,5 @@ Imports point inward: `domain` imports nothing from the other packages; `applica
[SD-001]: ./uc-001/sd.md [SD-001]: ./uc-001/sd.md
[ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.md [ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.md
[ADR-0004]: ./adr/adr-0004-pyinstaller-executable.md [ADR-0004]: ./adr/adr-0004-pyinstaller-executable.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54 [768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+4 -3
View File
@@ -11,8 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the term Level | [0b983b7] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the term Level | [0b983b7] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the term Reset | [2211d0f] |
--- ---
@@ -31,6 +31,7 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
| Level | en | `Level` | The number of the most frequent words of the word list that a study session uses: 100, 1000 or all. | DM, UC, US | DCD | | Level | en | `Level` | The number of the most frequent words of the word list that a study session uses: 100, 1000 or all. | DM, UC, US | DCD |
| Language pair | en | `LanguagePair` | The two languages of a word list, the language learned and the language of the translation. | DM, UC | DCD | | Language pair | en | `LanguagePair` | The two languages of a word list, the language learned and the language of the translation. | DM, UC | DCD |
| Progress file | en | `ProgressRepository` | The file `words_to_learn.csv` that keeps the words to learn between runs. | UC, US | DCD | | Progress file | en | `ProgressRepository` | The file `words_to_learn.csv` that keeps the words to learn between runs. | UC, US | DCD |
| Reset | en | `reset` | Removing the kept progress of a word list so that all its words are to learn again. | UC, US | DCD, OC |
| Spoken word | en | `SpeechPort` | The Malay word read aloud when its card is shown. | UC, US | DCD | | Spoken word | en | `SpeechPort` | The Malay word read aloud when its card is shown. | UC, US | DCD |
## Rules ## Rules
@@ -44,5 +45,5 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
[BC-001]: ./business-case.md [BC-001]: ./business-case.md
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[DM-001]: ./domain-model.md [DM-001]: ./domain-model.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1 [0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+84
View File
@@ -0,0 +1,84 @@
# MIL-011: Menu bar
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-011 |
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Purpose
Decide whether the program has the classic menu bar the Product Owner asked for: a File menu (open a word list, change the level, reset a word list, exit) and an About menu (contact), and whether the commands keep the progress of every word list safe.
## Deliverable
The window has a menu bar. File: Open (a CSV word list), Change level, Reset word list, Exit. About: Contact with the author's name and title and a link to the author's LinkedIn page. The progress of each word list is kept in its own file. README, the Usage page of the wiki in three languages and a screenshot show the menu.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | The menu bar has File (Open, Change level, Reset word list, separator, Exit) and About (Contact, LinkedIn entry) with the labels of the wireframe | e2e test reads the menu entries and their order | A different structure |
| 2 | Opening a valid CSV word list switches the cards to it after the level choice; opening an invalid file shows a message and keeps the earlier list; cancelling changes nothing | Unit and e2e tests with a faked file dialog and message box | A wrong list in use or a crash |
| 3 | The progress of each word list is a file of its own: `words_to_learn.csv` for the delivered `ms-da.csv` and `words_to_learn_<name>.csv` for another list; known words of one list never appear in another | Integration test with two lists | Progress mixed or lost |
| 4 | Change level shows the level choice again without a restart and keeps the known words | Unit and e2e tests | Restart needed or progress lost |
| 5 | Reset word list asks to confirm; on yes only the progress file of the open list is deleted and the level choice is shown; on no nothing changes; the word list file is never changed | Unit, integration and e2e tests | Another file touched |
| 6 | About, Contact opens `https://www.linkedin.com/in/tirsvad74` through `webbrowser` and shows "Jens Tirsvad Nielsen, Software engineer" | Unit test with a faked `webbrowser.open` | Another address |
| 7 | Exit closes the window, cancels the pending timer and ends the program with exit code 0 | e2e test | A leftover timer or an error |
| 8 | The menu logic is in a view model without Tkinter; the commands are one use case or application call each | View model unit tests without a display | A rule in the view |
| 9 | The documents of the plan (BC-001, US-001, UC-001, SSD-001, OC-001, SD-001, DCD-001, DICT-001, wireframe, PP-001) and README and wiki in three languages describe the menu, each reviewed `Go` | Review records `Go`; `check-readme.sh` exit 0 | A document is missing |
| 10 | Code review against QC-PY-001 has no failed Mandatory criterion; `pytest`, `ruff`, `mypy --strict`, `lint-imports` and `doxygen` are clean | `RC-*` verdict Go and exit codes 0 | Otherwise |
## Dependencies
| Depends on | Reason |
| --- | --- |
| MIL-009 | The program with the level screen, the executable and the data folder rules; MIL-010 (release) is independent and may be done before or after |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| Objective 1 and 2 (learning with word lists), extended by the new request of S01 | [BC-001] |
| US-001.09 to US-001.12 | [US-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
## Target Date
2026-10-17 — within the delivery window that ends 2026-10-21 ([BC-001] constraint).
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Update the documents for the menu bar | Amend BC-001 (In Scope: a menu bar with the commands), UC-001 (the menu commands as extensions at any card), SSD-001, OC-001 and SD-001 (`openWordList`, `changeLevel`, `resetWordList`, `exit`), DCD-001 (`ProgressRepository.reset`, the menu classes, `Application` commands), DICT-001 and the wireframe (menu bar and dialogs); each changed document gets a Version History row and a review. | No | UC-001, DCD-001 |
| 2 | Keep the progress of every word list apart | `progress_file_name(list_path)` in one place (`words_to_learn.csv` for `ms-da.csv`, `words_to_learn_<name>.csv` for another list) and `ProgressRepository.reset()` with its CSV adapter (deletes the file, no error when it is absent). | Yes | US-001.09, US-001.11 |
| 3 | Open another word list | The application loads a CSV chosen in the file dialog (starting in the data folder), validates it before it replaces the list in use, and shows the level choice for it; an invalid file gives a message and no change. | Yes | US-001.09 |
| 4 | Change level while learning | The application shows the level choice again on the same window and starts the session at the chosen level with the progress kept. | Yes | US-001.10 |
| 5 | Reset a word list | After a confirmation the application resets the progress of the open list and shows the level choice; the word list file is never changed. | Yes | US-001.11 |
| 6 | Add the menu bar and the About menu | `MenuViewModel` (entries, labels, order, no Tkinter) and `MenuView` (the Tk menu bar on the window, mnemonics, Exit that cancels the timer, About, Contact entry that opens the LinkedIn page); the contact data and labels are constants. | Yes | US-001.12 |
| 7 | Test the menu and the commands | Unit tests (progress file names, reset, view model, application commands with faked dialogs and `webbrowser`), integration tests with two word lists, e2e test of the real menu bar. | No | |
| 8 | Update README, wiki and screenshots | README and the Usage page of the wiki in English, Danish and Bahasa Malaysia, and a screenshot that includes the menu bar; the wiki is republished after the merge. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[DCD-001]: ../dcd.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+84
View File
@@ -0,0 +1,84 @@
# MIL-012: Release v0.2.0
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-012 |
| CrossReference | [BC-001], [US-001], [UC-001], [ADR-0004] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0264b60] |
---
## Purpose
Decide whether the project can be handed out as version 0.2.0: the version is raised, the tag `v0.2.0` is made on the commit that is built, and the release page has notes and, when the git host accepts the upload, the executable with its checksum. Version 0.2.0 is the minor release that adds the menu bar and the progress per word list to 0.1.0.
## Deliverable
`pyproject.toml`, `flash_card.__version__` and the `Doxyfile` say `0.2.0`; `CHANGELOG.md` has the entry for 0.2.0; the annotated tag `v0.2.0` is on the head of `main`; the release `v0.2.0` exists with notes; `flash-card.exe` and `flash-card.exe.sha256` are attached if the upload limit of the git host allows it, otherwise the notes say how to build the executable and the open issue stays open.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | The three version places say `0.2.0` and a test fails when `flash_card.__version__` and `pyproject.toml` differ | Test passes; grep shows the same version | A different version |
| 2 | `CHANGELOG.md` has a `[0.2.0]` entry listing the menu bar, the progress per word list and the message for a file that is not UTF-8, and `[0.1.0]` is unchanged | File read in the review record | A missing or changed entry |
| 3 | The upload limit of the git host is probed before the build with a draft release that is deleted afterwards, and the result is recorded | Result in the review record | No probe |
| 4 | The executable is built with `--clean` from the commit to be tagged and passes the smoke test of MIL-009 (window, embedded list, progress beside it, list override) | Smoke test output in the review record | A failing case |
| 5 | The tag `v0.2.0` is an annotated tag on that commit; `v0.1.0` is not moved | `git show v0.2.0` and `git rev-parse v0.1.0` | A moved or lightweight tag |
| 6 | The release `v0.2.0` is published with notes that state what is new, that the file is unsigned, the `data` folder rule and how to build | Notes read back from the API | A statement is missing |
| 7 | When the probe allowed it: the release has `flash-card.exe` and `flash-card.exe.sha256`, and the SHA-256 of the downloaded file equals the checksum; when the probe did not allow it: the notes say so and the issue "attach the files" stays open | Hash compared, or the statement is present | A wrong or unverifiable hash |
| 8 | README, CHANGELOG and the Install page of the wiki in three languages link `v0.2.0` and say whether the executable is attached; `check-readme.sh` exit 0 | Exit code 0 | A link is missing |
| 9 | `pytest`, `ruff`, `mypy --strict`, `lint-imports` and `doxygen` are clean after the version change; BC-001, US-001 and PP-001 are reviewed `Go` | `RC-*` verdict Go and exit codes 0 | Otherwise |
## Dependencies
| Depends on | Reason |
| --- | --- |
| MIL-011 | The menu bar that makes this release |
| MIL-010 | The release procedure and its open issue about the upload limit |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| Objective 4 (a repository others can use) | [BC-001] |
| US-001.08 Download the executable | [US-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
## Target Date
2026-10-18 — within the delivery window that ends 2026-10-21 ([BC-001] constraint).
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Update the documents for the release | BC-001 (releases in plural), US-001.08 (the latest release instead of v0.1.0) and PP-001; each changed document gets a Version History row and a review. | No | BC-001, US-001 |
| 2 | Raise the version to 0.2.0 | `pyproject.toml`, `flash_card.__version__` in `src/flash_card/__init__.py` and `PROJECT_NUMBER` of the `Doxyfile`; one unit test compares the version of the package with `pyproject.toml`, so the places cannot drift. | No | |
| 3 | Write the CHANGELOG entry and the release notes | `[0.2.0]` with Added (menu bar: open another word list, change level, reset word list, exit, contact; progress kept apart for each word list), Fixed (a file that is not UTF-8 gives a message) and the known limits; the same text is the release body. | No | |
| 4 | Probe the upload limit of the git host | Create a draft release, try uploads of 1, 5, 10 and 30 MB random files one after another and stop at the first refusal, delete the draft and its tag; record the largest accepted size. The token is read inside the script. | No | |
| 5 | Build and smoke-test the executable from the tagged commit | `python tools/build_executable.py --clean` from a clean tree of the head of `main` after the version change, the smoke test, and the SHA-256. | Yes | US-001.08 |
| 6 | Tag v0.2.0 | Annotated tag `v0.2.0` on the head of `main` and push it; `v0.1.0` stays where it is. | No | |
| 7 | Create the release and attach the files | Create the release `v0.2.0` with the notes through the API; attach `flash-card.exe` and `flash-card.exe.sha256` when the probe allows it and compare the hash after a download; otherwise say so in the notes and leave the issue open. | Yes | US-001.08 |
| 8 | Link the release | README, CHANGELOG and the Install page of the wiki in English, Danish and Bahasa Malaysia link `v0.2.0`; the wiki is republished. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[ADR-0004]: ../adr/adr-0004-pyinstaller-executable.md
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
@@ -0,0 +1,88 @@
# MIL-013: Container checks
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-013 |
| CrossReference | [BC-001], [DCD-001], [ADR-0004] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f86bc91] |
---
## Purpose
Decide whether the program can be built, tested and run without touching the Product Owner's own desktop: a Docker container gives a Debian desktop (GNOME) on a virtual screen, and Windows Sandbox (a disposable Windows virtual machine of Windows 11 Pro) runs the Windows executable on a desktop of its own. The checks that earlier took over the Windows desktop (mouse clicks, windows opening) move into the containers.
## Deliverable
- A Debian image (`python:3.13-slim-bookworm`) with GNOME's window manager and session, a virtual X screen, Tk and fonts, that runs the linters and the whole test suite (including the e2e tests of the real window), the headless run with screenshots, and a PyInstaller build of a Linux binary with its smoke test.
- A Windows Sandbox check of the Windows executable: a `.wsb` configuration and a PowerShell script (`tools/sandbox/`) that start a disposable Windows 11 virtual machine with the built `dist/` mapped read-only and an output folder mapped read-write, start the executable on the sandbox's own desktop, press the known button inside the sandbox, check the progress file, take screenshots, write the results to the output folder and shut the sandbox down. The mouse and the keyboard used are those of the sandbox, not of the host.
- macOS is not covered, and the documents say why (see ADR-0005).
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | `docker build` of the Debian image succeeds on the Product Owner's Docker Desktop (Linux containers) | Exit code 0 | A build error |
| 2 | In the Debian image the linters and the whole test suite run on the virtual screen with the GNOME window manager and end with exit code 0, with the e2e tests counted as passed (not skipped) | Output and exit code | A skipped or failing test |
| 3 | The full GNOME session (`gnome-session` with its shell) is tried in the container; the result is recorded; when it cannot run in a container the checks use GNOME's window manager (mutter) and the record says so | Result in the review record | An unstated fallback |
| 4 | During the container runs nothing opens or moves on the host desktop; during a sandbox run the only host effect is the sandbox's own window, and the sandbox is closed by its script | Nothing else seen on the host | A host window other than the sandbox or any host click |
| 5 | The screenshot script writes the level screen, the card front, the card back and the window with the menu bar as PNG files to a mounted folder, from the Debian image | Four readable PNG files | A missing or blank file |
| 6 | A Linux binary is built in the container and its smoke test passes: the window appears, the level screen is shown, a known word is pressed through the virtual input of the screen and the progress file has one card less, the list override is used | Script output and exit code 0 for 5 consecutive runs | A failing run |
| 7 | The Windows executable passes the same smoke test inside Windows Sandbox (window titled Flashy, embedded list, list override, known button writes the progress file), 5 consecutive sandbox runs, with the results read from the mapped output folder | Result files of 5 runs | A failing run, or a run that needs the host mouse |
| 8 | The lost-click symptom of RC-072 is investigated with the container test (a repeated press of the known button on the same executable) and the finding is recorded: program defect or test harness | Finding in the review record | No finding |
| 9 | README and the wiki Install page in three languages describe the commands and state that macOS is not covered, that the sandbox needs Windows 11 Pro with Windows Sandbox enabled, and that the Debian check is not a Windows check; `check-readme.sh` exit 0 | Exit code 0 | A missing statement |
| 10 | BC-001 and PP-001 are updated and ADR-0005 records the decision and its limits (including why macOS is not run in Docker); each reviewed `Go`; code review against QC-PY-001 has no failed Mandatory criterion; the linters are clean | `RC-*` verdict Go | Otherwise |
## ## Dependencies
| Depends on | Reason |
| --- | --- |
| MIL-012 | The version 0.2.0 of the program, the built executable and the open condition (RC-072) about its smoke test |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| Objective 4 (a repository others can use) and success criterion 2 (tests run on a clean environment) | [BC-001] |
| The condition of RC-072 on the executable smoke test (Linux part) | [MIL-012] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
## Target Date
2026-10-19 — within the delivery window that ends 2026-10-21 ([BC-001] constraint).
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Update the documents for the container checks | BC-001 (In Scope: Docker images for tests and headless runs; Out of Scope: macOS, a signed or installed Windows setup), PP-001 and a new ADR-0005 (the Debian image with GNOME, Windows Sandbox for the Windows executable, what is covered and why macOS is not run in Docker: Apple's licence allows macOS only on Apple hardware); each changed document gets a Version History row and a review. | No | BC-001, ADR-0004 |
| 2 | Add the Debian image with GNOME | `docker/debian.Dockerfile` and `.dockerignore`: `python:3.13-slim-bookworm`, `xvfb`, `xauth`, `dbus-x11`, `mutter`, `gnome-session` and `gnome-shell`, `tk`, `fonts-liberation`, `x11-utils`, `xdotool`, a non-root user; the project installed with `[dev,speech,tools,build]`. | No | ADR-0005 |
| 3 | Start a GNOME session on the virtual screen | `tools/docker/start-desktop.sh` starts `dbus`, Xvfb and `gnome-session` (or, if the session cannot run in a container, mutter alone) and waits until the desktop is up; the choice is printed and recorded. | No | |
| 4 | Add the check scripts | `tools/docker/run-checks.sh`, `screenshots.sh` and `smoke-binary.sh`; each one purpose, `set -euo pipefail`, shellcheck clean, all run under the desktop of task 3. | No | |
| 5 | Make the screenshot tool work on Linux | `tools/capture_screenshots.py` chooses the platform: Windows as now, Linux grabs the X screen with Pillow and crops to the window; the pure parts are unit tested. | No | |
| 6 | Add the headless smoke driver | A script that starts the program or the binary, finds the window with `xdotool`, presses the known button with the virtual input of the screen, and checks the progress file, with a repeat count so the lost-click symptom of RC-072 can be measured; the sandbox script does the same with the sandbox's input. | No | |
| 7 | Add the Windows Sandbox check | `tools/sandbox/flash-card.wsb` (mapped folders: `dist` read-only, `out` read-write; logon command), `tools/sandbox/smoke.ps1` (runs inside the sandbox: copy the executable, start it, wait for the window, press the known button with the sandbox's own input, read the progress file, take screenshots, write a result file and shut the sandbox down) and `tools/sandbox/run.ps1` on the host (starts the sandbox with the generated configuration and waits for the result file). | No | ADR-0005 |
| 8 | Run and record the checks | Build the image, run the scripts and the smoke tests 5 times each in the container and 5 times in the sandbox, record counts, sizes, durations and the GNOME session result; record the finding on the lost click. | No | |
| 9 | Test the pure parts | Unit tests for the platform choice and the box calculation of the capture tool and for the argument handling of the smoke driver. | No | |
| 10 | Update README and wiki | README and the Install page of the wiki in English, Danish and Bahasa Malaysia: the commands, the output folders, the limits (the Debian check is not a Windows check, the sandbox needs Windows 11 Pro, macOS is not covered); the wiki is republished after the merge. | No | |
---
[BC-001]: ../business-case.md
[DCD-001]: ../dcd.md
[ADR-0004]: ../adr/adr-0004-pyinstaller-executable.md
[MIL-012]: ./mil-012-release-0-2-0.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+21 -6
View File
@@ -4,15 +4,15 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | PP-001 | | ID | PP-001 |
| CrossReference | [BC-001], [SA-001], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010] | | CrossReference | [BC-001], [SA-001], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013] |
| Language | en | | Language | en |
| Domain | it | | Domain | it |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-009 Executable with PyInstaller (new request of S01) | [768ca34] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-012 Release v0.2.0 (new request of S01) | [0264b60] |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-010 Release v0.1.0 (new request of S01) | [9c8e294] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-013 Container checks (new request of S01) | [f86bc91] |
--- ---
@@ -42,6 +42,9 @@ This plan schedules the seven phases (gateways) that deliver the flash card prog
| Choose level | [MIL-008] | 2026-10-20 | 2026-10-20 | S01 | US-001.06 | A level choice (100, 1000, all words) before the cards; documents, wiki and screenshots updated | | | Choose level | [MIL-008] | 2026-10-20 | 2026-10-20 | S01 | US-001.06 | A level choice (100, 1000, all words) before the cards; documents, wiki and screenshots updated | |
| Executable with PyInstaller | [MIL-009] | 2026-10-16 | 2026-10-16 | S01 | US-001.07 | One Windows executable that runs without Python, built by a script; documents, wiki and README updated | | | Executable with PyInstaller | [MIL-009] | 2026-10-16 | 2026-10-16 | S01 | US-001.07 | One Windows executable that runs without Python, built by a script; documents, wiki and README updated | |
| Release v0.1.0 | [MIL-010] | 2026-10-16 | 2026-10-16 | S01 | US-001.08 | Tag v0.1.0 and a release with `flash-card.exe`, its checksum and release notes | | | Release v0.1.0 | [MIL-010] | 2026-10-16 | 2026-10-16 | S01 | US-001.08 | Tag v0.1.0 and a release with `flash-card.exe`, its checksum and release notes | |
| Menu bar | [MIL-011] | 2026-10-17 | 2026-10-17 | S01 | US-001.09 to US-001.12 | File menu (open word list, change level, reset word list, exit) and About menu (contact); documents, wiki and screenshots updated | |
| Release v0.2.0 | [MIL-012] | 2026-10-18 | 2026-10-18 | S01 | US-001.08 | Version 0.2.0, tag `v0.2.0` and a release with the menu bar, notes and, if the host accepts it, the executable | |
| Container checks | [MIL-013] | 2026-10-19 | 2026-10-19 | S01 | none | A Debian (GNOME) container and Windows Sandbox that run the tests, screenshots and the smoke tests without the host desktop | |
```plantuml ```plantuml
@startgantt @startgantt
@@ -66,6 +69,12 @@ Project starts 2026-10-14
[Executable Go/No-Go] happens 2026-10-16 [Executable Go/No-Go] happens 2026-10-16
[Release] starts 2026-10-16 and ends 2026-10-16 [Release] starts 2026-10-16 and ends 2026-10-16
[Release Go/No-Go] happens 2026-10-16 [Release Go/No-Go] happens 2026-10-16
[Menu bar] starts 2026-10-17 and ends 2026-10-17
[Menu bar Go/No-Go] happens 2026-10-17
[Release 0.2.0] starts 2026-10-18 and ends 2026-10-18
[Release 0.2.0 Go/No-Go] happens 2026-10-18
[Container checks] starts 2026-10-19 and ends 2026-10-19
[Container checks Go/No-Go] happens 2026-10-19
@endgantt @endgantt
``` ```
@@ -83,12 +92,15 @@ Project starts 2026-10-14
| Choice of level (new request after MIL-007) | MIL-008 | | Choice of level (new request after MIL-007) | MIL-008 |
| Executable built with PyInstaller (new request after MIL-008) | MIL-009 | | Executable built with PyInstaller (new request after MIL-008) | MIL-009 |
| Release with the executable attached (new request after MIL-009) | MIL-010 | | Release with the executable attached (new request after MIL-009) | MIL-010 |
| Menu bar with File and About menus (new request after MIL-009) | MIL-011 |
| Release v0.2.0 with the menu bar (new request after MIL-011) | MIL-012 |
| Debian container for tests and headless runs (new request after MIL-012) | MIL-013 |
| Framework documents of the baseline | Baseline pull request before MIL-001 | | Framework documents of the baseline | Baseline pull request before MIL-001 |
## Dependencies ## Dependencies
``` ```
Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008 → MIL-009 → MIL-010 Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008 → MIL-009 → MIL-010 → MIL-011 → MIL-012 → MIL-013
``` ```
A No-Go on a phase moves every later date by the days the rework takes; MIL-008 was added after MIL-007 and takes the unplanned day 2026-10-20, so no buffer is left inside the window; a No-Go moves the end date past 2026-10-21 and S01 decides whether to accept that. A No-Go on a phase moves every later date by the days the rework takes; MIL-008 was added after MIL-007 and takes the unplanned day 2026-10-20, so no buffer is left inside the window; a No-Go moves the end date past 2026-10-21 and S01 decides whether to accept that.
@@ -121,6 +133,9 @@ A No-Go on a phase moves every later date by the days the rework takes; MIL-008
[MIL-008]: ./milestones/mil-008-choose-level.md [MIL-008]: ./milestones/mil-008-choose-level.md
[MIL-009]: ./milestones/mil-009-executable.md [MIL-009]: ./milestones/mil-009-executable.md
[MIL-010]: ./milestones/mil-010-release.md [MIL-010]: ./milestones/mil-010-release.md
[MIL-011]: ./milestones/mil-011-menu-bar.md
[MIL-012]: ./milestones/mil-012-release-0-2-0.md
[MIL-013]: ./milestones/mil-013-container-checks.md
[MS-001]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/165 [MS-001]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/165
[MS-002]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/166 [MS-002]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/166
[MS-003]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/167 [MS-003]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/167
@@ -128,5 +143,5 @@ A No-Go on a phase moves every later date by the days the rework takes; MIL-008
[MS-005]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/169 [MS-005]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/169
[MS-006]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/170 [MS-006]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/170
[MS-007]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/171 [MS-007]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/171
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54 [0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
[9c8e294]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/9c8e2940217e615f82eda2d48aca9b2fd58f3687 [f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+50
View File
@@ -0,0 +1,50 @@
# RC-055: Review of the release v0.1.0
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-055 |
| CrossReference | [MIL-010] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [db87a6d] |
---
## Artifact Under Review
- Instance reviewed: [MIL-010] (the milestone's Go / No-Go criteria, checked against the release v0.1.0)
- Checklist used: none exists for a release; the seven criteria of MIL-010 are the checklist
- Scope: full review
- Language and domain: n/a (record of results)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The executable is rebuilt from the head of `main` that gets the tag and passes the smoke test | Pass | Built with `--clean` from commit `2c580cc` after removing `build/` and `dist/`; four smoke cases passed (window "Flashy" after about 2 s, embedded list, progress file beside the exe with 999 cards, list override with a 2 word list) |
| 2 | The tag `v0.1.0` is an annotated tag on that commit and the version files say `0.1.0` | Pass | `git show v0.1.0` shows the annotated tag on `2c580cc`; `pyproject.toml` and `__version__` say `0.1.0` |
| 3 | The release has the assets `flash-card.exe` and `flash-card.exe.sha256` | Fail | The upload of `flash-card.exe` (25.8 MB) was rejected with `413 Request Entity Too Large` from the server's reverse proxy; a 1 MB test upload was reset as well; only an 82 byte file passed. The checksum asset alone was deleted again, because a checksum without its file is misleading |
| 4 | The SHA-256 of the file downloaded from the release equals the checksum | Fail | There is no downloadable file; the hash of the local build was `0ac22621ecb8ddad53c5f3d2a35278bebd34cefe458510971106ec52d5bdb47d`, which cannot be verified by others because PyInstaller builds are not bit-for-bit reproducible |
| 5 | The release notes say unsigned Windows build, the `data` folder and how to build, and give the SHA-256 | Fail | The notes say unsigned, the `data` folder and how to build, and state that the file is not attached; they give no SHA-256 because there is no file |
| 6 | CHANGELOG, README and the Install page of the wiki link the release; `check-readme.sh` exit 0 | Pass | Links present in all five files and corrected to say the file is not attached; `check-readme.sh` exit 0 |
| 7 | BC-001, US-001 and PP-001 are updated and reviewed `Go` | Pass | RC-051 to RC-053 |
## Overall Verdict
Go-with-conditions — the tag and the release page exist and the build was verified, but criteria 3, 4 and 5 fail because the server refuses the upload of the executable. The milestone MIL-010 stays open and issue "Create the release and attach the files" stays open until the executable is attached. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Raise the upload limit of the git host's reverse proxy (nginx `client_max_body_size`, and Gitea's attachment limit if set) to at least 50 MB, then ask for the attachment of `flash-card.exe` and its SHA-256 to v0.1.0 (or confirm another place to host the file) | S01 | 2026-10-21 |
---
[MIL-010]: ../milestones/mil-010-release.md
[db87a6d]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/db87a6d33bcaaef5a8bc62bf3f804502130b76a9
@@ -0,0 +1,70 @@
# RC-056: Review of BC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-056 |
| CrossReference | [BC-001], [QC-BC-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [BC-001]
- Checklist used: [QC-BC-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | The Cost-Benefit table is qualitative and says why: no revenue is expected, so a quantitative return does not apply. |
| 2 | Risks are identified with documented impact and mitigation | Pass | The Risks table has 5 risks, each with an impact and a mitigation. |
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | The Success Criteria table has 8 rows with numeric targets and a measure each (for example 0 third-party packages; 5 commands or fewer). |
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | Scope has `### In Scope` and `### Out of Scope`; the menu bar is added to In Scope. |
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | The Stakeholders table cites S01, S02 and S03 and does not re-describe their roles. |
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Methodological and Standards Foundation names the framework, Larman, ISO/IEC 25010:2023 and PEP 8. |
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | Assumptions and Constraints are separate sections. |
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Recommendation reads `Proceed` with a one-sentence rationale. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of BC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[BC-001]: ../../../docs/business-case.md
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,70 @@
# RC-057: Review of US-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-057 |
| CrossReference | [US-001], [QC-US-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [US-001]
- Checklist used: [QC-US-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | INVEST Check section confirms the six properties for all twelve stories and states one dependency exception. |
| 2 | Written in "As a / I want / So that" form | Pass | All twelve stories use `As a / I want / so that`. |
| 3 | Clear, testable acceptance criteria are included | Pass | Every story, including US-001.09 to US-001.12, has Given/When/Then acceptance criteria. |
| 4 | Traceable to a use case or epic | Pass | Every story traces to a milestone and to UC-001. |
| 5 | Story is sized to fit within a single iteration | Pass | Each story is one milestone, which fits one iteration. |
| 6 | Story statement avoids technical implementation detail | Fail | Acceptance criteria name `words_to_learn.csv`, `ms-da.csv` and 3000 ms, which are assignment-mandated names and values rather than pure behaviour. |
| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | The role `Learner` is the actor of UCD-001. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of US-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion 6 (Optional) failed: Story statement avoids technical implementation detail; fix or accept the deferral | S01 | 2026-10-21 |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[US-001]: ../../../docs/user-stories.md
[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+74
View File
@@ -0,0 +1,74 @@
# RC-058: Review of UC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-058 |
| CrossReference | [UC-001], [QC-UC-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [UC-001]
- Checklist used: [QC-UC-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | N-A | The format is Fully Dressed; the Brief format criterion does not apply. |
| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | The format is Fully Dressed; the Casual format criterion does not apply. |
| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Scope, level, actor, stakeholders, pre- and postconditions, main success scenario and extensions are present. |
| 4 | Preconditions and postconditions are explicitly defined | Pass | Preconditions and postconditions are listed. |
| 5 | Primary actor is explicitly stated | Pass | Primary actor is `Learner`. |
| 6 | Stakeholders and their interests are stated | Pass | Stakeholders S01 and S02 and their interests are stated. |
| 7 | Main success scenario is written as clear, numbered steps | Pass | Main success scenario has 7 numbered steps. |
| 8 | Alternative/exception flows correctly reference `<<include>>`/`<<extend>>` use cases where relevant, per UML 2.5.1 | Pass | UCD-001 has no include or extend relationship, so no extension needs to reference one; the extensions (3a, 4a, 5a, 7a, 7b, 9a, *a) are plain alternative flows. |
| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | Pass | The Business Rules table gives a rule per step. |
| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Title and actor match UCD-001 and US-001; the menu extension matches US-001.09 to US-001.12. |
| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | `Level: user-goal` and `Scope: Flash Card program`. |
| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Steps are written as goals and answers; buttons, files and milliseconds are in the wireframe and milestones. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of UC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[UC-001]: ../../../docs/uc-001/uc.md
[QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,69 @@
# RC-059: Review of SSD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-059 |
| CrossReference | [SSD-001], [QC-SSD-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [SSD-001]
- Checklist used: [QC-SSD-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Input/output messages match the corresponding Use Case's main success scenario step-for-step | Pass | System Operations table maps each message to steps of the main success scenario (1 to 9) or to the menu extension *a; the timer step is the one stated deviation. |
| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | The system is `:System`; no internal object appears. |
| 3 | Object creation/destruction of the System instance handled explicitly where relevant | Pass | Lifecycle Notes state that the system lives for one run. |
| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed return arrows are drawn for every message that returns something; `exit` returns nothing and ends the program. |
| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | The Alternate Flows section states the extensions that add no operation and that they are out of scope. |
| 6 | Message names are verb phrases consistent with the use case's system responsibilities | Pass | `startStudySession`, `markKnown` and `markUnknown` are verb phrases. |
| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | Source Use Case section names UC-001 and the scenario. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of SSD-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[SSD-001]: ../../../docs/uc-001/ssd.md
[QC-SSD-001]: ../../../framework/qc/artifacts/qc-ssd.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,53 @@
# RC-060: Review of OC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-060 |
| CrossReference | [OC-001], [QC-OC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [OC-001]
- Checklist used: [QC-OC-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Method signature is complete: operation name, parameter types, and return type | Pass | Each contract gives name, parameter list and return type (`startStudySession(): CardDto | None`). |
| 2 | Preconditions explicitly list required state before execution | Pass | Preconditions are listed per contract. |
| 3 | Postconditions explicitly describe resulting state using Larman's "instance created/associated/attribute modified" style | Pass | Postconditions use created / associated / destroyed / attribute set wording. |
| 4 | Exceptions and error conditions are documented, including the triggering precondition failure | Pass | Each contract has an Exceptions table with the failing precondition. |
| 5 | Operation is explicitly traceable to a single SSD message | Pass | Each of the nine contracts traces to exactly one SSD message; the timer step has none and says so. |
| 6 | Contract avoids specifying implementation/algorithmic details (declarative, not procedural) | Pass | Postconditions are declarative. |
| 7 | Cross-references the Domain Model classes/associations affected by pre/postconditions | Pass | Domain Model concepts and IT terms are cited per contract. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[OC-001]: ../../../docs/uc-001/oc.md
[QC-OC-001]: ../../../framework/qc/artifacts/qc-operation-contract.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,54 @@
# RC-061: Review of SD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-061 |
| CrossReference | [SD-001], [QC-SD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [SD-001]
- Checklist used: [QC-SD-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Message passing strictly follows UML sync/async/return arrow syntax | Pass | Calls are solid arrows with dashed returns. |
| 2 | GRASP/GoF patterns applied and explicitly annotated where used (e.g. Controller, Observer, Mediator, Factory) | Pass | Each sequence has a Pattern Annotations table (Creator, Information Expert, Null Object, MVVM, Repository). |
| 3 | Lifelines show activation bars matching actual processing time/call nesting | Pass | Activations are drawn for the session and the view model. |
| 4 | Object creation and destruction shown with correct UML notation (`create`/`destroy` messages, X on lifeline) | Pass | `create` is used for `Deck` and `StudySession`; no object is destroyed during the program's life and that is stated by omission, the process ends. |
| 5 | Diagram realizes the postconditions of a specific Operation Contract | Pass | Each of the six sequences has a Postcondition Coverage table for its contract in OC-001; `changeLevel`, `showContact` and `exit` are single calls and need no sequence. |
| 6 | Responsibility assignment favors low coupling/high cohesion (no god-object receiving all messages) | Pass | Responsibility Check notes state that no object receives all messages. |
| 7 | Loop, alt, and opt combined fragments used correctly for conditional/repeated behavior | Pass | `alt` and `opt` fragments are used for the progress file and the empty deck. |
| 8 | Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, or its absence is justified | Fail | The exceptions of OC-001 (missing file, malformed file) are raised before the sequence starts and are not drawn as `alt` fragments; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion 8 (Optional) failed: Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, ; fix or accept the deferral | S01 | 2026-10-21 |
---
[SD-001]: ../../../docs/uc-001/sd.md
[QC-SD-001]: ../../../framework/qc/artifacts/qc-sequence-diagram.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,55 @@
# RC-062: Review of DCD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-062 |
| CrossReference | [DCD-001], [QC-DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [DCD-001]
- Checklist used: [QC-DCD-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | SOLID principles applied; no god classes with excessive responsibilities | Pass | Each class has one responsibility (Class Table); `Application` is the controller of the commands and does not hold card rules. |
| 2 | Visibility markers correct and consistent (`+` public, `-` private, `#` protected) | Pass | Visibility markers are on all members. |
| 3 | Relationships correctly distinguished: Association vs Aggregation vs Composition vs Dependency | Pass | Composition (Vocabulary and Card, MenuViewModel and MenuEntry), aggregation (Deck), association, dependency and realization are distinguished. |
| 4 | Multiplicities and navigability specified on all associations | Pass | Multiplicities are on the associations; dependencies are directed. |
| 5 | Applied design patterns are annotated explicitly (e.g. Singleton, Factory, Strategy) | Pass | Pattern Annotations table lists Repository, Ports and adapters, Null Object, MVVM, Composition root. |
| 6 | Method signatures are traceable to Operation Contracts and/or design Sequence Diagrams | Pass | Method Traceability table maps every operation, including the menu and `Application` commands and `ProgressRepository.reset`, to OC-001 and SD-001. |
| 7 | Class names and structure remain consistent with the Domain Model concepts they refine | Pass | The Class Table has a `Refines` column; names match the dictionary's IT terms (`reset` added). |
| 8 | No circular dependencies between classes/packages unless explicitly justified | Pass | Dependency Check states there are no cycles. |
| 9 | Dependencies between classes and packages point inward: domain classes do not depend on infrastructure, delivery or framework classes | Pass | Domain imports nothing; infrastructure and presentation point inward (Dependency Check). |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[DCD-001]: ../../../docs/dcd.md
[QC-DCD-001]: ../../../framework/qc/artifacts/qc-dcd.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,69 @@
# RC-063: Review of DICT-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-063 |
| CrossReference | [DICT-001], [QC-DICT-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [DICT-001]
- Checklist used: [QC-DICT-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Every row has a PO term, its language, an IT term and a definition | Pass | All 9 rows have PO term, language, IT term and definition. |
| 2 | Each PO term maps to exactly one IT term and the reverse (no synonyms) | Pass | One IT term per PO term; no synonym appears. |
| 3 | Every Domain Model concept has a row, and the Domain Model uses its PO term | Pass | The 5 concepts of DM-001 have a row and DM-001 uses the PO term; `Reset` was added. |
| 4 | The Operation Contracts, Sequence Diagrams, Design Class Diagrams and ERD use the IT term, not the PO term | Pass | DCD-001, OC-001 and SD-001 use `Card`, `Vocabulary`, `Deck`, `StudySession`. |
| 5 | Definitions are written in the PO language and are one sentence | Pass | Definitions are one English sentence each. |
| 6 | "Used as PO term in" and "Used as IT term in" name artifact types that exist in the project | Pass | Artifact types named in both columns exist in the project. |
| 7 | The dictionary's `Language` and `Domain` rows, and the language of every row, match the PO language and domain in the project registry | Pass | `Language | en` and `Domain | it` match the registry; every row says `en`. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of DICT-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-011 (menu bar).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[DICT-001]: ../../../docs/dictionary.md
[QC-DICT-001]: ../../../framework/qc/artifacts/qc-dictionary.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
@@ -0,0 +1,54 @@
# RC-064: Review of PP-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-064 |
| CrossReference | [PP-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [PP-001]
- Checklist used: none exists for this type, and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of PP-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — no checklist exists for the Project Plan type; the plan now schedules MIL-011 on 2026-10-17 inside the Business Case constraint (delivery by 2026-10-21) and the language checklist passes. Accepted as the Product Owner's stand-in, after the Product Owner accepted the MIL-011 plan in chat. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[PP-001]: ../../../docs/project-plan.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+66
View File
@@ -0,0 +1,66 @@
# RC-065: Review of MIL-011
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-065 |
| CrossReference | [MIL-011], [QC-MIL-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2211d0f] |
---
## Artifact Under Review
- Instance reviewed: [MIL-011]
- Checklist used: [QC-MIL-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | A concrete Deliverable section names the output evaluated at the gate. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | The Go / No-Go table has numbered criteria, each with an observable Go and No-Go. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the milestone or documents it needs. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability table maps to Business Case objectives and the user story. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership table names S01 as owner and approving reviewer. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies inside the 2026-10-14 to 2026-10-21 window of the Business Case. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of MIL-011 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[MIL-011]: ../../../docs/milestones/mil-011-menu-bar.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+64
View File
@@ -0,0 +1,64 @@
# RC-066: Review of PY-MIL-011
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-066 |
| CrossReference | [PY-MIL-011], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [ce4aacb] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-011]
- Checklist used: [QC-PY-001]
- Scope: full review
- Language and domain: n/a (technical type)
- 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 | PEP 8 casing holds in the new modules (`menu_view.py`, `menu_view_model.py`, `progress_file_name.py`); ruff rule set N passes. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose and are the DCD-001 IT terms (`MenuView`, `MenuViewModel`, `Application.open_word_list`, `reset`). |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass on 79 files with no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 49 source files and on the three tool scripts. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | Refused files raise `VocabularyFormatError` with `from` (also for bytes that are not UTF-8), a failed reset is logged and shown, a failed browser start is logged; no bare `except`, nothing swallowed silently. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
| 7 | Files, locks and connections are managed with context managers | Pass | Reading uses `with`; the delete uses `unlink(missing_ok=True)`; the Tk root is destroyed through `exit_application`. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | New and changed members have Doxygen docstrings; `doxygen Doxyfile` ends with 0 warnings. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`; failures use `logging`; the contact data is public author information given by S01. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | The new classes match the revised DCD-001 and OC-001/SD-001. Deviations: `MenuEntry` has a third field `underline`; `MenuViewModel` has `file_label` and `contact_menu_label`; `Application` has `word_list_folder` and takes a repository factory; `MenuView` depends on a small `MenuApplication` Protocol to avoid an import cycle. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 258 tests (5 consecutive full runs green): unit with fakes only, integration with two word lists, and e2e on the real menu bar with injected dialogs; none uses the network or a real browser. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; `webbrowser`, `tkinter.filedialog` and `tkinter.messagebox` are standard library. |
| 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 progress file rule is one function (`progress_file_name`); labels, contact data and dialog texts are constants once. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; a grep finds no `tkinter`, `csv`, `random` or `webbrowser` import in `domain` or `application`; the menu view model imports no Tkinter. |
| 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 | The view model owns the entries and the view draws and asks dialogs; `Application` coordinates; the repositories own their files; dialogs and the browser are injected. |
| 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 | `presentation/features/menu/` and `infrastructure/persistence/` follow the folder structure; tests stay in `unit`, `integration`, `e2e`. |
| 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 follow the rule. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-011 Go criteria: 1 the menu bar structure and order are read from the real `tk.Menu` (e2e); 2 open valid / invalid / cancelled covered (unit and e2e, including a Latin-1 file and invalid UTF-8 bytes); 3 two lists keep separate progress files, `ms-da.csv` keeps `words_to_learn.csv` (integration); 4 change level shows the level screen and keeps the known words (unit and e2e); 5 reset asks, deletes only the progress file of the open list, never the list file (unit, integration, e2e); 6 About, Contact opens `https://www.linkedin.com/in/tirsvad74` through an injected `webbrowser.open` (unit and e2e); 7 exit cancels the timer and destroys the window (e2e); 8 view model tested without Tkinter; 9 RC-056 to RC-065 `Go`, README and the Usage page of the wiki in three languages describe the menu; 10 this record, `pytest` 258 passed and linters clean. The executable was rebuilt with the menu and passes the smoke test (window, embedded list, list override, progress file). A real browser launch and the native file dialog were not exercised on this machine. Defect found by the reviewer and fixed in this milestone: a file that is not UTF-8 raised `UnicodeDecodeError` instead of a message. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-011]: ../../../src/flash_card
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[ce4aacb]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/ce4aacb4bc7ce742e3b2397e05a43572497402ef
@@ -0,0 +1,70 @@
# RC-067: Review of BC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-067 |
| CrossReference | [BC-001], [QC-BC-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0264b60] |
---
## Artifact Under Review
- Instance reviewed: [BC-001]
- Checklist used: [QC-BC-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | The Cost-Benefit table is qualitative and says why: no revenue is expected, so a quantitative return does not apply. |
| 2 | Risks are identified with documented impact and mitigation | Pass | The Risks table has 5 risks, each with an impact and a mitigation. |
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | The Success Criteria table has 8 rows with numeric targets and a measure each (for example 0 third-party packages; 5 commands or fewer). |
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | Scope has `### In Scope` and `### Out of Scope`; the releases are In Scope, an installer and signing stay Out of Scope. |
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | The Stakeholders table cites S01, S02 and S03 and does not re-describe their roles. |
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Methodological and Standards Foundation names the framework, Larman, ISO/IEC 25010:2023 and PEP 8. |
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | Assumptions and Constraints are separate sections. |
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Recommendation reads `Proceed` with a one-sentence rationale. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of BC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-012 (release v0.2.0).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[BC-001]: ../../../docs/business-case.md
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
@@ -0,0 +1,70 @@
# RC-068: Review of US-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-068 |
| CrossReference | [US-001], [QC-US-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0264b60] |
---
## Artifact Under Review
- Instance reviewed: [US-001]
- Checklist used: [QC-US-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Follows INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) | Pass | INVEST Check section confirms the six properties for all twelve stories and states one dependency exception. |
| 2 | Written in "As a / I want / So that" form | Pass | All twelve stories use `As a / I want / so that`. |
| 3 | Clear, testable acceptance criteria are included | Pass | Every story, including US-001.08 in its new wording, has Given/When/Then acceptance criteria. |
| 4 | Traceable to a use case or epic | Pass | Every story traces to a milestone and to UC-001. |
| 5 | Story is sized to fit within a single iteration | Pass | Each story is one milestone, which fits one iteration. |
| 6 | Story statement avoids technical implementation detail | Fail | Acceptance criteria name `words_to_learn.csv`, `ms-da.csv` and 3000 ms, which are assignment-mandated names and values rather than pure behaviour. |
| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | The role `Learner` is the actor of UCD-001. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of US-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-012 (release v0.2.0).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion 6 (Optional) failed: Story statement avoids technical implementation detail; fix or accept the deferral | S01 | 2026-10-21 |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[US-001]: ../../../docs/user-stories.md
[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
@@ -0,0 +1,54 @@
# RC-069: Review of PP-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-069 |
| CrossReference | [PP-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0264b60] |
---
## Artifact Under Review
- Instance reviewed: [PP-001]
- Checklist used: none exists for this type, and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of PP-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — no checklist exists for the Project Plan type; the plan now schedules MIL-012 on 2026-10-18 inside the Business Case constraint (delivery by 2026-10-21) and the language checklist passes. Accepted as the Product Owner's stand-in, after the Product Owner accepted the MIL-012 plan in chat. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[PP-001]: ../../../docs/project-plan.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
+66
View File
@@ -0,0 +1,66 @@
# RC-070: Review of MIL-012
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-070 |
| CrossReference | [MIL-012], [QC-MIL-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0264b60] |
---
## Artifact Under Review
- Instance reviewed: [MIL-012]
- Checklist used: [QC-MIL-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | A concrete Deliverable section names the output evaluated at the gate. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | The Go / No-Go table has numbered criteria, each with an observable Go and No-Go. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the milestone or documents it needs. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability table maps to Business Case objectives and the user story. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership table names S01 as owner and approving reviewer. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies inside the 2026-10-14 to 2026-10-21 window of the Business Case. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of MIL-012 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[MIL-012]: ../../../docs/milestones/mil-012-release-0-2-0.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
+64
View File
@@ -0,0 +1,64 @@
# RC-071: Review of PY-MIL-012
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-071 |
| CrossReference | [PY-MIL-012], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [7c1f1b6] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-012]
- Checklist used: [QC-PY-001]
- Scope: full review
- Language and domain: n/a (technical type)
- 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 | The change is one assignment and a test module; names follow PEP 8. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | `test_version_of_the_package_equals_pyproject_version` and similar names state the behaviour. |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass with no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Test functions are annotated `-> None`; `mypy --strict` passes on 49 source files. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No exception handling in the change. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults. |
| 7 | Files, locks and connections are managed with context managers | Pass | `pyproject.toml` is read with `tomllib` through a `with open` context. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | The module docstring keeps `@file` and `@brief`; `doxygen Doxyfile` ends with 0 warnings. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`, no logging in the change. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | No class or operation changed; the version string is not part of the design. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 3 tests compare `__version__`, `pyproject.toml` and the `Doxyfile` `PROJECT_NUMBER` and the `MAJOR.MINOR.PATCH` form; 3 consecutive full runs (261 tests) green. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; `tomllib` is standard library. |
| 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 version is stated once per file type; the test makes the three places agree. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept. |
| 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 | One test module with one reason to change. |
| 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 | The test sits in `tests/unit`. |
| 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 names follow the rule. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-012 tasks 2, 3, 4 and 8 (this change set): 1 the three version places say 0.2.0 and a test fails if they differ; 2 CHANGELOG has `[0.2.0]` and `[0.1.0]` is unchanged; 3 the upload limit probe ran before the build: a draft release with a 1 MB random file was refused with `413 Request Entity Too Large`, so the largest accepted size is below 1 MB; the draft release and its tag were deleted afterwards (the tag list and the release list were checked); 8 README, CHANGELOG and the Install page of the wiki in three languages link v0.2.0 and say that the executable is not attached; `check-readme.sh` exit 0. The tag, the build and the release are criteria 4 to 7 and are recorded in a separate review record. Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-012]: ../../../src/flash_card/__init__.py
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[7c1f1b6]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/7c1f1b6423a876d25afafd24f7f206d16ac6d669
+54
View File
@@ -0,0 +1,54 @@
# RC-072: Review of the release v0.2.0
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-072 |
| CrossReference | [MIL-012] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [5345fd7] |
---
## Artifact Under Review
- Instance reviewed: [MIL-012] (the milestone's Go / No-Go criteria, checked against the release v0.2.0)
- Checklist used: none exists for a release; the nine criteria of MIL-012 are the checklist
- Scope: full review
- Language and domain: n/a (record of results)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The three version places say `0.2.0` and a test fails when they differ | Pass | `pyproject.toml`, `flash_card.__version__` and `Doxyfile` say 0.2.0; `tests/unit/test_version.py` (3 tests) |
| 2 | `CHANGELOG.md` has a `[0.2.0]` entry (menu bar, progress per word list, UTF-8 message) and `[0.1.0]` is unchanged | Pass | Read in the pull request #94 |
| 3 | The upload limit of the git host is probed before the build with a draft release that is deleted afterwards, and the result is recorded | Pass | A draft release with a 1 MB random file was refused with `413 Request Entity Too Large`; the draft and its tag were deleted (release and tag lists checked) |
| 4 | The executable is built with `--clean` from the commit to be tagged and passes the smoke test of MIL-009 | Fail | Built with `--clean` from `d7ca24d` (25.8 MB). The first smoke run passed three cases (level screen window, embedded list, progress file with 999 cards at level 100); the list override case failed once because the simulated mouse click did not register, and in 3 of 8 reruns the same click was lost. The cause was not found: it may be the click automation (timing of the pointer and the focus) or a defect of the program. The checks were stopped at the Product Owner's request because they take over the desktop, and are to be repeated in a container or on an isolated desktop (MIL-013) |
| 5 | The tag `v0.2.0` is an annotated tag on that commit; `v0.1.0` is not moved | Fail | The tag points to `d7ca24d` and `v0.1.0` is unchanged (`2c580cc`), but the tag is lightweight: the push of my annotated tag failed on a connection error, and the release creation through the API then created a lightweight tag. Replacing it needs a force update of a tag, which is outside what an autonomous run may do without the Product Owner |
| 6 | The release `v0.2.0` is published with notes (new, unsigned, `data` folder rule, how to build) | Pass | Release page returns HTTP 200; notes say what is new, that the file is unsigned, the `data` folder and how to build |
| 7 | When the probe did not allow it: the notes say so and the issue "attach the files" stays open | Pass | The notes state that the executable is not attached and why; issue "Create the release and attach the files" stays open |
| 8 | README, CHANGELOG and the Install page of the wiki in three languages link `v0.2.0`; `check-readme.sh` exit 0 | Pass | Merged in #94; wiki republished |
| 9 | `pytest`, `ruff`, `mypy --strict`, `lint-imports` and `doxygen` are clean; BC-001, US-001 and PP-001 reviewed `Go` | Pass | 261 passed; RC-067 to RC-069 |
## Overall Verdict
Go-with-conditions — the version, the CHANGELOG, the tag (lightweight) and the release page are in place, and the executable is correctly stated as not attached. Criteria 4 and 5 fail or are incomplete: the smoke test of the executable is not complete, and the tag is not annotated. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Decide whether the lightweight tag `v0.2.0` may be replaced by an annotated tag on the same commit (a force update of the tag on the host); `v0.1.0` stays | S01 | 2026-10-18 |
| Repeat the smoke test of the executable on an isolated desktop or in a container and find the cause of the lost click (criterion 4), then close the issue "Build and smoke-test the executable" | S01 | 2026-10-21 |
| Attach `flash-card.exe` and its checksum when the git host's upload limit is raised (issue "Create the release and attach the files") | S01 | 2026-10-21 |
---
[MIL-012]: ../milestones/mil-012-release-0-2-0.md
[5345fd7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/5345fd77d404b7a8b7c3722fa5ada2bf14f234a7
@@ -0,0 +1,70 @@
# RC-073: Review of BC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-073 |
| CrossReference | [BC-001], [QC-BC-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f86bc91] |
---
## Artifact Under Review
- Instance reviewed: [BC-001]
- Checklist used: [QC-BC-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | The Cost-Benefit table is qualitative and says why: no revenue is expected, so a quantitative return does not apply. |
| 2 | Risks are identified with documented impact and mitigation | Pass | The Risks table has 5 risks, each with an impact and a mitigation. |
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | The Success Criteria table has 8 rows with numeric targets and a measure each (for example 0 third-party packages; 5 commands or fewer). |
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | Scope has `### In Scope` and `### Out of Scope`; the container and sandbox checks are In Scope; macOS and other virtual machines are Out of Scope. |
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | The Stakeholders table cites S01, S02 and S03 and does not re-describe their roles. |
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Methodological and Standards Foundation names the framework, Larman, ISO/IEC 25010:2023 and PEP 8. |
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | Assumptions and Constraints are separate sections. |
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Recommendation reads `Proceed` with a one-sentence rationale. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of BC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the change for MIL-013 (container and sandbox checks).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[BC-001]: ../../../docs/business-case.md
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
@@ -0,0 +1,54 @@
# RC-074: Review of PP-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-074 |
| CrossReference | [PP-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f86bc91] |
---
## Artifact Under Review
- Instance reviewed: [PP-001]
- Checklist used: none exists for this type, and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of PP-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — no checklist exists for the Project Plan type; the plan now schedules MIL-013 on 2026-10-19 inside the Business Case constraint (delivery by 2026-10-21) and the language checklist passes. Accepted as the Product Owner's stand-in, after the Product Owner accepted the MIL-013 plan in chat. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[PP-001]: ../../../docs/project-plan.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+55
View File
@@ -0,0 +1,55 @@
# RC-075: Review of ADR-0005
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-075 |
| CrossReference | [ADR-0005], [QC-ADR-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f86bc91] |
---
## Artifact Under Review
- Instance reviewed: [ADR-0005]
- Checklist used: [QC-ADR-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The ID (`ADR-NNNN`, 4-digit) matches the filename number and is unique and sequential | Pass | ID `ADR-0005` matches the file name number. |
| 2 | Context states the problem, the forces (cost, risk, constraints) and the options evaluated | Pass | Context states the problem (the host desktop is taken over) and four options. |
| 3 | At least two options were considered, or the absence of alternatives is justified | Pass | Four options were considered. |
| 4 | Decision is stated in one or two unhedged sentences and matches one of the evaluated options | Pass | Decision is stated plainly and is option 2. |
| 5 | Consequences list both Positive and Negative outcomes | Pass | Consequences have Positive and Negative lists. |
| 6 | Quality-attribute impacts (performance, security, portability, etc.) are named where the decision affects them | Pass | Portability, repeatability, image size and the weaker fallback are named. |
| 7 | Affected Artifacts are listed as links, and each is also in CrossReference | Pass | Affected Artifacts BC-001 and ADR-0004 are listed and in CrossReference. |
| 8 | Status changes are recorded as new Version History rows with a Change summary and Commit link; retained rows are never rewritten | Pass | First version; no status change yet. |
| 9 | A superseded or deprecated ADR names its successor, and the successor links back | N-A | Not superseded or deprecated. |
| 10 | Written in business or technical language of the domain, with no unexplained abbreviations | Pass | Written in plain technical English; product names are as the vendors write them. |
| 11 | Status is one of `Proposed`, `Accepted`, `Rejected`, `Deprecated` or `Superseded by ADR-NNNN`, and every Version History row uses only these | Pass | Status `Proposed` before this review; uses an allowed value. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[ADR-0005]: ../../../docs/adr/adr-0005-container-and-sandbox-checks.md
[QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+66
View File
@@ -0,0 +1,66 @@
# RC-076: Review of MIL-013
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-076 |
| CrossReference | [MIL-013], [QC-MIL-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f86bc91] |
---
## Artifact Under Review
- Instance reviewed: [MIL-013]
- Checklist used: [QC-MIL-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | A concrete Deliverable section names the output evaluated at the gate. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | The Go / No-Go table has numbered criteria, each with an observable Go and No-Go. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Dependencies table names the milestone or documents it needs. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Traceability table maps to Business Case objectives and the user story. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Ownership table names S01 as owner and approving reviewer. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Target Date lies inside the 2026-10-14 to 2026-10-21 window of the Business Case. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of MIL-013 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Executive English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[MIL-013]: ../../../docs/milestones/mil-013-container-checks.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
+64
View File
@@ -0,0 +1,64 @@
# RC-077: Review of PY-MIL-013
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-077 |
| CrossReference | [PY-MIL-013], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f8ccfca] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-013]
- Checklist used: [QC-PY-001]
- Scope: full review
- Language and domain: n/a (technical type)
- 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 | PEP 8 casing holds in `smoke_driver.py`, `capture_screenshots.py` and the new tests; ruff rule set N passes. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`known_button_point`, `parse_geometry`, `platform_kind`, `window_box`, `progress_ok`). |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass on 83 files; the one `type: ignore` for `os.killpg` names the errors it silences. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 49 source files and on the four tool scripts. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | Process start and stop failures are turned into a failed run with its reason, not swallowed; no bare `except`. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
| 7 | Files, locks and connections are managed with context managers | Pass | Temporary folders are context-managed or removed in `finally`; processes are stopped as a group. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, functions and constants have Doxygen docstrings; `doxygen Doxyfile` covers `src` and ends with 0 warnings. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print` in `src`; the tools write results with `sys.stdout.write` as JSON lines. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | No program class changed; the tools are packaging and check support outside the design classes (ADR-0005). |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 272 tests; the pure parts of the smoke driver and the capture tool are unit tested with fakes (no xdotool, no display); the whole suite passed twice in the Debian container including the real-window e2e tests. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; the container image installs the extras `dev`, `speech`, `tools` and `build`. |
| 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 click point, the geometry parsing and the success rule are one function each in `smoke_driver.py`. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; the tools import only the composition root. |
| 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 | Pure functions with injected `run`, `start` and `sleep`; no function mixes the platform choice with drawing. |
| 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 | `tools/docker/` holds the container scripts and the driver; the layers and the test folders are 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 follow the rule. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Reviewed in the Debian container (python:3.13-slim-bookworm, Python 3.13.16).
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-013]: ../../../tools
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[f8ccfca]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f8ccfca02ef3a518eae472e3acdcd2aef03b5950
+61
View File
@@ -0,0 +1,61 @@
# RC-078: Review of SH-MIL-013
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-078 |
| CrossReference | [SH-MIL-013], [QC-SH-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f8ccfca] |
---
## Artifact Under Review
- Instance reviewed: [SH-MIL-013]
- Checklist used: [QC-SH-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Starts with `#!/usr/bin/env bash` and `set -euo pipefail` (or a comment explains the exception) | Pass | Every executable script starts with `#!/usr/bin/env bash` and `set -euo pipefail`; `start-desktop.sh` is sourced, says so in its header and sets no shell options in the caller. |
| 2 | Every expansion is quoted; lists are arrays; tests use `[[ ]]` and `$(...)` | Pass | Expansions are quoted, tests use `[[ ]]`, command substitutions use `$(...)`; shellcheck reports nothing. |
| 3 | Names follow the conventions: `kebab-case.sh` files, `snake_case` functions and variables, `UPPER_SNAKE` constants and environment variables | Pass | File names are kebab-case; variables are snake_case, environment variables UPPER_SNAKE. |
| 4 | Passes `shellcheck` and `bash -n` with no unexplained `disable` comments | Pass | `shellcheck -x` and `bash -n` pass on all six scripts with no `disable` comment. |
| 5 | Errors go to standard error with an `error:` message and a non-zero exit code; bad or missing arguments print a usage line | Pass | Errors go to standard error with `error:`, a usage line is printed, and bad arguments exit with 2 (`selftest.sh` checks it). |
| 6 | Temporary files use `mktemp` with a `trap ... EXIT` cleanup; no fixed `/tmp` names | Pass | `start-desktop.sh` uses `mktemp -d` with a `trap ... EXIT`; no fixed `/tmp` names remain. |
| 7 | No secret is written in the script, echoed, or put on a command line; secrets come from the environment or a gitignored file | Pass | No secret is used or written. |
| 8 | A script that changes state outside its own directory defaults to a dry run or needs an explicit flag, and says so in its header | Pass | `in-container.sh` is the only script that deletes a folder; its header says so and it refuses the repository and `/` as work folder. |
| 9 | A header comment states purpose, usage, options, environment variables and exit codes | Pass | Each header gives purpose, usage, environment, tools and exit codes. |
| 10 | The script implements a task or design it cites; deviations are recorded | Pass | The scripts implement tasks 3, 4 and 6 of MIL-013 and ADR-0005; the fallback from GNOME Shell to mutter is recorded in the output. |
| 11 | Behaviour is tested for success, failure and any disabled or bypass path | Pass | `selftest.sh` tests the refusals and `bash -n`; the success paths are the container runs of `run-checks.sh`, `screenshots.sh` and `smoke-binary.sh` (recorded in RC-079). |
| 12 | Formatted with `shfmt` (or the project's formatter) | Fail | `shfmt` was not run; Optional, deferred. |
| 13 | Safe to re-run: a second run does not duplicate or corrupt what the first did | Pass | The second container run gave the same result as the first (272 passed, 5 of 5 smoke runs). |
| 14 | Bash version and external tools it needs are stated; GNU-only options are named | Pass | Bash and the external tools are listed under `Needs`; GNU `tar --exclude` is named by the tool list. |
| 15 | A command sequence, constant, path or message used more than once is defined once (a function, a variable or a sourced helper); a helper is shared between scripts only where it expresses the same knowledge | Pass | The desktop start is defined once in `start-desktop.sh` and sourced by the three scripts. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Container run of the self test: 11 cases ok.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion 12 (Optional) failed: Formatted with `shfmt` (or the project's formatter); fix or accept the deferral | S01 | 2026-10-21 |
---
[SH-MIL-013]: ../../../tools/docker
[QC-SH-001]: ../../../framework/qc/code/qc-programming-shell.md
[f8ccfca]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f8ccfca02ef3a518eae472e3acdcd2aef03b5950
@@ -0,0 +1,54 @@
# RC-079: Review of the container checks
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-079 |
| CrossReference | [MIL-013] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [2f192b6] |
---
## Artifact Under Review
- Instance reviewed: [MIL-013] (the milestone's Go / No-Go criteria, checked against the container and sandbox checks)
- Checklist used: none exists for this result; the ten criteria of MIL-013 are the checklist
- Scope: full review
- Language and domain: n/a (record of results)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | `docker build` of the Debian image succeeds | Pass | Image `flash-card-debian` built from `docker/debian.Dockerfile` (base `python:3.13-slim-bookworm`, Python 3.13.16); size 1697 MB; `binutils` was missing at first and was added after the first smoke build failed |
| 2 | Linters and the whole test suite run on the virtual desktop and end with exit code 0, e2e tests not skipped | Pass | `run-checks.sh` twice: ruff, ruff format, mypy (49 files), lint-imports (1 kept) and 272 tests passed in 3.5 s; 33 s per run |
| 3 | The full GNOME session is tried and the result recorded; fallback to mutter stated | Pass | `gnome-shell --x11` starts and then fails with `Gio.IOErrorEnum: Could not connect` in `LoginManagerSystemd` (no login manager in a container); the checks ran on GNOME's window manager mutter 43.8, which the script prints (`desktop: mutter on :99`) |
| 4 | Nothing opens or moves on the host desktop during the runs | Pass | All runs of this record were `docker run` commands with no display; note: earlier unit runs of the test suite on the host by a helper agent opened Tk windows briefly before the Product Owner's instruction; from then on the suite ran only in the container |
| 5 | The screenshot script writes the four PNG files from the Debian image | Pass | `screenshot-level.png`, `-front.png`, `-back.png`, `-menu.png` (20 to 23 KB each) written to the mounted folder and read by eye: the level screen, the card with the menu bar (File, About) are right |
| 6 | The Linux binary is built and passes the smoke test 5 consecutive runs, and the source program too | Pass | `smoke-binary.sh 5` twice: the PyInstaller binary 5 of 5 (0.64 s per run) and `python -m flash_card` 5 of 5 (0.38 s); each run: window found, click at the known button (650, 659), progress file has header plus 2 cards |
| 7 | The Windows executable passes the smoke test inside Windows Sandbox, 5 consecutive runs | Fail | Not run. `tools/sandbox/` (`flash-card.wsb`, `smoke.ps1`, `run.ps1`) is written, the PowerShell parses without errors and the configuration is well-formed XML, but starting Windows Sandbox opens a window on the host, and the Product Owner asked that no app runs on the development machine while working. The run waits for the Product Owner's go |
| 8 | The lost-click symptom of RC-072 is investigated and the finding recorded | Pass | 10 of 10 presses of the known button registered in the container (binary and source). The symptom was seen only with the simulated Windows mouse (about 3 of 8 reruns) and is most likely a timing problem of that click automation (cursor move, focus, button down and up) and not a program defect; this is a finding for Linux only and it is not proven for Windows until criterion 7 is run |
| 9 | README and the wiki Install page in three languages describe the commands and the limits; `check-readme.sh` exit 0 | Pass | Sections added; `check-readme.sh` exit 0; macOS stated as not covered, the Debian check stated as not a Windows check, the sandbox requirements stated |
| 10 | BC-001, PP-001 and ADR-0005 are reviewed Go; code review has no failed Mandatory criterion | Pass | RC-073 to RC-076; RC-077 (Python) and RC-078 (shell) are Go |
## Overall Verdict
Go-with-conditions — everything that can run without the host desktop runs and passes: the Debian checks (272 tests, linters), the screenshots, and the smoke test of the Linux binary and the source program. Criterion 7 (the Windows executable in Windows Sandbox) is not run and keeps the issue "Run and record the checks" open. Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Run `powershell -File tools\sandbox\run.ps1 -Runs 5` after `python tools/build_executable.py` at a time that suits the Product Owner (Windows Sandbox shows its own window), and record the result in a new review record | S01 | 2026-10-21 |
| Decide whether the GNOME Shell start in the container is worth a login-manager stub (it is not needed for the checks) | S01 | 2026-10-21 |
---
[MIL-013]: ../milestones/mil-013-container-checks.md
[2f192b6]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2f192b6936a92f85c6c200de3cac3d6cf3572903
+47
View File
@@ -0,0 +1,47 @@
# RC-080: Windows Sandbox run of the executable
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-080 |
| CrossReference | [MIL-013], [RC-079] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [6e3fcf8] |
---
## Artifact Under Review
- Instance reviewed: [MIL-013], criterion 7 (the Windows executable in Windows Sandbox), which [RC-079] left open
- Checklist used: none exists; criterion 7 of MIL-013 and the open condition of RC-072 (smoke test of the executable)
- Scope: delta re-review of criterion 7 of MIL-013 after the Product Owner started the run
- Language and domain: n/a (record of results)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 7 | The Windows executable passes the smoke test inside Windows Sandbox (window titled Flashy, embedded list, list override, known button writes the progress file), 5 consecutive sandbox runs, with the results read from the mapped output folder | Pass | `tools/sandbox/run.ps1 -Runs 5` on Windows 11 Pro; the sandbox ran `Microsoft Windows 11 Enterprise`; `dist/flash-card.exe` (25.8 MB, built from the head of `main` before the container changes) mapped read-only; **5 of 5 runs passed**: window found, click made by the sandbox's own mouse at the known button, progress file with a header and 2 cards (3 rows), 4.5 to 5.2 s per run including the copy and the start; a screenshot of the sandbox desktop shows the card with the menu bar and both button images; the sandbox shut itself down and no sandbox process was left |
| RC-072 #4 | The executable passes the smoke test of MIL-009 | Pass | The list-override case (`ms-da.csv` next to the program, 3 words, one known word, 2 left) passed in all 5 runs; with the earlier 3 of 8 lost clicks on the host, the finding is that they came from the host click automation, not from the program |
## Overall Verdict
Go — criterion 7 of MIL-013 and the open condition of RC-072 on the smoke test of the executable are met. The issue "Run and record the checks" can be closed. The tag of v0.2.0 is still lightweight (RC-072 criterion 5) and the executable is still not attached to a release (upload limit of the host). Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Decide whether the lightweight tag `v0.2.0` is replaced by an annotated tag (force update of the tag on the host) | S01 | 2026-10-21 |
| Raise the upload limit of the git host so that `flash-card.exe` and its checksum can be attached to a release | S01 | 2026-10-21 |
---
[MIL-013]: ../milestones/mil-013-container-checks.md
[RC-079]: ./rc-079-container-checks.md
[6e3fcf8]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/6e3fcf8aa3d66b9fa84339e1dce90d5f53cbed67
+45
View File
@@ -0,0 +1,45 @@
# RC-081: The tag v0.2.0 is annotated
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-081 |
| CrossReference | [MIL-012], [RC-072] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [8f135b9] |
---
## Artifact Under Review
- Instance reviewed: [MIL-012], criterion 5 (the tag `v0.2.0` is an annotated tag on the commit that is built; `v0.1.0` is not moved), which [RC-072] marked Fail
- Checklist used: none exists; criterion 5 of MIL-012
- Scope: delta re-review of criterion 5 after the Product Owner asked for the tag to be annotated
- Language and domain: n/a (record of results)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 5 | The tag `v0.2.0` is an annotated tag on that commit; `v0.1.0` is not moved | Pass | At the Product Owner's request the lightweight tag on the host was replaced by the annotated tag object `a92bdd9` ("Flash Card 0.2.0: menu bar and progress per word list") with a forced update of that one tag; it dereferences to the same commit `d7ca24d`; `v0.1.0` is unchanged (`2c580cc`); the release `v0.2.0` still exists, is not a draft and points to the tag |
## Overall Verdict
Go — criterion 5 of MIL-012 is met. The remaining condition of RC-072 is the attachment of the executable to the release (upload limit of the git host). Reviewed by AI as stand-in for S01; not independent of the author.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Raise the upload limit of the git host, then attach `flash-card.exe` and its checksum to `v0.2.0` | S01 | 2026-10-21 |
---
[MIL-012]: ../milestones/mil-012-release-0-2-0.md
[RC-072]: ./rc-072-release-v0-2-0.md
[8f135b9]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/8f135b94bf88578c01800eef9ca0fc2d29dff485
+60 -11
View File
@@ -13,7 +13,10 @@
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added MIL-008 and the review records of the level choice (no checklist exists for this type) | [0b983b7] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added MIL-008 and the review records of the level choice (no checklist exists for this type) | [0b983b7] |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [768ca34] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [768ca34] |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [061b4f4] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [061b4f4] |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [9c8e294] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [9c8e294] |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [2211d0f] |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [0264b60] |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the rows and review records of the executable request (no checklist exists for this type) | [f86bc91] |
--- ---
@@ -27,18 +30,18 @@ updated whenever an artifact instance is created or reviewed.
| Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) | | Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| BC-001 | Business Case | en | it | - | SA-001, PP-001 | [RC-051] | | BC-001 | Business Case | en | it | - | SA-001, PP-001 | [RC-073] |
| SA-001 | Stakeholder Analysis | en | it | BC-001 | PP-001, DICT-001 | [RC-002] | | SA-001 | Stakeholder Analysis | en | it | BC-001 | PP-001, DICT-001 | [RC-002] |
| PP-001 | Project Plan | en | it | BC-001, SA-001 | MIL-001 to MIL-008 | [RC-053] | | PP-001 | Project Plan | en | it | BC-001, SA-001 | MIL-001 to MIL-008 | [RC-074] |
| UCD-001 | Use Case Diagram | en | it | SA-001, BC-001 | US-001, UC-001 | [RC-004] | | UCD-001 | Use Case Diagram | en | it | SA-001, BC-001 | US-001, UC-001 | [RC-004] |
| US-001 | User Story | en | it | UCD-001, BC-001 | MIL-002 to MIL-006, MIL-008 | [RC-052] | | US-001 | User Story | en | it | UCD-001, BC-001 | MIL-002 to MIL-006, MIL-008 | [RC-068] |
| UC-001 | Use Case | en | it | UCD-001, US-001 | DM-001, SSD-001 | [RC-033] | | UC-001 | Use Case | en | it | UCD-001, US-001 | DM-001, SSD-001 | [RC-058] |
| DM-001 | Domain Model | en | it | UC-001 | SSD-001, DICT-001, DCD-001 | [RC-034] | | DM-001 | Domain Model | en | it | UC-001 | SSD-001, DICT-001, DCD-001 | [RC-034] |
| SSD-001 | System Sequence Diagram | en | it | UC-001, DM-001 | DCD-001 | [RC-036] | | SSD-001 | System Sequence Diagram | en | it | UC-001, DM-001 | DCD-001 | [RC-059] |
| OC-001 | Operation Contract | - | - | SSD-001, DM-001 | SD-001 | [RC-037] | | OC-001 | Operation Contract | - | - | SSD-001, DM-001 | SD-001 | [RC-060] |
| SD-001 | Sequence Diagram | - | - | OC-001 | DCD-001 | [RC-038] | | SD-001 | Sequence Diagram | - | - | OC-001 | DCD-001 | [RC-061] |
| DCD-001 | Design Class Diagram | - | - | DM-001, SD-001, DICT-001 | src/flash_card | [RC-045] | | DCD-001 | Design Class Diagram | - | - | DM-001, SD-001, DICT-001 | src/flash_card | [RC-062] |
| DICT-001 | Domain Dictionary | en | it | BC-001, SA-001, DM-001 | DCD-001 | [RC-035] | | DICT-001 | Domain Dictionary | en | it | BC-001, SA-001, DM-001 | DCD-001 | [RC-063] |
| ADR-0001 | Architecture Decision Record | - | - | BC-001 | MIL-001, MIL-003, MIL-006 | [RC-013] | | ADR-0001 | Architecture Decision Record | - | - | BC-001 | MIL-001, MIL-003, MIL-006 | [RC-013] |
| ADR-0002 | Architecture Decision Record | - | - | BC-001 | DCD-001 | [RC-014] | | ADR-0002 | Architecture Decision Record | - | - | BC-001 | DCD-001 | [RC-014] |
| ADR-0003 | Architecture Decision Record | - | - | BC-001 | - | [RC-015] | | ADR-0003 | Architecture Decision Record | - | - | BC-001 | - | [RC-015] |
@@ -52,7 +55,11 @@ updated whenever an artifact instance is created or reviewed.
| MIL-008 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-008 | [RC-040] | | MIL-008 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-008 | [RC-040] |
| MIL-009 | Milestone / Gateway | en | it | BC-001 | - | [RC-050] | | MIL-009 | Milestone / Gateway | en | it | BC-001 | - | [RC-050] |
| ADR-0004 | Architecture Decision Record | - | - | BC-001 | - | [RC-046] | | ADR-0004 | Architecture Decision Record | - | - | BC-001 | - | [RC-046] |
| MIL-010 | Milestone / Gateway | en | it | BC-001 | - | [RC-054] | | MIL-010 | Milestone / Gateway | en | it | BC-001 | - | [RC-054], [RC-055] |
| MIL-011 | Milestone / Gateway | en | it | BC-001 | - | [RC-065] |
| MIL-012 | Milestone / Gateway | en | it | BC-001 | - | [RC-070], [RC-072], [RC-081] |
| MIL-013 | Milestone / Gateway | en | it | BC-001 | - | [RC-076], [RC-079], [RC-080] |
| ADR-0005 | Architecture Decision Record | - | - | BC-001 | - | [RC-075] |
| PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] | | PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] |
| PY-MIL-002 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-024] | | PY-MIL-002 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-024] |
| PY-MIL-003 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-025] | | PY-MIL-003 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-025] |
@@ -62,7 +69,11 @@ updated whenever an artifact instance is created or reviewed.
| PY-MIL-007 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-029] | | PY-MIL-007 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-029] |
| PY-MIL-008 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-041] | | PY-MIL-008 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-041] |
| PY-MIL-009 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-048] | | PY-MIL-009 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-048] |
| PY-MIL-011 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-066] |
| PY-FIX-001 | Python Source Code | - | - | PY-MIL-008 | - | [RC-049] | | PY-FIX-001 | Python Source Code | - | - | PY-MIL-008 | - | [RC-049] |
| PY-MIL-012 | Python Source Code | - | - | MIL-012 | - | [RC-071] |
| PY-MIL-013 | Python Source Code | - | - | MIL-013 | - | [RC-077] |
| SH-MIL-013 | Shell Script | - | - | MIL-013 | - | [RC-078] |
## Coverage Notes ## Coverage Notes
@@ -167,3 +178,41 @@ updated whenever an artifact instance is created or reviewed.
[RC-054]: ./reviews/rc-054-mil-010.md [RC-054]: ./reviews/rc-054-mil-010.md
[MIL-010]: ../milestones/mil-010-release.md [MIL-010]: ../milestones/mil-010-release.md
[9c8e294]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/9c8e2940217e615f82eda2d48aca9b2fd58f3687 [9c8e294]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/9c8e2940217e615f82eda2d48aca9b2fd58f3687
[RC-055]: ./reviews/rc-055-release-v0-1-0.md
[RC-056]: ./reviews/rc-056-business-case-menu.md
[RC-057]: ./reviews/rc-057-user-stories-menu.md
[RC-058]: ./reviews/rc-058-use-case-menu.md
[RC-059]: ./reviews/rc-059-system-sequence-diagram-menu.md
[RC-060]: ./reviews/rc-060-operation-contracts-menu.md
[RC-061]: ./reviews/rc-061-sequence-diagrams-menu.md
[RC-062]: ./reviews/rc-062-design-class-diagram-menu.md
[RC-063]: ./reviews/rc-063-domain-dictionary-menu.md
[RC-064]: ./reviews/rc-064-project-plan-menu.md
[RC-065]: ./reviews/rc-065-mil-011.md
[MIL-011]: ../milestones/mil-011-menu-bar.md
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
[RC-066]: ./reviews/rc-066-py-mil-011.md
[PY-MIL-011]: ../../src/flash_card
[RC-067]: ./reviews/rc-067-business-case-r020.md
[RC-068]: ./reviews/rc-068-user-stories-r020.md
[RC-069]: ./reviews/rc-069-project-plan-r020.md
[RC-070]: ./reviews/rc-070-mil-012.md
[MIL-012]: ../milestones/mil-012-release-0-2-0.md
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
[RC-071]: ./reviews/rc-071-py-mil-012.md
[PY-MIL-012]: ../../src/flash_card/__init__.py
[RC-072]: ./reviews/rc-072-release-v0-2-0.md
[RC-073]: ./reviews/rc-073-business-case-containers.md
[RC-074]: ./reviews/rc-074-project-plan-containers.md
[RC-075]: ./reviews/rc-075-adr-0005.md
[RC-076]: ./reviews/rc-076-mil-013.md
[MIL-013]: ../milestones/mil-013-container-checks.md
[ADR-0005]: ../adr/adr-0005-container-and-sandbox-checks.md
[f86bc91]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f86bc9136e7719fbcd049a4f803959019b7d2df3
[RC-077]: ./reviews/rc-077-py-mil-013.md
[RC-078]: ./reviews/rc-078-sh-mil-013.md
[PY-MIL-013]: ../../tools
[SH-MIL-013]: ../../tools/docker
[RC-079]: ./reviews/rc-079-container-checks.md
[RC-080]: ./reviews/rc-080-sandbox-run.md
[RC-081]: ./reviews/rc-081-tag-annotated.md
+116 -3
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the menu contracts | [2211d0f] |
--- ---
@@ -93,6 +93,119 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
| --- | --- | | --- | --- |
| `current_card` is `None` (no card is shown) | Nothing is changed and nothing is stored | | `current_card` is `None` (no card is shown) | Nothing is changed and nothing is stored |
## Contract: openWordList
| Item | Value |
| --- | --- |
| Operation | `openWordList(path: Path): tuple[Level, ...]` |
| Traces to | `openWordList` in [SSD-001] |
| Domain Model concepts | Word List, Study Session, Words To Learn ([DM-001]); IT terms `Vocabulary`, `StudySession`, `Deck` ([DICT-001]) |
**Preconditions**
- `path` names an existing file.
**Postconditions**
- The `Vocabulary` of `path` is the word list in use and its progress file (`words_to_learn.csv` for `ms-da.csv`, else `words_to_learn_<name>.csv` in the data folder) is the progress in use.
- Any earlier `StudySession` was dropped and the levels are returned; no card is shown until a level is chosen.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| The file does not exist or is not a word list (no header of two language codes, a row that is not two cells, or no card) | `VocabularyNotFoundError` or `VocabularyFormatError` is raised; the earlier word list and session stay in use and no file is changed |
## Contract: changeLevel
| Item | Value |
| --- | --- |
| Operation | `changeLevel(): tuple[Level, ...]` |
| Traces to | `changeLevel` in [SSD-001] |
| Domain Model concepts | Level, Study Session ([DM-001]); IT terms `Level`, `StudySession` ([DICT-001]) |
**Preconditions**
- A word list is in use.
**Postconditions**
- The pending flip timer was cancelled, the `StudySession` was dropped and the levels are returned; the progress file was not changed.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| None | – |
## Contract: resetWordList
| Item | Value |
| --- | --- |
| Operation | `resetWordList(): tuple[Level, ...]` |
| Traces to | `resetWordList` in [SSD-001] |
| Domain Model concepts | Words To Learn, Word List ([DM-001]); IT terms `Deck`, `Vocabulary` ([DICT-001]) |
**Preconditions**
- A word list is in use and the Learner confirmed.
**Postconditions**
- The progress file of the word list in use does not exist; the word list file and the progress files of other word lists are unchanged.
- The `StudySession` was dropped and the levels are returned; the next session starts from a `Deck` of the whole `Vocabulary`.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| The progress file does not exist | Nothing is deleted and no error is raised |
| The progress file cannot be deleted (`OSError`) | The error is logged and shown to the Learner; the earlier session stays |
## Contract: showContact
| Item | Value |
| --- | --- |
| Operation | `showContact(): tuple[str, str, str]` |
| Traces to | `showContact` in [SSD-001] |
| Domain Model concepts | none (author information, outside the domain) |
**Preconditions**
- None.
**Postconditions**
- The name, the title and the link of the author are returned and the link was opened in the web browser.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| No web browser can be started | The failure is logged; nothing else changes |
## Contract: exit
| Item | Value |
| --- | --- |
| Operation | `exit(): None` |
| Traces to | `exit` in [SSD-001] |
| Domain Model concepts | Study Session ([DM-001]); IT term `StudySession` ([DICT-001]) |
**Preconditions**
- None.
**Postconditions**
- The pending flip timer was cancelled, the window was closed and the program ended with exit code 0; the progress file is as it was after the last `markKnown`.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| None | – |
## Contract: markUnknown ## Contract: markUnknown
| Item | Value | | Item | Value |
@@ -124,5 +237,5 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
[DM-001]: ../domain-model.md [DM-001]: ../domain-model.md
[DICT-001]: ../dictionary.md [DICT-001]: ../dictionary.md
[SD-001]: ./sd.md [SD-001]: ./sd.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1 [0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+89 -3
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the sequences startStudySession (levels) and chooseLevel (level filter) | [0b983b7] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the sequences startStudySession (levels) and chooseLevel (level filter) | [0b983b7] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the sequences openWordList and resetWordList | [2211d0f] |
--- ---
@@ -109,6 +109,92 @@ deactivate S
The composition root only wires; the level selects, the deck holds, the session coordinates; no object receives all messages. The composition root only wires; the level selects, the deck holds, the session coordinates; no object receives all messages.
## Sequence: openWordList
**Realizes:** `openWordList` in [OC-001]
### Diagram
```plantuml
@startuml
participant ":MenuView" as V
participant ":Application" as A
participant ":CsvVocabularyRepository" as VR
V -> A : open_word_list(path)
activate A
create VR
A -> VR : create(path)
A -> VR : load()
alt file is a word list
VR --> A : vocabulary
A -> A : drop session, use progress file of path
A --> V : levels
else file is refused
VR --> A : VocabularyError
A --> V : message, earlier list stays
end
deactivate A
@enduml
```
### Pattern Annotations
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| Controller | `Application` | Receives the menu command and coordinates the repositories and the session |
| Repository | `CsvVocabularyRepository` | The file format stays in the adapter |
### Postcondition Coverage
| Postcondition (from contract) | Satisfied by message |
| --- | --- |
| The vocabulary and the progress file of `path` are in use | `load()`, `use progress file of path` |
| The earlier session was dropped and the levels are returned | `drop session`, `levels` |
| A refused file leaves everything as it was | the `else` branch |
### Responsibility Check
The view forwards the command; the application validates through the repository before it replaces anything.
## Sequence: resetWordList
**Realizes:** `resetWordList` in [OC-001]
### Diagram
```plantuml
@startuml
participant ":MenuView" as V
participant ":Application" as A
participant ":CsvProgressRepository" as PR
V -> A : reset_word_list()
activate A
A -> PR : reset()
PR --> A : done
A -> A : drop session
A --> V : levels
deactivate A
@enduml
```
### Pattern Annotations
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| Controller | `Application` | Sequences the reset and the return to the level screen |
| Repository | `CsvProgressRepository` | Knows where the progress file is |
### Postcondition Coverage
| Postcondition (from contract) | Satisfied by message |
| --- | --- |
| The progress file of the word list in use does not exist | `reset()` |
| The session was dropped and the levels are returned | `drop session`, `levels` |
### Responsibility Check
The confirmation is asked by the view before `reset_word_list` is called; the repository deletes only its own file.
## Sequence: markKnown ## Sequence: markKnown
**Realizes:** `markKnown` in [OC-001] **Realizes:** `markKnown` in [OC-001]
@@ -209,5 +295,5 @@ Same as `markKnown`; the session is the only object that speaks and draws.
[OC-001]: ./oc.md [OC-001]: ./oc.md
[DCD-001]: ../dcd.md [DCD-001]: ../dcd.md
[DICT-001]: ../dictionary.md [DICT-001]: ../dictionary.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1 [0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+17 -3
View File
@@ -11,8 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the menu operations | [2211d0f] |
--- ---
@@ -36,6 +36,15 @@ L -> S : markKnown()
S --> L : next card (word, language name) S --> L : next card (word, language name)
L -> S : markUnknown() L -> S : markUnknown()
S --> L : next card (word, language name) S --> L : next card (word, language name)
L -> S : openWordList(path)
S --> L : levels, or the reason the file is refused
L -> S : changeLevel()
S --> L : levels
L -> S : resetWordList()
S --> L : levels
L -> S : showContact()
S --> L : name, title, link opened
L -> S : exit()
@enduml @enduml
``` ```
@@ -48,6 +57,11 @@ S --> L : next card (word, language name)
| 3 | (timer) turn the card over | none | the same card, translation side | 6 | | 3 | (timer) turn the card over | none | the same card, translation side | 6 |
| 4 | markKnown | none | next card, or "nothing left to learn" | 7, 8, 9 | | 4 | markKnown | none | next card, or "nothing left to learn" | 7, 8, 9 |
| 5 | markUnknown | none | next card, or "nothing left to learn" | 7a, 9 | | 5 | markUnknown | none | next card, or "nothing left to learn" | 7a, 9 |
| 6 | openWordList | path of a CSV file | the levels, or the reason the file is refused | *a.1 |
| 7 | changeLevel | none | the levels | *a.2 |
| 8 | resetWordList | none (the Learner confirmed) | the levels | *a.3 |
| 9 | showContact | none | the author's name and title; the link is opened | *a.4 |
| 10 | exit | none | none; the program ends | *a.5 |
The turn-over in step 3 is started by the system's own timer, not by a message from the Learner; it is shown as a return without a call, which is the one deviation from "every return answers a message". When the level is given at start (extension 3a of [UC-001]) the Learner's first message is `chooseLevel` and `startStudySession` is not sent. The turn-over in step 3 is started by the system's own timer, not by a message from the Learner; it is shown as a return without a call, which is the one deviation from "every return answers a message". When the level is given at start (extension 3a of [UC-001]) the Learner's first message is `chooseLevel` and `startStudySession` is not sent.
@@ -64,5 +78,5 @@ The system instance lives for one run of the program. The words to learn are cre
[UC-001]: ./uc.md [UC-001]: ./uc.md
[DM-001]: ../domain-model.md [DM-001]: ../domain-model.md
[OC-001]: ./oc.md [OC-001]: ./oc.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1 [0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+10 -3
View File
@@ -11,8 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the choice of level (US-001.06) | [0b983b7] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the choice of level (US-001.06) | [0b983b7] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the menu commands as an extension (US-001.09 to US-001.12) | [2211d0f] |
--- ---
@@ -59,6 +59,12 @@
1. The system tells the Learner that there is nothing left to learn at this level, and further answers change nothing. 1. The system tells the Learner that there is nothing left to learn at this level, and further answers change nothing.
- 4a. The kept progress or the word list is unreadable: - 4a. The kept progress or the word list is unreadable:
1. The system reports the problem to the Learner and ends; it does not overwrite the kept progress. 1. The system reports the problem to the Learner and ends; it does not overwrite the kept progress.
- *a. At any time the Learner uses a menu command:
1. Open a word list: the system asks for a file and checks it; when it is a word list, the system replaces the word list in use and goes on at step 2; when it is not, the system tells the Learner why and keeps the word list in use. When the Learner cancels nothing changes.
2. Change level: the system goes on at step 2 with the word list in use; the kept progress stays.
3. Reset word list: the system asks the Learner to confirm; on yes it removes the kept progress of the word list in use and goes on at step 2, so all its words are to learn again; on no nothing changes.
4. Contact: the system shows the author's name and title and opens the author's LinkedIn page in the web browser.
5. Exit: the system ends the program.
### Special Requirements / Business Rules ### Special Requirements / Business Rules
@@ -70,6 +76,7 @@
| 6 | The turn-over comes three seconds after the card appears | | 6 | The turn-over comes three seconds after the card appears |
| 8 | A word is identified by its Malay and Danish text; the first equal word is removed; known words stay known at every level and words outside the level are kept | | 8 | A word is identified by its Malay and Danish text; the first equal word is removed; known words stay known at every level and words outside the level are kept |
| 4 | The word list names its two languages and holds one word and one translation per card | | 4 | The word list names its two languages and holds one word and one translation per card |
| *a | The kept progress of each word list is separate; resetting one word list never changes another word list or the word list itself |
### Open Issues ### Open Issues
@@ -83,5 +90,5 @@
[SA-001]: ../stakeholder-analysis.md [SA-001]: ../stakeholder-analysis.md
[DM-001]: ../domain-model.md [DM-001]: ../domain-model.md
[SSD-001]: ./ssd.md [SSD-001]: ./ssd.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1 [0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
[2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
+24
View File
@@ -60,3 +60,27 @@ The Product Owner gave this screen in the first prompt (the course's target scre
| Front (first 3000 ms) | `card_front.png` | Malay | the Malay word | black | | Front (first 3000 ms) | `card_front.png` | Malay | the Malay word | black |
| Back (after 3000 ms) | `card_back.png` | Danish | the Danish translation | white | | Back (after 3000 ms) | `card_back.png` | Danish | the Danish translation | white |
| Finished | `card_front.png` | (empty) | "Nothing left to learn" | black | | Finished | `card_front.png` | (empty) | "Nothing left to learn" | black |
## Menu bar and dialogs (all screens)
```text
+----------------------------------------------------------+
| File About | classic menu bar of the window
+----------------------------------------------------------+
File About
+------------------------+ +-----------------------------------------------+
| Open... | | Contact > |
| Change level... | | +-----------------------------------------+|
| Reset word list... | | | Jens Tirsvad Nielsen, Software engineer ||
| ---------------------- | | | (LinkedIn) ||
| Exit | | +-----------------------------------------+|
+------------------------+ +-----------------------------------------------+
```
| Element | Behaviour |
| --- | --- |
| File, Open... | Standard file dialog for `*.csv` (all files allowed), starting in the data folder; a refused file shows an error message with the reason |
| File, Change level... | Replaces the screen by Screen 1 |
| File, Reset word list... | Question "Start `<name>` from the beginning? All words will be to learn again." with Yes and No; Yes replaces the screen by Screen 1 |
| File, Exit | Closes the window |
| About, Contact, the author entry | Opens https://www.linkedin.com/in/tirsvad74 in the web browser |
+64 -8
View File
@@ -4,21 +4,21 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | US-001 | | ID | US-001 |
| CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-008], [MIL-009], [MIL-010] | | CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-008], [MIL-009], [MIL-010], [MIL-011] |
| Language | en | | Language | en |
| Domain | it | | Domain | it |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.07 Start without Python | [768ca34] | | 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.09 to US-001.12 (menu bar) | [2211d0f] |
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.08 Download the executable | [9c8e294] | | 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | US-001.08 names the latest release instead of v0.1.0 | [0264b60] |
--- ---
## Purpose and Scope ## Purpose and Scope
These stories cover the epic "Study Flash Cards": the four steps of the course assignment (the window, new cards, flipping the cards, saving progress), the spoken Malay word the choice of level the start without an installed Python and the download of the executable from a release. Each story is delivered by one milestone. The wireframe the stories follow is in `docs/uc-001/wireframe.md`. These stories cover the epic "Study Flash Cards": the four steps of the course assignment (the window, new cards, flipping the cards, saving progress), the spoken Malay word the choice of level the start without an installed Python the download of the executable from a release and the menu bar. Each story is delivered by one milestone. The wireframe the stories follow is in `docs/uc-001/wireframe.md`.
## Story List ## Story List
@@ -126,7 +126,7 @@ These stories cover the epic "Study Flash Cards": the four steps of the course a
**Acceptance Criteria** **Acceptance Criteria**
- Given the release `v0.1.0` exists, when I open the releases page of the repository, then I find `flash-card.exe` and a checksum file attached to it. - Given a release exists (for example `v0.2.0`), when I open the releases page of the repository, then I find release notes and, when the git host accepts the upload, `flash-card.exe` and a checksum file attached to it.
- Given I downloaded `flash-card.exe`, when I compute its SHA-256 hash, then it equals the value in the checksum file and in the release notes. - Given I downloaded `flash-card.exe`, when I compute its SHA-256 hash, then it equals the value in the checksum file and in the release notes.
- Given I read the release notes, when I look for what the file is, then they say that it is an unsigned Windows build, what the `data` folder next to it holds, and how to build it myself. - Given I read the release notes, when I look for what the file is, then they say that it is an unsigned Windows build, what the `data` folder next to it holds, and how to build it myself.
@@ -134,9 +134,64 @@ These stories cover the epic "Study Flash Cards": the four steps of the course a
| --- | --- | --- | | --- | --- | --- |
| [MIL-010], [UC-001] | fits one iteration | none | | [MIL-010], [UC-001] | fits one iteration | none |
### US-001.09 — Open another word list
**As a** Learner, **I want** to open another word list from a CSV file with the File menu, **so that** I can learn another language pair than the one delivered.
**Acceptance Criteria**
- Given the program is running, when I choose File, Open, then a file dialog lets me pick a CSV file that starts in the data folder.
- Given I picked a valid word list, when the dialog closes, then I choose a level and the cards come from that list, and my progress in it is kept apart from the progress in other lists.
- Given I picked a file that is not a word list (no header of two language codes, or a row that is not two cells), when the dialog closes, then a message names the problem and the earlier word list stays in use.
- Given I cancel the dialog, when it closes, then nothing changes.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [MIL-011], [UC-001] | fits one iteration | none |
### US-001.10 — Change the level while learning
**As a** Learner, **I want** to change the level from the File menu, **so that** I do not have to restart the program to make the word list easier or harder.
**Acceptance Criteria**
- Given a card is shown, when I choose File, Change level, then I see the level choices again.
- Given I choose a level, when the cards continue, then they come from that level and the words I know stay known.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [MIL-011], [UC-001] | fits one iteration | none |
### US-001.11 — Learn a word list from the start again
**As a** Learner, **I want** to reset the current word list from the File menu, **so that** all its words are to learn again.
**Acceptance Criteria**
- Given a word list is open, when I choose File, Reset word list, then I am asked to confirm and the message names the word list.
- Given I confirm, when the reset is done, then the progress of that word list is gone, all its words are to learn again and the level choice is shown; the progress of other word lists and the word list file itself are not changed.
- Given I do not confirm, when the question closes, then nothing changes.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [MIL-011], [UC-001] | fits one iteration | none |
### US-001.12 — Find the author and leave the program
**As a** Learner, **I want** an About menu with the author's contact and a File, Exit command, **so that** I can reach the author and close the program in the usual way.
**Acceptance Criteria**
- Given the program is running, when I choose About, Contact, then I see an entry with the author's name and title (Jens Tirsvad Nielsen, Software engineer) and, when I choose it, the author's LinkedIn page opens in my web browser.
- Given the program is running, when I choose File, Exit, then the window closes and the program ends.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [MIL-011], [UC-001] | fits one iteration | none |
## INVEST Check ## INVEST Check
All eight stories are Independent in delivery order (each builds on the one before but can be accepted alone), Negotiable in wording, Valuable to the Learner, Estimable, Small (one milestone each) and Testable (every criterion is Given/When/Then). Exception: US-001.02 to US-001.08 depend on the window of US-001.01, which is stated in their milestones' Dependencies. All twelve stories are Independent in delivery order (each builds on the one before but can be accepted alone), Negotiable in wording, Valuable to the Learner, Estimable, Small (one milestone each) and Testable (every criterion is Given/When/Then). Exception: US-001.02 to US-001.12 depend on the window of US-001.01, which is stated in their milestones' Dependencies.
--- ---
@@ -151,5 +206,6 @@ All eight stories are Independent in delivery order (each builds on the one befo
[MIL-008]: ./milestones/mil-008-choose-level.md [MIL-008]: ./milestones/mil-008-choose-level.md
[MIL-009]: ./milestones/mil-009-executable.md [MIL-009]: ./milestones/mil-009-executable.md
[MIL-010]: ./milestones/mil-010-release.md [MIL-010]: ./milestones/mil-010-release.md
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54 [MIL-011]: ./milestones/mil-011-menu-bar.md
[9c8e294]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/9c8e2940217e615f82eda2d48aca9b2fd58f3687 [2211d0f]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/2211d0f4cbd8182314d18cbdefd939ae50c39e6d
[0264b60]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0264b6084f26bac5af716ea71596ce3be961fec8
+22 -2
View File
@@ -56,9 +56,9 @@ python -m pytest
Vinduestesten i `tests/e2e/` springes over, når der ikke er en skærm. Vinduestesten i `tests/e2e/` springes over, når der ikke er en skærm.
## Download programfilen (Windows) ## Udgivelse og programfil (Windows)
Hent `flash-card.exe` og `flash-card.exe.sha256` fra [udgivelsessiden](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0). Sammenlign filens SHA-256-hash med kontrolsumfilen (`certutil -hashfile flash-card.exe SHA256` i Windows). Filen er ikke signeret, så Windows kan vise en SmartScreen-advarsel. Udgivelsessiderne for [v0.2.0](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.2.0) og [v0.1.0](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0) har udgivelsesnoterne. Programfilen er endnu ikke vedhæftet, fordi serverens grænse for upload afviser en fil af denne størrelse (HTTP 413); byg den som beskrevet nedenfor. Filen er ikke signeret, så Windows kan vise en SmartScreen-advarsel.
## Byg en programfil (Windows) ## Byg en programfil (Windows)
@@ -74,6 +74,26 @@ Scriptet bruger PyInstaller og skriver `dist/flash-card.exe`: én fil uden konso
- `python tools/build_executable.py --no-speech` udelader talepakkerne; `--clean` bygger forfra. - `python tools/build_executable.py --no-speech` udelader talepakkerne; `--clean` bygger forfra.
- Programfilen er ikke i repositoryet. Byg den på det system, hvor den skal køre; Windows kan vise en SmartScreen-advarsel, fordi filen ikke er signeret. - Programfilen er ikke i repositoryet. Byg den på det system, hvor den skal køre; Windows kan vise en SmartScreen-advarsel, fordi filen ikke er signeret.
## Kontroller uden skrivebord (Docker)
Testene, linterne, skærmbillederne og en røgtest af en Linux-binær kører i en Debian-container på en virtuel skærm, så der ikke åbnes noget vindue på dit eget skrivebord.
```bash
docker build -f docker/debian.Dockerfile -t flash-card-debian .
# linters og hele testsuiten, også testene af det rigtige vindue
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh run-checks.sh
# skærmbilleder til ./build/container-out
docker run --rm -v "$PWD:/repo:ro" -v "$PWD/build/container-out:/out" flash-card-debian bash /repo/tools/docker/in-container.sh screenshots.sh /out
# byg Linux-binæren og tryk på "kender" 5 gange (binær og kildekode)
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh smoke-binary.sh 5
```
I Windows PowerShell skriver du `${PWD}` i stedet for `$PWD`. Repositoryet monteres skrivebeskyttet og kopieres inde i containeren.
- Skrivebordet er Xvfb med GNOMEs vindueshåndtering (mutter). GNOME Shell prøves først og starter ikke i en container (den kræver en login-manager), så scripts falder tilbage til mutter og skriver hvilken der kørte.
- Windows-programfilen kontrolleres i **Windows Sandbox** (Windows 11 Pro, funktionen aktiveret): `powershell -File tools\sandbox\run.ps1 -Runs 5` efter `python tools/build_executable.py`. Sandboxen viser sit eget vindue, mens den kører; klikket sker inde i sandboxen.
- Ikke dækket: macOS (kan ikke køres i Docker på Windows eller Linux, og Apples licens tillader det kun på Apple-hardware), og selve Windows i Debian-containeren.
## Dokumentation af kildekoden ## Dokumentation af kildekoden
```bash ```bash
+22 -2
View File
@@ -56,9 +56,9 @@ python -m pytest
Ujian tetingkap dalam `tests/e2e/` dilangkau apabila tiada paparan. Ujian tetingkap dalam `tests/e2e/` dilangkau apabila tiada paparan.
## Muat turun fail boleh laku (Windows) ## Keluaran dan fail boleh laku (Windows)
Muat turun `flash-card.exe` dan `flash-card.exe.sha256` daripada [halaman keluaran](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0). Bandingkan cincangan SHA-256 fail dengan fail semak jumlah (`certutil -hashfile flash-card.exe SHA256` pada Windows). Fail tidak ditandatangani, jadi Windows mungkin menunjukkan amaran SmartScreen. Halaman keluaran [v0.2.0](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.2.0) dan [v0.1.0](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0) mempunyai nota keluaran. Fail boleh laku belum dilampirkan di sana kerana had muat naik pelayan menolak fail sebesar ini (HTTP 413); bina seperti yang diterangkan di bawah. Fail tidak ditandatangani, jadi Windows mungkin menunjukkan amaran SmartScreen.
## Bina fail boleh laku (Windows) ## Bina fail boleh laku (Windows)
@@ -74,6 +74,26 @@ Skrip menggunakan PyInstaller dan menulis `dist/flash-card.exe`: satu fail tanpa
- `python tools/build_executable.py --no-speech` meninggalkan pakej suara; `--clean` membina semula dari awal. - `python tools/build_executable.py --no-speech` meninggalkan pakej suara; `--clean` membina semula dari awal.
- Fail boleh laku tidak disimpan dalam repositori. Bina di sistem tempat ia akan dijalankan; Windows mungkin menunjukkan amaran SmartScreen kerana fail tidak ditandatangani. - Fail boleh laku tidak disimpan dalam repositori. Bina di sistem tempat ia akan dijalankan; Windows mungkin menunjukkan amaran SmartScreen kerana fail tidak ditandatangani.
## Semakan tanpa desktop (Docker)
Ujian, linter, tangkapan skrin dan ujian asap bagi binari Linux dijalankan dalam kontena Debian pada skrin maya, jadi tiada tetingkap dibuka pada desktop anda sendiri.
```bash
docker build -f docker/debian.Dockerfile -t flash-card-debian .
# linter dan seluruh suite ujian, termasuk ujian tetingkap sebenar
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh run-checks.sh
# tangkapan skrin ke ./build/container-out
docker run --rm -v "$PWD:/repo:ro" -v "$PWD/build/container-out:/out" flash-card-debian bash /repo/tools/docker/in-container.sh screenshots.sh /out
# bina binari Linux dan tekan butang "tahu" 5 kali (binari dan kod sumber)
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh smoke-binary.sh 5
```
Dalam Windows PowerShell tulis `${PWD}` untuk `$PWD`. Repositori dipasang baca sahaja dan disalin di dalam kontena.
- Desktop ialah Xvfb dengan pengurus tetingkap GNOME (mutter). GNOME Shell dicuba dahulu dan tidak bermula dalam kontena (ia memerlukan pengurus log masuk), jadi skrip beralih kepada mutter dan mencetak yang mana dijalankan.
- Fail boleh laku Windows disemak dalam **Windows Sandbox** (Windows 11 Pro, ciri diaktifkan): `powershell -File tools\sandbox\run.ps1 -Runs 5` selepas `python tools/build_executable.py`. Sandbox menunjukkan tetingkapnya sendiri semasa berjalan; klik dibuat di dalam sandbox.
- Tidak dilindungi: macOS (tidak boleh dijalankan dalam Docker pada Windows atau Linux dan lesen Apple hanya membenarkannya pada perkakasan Apple), dan Windows sebenar dalam kontena Debian.
## Dokumentasi kod sumber ## Dokumentasi kod sumber
```bash ```bash
+22 -2
View File
@@ -56,9 +56,9 @@ python -m pytest
The window test in `tests/e2e/` is skipped when no display is available. The window test in `tests/e2e/` is skipped when no display is available.
## Download the executable (Windows) ## Release and executable (Windows)
Download `flash-card.exe` and `flash-card.exe.sha256` from the [release page](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0). Compare the SHA-256 hash of the file with the checksum file (`certutil -hashfile flash-card.exe SHA256` on Windows). The file is not signed, so Windows may show a SmartScreen warning. The release pages of [v0.2.0](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.2.0) and [v0.1.0](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/releases/tag/v0.1.0) have the release notes. The executable is not attached there yet, because the server's upload limit rejects a file of this size (HTTP 413); build it as described below. The file is not signed, so Windows may show a SmartScreen warning.
## Build an executable (Windows) ## Build an executable (Windows)
@@ -74,6 +74,26 @@ The script uses PyInstaller and writes `dist/flash-card.exe`: one file, no conso
- `python tools/build_executable.py --no-speech` leaves the speech packages out; `--clean` rebuilds from scratch. - `python tools/build_executable.py --no-speech` leaves the speech packages out; `--clean` rebuilds from scratch.
- The executable is not committed to the repository. Build it on the system you want to run it on; Windows may show a SmartScreen warning because the file is not signed. - The executable is not committed to the repository. Build it on the system you want to run it on; Windows may show a SmartScreen warning because the file is not signed.
## Checks without a desktop (Docker)
The tests, the linters, the screenshots and a smoke test of a Linux binary run in a Debian container on a virtual screen, so no window opens on your own desktop.
```bash
docker build -f docker/debian.Dockerfile -t flash-card-debian .
# linters and the whole test suite, including the real window tests
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh run-checks.sh
# screenshots into ./build/container-out
docker run --rm -v "$PWD:/repo:ro" -v "$PWD/build/container-out:/out" flash-card-debian bash /repo/tools/docker/in-container.sh screenshots.sh /out
# build the Linux binary and press the known button 5 times (binary and source)
docker run --rm -v "$PWD:/repo:ro" flash-card-debian bash /repo/tools/docker/in-container.sh smoke-binary.sh 5
```
On Windows PowerShell write `${PWD}` for `$PWD`. The repository is mounted read-only and copied inside the container.
- The desktop is Xvfb with GNOME's window manager (mutter). GNOME Shell is tried first and does not start in a container (it needs a login manager), so the scripts fall back to mutter and print which one ran.
- The Windows executable is checked in **Windows Sandbox** (Windows 11 Pro, feature enabled): `powershell -File tools\sandbox\run.ps1 -Runs 5` after `python tools/build_executable.py`. The sandbox shows its own window while it runs; the click is made inside the sandbox.
- Not covered: macOS (it cannot be run in Docker on Windows or Linux and Apple's licence allows it only on Apple hardware), and Windows itself in the Debian container.
## Documentation of the source code ## Documentation of the source code
```bash ```bash
+14
View File
@@ -13,6 +13,20 @@
Et ord, du har markeret som kendt, fjernes og kommer ikke igen. Når der ikke er flere ord på niveauet, står der "Nothing left to learn" på kortet; start programmet igen og vælg et højere niveau. Et ord, du har markeret som kendt, fjernes og kommer ikke igen. Når der ikke er flere ord på niveauet, står der "Nothing left to learn" på kortet; start programmet igen og vælg et højere niveau.
## Menulinjen
![Vinduet med menulinjen](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-menu.png)
| Menu | Kommando | Hvad den gør |
| --- | --- | --- |
| File | Open... | Åbner en anden ordliste fra en CSV-fil (overskrift med to sprogkoder, fx `en;da`). Fremgang gemmes adskilt for hver liste |
| File | Change level... | Viser niveauvalget igen uden genstart; kendte ord forbliver kendte |
| File | Reset word list... | Fjerner efter en bekræftelse fremgangen for den åbne liste, så alle dens ord skal læres igen; listefilen ændres ikke |
| File | Exit | Lukker programmet |
| About | Contact | Forfatteren, Jens Tirsvad Nielsen, softwareudvikler; åbner LinkedIn-siden i din browser |
Fremgangen for den medfølgende `ms-da.csv` er `data/words_to_learn.csv`; en anden liste får `data/words_to_learn_<navn>.csv`. Den åbnede liste huskes ikke efter en genstart.
## Din fremgang ## Din fremgang
- Første gang du trykker på det grønne flueben, skriver programmet `data/words_to_learn.csv` med de ord, du endnu ikke kan. - Første gang du trykker på det grønne flueben, skriver programmet `data/words_to_learn.csv` med de ord, du endnu ikke kan.
+14
View File
@@ -13,6 +13,20 @@
Perkataan yang anda tandakan sebagai diketahui dibuang dan tidak muncul lagi. Apabila tiada perkataan lagi pada tahap itu, kad menunjukkan "Nothing left to learn"; mulakan program semula dan pilih tahap yang lebih tinggi. Perkataan yang anda tandakan sebagai diketahui dibuang dan tidak muncul lagi. Apabila tiada perkataan lagi pada tahap itu, kad menunjukkan "Nothing left to learn"; mulakan program semula dan pilih tahap yang lebih tinggi.
## Bar menu
![Tetingkap dengan bar menu](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-menu.png)
| Menu | Perintah | Apa yang dilakukan |
| --- | --- | --- |
| File | Open... | Membuka senarai perkataan lain daripada fail CSV (baris pertama dengan dua kod bahasa, contohnya `en;da`). Kemajuan disimpan berasingan bagi setiap senarai |
| File | Change level... | Menunjukkan pilihan tahap semula tanpa memulakan semula; perkataan yang diketahui kekal diketahui |
| File | Reset word list... | Selepas pengesahan membuang kemajuan senarai yang dibuka, supaya semua perkataannya perlu dipelajari semula; fail senarai tidak diubah |
| File | Exit | Menutup program |
| About | Contact | Pengarang, Jens Tirsvad Nielsen, jurutera perisian; membuka halaman LinkedIn dalam pelayar anda |
Kemajuan bagi `ms-da.csv` yang disertakan ialah `data/words_to_learn.csv`; senarai lain mendapat `data/words_to_learn_<nama>.csv`. Senarai yang dibuka tidak diingati selepas dimulakan semula.
## Kemajuan anda ## Kemajuan anda
- Kali pertama anda menekan tanda semak hijau, program menulis `data/words_to_learn.csv` dengan perkataan yang anda belum tahu. - Kali pertama anda menekan tanda semak hijau, program menulis `data/words_to_learn.csv` dengan perkataan yang anda belum tahu.
+14
View File
@@ -13,6 +13,20 @@
A word you marked as known is removed and does not come up again. When no word of the level is left, the card says "Nothing left to learn"; restart the program and choose a higher level. A word you marked as known is removed and does not come up again. When no word of the level is left, the card says "Nothing left to learn"; restart the program and choose a higher level.
## The menu bar
![The window with the menu bar](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-menu.png)
| Menu | Command | What it does |
| --- | --- | --- |
| File | Open... | Opens another word list from a CSV file (header with two language codes, for example `en;da`). Progress is kept apart for each list |
| File | Change level... | Shows the level choice again without a restart; known words stay known |
| File | Reset word list... | After a confirmation removes the progress of the open list, so all its words are to learn again; the list file is not changed |
| File | Exit | Closes the program |
| About | Contact | The author, Jens Tirsvad Nielsen, Software engineer; opens the LinkedIn page in your browser |
The progress of the delivered `ms-da.csv` is `data/words_to_learn.csv`; another list gets `data/words_to_learn_<name>.csv`. The opened list is not remembered after a restart.
## Your progress ## Your progress
- The first time you press the green tick, the program writes `data/words_to_learn.csv` with the words you do not know yet. - The first time you press the green tick, the program writes `data/words_to_learn.csv` with the words you do not know yet.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "flash-card" name = "flash-card"
version = "0.1.0" version = "0.2.0"
description = "Flash card program for learning Malay words with Danish translations (Udemy 100 Days of Code, day 31)" description = "Flash card program for learning Malay words with Danish translations (Udemy 100 Days of Code, day 31)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
+1 -1
View File
@@ -4,4 +4,4 @@
@brief Package root of the flash card program. @brief Package root of the flash card program.
""" """
__version__ = "0.1.0" __version__ = "0.2.0"
+74
View File
@@ -78,6 +78,12 @@ CSV_ENCODING: Final[str] = "utf-8-sig"
VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv" VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv"
## @brief File name of the words still to learn. ## @brief File name of the words still to learn.
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv" PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
## @brief File name of the words to learn of another word list; {stem} is its stem.
PROGRESS_FILE_NAME_TEMPLATE: Final[str] = "words_to_learn_{stem}.csv"
## @brief Pattern of the characters replaced in a stem (not letter, digit, - or _).
PROGRESS_STEM_FORBIDDEN: Final[str] = r"[^\w-]"
## @brief Replacement of a forbidden character in the progress file name.
PROGRESS_STEM_REPLACEMENT: Final[str] = "_"
## @brief Root folder of the project, for a source run. ## @brief Root folder of the project, for a source run.
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2] PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
@@ -105,3 +111,71 @@ CARD_BACK_IMAGE_NAME: Final[str] = "card_back.png"
RIGHT_IMAGE_NAME: Final[str] = "right.png" RIGHT_IMAGE_NAME: Final[str] = "right.png"
## @brief File name of the wrong button image. ## @brief File name of the wrong button image.
WRONG_IMAGE_NAME: Final[str] = "wrong.png" WRONG_IMAGE_NAME: Final[str] = "wrong.png"
## @brief Label of the File menu.
MENU_FILE_LABEL: Final[str] = "File"
## @brief Label of the File, Open entry.
MENU_OPEN_LABEL: Final[str] = "Open..."
## @brief Label of the File, Change level entry.
MENU_CHANGE_LEVEL_LABEL: Final[str] = "Change level..."
## @brief Label of the File, Reset word list entry.
MENU_RESET_LABEL: Final[str] = "Reset word list..."
## @brief Label of the File, Exit entry.
MENU_EXIT_LABEL: Final[str] = "Exit"
## @brief Label of the About menu.
MENU_ABOUT_LABEL: Final[str] = "About"
## @brief Label of the About, Contact submenu.
MENU_CONTACT_LABEL: Final[str] = "Contact"
## @brief Index of the mnemonic letter of the File menu.
MENU_FILE_UNDERLINE: Final[int] = 0
## @brief Index of the mnemonic letter of the Open entry.
MENU_OPEN_UNDERLINE: Final[int] = 0
## @brief Index of the mnemonic letter of the Change level entry.
MENU_CHANGE_LEVEL_UNDERLINE: Final[int] = 0
## @brief Index of the mnemonic letter of the Reset word list entry.
MENU_RESET_UNDERLINE: Final[int] = 0
## @brief Index of the mnemonic letter of the Exit entry.
MENU_EXIT_UNDERLINE: Final[int] = 1
## @brief Index of the mnemonic letter of the About menu.
MENU_ABOUT_UNDERLINE: Final[int] = 0
## @brief Index of the mnemonic letter of the Contact submenu.
MENU_CONTACT_UNDERLINE: Final[int] = 0
## @brief Command name of the Open entry, as the menu view maps it.
MENU_COMMAND_OPEN: Final[str] = "open_word_list"
## @brief Command name of the Change level entry.
MENU_COMMAND_CHANGE_LEVEL: Final[str] = "change_level"
## @brief Command name of the Reset word list entry.
MENU_COMMAND_RESET: Final[str] = "reset_word_list"
## @brief Command name of the Exit entry.
MENU_COMMAND_EXIT: Final[str] = "exit_program"
## @brief Name of the author shown under About, Contact.
CONTACT_NAME: Final[str] = "Jens Tirsvad Nielsen"
## @brief Title of the author shown under About, Contact.
CONTACT_TITLE: Final[str] = "Software engineer"
## @brief LinkedIn page of the author.
CONTACT_URL: Final[str] = "https://www.linkedin.com/in/tirsvad74"
## @brief Text of the contact entry: name and title; {name} and {title}.
CONTACT_LABEL_TEMPLATE: Final[str] = "{name}, {title}"
## @brief Title of the open file dialog.
OPEN_DIALOG_TITLE: Final[str] = "Open a word list"
## @brief File types of the open file dialog: description and pattern.
OPEN_FILE_TYPES: Final[tuple[tuple[str, str], ...]] = (
("CSV files", "*.csv"),
("All files", "*.*"),
)
## @brief Title of the reset confirmation.
RESET_CONFIRM_TITLE: Final[str] = "Reset word list"
## @brief Question of the reset confirmation; {name} is the word list file name.
RESET_CONFIRM_TEMPLATE: Final[str] = (
"Start {name} from the beginning? All words will be to learn again."
)
## @brief Title of the message that tells a word list could not be opened.
OPEN_ERROR_TITLE: Final[str] = "Cannot open word list"
## @brief Message of a word list that could not be opened; {name} and {reason}.
OPEN_ERROR_TEMPLATE: Final[str] = "{name} could not be used.\n{reason}"
## @brief Title of the message that tells a word list could not be reset.
RESET_ERROR_TITLE: Final[str] = "Cannot reset word list"
## @brief Message of a word list that could not be reset; {name} and {reason}.
RESET_ERROR_TEMPLATE: Final[str] = "{name} could not be reset.\n{reason}"
@@ -27,3 +27,11 @@ class ProgressRepository(Protocol):
@return Nothing. @return Nothing.
""" """
... ...
def reset(self) -> None:
"""Forget the saved words to learn, so every word is to learn again.
@return Nothing.
@throws OSError When the saved progress cannot be removed.
"""
...
@@ -42,3 +42,11 @@ class CsvProgressRepository:
@return Nothing. @return Nothing.
""" """
write_word_list(self._path, deck.language_pair, deck.cards) write_word_list(self._path, deck.language_pair, deck.cards)
def reset(self) -> None:
"""Delete the words to learn file; nothing happens when it is absent.
@return Nothing.
@throws OSError When the file cannot be deleted.
"""
self._path.unlink(missing_ok=True)
@@ -67,7 +67,8 @@ def read_word_list(path: Path) -> tuple[LanguagePair, tuple[Card, ...]]:
@param path The CSV file: a header of two language codes, then one card a row. @param path The CSV file: a header of two language codes, then one card a row.
@return The language pair and the cards in file order. @return The language pair and the cards in file order.
@throws VocabularyNotFoundError When the file does not exist. @throws VocabularyNotFoundError When the file does not exist.
@throws VocabularyFormatError When the file is empty or a row is malformed. @throws VocabularyFormatError When the file is empty, not UTF-8 text, cannot
be read or a row is malformed.
""" """
pair: LanguagePair | None = None pair: LanguagePair | None = None
cards: list[Card] = [] cards: list[Card] = []
@@ -83,6 +84,10 @@ def read_word_list(path: Path) -> tuple[LanguagePair, tuple[Card, ...]]:
cards.append(_parse_card(path, reader.line_num, row)) cards.append(_parse_card(path, reader.line_num, row))
except FileNotFoundError as error: except FileNotFoundError as error:
raise VocabularyNotFoundError(f"word list not found: {path}") from error raise VocabularyNotFoundError(f"word list not found: {path}") from error
except UnicodeDecodeError as error:
raise VocabularyFormatError(f"{path}: file is not UTF-8 text") from error
except OSError as error:
raise VocabularyFormatError(f"{path}: cannot be read: {error}") from error
if pair is None: if pair is None:
raise _format_error(path, 1, "file is empty") raise _format_error(path, 1, "file is empty")
return pair, tuple(cards) return pair, tuple(cards)
@@ -0,0 +1,30 @@
"""Name of the words to learn file of a word list.
@file progress_file_name.py
@brief Keeps the progress of every word list in a file of its own.
"""
import re
from pathlib import Path
from flash_card.constants import (
PROGRESS_FILE_NAME,
PROGRESS_FILE_NAME_TEMPLATE,
PROGRESS_STEM_FORBIDDEN,
PROGRESS_STEM_REPLACEMENT,
VOCABULARY_FILE_NAME,
)
def progress_file_name(list_path: Path) -> str:
"""Name the words to learn file of a word list.
@param list_path Path of the word list file.
@return The default file name for the delivered list, else a name made of
the list's file name stem with other characters than letters, digits,
hyphen and underscore replaced by an underscore.
"""
if list_path.name == VOCABULARY_FILE_NAME:
return PROGRESS_FILE_NAME
stem = re.sub(PROGRESS_STEM_FORBIDDEN, PROGRESS_STEM_REPLACEMENT, list_path.stem)
return PROGRESS_FILE_NAME_TEMPLATE.format(stem=stem)
+120 -17
View File
@@ -1,7 +1,7 @@
"""Entry point and composition root of the flash card program. """Entry point and composition root of the flash card program.
@file app.py @file app.py
@brief Creates the window, shows the level screen and then the card screen. @brief Creates the window and menu bar, shows the level screen and then the card.
""" """
import argparse import argparse
@@ -19,7 +19,6 @@ from flash_card.constants import (
BUNDLED_VOCABULARY_PATH, BUNDLED_VOCABULARY_PATH,
DEFAULT_DATA_DIR, DEFAULT_DATA_DIR,
LEVEL_ARGUMENT_VALUES, LEVEL_ARGUMENT_VALUES,
PROGRESS_FILE_NAME,
VOCABULARY_FILE_NAME, VOCABULARY_FILE_NAME,
) )
from flash_card.domain.entities.card import Card from flash_card.domain.entities.card import Card
@@ -33,6 +32,9 @@ from flash_card.infrastructure.persistence.csv_progress_repository import (
from flash_card.infrastructure.persistence.csv_vocabulary_repository import ( from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
CsvVocabularyRepository, CsvVocabularyRepository,
) )
from flash_card.infrastructure.persistence.progress_file_name import (
progress_file_name,
)
from flash_card.infrastructure.speech.gtts_speech import GttsSpeech from flash_card.infrastructure.speech.gtts_speech import GttsSpeech
from flash_card.infrastructure.speech.null_speech import NullSpeech from flash_card.infrastructure.speech.null_speech import NullSpeech
from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView
@@ -41,6 +43,8 @@ from flash_card.presentation.features.flash_card.flash_card_view_model import (
) )
from flash_card.presentation.features.level.level_view import LevelView from flash_card.presentation.features.level.level_view import LevelView
from flash_card.presentation.features.level.level_view_model import LevelViewModel from flash_card.presentation.features.level.level_view_model import LevelViewModel
from flash_card.presentation.features.menu.menu_view import MenuView
from flash_card.presentation.features.menu.menu_view_model import MenuViewModel
## @brief Logger of the entry point. ## @brief Logger of the entry point.
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -64,34 +68,42 @@ def build_speech(enabled: bool) -> SpeechPort:
class Application: class Application:
"""The running program: one window that shows the level screen, then the card.""" """The running program: one window with a menu, the level screen and the card."""
def __init__( def __init__(
self, self,
root: tk.Tk, root: tk.Tk,
vocabulary_repository: VocabularyRepository,
progress_repository: ProgressRepository,
speech: SpeechPort, speech: SpeechPort,
choose: Callable[[Sequence[Card]], Card] = random.choice, choose: Callable[[Sequence[Card]], Card],
repositories_for: Callable[
[Path], tuple[VocabularyRepository, ProgressRepository]
],
list_path: Path,
) -> None: ) -> None:
"""Create the application on a window; no screen is shown yet. """Create the application on a window; no screen is shown yet.
@param root The Tk root window. @param root The Tk root window.
@param vocabulary_repository Source of the delivered word list.
@param progress_repository Store of the words still to learn.
@param speech Speaks the front text of each drawn card. @param speech Speaks the front text of each drawn card.
@param choose Picks one card from a non-empty sequence. @param choose Picks one card from a non-empty sequence.
@param repositories_for Builds the word list and progress repositories of
a word list file; the composition root supplies it.
@param list_path The word list file in use.
""" """
## @brief The Tk root window. ## @brief The Tk root window.
self.root = root self.root = root
## @brief Source of the delivered word list.
self._vocabulary_repository = vocabulary_repository
## @brief Store of the words still to learn.
self._progress_repository = progress_repository
## @brief Speaks the front text of each drawn card. ## @brief Speaks the front text of each drawn card.
self._speech = speech self._speech = speech
## @brief Picks one card from a non-empty sequence. ## @brief Picks one card from a non-empty sequence.
self._choose = choose self._choose = choose
## @brief Builds the repositories of a word list file.
self._repositories_for = repositories_for
## @brief The word list file in use.
self._list_path = list_path
vocabulary_repository, progress_repository = repositories_for(list_path)
## @brief Source of the word list in use.
self._vocabulary_repository = vocabulary_repository
## @brief Store of the words still to learn of the word list in use.
self._progress_repository = progress_repository
## @brief The screen shown now, or None before the first one. ## @brief The screen shown now, or None before the first one.
self._view: LevelView | FlashCardView | None = None self._view: LevelView | FlashCardView | None = None
## @brief The exit code of the program. ## @brief The exit code of the program.
@@ -113,6 +125,22 @@ class Application:
""" """
return self._exit_code return self._exit_code
@property
def word_list_name(self) -> str:
"""Return the file name of the word list in use.
@return The file name.
"""
return self._list_path.name
@property
def word_list_folder(self) -> Path:
"""Return the folder of the word list in use.
@return The folder that holds the word list file.
"""
return self._list_path.parent
def show_level_screen(self) -> None: def show_level_screen(self) -> None:
"""Show the level screen; a choice starts the session. """Show the level screen; a choice starts the session.
@@ -134,9 +162,60 @@ class Application:
self._choose, self._choose,
level, level,
) )
self._close_screen()
self._view = FlashCardView(self.root, FlashCardViewModel(session))
def open_word_list(self, path: Path) -> None:
"""Switch to another word list and show the level screen.
The file is loaded first; nothing changes when it is unusable.
@param path The CSV word list file.
@return Nothing.
@throws VocabularyError When the file is missing or malformed.
"""
vocabulary_repository, progress_repository = self._repositories_for(path)
vocabulary_repository.load()
self._vocabulary_repository = vocabulary_repository
self._progress_repository = progress_repository
self._list_path = path
self.change_level()
def change_level(self) -> None:
"""Drop the current screen and its timer and show the level screen.
@return Nothing.
"""
self._close_screen()
self.show_level_screen()
def reset_word_list(self) -> None:
"""Forget the progress of the word list in use and show the level screen.
@return Nothing.
@throws OSError When the progress cannot be removed.
"""
self._progress_repository.reset()
self.change_level()
def exit_application(self) -> None:
"""Drop the current screen and its timer and close the window.
@return Nothing.
"""
self._close_screen()
self.root.destroy()
def _close_screen(self) -> None:
"""Remove the screen shown now, if any.
@return Nothing.
"""
if isinstance(self._view, LevelView): if isinstance(self._view, LevelView):
self._view.destroy() self._view.destroy()
self._view = FlashCardView(self.root, FlashCardViewModel(session)) elif isinstance(self._view, FlashCardView):
self._view.close()
self._view = None
def _choose_level(self, level: Level) -> None: def _choose_level(self, level: Level) -> None:
"""Start the session after a click; close the window when it fails. """Start the session after a click; close the window when it fails.
@@ -163,20 +242,42 @@ def resolve_data_files(data_dir: Path | None) -> tuple[Path, Path]:
vocabulary = folder / VOCABULARY_FILE_NAME vocabulary = folder / VOCABULARY_FILE_NAME
if data_dir is None and not vocabulary.exists(): if data_dir is None and not vocabulary.exists():
vocabulary = BUNDLED_VOCABULARY_PATH vocabulary = BUNDLED_VOCABULARY_PATH
return vocabulary, folder / PROGRESS_FILE_NAME return vocabulary, folder / progress_file_name(vocabulary)
def _repository_builder(
folder: Path,
) -> Callable[[Path], tuple[VocabularyRepository, ProgressRepository]]:
"""Make the function that builds the CSV repositories of a word list.
@param folder The data folder that holds every progress file.
@return A function from a word list path to its repositories; the progress
file is in the data folder, named after the list.
"""
def build(list_path: Path) -> tuple[VocabularyRepository, ProgressRepository]:
return (
CsvVocabularyRepository(list_path),
CsvProgressRepository(folder / progress_file_name(list_path)),
)
return build
def create_application( def create_application(
data_dir: Path | None = None, data_dir: Path | None = None,
speech_enabled: bool = True, speech_enabled: bool = True,
level: Level | None = None, level: Level | None = None,
word_list: Path | None = None,
) -> Application: ) -> Application:
"""Create the window and show the level screen, or start at the given level. """Create the window with its menu bar and show the level screen, or start.
@param data_dir Folder of the data files, or None for the default folder @param data_dir Folder of the data files, or None for the default folder
with the delivered word list as fallback. with the delivered word list as fallback.
@param speech_enabled False to run without spoken words. @param speech_enabled False to run without spoken words.
@param level The level to start at, or None to show the level screen. @param level The level to start at, or None to show the level screen.
@param word_list A word list file to use instead of the one of the data
folder, or None; its progress file is still in the data folder.
@return The application, its window in the root attribute. @return The application, its window in the root attribute.
@throws VocabularyError When a level is given and the word list or saved @throws VocabularyError When a level is given and the word list or saved
progress is unusable. progress is unusable.
@@ -185,10 +286,12 @@ def create_application(
root = tk.Tk() root = tk.Tk()
application = Application( application = Application(
root, root,
CsvVocabularyRepository(vocabulary_path),
CsvProgressRepository(progress_path),
build_speech(speech_enabled), build_speech(speech_enabled),
random.choice,
_repository_builder(progress_path.parent),
vocabulary_path if word_list is None else word_list,
) )
MenuView(root, MenuViewModel(), application)
if level is None: if level is None:
application.show_level_screen() application.show_level_screen()
return application return application
@@ -146,6 +146,15 @@ class FlashCardView:
""" """
return self._card_back_image return self._card_back_image
def close(self) -> None:
"""Cancel the flip timer and remove the widgets of the screen.
@return Nothing.
"""
self._cancel_flip_timer()
for widget in (self.canvas, self.wrong_button, self.right_button):
widget.destroy()
def next_card(self) -> None: def next_card(self) -> None:
"""Show the next card front and restart the flip timer. """Show the next card front and restart the flip timer.
@@ -0,0 +1 @@
"""Menu bar feature."""
@@ -0,0 +1,241 @@
"""View of the menu bar.
@file menu_view.py
@brief Builds the menu bar of the wireframe and asks the dialogs of its commands.
"""
import logging
import tkinter as tk
import webbrowser
from collections.abc import Callable
from pathlib import Path
from tkinter import filedialog, messagebox
from typing import Protocol
from flash_card.constants import (
MENU_ABOUT_UNDERLINE,
MENU_COMMAND_CHANGE_LEVEL,
MENU_COMMAND_EXIT,
MENU_COMMAND_OPEN,
MENU_COMMAND_RESET,
MENU_CONTACT_UNDERLINE,
MENU_FILE_UNDERLINE,
OPEN_DIALOG_TITLE,
OPEN_ERROR_TEMPLATE,
OPEN_ERROR_TITLE,
OPEN_FILE_TYPES,
RESET_CONFIRM_TEMPLATE,
RESET_CONFIRM_TITLE,
RESET_ERROR_TEMPLATE,
RESET_ERROR_TITLE,
)
from flash_card.domain.errors.vocabulary_errors import VocabularyError
from flash_card.presentation.features.menu.menu_view_model import MenuViewModel
## @brief Logger of the menu view.
_logger = logging.getLogger(__name__)
class MenuApplication(Protocol):
"""What the menu view needs from the application."""
@property
def word_list_name(self) -> str:
"""Return the file name of the word list in use.
@return The file name.
"""
...
@property
def word_list_folder(self) -> Path:
"""Return the folder of the word list in use.
@return The folder.
"""
...
def open_word_list(self, path: Path) -> None:
"""Switch to the word list in a file.
@param path The CSV file.
@return Nothing.
"""
...
def change_level(self) -> None:
"""Show the level screen.
@return Nothing.
"""
...
def reset_word_list(self) -> None:
"""Forget the progress of the word list in use.
@return Nothing.
"""
...
def exit_application(self) -> None:
"""Close the window.
@return Nothing.
"""
...
class MenuView:
"""The menu bar of the window: File and About."""
def __init__(
self,
root: tk.Tk,
view_model: MenuViewModel,
application: MenuApplication,
*,
ask_open_file: Callable[..., str] = filedialog.askopenfilename,
confirm: Callable[..., bool] = messagebox.askyesno,
show_error: Callable[..., str] = messagebox.showerror,
open_url: Callable[[str], bool] = webbrowser.open,
) -> None:
"""Build the menu bar on a root.
@param root The Tk root window.
@param view_model The labels and the contact data.
@param application The commands the entries run.
@param ask_open_file Shows the file dialog and returns the chosen path,
or an empty value when cancelled.
@param confirm Asks a yes or no question.
@param show_error Shows an error message.
@param open_url Opens an address in the web browser.
"""
## @brief The Tk root window.
self._root = root
## @brief The labels and the contact data.
self._view_model = view_model
## @brief The commands the entries run.
self._application = application
## @brief Shows the file dialog.
self._ask_open_file = ask_open_file
## @brief Asks a yes or no question.
self._confirm = confirm
## @brief Shows an error message.
self._show_error = show_error
## @brief Opens an address in the web browser.
self._open_url = open_url
## @brief The menu bar of the window.
self.menubar = tk.Menu(root, tearoff=False)
## @brief The File menu.
self.file_menu = tk.Menu(self.menubar, tearoff=False)
## @brief The About menu.
self.about_menu = tk.Menu(self.menubar, tearoff=False)
## @brief The Contact submenu of the About menu.
self.contact_menu = tk.Menu(self.about_menu, tearoff=False)
self._build()
root.config(menu=self.menubar)
def open_word_list(self) -> None:
"""Ask for a CSV file and switch to it; a refused file shows a message.
@return Nothing.
"""
chosen = self._ask_open_file(
parent=self._root,
title=OPEN_DIALOG_TITLE,
initialdir=str(self._application.word_list_folder),
filetypes=OPEN_FILE_TYPES,
)
if not chosen:
return
path = Path(chosen)
try:
self._application.open_word_list(path)
except VocabularyError as error:
_logger.error("%s", error)
self._show_error(
OPEN_ERROR_TITLE,
OPEN_ERROR_TEMPLATE.format(name=path.name, reason=error),
parent=self._root,
)
def change_level(self) -> None:
"""Show the level screen again.
@return Nothing.
"""
self._application.change_level()
def reset_word_list(self) -> None:
"""Ask to confirm, then start the word list in use from the beginning.
@return Nothing.
"""
name = self._application.word_list_name
question = RESET_CONFIRM_TEMPLATE.format(name=name)
if not self._confirm(RESET_CONFIRM_TITLE, question, parent=self._root):
return
try:
self._application.reset_word_list()
except OSError as error:
_logger.error("%s", error)
self._show_error(
RESET_ERROR_TITLE,
RESET_ERROR_TEMPLATE.format(name=name, reason=error),
parent=self._root,
)
def show_contact(self) -> None:
"""Open the LinkedIn page of the author in the web browser.
@return Nothing.
"""
try:
self._open_url(self._view_model.contact_url)
except (webbrowser.Error, OSError) as error:
_logger.error("%s", error)
def exit_program(self) -> None:
"""Close the window.
@return Nothing.
"""
self._application.exit_application()
def _build(self) -> None:
"""Fill the menus and add them to the menu bar.
@return Nothing.
"""
commands = {
MENU_COMMAND_OPEN: self.open_word_list,
MENU_COMMAND_CHANGE_LEVEL: self.change_level,
MENU_COMMAND_RESET: self.reset_word_list,
MENU_COMMAND_EXIT: self.exit_program,
}
for entry in self._view_model.file_entries:
if entry.command == MENU_COMMAND_EXIT:
self.file_menu.add_separator()
self.file_menu.add_command(
label=entry.label,
underline=entry.underline,
command=commands[entry.command],
)
self.contact_menu.add_command(
label=self._view_model.contact_label, command=self.show_contact
)
self.about_menu.add_cascade(
label=self._view_model.contact_menu_label,
underline=MENU_CONTACT_UNDERLINE,
menu=self.contact_menu,
)
self.menubar.add_cascade(
label=self._view_model.file_label,
underline=MENU_FILE_UNDERLINE,
menu=self.file_menu,
)
self.menubar.add_cascade(
label=self._view_model.about_label,
underline=MENU_ABOUT_UNDERLINE,
menu=self.about_menu,
)
@@ -0,0 +1,102 @@
"""View model of the menu bar.
@file menu_view_model.py
@brief Exposes the labels, the order and the contact data; free of Tkinter.
"""
from dataclasses import dataclass
from flash_card.constants import (
CONTACT_LABEL_TEMPLATE,
CONTACT_NAME,
CONTACT_TITLE,
CONTACT_URL,
MENU_ABOUT_LABEL,
MENU_CHANGE_LEVEL_LABEL,
MENU_CHANGE_LEVEL_UNDERLINE,
MENU_COMMAND_CHANGE_LEVEL,
MENU_COMMAND_EXIT,
MENU_COMMAND_OPEN,
MENU_COMMAND_RESET,
MENU_CONTACT_LABEL,
MENU_EXIT_LABEL,
MENU_EXIT_UNDERLINE,
MENU_FILE_LABEL,
MENU_OPEN_LABEL,
MENU_OPEN_UNDERLINE,
MENU_RESET_LABEL,
MENU_RESET_UNDERLINE,
)
@dataclass(frozen=True, slots=True)
class MenuEntry:
"""One entry of the File menu."""
## @brief Text of the entry.
label: str
## @brief Name of the command of the menu view the entry runs.
command: str
## @brief Index of the mnemonic letter in the label.
underline: int
class MenuViewModel:
"""Data the menu view shows."""
@property
def file_label(self) -> str:
"""Return the label of the File menu.
@return The label.
"""
return MENU_FILE_LABEL
@property
def file_entries(self) -> tuple[MenuEntry, ...]:
"""Return the entries of the File menu.
@return Open, Change level, Reset word list and Exit, in this order.
"""
return (
MenuEntry(MENU_OPEN_LABEL, MENU_COMMAND_OPEN, MENU_OPEN_UNDERLINE),
MenuEntry(
MENU_CHANGE_LEVEL_LABEL,
MENU_COMMAND_CHANGE_LEVEL,
MENU_CHANGE_LEVEL_UNDERLINE,
),
MenuEntry(MENU_RESET_LABEL, MENU_COMMAND_RESET, MENU_RESET_UNDERLINE),
MenuEntry(MENU_EXIT_LABEL, MENU_COMMAND_EXIT, MENU_EXIT_UNDERLINE),
)
@property
def about_label(self) -> str:
"""Return the label of the About menu.
@return The label.
"""
return MENU_ABOUT_LABEL
@property
def contact_menu_label(self) -> str:
"""Return the label of the Contact submenu.
@return The label.
"""
return MENU_CONTACT_LABEL
@property
def contact_label(self) -> str:
"""Return the text of the contact entry.
@return The name and the title of the author.
"""
return CONTACT_LABEL_TEMPLATE.format(name=CONTACT_NAME, title=CONTACT_TITLE)
@property
def contact_url(self) -> str:
"""Return the address the contact entry opens.
@return The LinkedIn page of the author.
"""
return CONTACT_URL
+49 -10
View File
@@ -6,7 +6,7 @@
import shutil import shutil
import tkinter as tk import tkinter as tk
from collections.abc import Iterator, Sequence from collections.abc import Callable, Iterator, Sequence
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -21,13 +21,14 @@ from flash_card.constants import (
LEVEL_SCREEN_HEIGHT, LEVEL_SCREEN_HEIGHT,
LEVEL_TITLE, LEVEL_TITLE,
LEVEL_TITLE_FONT, LEVEL_TITLE_FONT,
PROGRESS_FILE_NAME,
VOCABULARY_FILE_NAME, VOCABULARY_FILE_NAME,
WINDOW_PADDING, WINDOW_PADDING,
WINDOW_TITLE, WINDOW_TITLE,
) )
from flash_card.domain.entities.card import Card from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck from flash_card.domain.entities.deck import Deck
from flash_card.domain.repositories.progress_repository import ProgressRepository
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
from flash_card.domain.value_objects.level import Level from flash_card.domain.value_objects.level import Level
from flash_card.infrastructure.persistence.csv_progress_repository import ( from flash_card.infrastructure.persistence.csv_progress_repository import (
CsvProgressRepository, CsvProgressRepository,
@@ -35,6 +36,9 @@ from flash_card.infrastructure.persistence.csv_progress_repository import (
from flash_card.infrastructure.persistence.csv_vocabulary_repository import ( from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
CsvVocabularyRepository, CsvVocabularyRepository,
) )
from flash_card.infrastructure.persistence.progress_file_name import (
progress_file_name,
)
from flash_card.infrastructure.speech.null_speech import NullSpeech from flash_card.infrastructure.speech.null_speech import NullSpeech
from flash_card.presentation import app from flash_card.presentation import app
from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView
@@ -57,15 +61,25 @@ def _word(view: FlashCardView) -> str:
def _clear(root: tk.Tk, view: object) -> None: def _clear(root: tk.Tk, view: object) -> None:
"""Destroy the widgets of whichever screen is shown, and pending timers.""" """Destroy the widgets of whichever screen is shown, and pending timers."""
if isinstance(view, FlashCardView): if isinstance(view, FlashCardView):
if view.flip_timer is not None: view.close()
root.after_cancel(view.flip_timer)
view.flip_timer = None
for widget in (view.canvas, view.wrong_button, view.right_button):
widget.destroy()
elif isinstance(view, LevelView): elif isinstance(view, LevelView):
view.destroy() view.destroy()
def repositories_for(
folder: Path,
) -> Callable[[Path], tuple[VocabularyRepository, ProgressRepository]]:
"""Build the CSV repositories of a list, its progress file in folder."""
def build(list_path: Path) -> tuple[VocabularyRepository, ProgressRepository]:
return (
CsvVocabularyRepository(list_path),
CsvProgressRepository(folder / progress_file_name(list_path)),
)
return build
@pytest.fixture @pytest.fixture
def data_dir(tmp_path: Path) -> Path: def data_dir(tmp_path: Path) -> Path:
shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, tmp_path) shutil.copy(DEFAULT_DATA_DIR / VOCABULARY_FILE_NAME, tmp_path)
@@ -76,10 +90,10 @@ def data_dir(tmp_path: Path) -> Path:
def application(tk_root: tk.Tk, data_dir: Path) -> Iterator[app.Application]: def application(tk_root: tk.Tk, data_dir: Path) -> Iterator[app.Application]:
instance = app.Application( instance = app.Application(
tk_root, tk_root,
CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME),
CsvProgressRepository(data_dir / PROGRESS_FILE_NAME),
NullSpeech(), NullSpeech(),
_last, _last,
repositories_for(data_dir),
data_dir / VOCABULARY_FILE_NAME,
) )
yield instance yield instance
_clear(tk_root, instance.view) _clear(tk_root, instance.view)
@@ -185,7 +199,7 @@ def test_level_whose_words_are_all_known_shows_the_finished_message(
) -> None: ) -> None:
cards = CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().cards cards = CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().cards
pair = CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().language_pair pair = CsvVocabularyRepository(data_dir / VOCABULARY_FILE_NAME).load().language_pair
CsvProgressRepository(data_dir / PROGRESS_FILE_NAME).save(Deck(pair, cards[100:])) CsvProgressRepository(data_dir / "words_to_learn.csv").save(Deck(pair, cards[100:]))
application.show_level_screen() application.show_level_screen()
level_view = application.view level_view = application.view
assert isinstance(level_view, LevelView) assert isinstance(level_view, LevelView)
@@ -207,8 +221,10 @@ def test_create_application_with_level_skips_the_level_screen(
try: try:
assert isinstance(application.view, FlashCardView) assert isinstance(application.view, FlashCardView)
assert not [c for c in tk_root.winfo_children() if isinstance(c, tk.Frame)] assert not [c for c in tk_root.winfo_children() if isinstance(c, tk.Frame)]
assert str(tk_root.cget("menu"))
finally: finally:
_clear(tk_root, application.view) _clear(tk_root, application.view)
tk_root.config(menu="")
def test_create_application_without_level_shows_the_level_screen( def test_create_application_without_level_shows_the_level_screen(
@@ -221,5 +237,28 @@ def test_create_application_without_level_shows_the_level_screen(
try: try:
assert isinstance(application.view, LevelView) assert isinstance(application.view, LevelView)
assert application.root is tk_root assert application.root is tk_root
assert str(tk_root.cget("menu"))
finally: finally:
_clear(tk_root, application.view) _clear(tk_root, application.view)
tk_root.config(menu="")
def test_create_application_with_word_list_keeps_progress_in_the_data_folder(
tk_root: tk.Tk, data_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(tk, "Tk", lambda: tk_root)
other = tmp_path / "other" / "animals.csv"
other.parent.mkdir()
other.write_text("ms;da\nkucing;kat\n", encoding="utf-8")
application = app.create_application(data_dir, False, Level(None), other)
try:
view = application.view
assert isinstance(view, FlashCardView)
assert application.word_list_name == "animals.csv"
view.right_button.invoke()
assert (data_dir / "words_to_learn_animals.csv").is_file()
finally:
_clear(tk_root, application.view)
tk_root.config(menu="")
+306
View File
@@ -0,0 +1,306 @@
"""End-to-end tests of the menu bar and its commands on a real Tk window.
@file test_menu_bar.py
@brief Builds the real menu bar with injected dialogs and checks the commands.
"""
import tkinter as tk
from collections.abc import Callable, Iterator, Sequence
from pathlib import Path
import pytest
from flash_card.constants import CONTACT_URL, WINDOW_TITLE
from flash_card.domain.entities.card import Card
from flash_card.domain.repositories.progress_repository import ProgressRepository
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
from flash_card.infrastructure.persistence.csv_progress_repository import (
CsvProgressRepository,
)
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
CsvVocabularyRepository,
)
from flash_card.infrastructure.persistence.progress_file_name import (
progress_file_name,
)
from flash_card.infrastructure.speech.null_speech import NullSpeech
from flash_card.presentation import app
from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView
from flash_card.presentation.features.level.level_view import LevelView
from flash_card.presentation.features.menu.menu_view import MenuView
from flash_card.presentation.features.menu.menu_view_model import MenuViewModel
DEFAULT_LIST = "ms;da\naku;mig\nkau;du\n"
OTHER_LIST = "ms;da\nkucing;kat\n"
CONTACT_LABEL = "Jens Tirsvad Nielsen, Software engineer"
FILE_INDEX = 0
ABOUT_INDEX = 1
OPEN, CHANGE_LEVEL, RESET, SEPARATOR, EXIT = range(5)
def _last(cards: Sequence[Card]) -> Card:
return cards[-1]
class _Dialogs:
"""Records and answers the dialogs the menu view asks."""
def __init__(self) -> None:
self.file = ""
self.answer = True
self.asked_files: list[dict[str, object]] = []
self.questions: list[str] = []
self.errors: list[tuple[str, str]] = []
self.urls: list[str] = []
def ask_open_file(self, **options: object) -> str:
self.asked_files.append(options)
return self.file
def confirm(self, title: str, message: str, **options: object) -> bool:
self.questions.append(message)
return self.answer
def show_error(self, title: str, message: str, **options: object) -> str:
self.errors.append((title, message))
return "ok"
def open_url(self, url: str) -> bool:
self.urls.append(url)
return True
class _Window:
def __init__(
self,
root: tk.Tk,
application: app.Application,
menu: MenuView,
dialogs: _Dialogs,
folder: Path,
) -> None:
self.root = root
self.application = application
self.menu = menu
self.dialogs = dialogs
self.folder = folder
def _repositories_for(
folder: Path,
) -> Callable[[Path], tuple[VocabularyRepository, ProgressRepository]]:
def build(list_path: Path) -> tuple[VocabularyRepository, ProgressRepository]:
return (
CsvVocabularyRepository(list_path),
CsvProgressRepository(folder / progress_file_name(list_path)),
)
return build
def _close(window: _Window) -> None:
view = window.application.view
if isinstance(view, FlashCardView):
view.close()
elif isinstance(view, LevelView):
view.destroy()
window.root.config(menu="")
window.menu.menubar.destroy()
@pytest.fixture
def window(tk_root: tk.Tk, tmp_path: Path) -> Iterator[_Window]:
default = tmp_path / "ms-da.csv"
default.write_bytes(DEFAULT_LIST.encode("utf-8"))
application = app.Application(
tk_root, NullSpeech(), _last, _repositories_for(tmp_path), default
)
dialogs = _Dialogs()
menu = MenuView(
tk_root,
MenuViewModel(),
application,
ask_open_file=dialogs.ask_open_file,
confirm=dialogs.confirm,
show_error=dialogs.show_error,
open_url=dialogs.open_url,
)
application.show_level_screen()
instance = _Window(tk_root, application, menu, dialogs, tmp_path)
yield instance
_close(instance)
def _labels(menu: tk.Menu, last: int) -> list[str]:
return [
"-" if menu.type(i) == "separator" else str(menu.entrycget(i, "label"))
for i in range(last + 1)
]
def _start_first_level(window: _Window) -> FlashCardView:
level_view = window.application.view
assert isinstance(level_view, LevelView)
level_view.buttons[0].invoke()
card_view = window.application.view
assert isinstance(card_view, FlashCardView)
return card_view
def _word(view: FlashCardView) -> str:
return str(view.canvas.itemcget(view.word_text_id, "text"))
def test_menu_bar_has_file_and_about_in_order(window: _Window) -> None:
menubar = window.menu.menubar
assert str(window.root.cget("menu")) == str(menubar)
assert menubar.index("end") == ABOUT_INDEX
assert _labels(menubar, ABOUT_INDEX) == ["File", "About"]
def test_file_menu_has_four_entries_and_a_separator_before_exit(
window: _Window,
) -> None:
file_menu = window.menu.file_menu
assert file_menu.index("end") == EXIT
assert _labels(file_menu, EXIT) == [
"Open...",
"Change level...",
"Reset word list...",
"-",
"Exit",
]
assert file_menu.type(SEPARATOR) == "separator"
def test_file_entries_have_mnemonics(window: _Window) -> None:
file_menu = window.menu.file_menu
assert int(str(file_menu.entrycget(OPEN, "underline"))) == 0
assert int(str(file_menu.entrycget(EXIT, "underline"))) == 1
assert int(str(window.menu.menubar.entrycget(FILE_INDEX, "underline"))) == 0
def test_about_menu_leads_to_the_contact_entry(window: _Window) -> None:
about = window.menu.about_menu
assert _labels(about, 0) == ["Contact"]
assert about.type(0) == "cascade"
contact = window.menu.contact_menu
assert contact.index("end") == 0
assert _labels(contact, 0) == [CONTACT_LABEL]
def test_open_starts_the_chosen_list_after_a_level_choice(window: _Window) -> None:
other = window.folder / "animals.csv"
other.write_bytes(OTHER_LIST.encode("utf-8"))
window.dialogs.file = str(other)
window.menu.file_menu.invoke(OPEN)
assert isinstance(window.application.view, LevelView)
assert window.application.word_list_name == "animals.csv"
assert _word(_start_first_level(window)) == "kucing"
asked = window.dialogs.asked_files[0]
assert asked["initialdir"] == str(window.folder)
assert ("CSV files", "*.csv") in asked["filetypes"] # type: ignore[operator]
assert window.root.title() == WINDOW_TITLE
@pytest.mark.parametrize(
"content",
[None, b"ms;da\n\xff\xfe;x\n", "ms;da\nkøkken;x\n".encode("latin-1")],
ids=["missing", "invalid-utf8", "latin-1"],
)
def test_open_of_an_invalid_file_shows_a_message_and_keeps_the_list(
window: _Window, content: bytes | None
) -> None:
bad = window.folder / "bad.csv"
if content is not None:
bad.write_bytes(content)
window.dialogs.file = str(bad)
level_view = window.application.view
window.menu.file_menu.invoke(OPEN)
assert len(window.dialogs.errors) == 1
assert "bad.csv" in window.dialogs.errors[0][1]
assert window.application.view is level_view
assert window.application.word_list_name == "ms-da.csv"
assert _word(_start_first_level(window)) == "kau"
def test_open_cancelled_changes_nothing(window: _Window) -> None:
window.dialogs.file = ""
level_view = window.application.view
window.menu.file_menu.invoke(OPEN)
assert window.application.view is level_view
assert window.application.word_list_name == "ms-da.csv"
assert not window.dialogs.errors
def test_change_level_shows_the_level_screen_and_keeps_known_words(
window: _Window,
) -> None:
card_view = _start_first_level(window)
card_view.right_button.invoke()
progress = window.folder / "words_to_learn.csv"
saved = progress.read_bytes()
window.menu.file_menu.invoke(CHANGE_LEVEL)
assert isinstance(window.application.view, LevelView)
assert card_view.flip_timer is None
assert progress.read_bytes() == saved
def test_reset_asks_and_on_yes_removes_progress_and_shows_level_screen(
window: _Window,
) -> None:
card_view = _start_first_level(window)
card_view.right_button.invoke()
progress = window.folder / "words_to_learn.csv"
assert progress.is_file()
window.menu.file_menu.invoke(RESET)
assert window.dialogs.questions == [
"Start ms-da.csv from the beginning? All words will be to learn again."
]
assert not progress.exists()
assert isinstance(window.application.view, LevelView)
assert (window.folder / "ms-da.csv").read_bytes() == DEFAULT_LIST.encode("utf-8")
def test_reset_answered_no_changes_nothing(window: _Window) -> None:
card_view = _start_first_level(window)
card_view.right_button.invoke()
progress = window.folder / "words_to_learn.csv"
window.dialogs.answer = False
window.menu.file_menu.invoke(RESET)
assert progress.is_file()
assert window.application.view is card_view
def test_contact_opens_the_linkedin_page(window: _Window) -> None:
window.menu.contact_menu.invoke(0)
assert window.dialogs.urls == [CONTACT_URL]
assert CONTACT_URL == "https://www.linkedin.com/in/tirsvad74"
def test_exit_cancels_the_timer_and_destroys_the_window(
window: _Window, monkeypatch: pytest.MonkeyPatch
) -> None:
destroyed: list[bool] = []
monkeypatch.setattr(window.root, "destroy", lambda: destroyed.append(True))
card_view = _start_first_level(window)
assert card_view.flip_timer is not None
window.menu.file_menu.invoke(EXIT)
assert destroyed == [True]
assert card_view.flip_timer is None
assert window.application.exit_code == 0
@@ -0,0 +1,95 @@
"""Integration tests: every word list keeps its progress in a file of its own."""
from collections.abc import Sequence
from pathlib import Path
from flash_card.application.use_cases.open_study_session import open_study_session
from flash_card.domain.entities.card import Card
from flash_card.domain.value_objects.level import Level
from flash_card.infrastructure.persistence.csv_progress_repository import (
CsvProgressRepository,
)
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
CsvVocabularyRepository,
)
from flash_card.infrastructure.persistence.progress_file_name import (
progress_file_name,
)
from flash_card.infrastructure.speech.null_speech import NullSpeech
MS_DA = "ms;da\naku;mig\nkau;du\nini;denne\n"
ANIMALS = "ms;da\nkucing;kat\nanjing;hund\n"
def _first(cards: Sequence[Card]) -> Card:
return cards[0]
def _lists(folder: Path) -> tuple[Path, Path]:
default = folder / "ms-da.csv"
other = folder / "animals.csv"
default.write_bytes(MS_DA.encode("utf-8"))
other.write_bytes(ANIMALS.encode("utf-8"))
return default, other
def _progress(folder: Path, list_path: Path) -> CsvProgressRepository:
return CsvProgressRepository(folder / progress_file_name(list_path))
def _know_first_word(folder: Path, list_path: Path) -> str:
session = open_study_session(
CsvVocabularyRepository(list_path),
_progress(folder, list_path),
NullSpeech(),
_first,
Level(None),
)
card = session.next_card()
assert card is not None
session.mark_known()
return card.front_text
def _remaining(folder: Path, list_path: Path) -> list[str]:
deck = _progress(folder, list_path).load()
assert deck is not None
return [card.front for card in deck.cards]
def test_known_words_of_one_list_never_appear_in_the_other(tmp_path: Path) -> None:
default, other = _lists(tmp_path)
assert _know_first_word(tmp_path, default) == "aku"
assert _know_first_word(tmp_path, other) == "kucing"
assert _remaining(tmp_path, default) == ["kau", "ini"]
assert _remaining(tmp_path, other) == ["anjing"]
assert (tmp_path / "words_to_learn.csv").is_file()
assert (tmp_path / "words_to_learn_animals.csv").is_file()
def test_reset_of_one_list_deletes_only_its_progress_file(tmp_path: Path) -> None:
default, other = _lists(tmp_path)
_know_first_word(tmp_path, default)
_know_first_word(tmp_path, other)
_progress(tmp_path, other).reset()
assert not (tmp_path / "words_to_learn_animals.csv").exists()
assert _remaining(tmp_path, default) == ["kau", "ini"]
def test_reset_leaves_the_word_list_file_byte_identical(tmp_path: Path) -> None:
default, other = _lists(tmp_path)
_know_first_word(tmp_path, other)
_progress(tmp_path, other).reset()
assert other.read_bytes() == ANIMALS.encode("utf-8")
assert default.read_bytes() == MS_DA.encode("utf-8")
def test_delivered_list_keeps_words_to_learn_file_name(tmp_path: Path) -> None:
default, _other = _lists(tmp_path)
assert progress_file_name(default) == "words_to_learn.csv"
@@ -110,3 +110,18 @@ def test_progress_save_then_load_returns_same_cards(tmp_path: Path) -> None:
assert deck is not None assert deck is not None
assert deck.cards == CARDS assert deck.cards == CARDS
assert deck.language_pair == PAIR assert deck.language_pair == PAIR
def test_reset_deletes_the_progress_file(tmp_path: Path) -> None:
path = tmp_path / "words_to_learn.csv"
path.write_text("ms;da\naku;mig\n", encoding="utf-8")
repository = CsvProgressRepository(path)
repository.reset()
assert not path.exists()
assert repository.load() is None
def test_reset_does_nothing_when_the_file_is_absent(tmp_path: Path) -> None:
CsvProgressRepository(tmp_path / "words_to_learn.csv").reset()
@@ -93,3 +93,18 @@ def test_repository_load_raises_format_error_when_no_card(tmp_path: Path) -> Non
path = _write(tmp_path / "w.csv", "ms;da\n") path = _write(tmp_path / "w.csv", "ms;da\n")
with pytest.raises(VocabularyFormatError): with pytest.raises(VocabularyFormatError):
CsvVocabularyRepository(path).load() CsvVocabularyRepository(path).load()
@pytest.mark.parametrize(
"content",
[b"ms;da\n\xff\xfe;x\n", "ms;da\nkøkken;x\n".encode("latin-1")],
ids=["invalid-utf8", "latin-1"],
)
def test_read_word_list_raises_format_error_when_not_utf8(
tmp_path: Path, content: bytes
) -> None:
path = tmp_path / "w.csv"
path.write_bytes(content)
with pytest.raises(VocabularyFormatError, match=r"w\.csv.*not UTF-8") as info:
read_word_list(path)
assert isinstance(info.value.__cause__, UnicodeDecodeError)
@@ -0,0 +1,36 @@
"""Tests of the name of the words to learn file of a word list."""
from pathlib import Path
import pytest
from flash_card.infrastructure.persistence.progress_file_name import (
progress_file_name,
)
def test_progress_file_name_is_default_for_the_delivered_list() -> None:
assert progress_file_name(Path("data") / "ms-da.csv") == "words_to_learn.csv"
def test_progress_file_name_uses_the_stem_for_another_list() -> None:
assert progress_file_name(Path("x") / "animals.csv") == "words_to_learn_animals.csv"
def test_progress_file_name_keeps_hyphen_and_underscore() -> None:
name = progress_file_name(Path("my_list-2.csv"))
assert name == "words_to_learn_my_list-2.csv"
@pytest.mark.parametrize(
("file_name", "expected"),
[
("my list.csv", "words_to_learn_my_list.csv"),
("a.b;c.csv", "words_to_learn_a_b_c.csv"),
("ord (1).csv", "words_to_learn_ord__1_.csv"),
],
)
def test_progress_file_name_replaces_other_characters(
file_name: str, expected: str
) -> None:
assert progress_file_name(Path(file_name)) == expected
+143 -2
View File
@@ -63,6 +63,15 @@ class _FakeLevelView:
class _FakeFlashCardView: class _FakeFlashCardView:
def __init__(self, root: object, view_model: FlashCardViewModel) -> None: def __init__(self, root: object, view_model: FlashCardViewModel) -> None:
self.view_model = view_model self.view_model = view_model
self.closed = False
def close(self) -> None:
self.closed = True
class _FakeMenuView:
def __init__(self, root: object, view_model: object, application: object) -> None:
self.application = application
class _Vocabulary: class _Vocabulary:
@@ -76,12 +85,18 @@ class _Vocabulary:
class _Progress: class _Progress:
def __init__(self) -> None:
self.resets = 0
def load(self) -> Deck | None: def load(self) -> Deck | None:
return None return None
def save(self, deck: Deck) -> None: def save(self, deck: Deck) -> None:
return None return None
def reset(self) -> None:
self.resets += 1
class _Speech: class _Speech:
def speak(self, text: str, language_code: str) -> None: def speak(self, text: str, language_code: str) -> None:
@@ -96,6 +111,7 @@ def _first(cards: Sequence[Card]) -> Card:
def fake_tk_root(monkeypatch: pytest.MonkeyPatch) -> None: def fake_tk_root(monkeypatch: pytest.MonkeyPatch) -> None:
"""Keep every test in this module away from a real Tk root.""" """Keep every test in this module away from a real Tk root."""
monkeypatch.setattr(app.tk, "Tk", _FakeRoot) monkeypatch.setattr(app.tk, "Tk", _FakeRoot)
monkeypatch.setattr(app, "MenuView", _FakeMenuView)
@pytest.fixture @pytest.fixture
@@ -104,9 +120,31 @@ def fake_views(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(app, "FlashCardView", _FakeFlashCardView) monkeypatch.setattr(app, "FlashCardView", _FakeFlashCardView)
def _application(root: _FakeRoot, error: bool = False) -> app.Application: LIST_PATH = Path("lists") / "ms-da.csv"
OTHER_PATH = Path("lists") / "animals.csv"
class _Factory:
"""Repositories for a path, recorded; the paths in bad are unusable."""
def __init__(self, error: bool = False, bad: tuple[Path, ...] = ()) -> None:
self._error = error
self._bad = bad
self.progress: dict[Path, _Progress] = {}
self.calls: list[Path] = []
def __call__(self, path: Path) -> tuple[_Vocabulary, _Progress]:
self.calls.append(path)
progress = self.progress.setdefault(path, _Progress())
return _Vocabulary(self._error or path in self._bad), progress
def _application(
root: _FakeRoot, error: bool = False, factory: _Factory | None = None
) -> app.Application:
repositories_for = _Factory(error) if factory is None else factory
return app.Application( return app.Application(
cast(tk.Tk, root), _Vocabulary(error), _Progress(), _Speech(), _first cast(tk.Tk, root), _Speech(), _first, repositories_for, LIST_PATH
) )
@@ -293,3 +331,106 @@ def test_create_application_draws_first_card_from_bundled_list(
assert view.view_model.text == "saya" assert view.view_model.text == "saya"
view.view_model.is_known() view.view_model.is_known()
assert (data / "words_to_learn.csv").is_file() assert (data / "words_to_learn.csv").is_file()
def test_word_list_name_and_folder_are_those_of_the_list_in_use() -> None:
application = _application(_FakeRoot())
assert application.word_list_name == "ms-da.csv"
assert application.word_list_folder == Path("lists")
def test_open_word_list_replaces_repositories_and_shows_level_screen(
fake_views: None,
) -> None:
factory = _Factory()
application = _application(_FakeRoot(), factory=factory)
application.start(Level(None))
card_view = application.view
assert isinstance(card_view, _FakeFlashCardView)
application.open_word_list(OTHER_PATH)
assert card_view.closed
assert isinstance(application.view, _FakeLevelView)
assert application.word_list_name == "animals.csv"
application.start(Level(None))
application.reset_word_list()
assert factory.progress[OTHER_PATH].resets == 1
assert factory.progress[LIST_PATH].resets == 0
def test_open_word_list_raises_and_changes_nothing_when_list_is_invalid(
fake_views: None,
) -> None:
factory = _Factory(bad=(OTHER_PATH,))
application = _application(_FakeRoot(), factory=factory)
application.start(Level(None))
card_view = application.view
assert isinstance(card_view, _FakeFlashCardView)
with pytest.raises(VocabularyNotFoundError):
application.open_word_list(OTHER_PATH)
assert application.view is card_view
assert not card_view.closed
assert application.word_list_name == "ms-da.csv"
application.reset_word_list()
assert factory.progress[LIST_PATH].resets == 1
assert factory.progress[OTHER_PATH].resets == 0
def test_change_level_closes_the_card_screen_and_shows_the_level_screen(
fake_views: None,
) -> None:
application = _application(_FakeRoot())
application.start(Level(None))
card_view = application.view
assert isinstance(card_view, _FakeFlashCardView)
application.change_level()
assert card_view.closed
assert isinstance(application.view, _FakeLevelView)
def test_change_level_destroys_the_level_screen_it_replaces(
fake_views: None,
) -> None:
application = _application(_FakeRoot())
application.show_level_screen()
first = application.view
assert isinstance(first, _FakeLevelView)
application.change_level()
assert first.destroyed
assert application.view is not first
def test_reset_word_list_resets_progress_then_shows_the_level_screen(
fake_views: None,
) -> None:
factory = _Factory()
application = _application(_FakeRoot(), factory=factory)
application.start(Level(None))
application.reset_word_list()
assert factory.progress[LIST_PATH].resets == 1
assert isinstance(application.view, _FakeLevelView)
def test_exit_application_closes_the_screen_and_destroys_the_root(
fake_views: None,
) -> None:
root = _FakeRoot()
application = _application(root)
application.start(Level(None))
card_view = application.view
assert isinstance(card_view, _FakeFlashCardView)
application.exit_application()
assert card_view.closed
assert root.destroyed
assert application.exit_code == 0
+41
View File
@@ -0,0 +1,41 @@
"""Tests of the pure parts of the screenshot tool.
@file test_capture_screenshots.py
@brief Checks platform_kind and window_box of tools/capture_screenshots.py.
"""
import importlib.util
from pathlib import Path
from types import ModuleType
import pytest
pytest.importorskip("PIL")
_TOOL_PATH = Path(__file__).resolve().parents[2] / "tools" / "capture_screenshots.py"
def _load_tool() -> ModuleType:
spec = importlib.util.spec_from_file_location("capture_screenshots", _TOOL_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_platform_kind_is_windows_for_win32() -> None:
assert _load_tool().platform_kind("win32") == "windows"
def test_platform_kind_is_x11_for_linux() -> None:
assert _load_tool().platform_kind("linux") == "x11"
def test_window_box_adds_menu_height_on_x11() -> None:
assert _load_tool().window_box("x11", 10, 100, 900, 726, 28) == (10, 72, 910, 826)
def test_window_box_ignores_menu_height_on_windows() -> None:
box = _load_tool().window_box("windows", 10, 100, 900, 726, 28)
assert box == (10, 100, 910, 826)
+44
View File
@@ -0,0 +1,44 @@
"""Tests of the menu view model; no Tkinter is involved."""
import pytest
from flash_card.presentation.features.menu.menu_view_model import (
MenuEntry,
MenuViewModel,
)
def test_file_entries_are_open_change_level_reset_and_exit_in_order() -> None:
labels = [entry.label for entry in MenuViewModel().file_entries]
assert labels == ["Open...", "Change level...", "Reset word list...", "Exit"]
def test_file_entries_name_distinct_commands() -> None:
commands = [entry.command for entry in MenuViewModel().file_entries]
assert commands == [
"open_word_list",
"change_level",
"reset_word_list",
"exit_program",
]
def test_menu_labels_are_file_about_and_contact() -> None:
model = MenuViewModel()
assert model.file_label == "File"
assert model.about_label == "About"
assert model.contact_menu_label == "Contact"
def test_contact_shows_name_and_title() -> None:
assert MenuViewModel().contact_label == "Jens Tirsvad Nielsen, Software engineer"
def test_contact_url_is_the_linkedin_page() -> None:
assert MenuViewModel().contact_url == "https://www.linkedin.com/in/tirsvad74"
def test_menu_entry_is_immutable() -> None:
entry = MenuEntry("Exit", "exit_program", 1)
with pytest.raises(AttributeError):
entry.label = "Quit" # type: ignore[misc]
+122
View File
@@ -0,0 +1,122 @@
"""Tests of the smoke driver tool.
@file test_smoke_driver.py
@brief Checks the pure helpers and the run loop of tools/docker/smoke_driver.py
with a fake xdotool and a fake program.
"""
import importlib.util
import json
import sys
from collections.abc import Sequence
from pathlib import Path
from types import ModuleType
import pytest
_TOOL_PATH = (
Path(__file__).resolve().parents[2] / "tools" / "docker" / "smoke_driver.py"
)
_GEOMETRY = "WINDOW=7\nX=0\nY=0\nWIDTH=900\nHEIGHT=800\nSCREEN=0\n"
def _load_tool() -> ModuleType:
spec = importlib.util.spec_from_file_location("smoke_driver", _TOOL_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
# dataclasses looks the module up by name while the class is built
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
class _FakeProgram:
def __init__(self) -> None:
self.stopped = False
def stop(self) -> None:
self.stopped = True
class _FakeSystem:
"""Fake xdotool and program; a click writes the progress file."""
def __init__(self, has_window: bool, writes_progress: bool) -> None:
self.has_window = has_window
self.writes_progress = writes_progress
self.program = _FakeProgram()
self.data_dir: Path | None = None
def start(self, command: Sequence[str]) -> _FakeProgram:
self.data_dir = Path(command[command.index("--data-dir") + 1])
return self.program
def run(self, args: Sequence[str]) -> str:
if args[1] == "search":
return "7\n" if self.has_window else ""
if args[1] == "getwindowgeometry":
return _GEOMETRY
if args[1] == "mousemove" and self.writes_progress:
assert self.data_dir is not None
progress = self.data_dir / "words_to_learn.csv"
progress.write_text("ms;da\nw1;t1\nw2;t2\n", encoding="utf-8")
return ""
def test_known_button_point_is_101_above_bottom() -> None:
assert _load_tool().known_button_point(900, 800) == (650, 699)
def test_words_file_text_has_header_and_cards() -> None:
assert _load_tool().words_file_text(2) == "ms;da\nw1;t1\nw2;t2\n"
def test_parse_geometry_reads_width_and_height() -> None:
assert _load_tool().parse_geometry(_GEOMETRY) == (900, 800)
def test_parse_geometry_fails_without_size() -> None:
with pytest.raises(ValueError):
_load_tool().parse_geometry("X=0\n")
def test_progress_ok_needs_exactly_one_card_less() -> None:
tool = _load_tool()
assert tool.progress_ok(3, 3)
assert not tool.progress_ok(4, 3)
assert not tool.progress_ok(0, 3)
def test_main_returns_zero_when_click_saves_progress(
capsys: pytest.CaptureFixture[str],
) -> None:
system = _FakeSystem(has_window=True, writes_progress=True)
code = _load_tool().main(
["--repeat", "2", "--words", "3", "--command", "prog", "-m", "x"],
run=system.run,
start=system.start,
sleep=lambda _: None,
)
lines = [json.loads(x) for x in capsys.readouterr().out.splitlines()]
assert code == 0
assert lines[0]["ok"] is True
assert lines[0]["clicked"] == [650, 699]
assert lines[-1] == {"runs": 2, "passed": 2}
assert system.program.stopped
def test_main_fails_when_no_window_appears(
capsys: pytest.CaptureFixture[str],
) -> None:
system = _FakeSystem(has_window=False, writes_progress=False)
code = _load_tool().main(
["--repeat", "1", "--timeout", "1", "--command", "prog"],
run=system.run,
start=system.start,
sleep=lambda _: None,
)
first = json.loads(capsys.readouterr().out.splitlines()[0])
assert code == 1
assert first["window"] is False
assert system.program.stopped
+35
View File
@@ -0,0 +1,35 @@
"""Keep the version declared in code, pyproject.toml and the Doxyfile in step.
@file test_version.py
@brief Unit tests that the three version declarations cannot drift.
"""
import re
import tomllib
import flash_card
from flash_card.constants import PROJECT_ROOT
_SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
_PROJECT_NUMBER_PATTERN = re.compile(r"^PROJECT_NUMBER\s*=\s*(\S+)\s*$", re.MULTILINE)
def _pyproject_version() -> str:
with (PROJECT_ROOT / "pyproject.toml").open("rb") as file:
version: str = tomllib.load(file)["project"]["version"]
return version
def test_version_matches_package_version_when_read_from_pyproject() -> None:
assert _pyproject_version() == flash_card.__version__
def test_version_has_major_minor_patch_form() -> None:
assert _SEMVER_PATTERN.match(flash_card.__version__) is not None
def test_version_matches_doxyfile_project_number() -> None:
text = (PROJECT_ROOT / "Doxyfile").read_text(encoding="utf-8")
match = _PROJECT_NUMBER_PATTERN.search(text)
assert match is not None
assert match.group(1) == flash_card.__version__
+77 -10
View File
@@ -13,15 +13,17 @@ from __future__ import annotations
import argparse import argparse
import ctypes import ctypes
import os
import random import random
import shutil import shutil
import sys import sys
import tempfile import tempfile
import time import time
import tkinter as tk
from collections.abc import Sequence from collections.abc import Sequence
from pathlib import Path from pathlib import Path
from PIL import ImageGrab from PIL import Image, ImageGrab
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
@@ -43,6 +45,46 @@ DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "docs" / "images"
SCREENSHOT_SEED = 7 SCREENSHOT_SEED = 7
## @brief Extra time in seconds to let the window settle before a grab. ## @brief Extra time in seconds to let the window settle before a grab.
SETTLE_SECONDS = 0.5 SETTLE_SECONDS = 0.5
## @brief Height in pixels of the menu bar above the client area on X11.
MENU_BAR_HEIGHT_X11 = 28
## @brief Platform kind of Windows.
KIND_WINDOWS = "windows"
## @brief Platform kind of every other platform (an X11 display).
KIND_X11 = "x11"
def platform_kind(platform: str) -> str:
"""Choose how to grab the screen.
@param platform A `sys.platform` value.
@return "windows" for win32, otherwise "x11".
"""
return KIND_WINDOWS if platform == "win32" else KIND_X11
def window_box(
kind: str, rootx: int, rooty: int, width: int, height: int, menu_height: int
) -> tuple[int, int, int, int]:
"""Return the screen box to grab for a window.
@param kind The platform kind from `platform_kind`.
@param rootx Left edge of the client area.
@param rooty Top edge of the client area.
@param width Width of the client area.
@param height Height of the client area.
@param menu_height Height of the menu bar above the client area; it is
added on X11 only, as on Windows the frame box is read from the system.
@return Left, top, right and bottom in screen pixels.
"""
top = rooty - menu_height if kind == KIND_X11 else rooty
return rootx, top, rootx + width, rooty + height
def _grab(box: tuple[int, int, int, int]) -> Image.Image:
"""Grab a screen box on the current platform."""
if platform_kind(sys.platform) == KIND_X11:
return ImageGrab.grab(bbox=box, xdisplay=os.environ.get("DISPLAY"))
return ImageGrab.grab(bbox=box)
def _make_dpi_aware() -> None: def _make_dpi_aware() -> None:
@@ -51,6 +93,23 @@ def _make_dpi_aware() -> None:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # type: ignore[attr-defined,unused-ignore] ctypes.windll.shcore.SetProcessDpiAwareness(2) # type: ignore[attr-defined,unused-ignore]
def _frame_box(root: tk.Tk) -> tuple[int, int, int, int]:
"""Return the screen box of the whole window, with title bar and menu bar.
@param root The Tk root window.
@return Left, top, right and bottom in screen pixels.
"""
import ctypes.wintypes # Windows only, so it is imported here
handle = ctypes.windll.user32.GetParent(root.winfo_id()) # type: ignore[attr-defined,unused-ignore]
rect = ctypes.wintypes.RECT()
ctypes.windll.user32.GetWindowRect(handle, ctypes.byref(rect)) # type: ignore[attr-defined,unused-ignore]
# Windows 11 adds an invisible resize border of about 7 pixels at the left,
# the right and the bottom; cut it so no other window is in the picture.
border = 7
return rect.left + border, rect.top, rect.right - border, rect.bottom - border
def _pump(root: object, seconds: float) -> None: def _pump(root: object, seconds: float) -> None:
"""Run the Tk event loop for a while. """Run the Tk event loop for a while.
@@ -67,13 +126,15 @@ def capture(output_dir: Path) -> list[Path]:
"""Capture the level screen, then the front and the back of one card. """Capture the level screen, then the front and the back of one card.
@param output_dir Folder to write `screenshot-level.png`, @param output_dir Folder to write `screenshot-level.png`,
`screenshot-front.png` and `screenshot-back.png` into. `screenshot-front.png`, `screenshot-back.png` and `screenshot-menu.png`
@return The three files written: level, front, back. (the whole window with its menu bar) into.
@return The four files written: level, front, back, menu.
""" """
_make_dpi_aware() _make_dpi_aware()
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
random.seed(SCREENSHOT_SEED) random.seed(SCREENSHOT_SEED)
level = output_dir / "screenshot-level.png" level = output_dir / "screenshot-level.png"
menu = output_dir / "screenshot-menu.png"
front = output_dir / "screenshot-front.png" front = output_dir / "screenshot-front.png"
back = output_dir / "screenshot-back.png" back = output_dir / "screenshot-back.png"
with tempfile.TemporaryDirectory() as data_dir: with tempfile.TemporaryDirectory() as data_dir:
@@ -83,13 +144,14 @@ def capture(output_dir: Path) -> list[Path]:
root.lift() root.lift()
root.attributes("-topmost", True) root.attributes("-topmost", True)
_pump(root, SETTLE_SECONDS) _pump(root, SETTLE_SECONDS)
window_box = ( kind = platform_kind(sys.platform)
client = (
root.winfo_rootx(), root.winfo_rootx(),
root.winfo_rooty(), root.winfo_rooty(),
root.winfo_rootx() + root.winfo_width(), root.winfo_width(),
root.winfo_rooty() + root.winfo_height(), root.winfo_height(),
) )
ImageGrab.grab(bbox=window_box).save(level) _grab(window_box(kind, *client, 0)).save(level)
application.start(Level(None)) application.start(Level(None))
view = application.view view = application.view
if not isinstance(view, FlashCardView): if not isinstance(view, FlashCardView):
@@ -104,11 +166,16 @@ def capture(output_dir: Path) -> list[Path]:
+ view.right_button.winfo_height() + view.right_button.winfo_height()
+ 20, + 20,
) )
ImageGrab.grab(bbox=box).save(front) _grab(box).save(front)
if kind == KIND_WINDOWS:
menu_box = _frame_box(root)
else:
menu_box = window_box(kind, *client, MENU_BAR_HEIGHT_X11)
_grab(menu_box).save(menu)
_pump(root, FLIP_DELAY_MS / 1000 + SETTLE_SECONDS) _pump(root, FLIP_DELAY_MS / 1000 + SETTLE_SECONDS)
ImageGrab.grab(bbox=box).save(back) _grab(box).save(back)
root.destroy() root.destroy()
return [level, front, back] return [level, front, back, menu]
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Sequence[str] | None = None) -> int:
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Run one of the check scripts on a COPY of the repository, so that the
# mounted repository stays untouched (no build/, dist/ or caches in it).
#
# Purpose: isolate the checks from the host (ADR-0005).
# Usage: in-container.sh <script> [args...]
# in-container.sh run-checks.sh
# in-container.sh screenshots.sh /out
# in-container.sh smoke-binary.sh 5
# Env: REPO_DIR (default /repo, mounted read-only), WORK_DIR (default
# $HOME/work; it is deleted and recreated, so it must be a folder of
# the container, never the mounted repository)
# Needs: bash 5, tar
# Exit: 2 on bad arguments or a missing script; else the exit code of the
# script that was run
set -euo pipefail
repo="${REPO_DIR:-/repo}"
work="${WORK_DIR:-${HOME}/work}"
usage() {
echo "usage: in-container.sh <script> [args...]" >&2
echo "scripts: run-checks.sh screenshots.sh smoke-binary.sh" >&2
}
if [[ $# -lt 1 ]]; then
usage
exit 2
fi
script="$1"
shift
if [[ ! -f "${repo}/tools/docker/${script}" ]]; then
echo "error: no such script: tools/docker/${script}" >&2
usage
exit 2
fi
if [[ "${work}" == "${repo}" || "${work}" == "/" ]]; then
echo "error: WORK_DIR must not be the repository or /" >&2
exit 2
fi
rm -rf "${work}"
mkdir -p "${work}"
tar -C "${repo}" \
--exclude=.git --exclude=.venv --exclude=build --exclude=dist \
--exclude=docs/doxygen --exclude=.codex --exclude='__pycache__' \
-cf - . | tar -C "${work}" -xf -
cd "${work}"
export PYTHONPATH="${work}/src"
exec bash "tools/docker/${script}" "$@"
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Run the linters and the whole test suite (with the e2e tests of the real Tk
# window) on the virtual desktop.
#
# Purpose: the quality gate without a desktop of the host (ADR-0005).
# Usage: run-checks.sh (from the repository root, via in-container.sh)
# Env: see start-desktop.sh
# Needs: ruff, mypy, import-linter, pytest (the dev extra), the desktop
# Exit: 0 only when every check passes
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
# shellcheck source=tools/docker/start-desktop.sh
. tools/docker/start-desktop.sh
python -m ruff check src tests tools
python -m ruff format --check src tests tools
python -m mypy
lint-imports
python -m pytest -ra -q
echo "checks passed on desktop: ${DESKTOP_KIND}"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Take the screenshots (level screen, card front, card back, window with the
# menu bar) on the virtual desktop.
#
# Purpose: screenshots without a desktop of the host (ADR-0005).
# Usage: screenshots.sh [output-folder] (default /out)
# Env: see start-desktop.sh
# Needs: Pillow (the tools extra), the desktop
# Exit: 0 when the four files were written; else the tool's exit code
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
# shellcheck source=tools/docker/start-desktop.sh
. tools/docker/start-desktop.sh
out="${1:-/out}"
mkdir -p "${out}"
python tools/capture_screenshots.py --output-dir "${out}"
ls -l "${out}"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Tests of the check scripts themselves: argument errors and refusals. The
# success paths are the runs of run-checks.sh, screenshots.sh and
# smoke-binary.sh.
#
# Purpose: QC-SH-001 criterion 11 (success, failure and bypass paths).
# Usage: selftest.sh (from the repository root)
# Needs: bash 5
# Exit: 0 when every case behaves as stated; 1 when one differs
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
fail=0
expect_exit() {
local expected="$1" label="$2"
shift 2
local code=0
"$@" >/dev/null 2>&1 || code=$?
if [[ "${code}" -ne "${expected}" ]]; then
echo "FAIL: ${label}: exit ${code}, expected ${expected}" >&2
fail=1
else
echo "ok: ${label}"
fi
}
expect_exit 2 "in-container without arguments" bash tools/docker/in-container.sh
expect_exit 2 "in-container with an unknown script" bash tools/docker/in-container.sh nope.sh
expect_exit 2 "in-container refuses the repository as work folder" \
env REPO_DIR="$(pwd)" WORK_DIR="$(pwd)" bash tools/docker/in-container.sh run-checks.sh
expect_exit 2 "smoke-binary with a text count" bash tools/docker/smoke-binary.sh many
expect_exit 2 "smoke-binary with zero" bash tools/docker/smoke-binary.sh 0
for script in start-desktop run-checks screenshots smoke-binary in-container selftest; do
expect_exit 0 "bash -n ${script}.sh" bash -n "tools/docker/${script}.sh"
done
exit "${fail}"
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Build the Linux binary with PyInstaller and smoke-test it, and the source
# program, on the virtual desktop. The smoke test starts the program, finds the
# window, presses the known button with the virtual mouse of the desktop and
# checks the progress file.
#
# Purpose: the "does the packaged program work" check without a desktop of the
# host (ADR-0005, RC-072).
# Usage: smoke-binary.sh [repeat-count] (default 5)
# Env: see start-desktop.sh
# Needs: pyinstaller (the build extra), binutils, xdotool, the desktop
# Exit: 0 only when every run of both programs passes; 2 on a bad count
set -euo pipefail
repeat="${1:-5}"
if ! [[ "${repeat}" =~ ^[1-9][0-9]*$ ]]; then
echo "error: repeat-count must be a positive integer, got '${repeat}'" >&2
echo "usage: smoke-binary.sh [repeat-count]" >&2
exit 2
fi
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
# shellcheck source=tools/docker/start-desktop.sh
. tools/docker/start-desktop.sh
python tools/build_executable.py --clean
test -x dist/flash-card
echo "== binary"
python tools/docker/smoke_driver.py --repeat "${repeat}" --command dist/flash-card
echo "== source"
python tools/docker/smoke_driver.py --repeat "${repeat}" --command python -m flash_card
+252
View File
@@ -0,0 +1,252 @@
"""Smoke driver that clicks the known button of the running program.
@file smoke_driver.py
@brief Starts the program on the current X display, finds its window with
xdotool, clicks the tick button and checks that one card was saved as known.
Uses the standard library only, so it runs in a bare container. It needs
`xdotool` and a DISPLAY. Exit code 0 only when every run is ok.
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import signal
import subprocess
import sys
import tempfile
import time
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
## @brief Title of the program window.
WINDOW_NAME = "Flashy"
## @brief X of the tick button, in window pixels.
KNOWN_BUTTON_X = 650
## @brief Distance of the tick button centre above the bottom window edge.
KNOWN_BUTTON_BOTTOM_OFFSET = 101
## @brief Seconds between two polls.
POLL_SECONDS = 0.25
## @brief Seconds to wait for the progress file after the click.
PROGRESS_TIMEOUT_SECONDS = 5.0
## @brief Name of the vocabulary file written for a run.
VOCABULARY_NAME = "ms-da.csv"
## @brief Name of the progress file the program writes.
PROGRESS_NAME = "words_to_learn.csv"
class Running(Protocol):
"""A started program that can be stopped."""
def stop(self) -> None:
"""Stop the program and its children."""
## @brief Runs a command and returns its stdout ("" when it fails).
RunCommand = Callable[[Sequence[str]], str]
## @brief Starts the program and returns a handle to stop it.
StartProgram = Callable[[Sequence[str]], Running]
## @brief Waits for a number of seconds.
Sleep = Callable[[float], None]
def known_button_point(width: int, height: int) -> tuple[int, int]:
"""Return the click point of the known button inside the window.
@param width Window width in pixels (the button x is fixed).
@param height Window height in pixels.
@return X and Y relative to the window.
"""
return KNOWN_BUTTON_X, height - KNOWN_BUTTON_BOTTOM_OFFSET
def words_file_text(count: int) -> str:
"""Return the text of a vocabulary file with count cards.
@param count Number of cards.
@return A `ms;da` header and the lines `w1;t1`, `w2;t2`, ...
"""
lines = ["ms;da", *(f"w{i};t{i}" for i in range(1, count + 1))]
return "\n".join(lines) + "\n"
def parse_geometry(shell_output: str) -> tuple[int, int]:
"""Read the size from `xdotool getwindowgeometry --shell`.
@param shell_output The KEY=VALUE lines.
@return Width and height.
@throws ValueError When WIDTH or HEIGHT is missing.
"""
values: dict[str, int] = {}
for line in shell_output.splitlines():
key, _, value = line.partition("=")
if key in ("WIDTH", "HEIGHT") and value.strip().isdigit():
values[key] = int(value)
if "WIDTH" not in values or "HEIGHT" not in values:
raise ValueError("no WIDTH and HEIGHT in the geometry output")
return values["WIDTH"], values["HEIGHT"]
def progress_ok(rows: int, words: int) -> bool:
"""Tell whether the progress file shows exactly one card known.
@param rows Lines in the progress file, the header included.
@param words Cards at the start.
@return True when the file has the header and words - 1 cards.
"""
return rows == words
class _Process:
"""A real subprocess, stopped as a whole process group."""
def __init__(self, process: subprocess.Popen[bytes]) -> None:
self._process = process
def stop(self) -> None:
"""Terminate the process group, then kill it if it stays."""
try:
os.killpg(self._process.pid, signal.SIGTERM) # type: ignore[attr-defined,unused-ignore]
self._process.wait(timeout=3)
except (ProcessLookupError, subprocess.TimeoutExpired):
self._process.kill()
def real_run(args: Sequence[str]) -> str:
"""Run a command and return stdout, or "" when it fails."""
try:
done = subprocess.run(args, capture_output=True, text=True, check=False)
except OSError:
return ""
return done.stdout if done.returncode == 0 else ""
def real_start(command: Sequence[str]) -> Running:
"""Start the program in its own process group."""
return _Process(subprocess.Popen(command, start_new_session=True))
@dataclass(frozen=True)
class RunResult:
"""The outcome of one run."""
window: bool
clicked: tuple[int, int] | None
progress_rows: int
ok: bool
def _count_rows(path: Path) -> int:
if not path.exists():
return 0
return len(path.read_text(encoding="utf-8").splitlines())
def _wait_for_window(run: RunCommand, sleep: Sleep, timeout: float) -> str | None:
waited = 0.0
while True:
found = run(["xdotool", "search", "--name", WINDOW_NAME]).split()
if found:
return found[0]
if waited >= timeout:
return None
sleep(POLL_SECONDS)
waited += POLL_SECONDS
def _wait_for_progress(path: Path, sleep: Sleep) -> int:
waited = 0.0
rows = _count_rows(path)
while rows == 0 and waited < PROGRESS_TIMEOUT_SECONDS:
sleep(POLL_SECONDS)
waited += POLL_SECONDS
rows = _count_rows(path)
return rows
def run_once(
command: Sequence[str],
words: int,
timeout: float,
run: RunCommand,
start: StartProgram,
sleep: Sleep = time.sleep,
) -> RunResult:
"""Start the program, click the known button and check the progress file.
@param command The program command.
@param words Number of cards in the vocabulary.
@param timeout Seconds to wait for the window.
@param run Runs xdotool.
@param start Starts the program.
@param sleep Waits between polls.
@return The result of the run.
"""
folder = Path(tempfile.mkdtemp(prefix="flash-card-smoke-"))
data = folder / "data"
data.mkdir()
(data / VOCABULARY_NAME).write_text(words_file_text(words), encoding="utf-8")
argv = [*command, "--data-dir", str(data), "--level", "all", "--no-speech"]
program = start(argv)
try:
window = _wait_for_window(run, sleep, timeout)
if window is None:
return RunResult(False, None, 0, False)
width, height = parse_geometry(
run(["xdotool", "getwindowgeometry", "--shell", window])
)
x, y = known_button_point(width, height)
run(["xdotool", "mousemove", "--window", window, str(x), str(y), "click", "1"])
rows = _wait_for_progress(data / PROGRESS_NAME, sleep)
return RunResult(True, (x, y), rows, progress_ok(rows, words))
finally:
program.stop()
shutil.rmtree(folder, ignore_errors=True)
def main(
argv: Sequence[str] | None = None,
run: RunCommand = real_run,
start: StartProgram = real_start,
sleep: Sleep = time.sleep,
) -> int:
"""Run the driver.
@param argv Command line arguments, or None for `sys.argv`.
@param run Runs xdotool.
@param start Starts the program.
@param sleep Waits between polls.
@return 0 when every run is ok, otherwise 1.
"""
parser = argparse.ArgumentParser(description=(__doc__ or "").splitlines()[0])
parser.add_argument("--repeat", type=int, default=5)
parser.add_argument("--timeout", type=float, default=30.0)
parser.add_argument("--words", type=int, default=3)
parser.add_argument("--command", nargs=argparse.REMAINDER, required=True)
args = parser.parse_args(argv)
passed = 0
for index in range(1, args.repeat + 1):
began = time.monotonic()
result = run_once(args.command, args.words, args.timeout, run, start, sleep)
passed += result.ok
line = {
"run": index,
"window": result.window,
"clicked": list(result.clicked) if result.clicked else None,
"progress_rows": result.progress_rows,
"ok": result.ok,
"seconds": round(time.monotonic() - began, 2),
}
sys.stdout.write(json.dumps(line) + "\n")
summary = {"runs": args.repeat, "passed": passed}
sys.stdout.write(json.dumps(summary) + "\n")
return 0 if passed == args.repeat else 1
if __name__ == "__main__":
raise SystemExit(main())
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Start a virtual desktop for the checks. Source this file; it must not be run.
#
# Purpose: give the checks a desktop of their own (a virtual X screen) so that
# no window opens on the host (ADR-0005).
# Usage: . tools/docker/start-desktop.sh
# Options: none
# Env in: DISPLAY (default :99), SCREEN_SIZE (default 1280x1024x24),
# SHELL_WAIT_SECONDS (seconds to wait for GNOME Shell, default 25)
# Env out: DISPLAY, DBUS_SESSION_BUS_ADDRESS, DESKTOP_KIND (gnome-shell or
# mutter), DESKTOP_LOG_DIR (logs and the file desktop-kind)
# Needs: bash 5, Xvfb, xdpyinfo, xprop, dbus-launch, gnome-shell, mutter
# Exit: returns 0 when a desktop runs; 1 when no window manager came up
#
# Steps: Xvfb; a private D-Bus session; GNOME Shell first, and GNOME's window
# manager (mutter) when the shell does not come up in a container (it needs a
# login manager that a container does not have). The kind that runs is printed.
export DISPLAY="${DISPLAY:-:99}"
screen_size="${SCREEN_SIZE:-1280x1024x24}"
shell_wait_seconds="${SHELL_WAIT_SECONDS:-25}"
DESKTOP_LOG_DIR="$(mktemp -d)"
export DESKTOP_LOG_DIR
trap 'rm -rf "${DESKTOP_LOG_DIR}"' EXIT
if ! xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1; then
Xvfb "${DISPLAY}" -screen 0 "${screen_size}" -nolisten tcp >"${DESKTOP_LOG_DIR}/xvfb.log" 2>&1 &
for _ in $(seq 1 40); do
xdpyinfo -display "${DISPLAY}" >/dev/null 2>&1 && break
sleep 0.25
done
fi
if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
eval "$(dbus-launch --sh-syntax)"
export DBUS_SESSION_BUS_ADDRESS
fi
# Name of the window manager that owns the screen, or nothing.
wm_name() {
local check
check="$(xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | awk '{print $NF}')"
[[ -n "${check}" ]] || return 0
xprop -id "${check}" _NET_WM_NAME 2>/dev/null | sed -n 's/.*= "\(.*\)"/\1/p'
}
DESKTOP_KIND=""
if command -v gnome-shell >/dev/null 2>&1; then
gnome-shell --x11 --sm-disable >"${DESKTOP_LOG_DIR}/gnome-shell.log" 2>&1 &
gnome_pid=$!
for _ in $(seq 1 $((shell_wait_seconds * 2))); do
if kill -0 "${gnome_pid}" 2>/dev/null && wm_name | grep -qi "gnome"; then
DESKTOP_KIND="gnome-shell"
break
fi
sleep 0.5
done
if [[ -z "${DESKTOP_KIND}" ]]; then
kill "${gnome_pid}" 2>/dev/null || true
fi
fi
if [[ -z "${DESKTOP_KIND}" ]]; then
mutter --x11 --sm-disable >"${DESKTOP_LOG_DIR}/mutter.log" 2>&1 &
for _ in $(seq 1 40); do
if wm_name | grep -qi "mutter"; then
DESKTOP_KIND="mutter"
break
fi
sleep 0.25
done
fi
if [[ -z "${DESKTOP_KIND}" ]]; then
echo "error: no window manager came up on ${DISPLAY}" >&2
return 1
fi
export DESKTOP_KIND
echo "${DESKTOP_KIND}" >"${DESKTOP_LOG_DIR}/desktop-kind"
echo "desktop: ${DESKTOP_KIND} on ${DISPLAY}"
+36
View File
@@ -0,0 +1,36 @@
<!--
Windows Sandbox configuration of the flash card smoke test (ADR-0005).
Do not open this file directly: tools/sandbox/run.ps1 replaces the three
@...@ placeholders with absolute host paths and starts the sandbox.
dist read-only the built flash-card.exe
scripts read-only smoke.ps1, which runs inside the sandbox
out read-write result.json and screenshots written by smoke.ps1
-->
<Configuration>
<VGpu>Disable</VGpu>
<Networking>Disable</Networking>
<AudioInput>Disable</AudioInput>
<VideoInput>Disable</VideoInput>
<ClipboardRedirection>Disable</ClipboardRedirection>
<MappedFolders>
<MappedFolder>
<HostFolder>@DIST@</HostFolder>
<SandboxFolder>C:\fc\dist</SandboxFolder>
<ReadOnly>true</ReadOnly>
</MappedFolder>
<MappedFolder>
<HostFolder>@SCRIPTS@</HostFolder>
<SandboxFolder>C:\fc\scripts</SandboxFolder>
<ReadOnly>true</ReadOnly>
</MappedFolder>
<MappedFolder>
<HostFolder>@OUT@</HostFolder>
<SandboxFolder>C:\fc\out</SandboxFolder>
<ReadOnly>false</ReadOnly>
</MappedFolder>
</MappedFolders>
<LogonCommand>
<Command>powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\fc\scripts\smoke.ps1 -Runs @RUNS@</Command>
</LogonCommand>
</Configuration>
+56
View File
@@ -0,0 +1,56 @@
<#
.SYNOPSIS
Start the Windows Sandbox smoke test of dist\flash-card.exe from the host.
.DESCRIPTION
Writes a configuration from flash-card.wsb with absolute paths, starts
Windows Sandbox, waits for out\result.json and prints the summary.
The tests themselves (window, click, progress file) run inside the sandbox
with the sandbox's own mouse; the host's mouse and focus are not used.
Windows Sandbox shows its own window while it runs. Needs Windows 11 Pro
with the feature "Windows Sandbox" enabled.
.EXAMPLE
powershell -File tools\sandbox\run.ps1 -Runs 5
#>
param(
[int]$Runs = 5,
[int]$TimeoutMinutes = 15
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$dist = Join-Path $root 'dist'
$scripts = $PSScriptRoot
$out = Join-Path $root 'build\sandbox-out'
if (-not (Test-Path (Join-Path $dist 'flash-card.exe'))) {
throw "dist\flash-card.exe not found: build it first with python tools\build_executable.py"
}
if (-not (Get-Command WindowsSandbox.exe -ErrorAction SilentlyContinue)) {
throw "WindowsSandbox.exe not found: enable the Windows feature 'Windows Sandbox' (Windows 11 Pro)"
}
if (Get-Process -Name 'WindowsSandbox*' -ErrorAction SilentlyContinue) {
throw "A Windows Sandbox is already running: close it first (only one can run at a time)"
}
Remove-Item $out -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $out -Force | Out-Null
$config = (Get-Content (Join-Path $scripts 'flash-card.wsb') -Raw) `
-replace '@DIST@', $dist -replace '@SCRIPTS@', $scripts -replace '@OUT@', $out -replace '@RUNS@', $Runs
$wsb = Join-Path $out 'run.wsb'
Set-Content -Path $wsb -Value $config -Encoding UTF8
Write-Host "Starting Windows Sandbox ($Runs runs); results in $out"
Start-Process -FilePath WindowsSandbox.exe -ArgumentList "`"$wsb`""
$result = Join-Path $out 'result.json'
$end = (Get-Date).AddMinutes($TimeoutMinutes)
while ((Get-Date) -lt $end -and -not (Test-Path $result)) { Start-Sleep -Seconds 5 }
if (-not (Test-Path $result)) { throw "No result.json after $TimeoutMinutes minutes" }
Start-Sleep -Seconds 3
$summary = Get-Content $result -Raw | ConvertFrom-Json
Write-Host ("passed {0} of {1} runs in {2}" -f $summary.passed, $summary.runs, $summary.os)
$summary.results | Format-Table run, window, clicked, progress_rows, ok, seconds
if ($summary.passed -ne $summary.runs) { exit 1 }
+132
View File
@@ -0,0 +1,132 @@
<#
.SYNOPSIS
Smoke test of flash-card.exe, run INSIDE Windows Sandbox (ADR-0005).
.DESCRIPTION
Starts the executable on the sandbox's own desktop, presses the known button
with the sandbox's own mouse, reads the progress file, takes screenshots and
writes result.json to C:\fc\out. Then it shuts the sandbox down.
It never touches the host: the host sees only the mapped folders.
#>
param(
[int]$Runs = 5,
[string]$Exe = 'C:\fc\dist\flash-card.exe',
[string]$Out = 'C:\fc\out'
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
Add-Type @'
using System;
using System.Text;
using System.Runtime.InteropServices;
public static class Win {
public delegate bool EnumProc(IntPtr h, IntPtr l);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc p, IntPtr l);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
[DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr h, ref POINT p);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(int v);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; }
public static IntPtr Find(string title) {
IntPtr found = IntPtr.Zero;
EnumWindows(delegate (IntPtr h, IntPtr l) {
if (IsWindowVisible(h)) {
var sb = new StringBuilder(256);
GetWindowText(h, sb, 256);
if (sb.ToString() == title) { found = h; return false; }
}
return true;
}, IntPtr.Zero);
return found;
}
}
'@
[void][Win]::SetProcessDpiAwareness(2)
function Wait-Window([int]$Seconds) {
$end = (Get-Date).AddSeconds($Seconds)
while ((Get-Date) -lt $end) {
$h = [Win]::Find('Flashy')
if ($h -ne [IntPtr]::Zero) { return $h }
Start-Sleep -Milliseconds 250
}
return [IntPtr]::Zero
}
function Save-Screen([string]$Path) {
$b = [System.Windows.Forms.SystemInformation]::VirtualScreen
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.CopyFromScreen($b.Left, $b.Top, 0, 0, $bmp.Size)
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
$g.Dispose(); $bmp.Dispose()
}
Add-Type -AssemblyName System.Windows.Forms
$work = Join-Path $env:TEMP 'fc-smoke'
$results = @()
for ($n = 1; $n -le $Runs; $n++) {
$run = [ordered]@{ run = $n; window = $false; clicked = $false; progress_rows = $null; ok = $false; seconds = 0 }
$t0 = Get-Date
$dir = Join-Path $work "run$n"
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path (Join-Path $dir 'data') -Force | Out-Null
Copy-Item $Exe (Join-Path $dir 'flash-card.exe')
# three words: after one known word the progress file has a header and two cards
[IO.File]::WriteAllText((Join-Path $dir 'data\ms-da.csv'), "ms;da`naaa;bbb`nccc;ddd`neee;fff`n", (New-Object Text.UTF8Encoding $false))
$p = Start-Process -FilePath (Join-Path $dir 'flash-card.exe') -ArgumentList '--level', 'all', '--no-speech' -WorkingDirectory $dir -PassThru
try {
$h = Wait-Window 60
if ($h -ne [IntPtr]::Zero) {
$run.window = $true
Start-Sleep -Milliseconds 1500
if ($n -eq 1) { Save-Screen (Join-Path $Out 'sandbox-card.png') }
$r = New-Object Win+RECT
[void][Win]::GetClientRect($h, [ref]$r)
$pt = New-Object Win+POINT
[void][Win]::ClientToScreen($h, [ref]$pt)
$x = $pt.X + 650
$y = $pt.Y + $r.Bottom - 101
[void][Win]::SetForegroundWindow($h)
Start-Sleep -Milliseconds 400
[void][Win]::SetCursorPos($x - 12, $y - 12)
Start-Sleep -Milliseconds 300
[void][Win]::SetCursorPos($x, $y)
Start-Sleep -Milliseconds 400
[Win]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
Start-Sleep -Milliseconds 120
[Win]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
$run.clicked = $true
$prog = Join-Path $dir 'data\words_to_learn.csv'
$end = (Get-Date).AddSeconds(6)
while ((Get-Date) -lt $end -and -not (Test-Path $prog)) { Start-Sleep -Milliseconds 250 }
if (Test-Path $prog) {
$rows = ([IO.File]::ReadAllLines($prog) | Where-Object { $_.Trim() -ne '' }).Count
$run.progress_rows = $rows
$run.ok = ($rows -eq 3)
}
}
}
finally {
if (-not $p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
Get-Process -Name 'flash-card' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
}
$run.seconds = [math]::Round(((Get-Date) - $t0).TotalSeconds, 1)
$results += [pscustomobject]$run
Start-Sleep -Seconds 1
}
$summary = [ordered]@{
runs = $Runs
passed = ($results | Where-Object { $_.ok }).Count
os = (Get-CimInstance Win32_OperatingSystem).Caption
results = $results
}
$summary | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $Out 'result.json') -Encoding UTF8
shutdown.exe /s /t 5