Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc9c5ea3c1 | ||
|
|
f311e71bde | ||
|
|
061b4f48b5 | ||
|
|
7bf6d5804b | ||
|
|
4ae4ece10f | ||
|
|
bc513810ec | ||
|
|
5934c396c2 | ||
|
|
d37f165c19 | ||
|
|
768ca344c9 | ||
|
|
d5873748cf |
@@ -75,3 +75,6 @@ dmypy.json
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# PyInstaller spec files
|
||||
*.spec
|
||||
|
||||
@@ -23,7 +23,7 @@ The program first asks for a level: the first 100 words, the first 1000 words or
|
||||
| `tests/` | `pytest` tests: `unit`, `integration`, `e2e` |
|
||||
| `data/` | The word list `ms-da.csv` and, after the first known word, your own `words_to_learn.csv` |
|
||||
| `docs/` | Project documents (business case, plan, design, review records), `docs/wiki/` and `docs/images/` |
|
||||
| `tools/` | Helper scripts: `generate_images.py` and `capture_screenshots.py` |
|
||||
| `tools/` | Helper scripts: `generate_images.py`, `capture_screenshots.py` and `build_executable.py` |
|
||||
| `Doxyfile` | Doxygen configuration for the source code documentation |
|
||||
|
||||
The core program uses only the Python standard library. Spoken Malay words are an optional extra.
|
||||
@@ -58,6 +58,7 @@ The wiki ([English][wiki-en], [Dansk][wiki-da], [Bahasa Malaysia][wiki-ms]) expl
|
||||
| Python 3.13 or later, with Tkinter | The program and its window (Tkinter is part of the standard Python installers for Windows and macOS; on Debian or Ubuntu install `python3-tk`) |
|
||||
| `git` | To clone the repository |
|
||||
| Doxygen (optional) | To build the source code documentation |
|
||||
| `pyinstaller` (optional, extra `build`) | To build a single executable file (see Run) |
|
||||
| `gTTS` and `playsound==1.2.2` (optional, extra `speech`) | To hear the Malay word; needs a network connection |
|
||||
|
||||
## 🛠️ Setup
|
||||
@@ -103,6 +104,15 @@ The program is run from the activated `.venv`. The window opens with the level s
|
||||
|
||||
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`.
|
||||
|
||||
### Executable (Windows, no Python needed to run it)
|
||||
|
||||
```powershell
|
||||
python -m pip install -e ".[build,speech]"
|
||||
python tools/build_executable.py
|
||||
```
|
||||
|
||||
The script builds `dist/flash-card.exe`: one file, no console window, with the images, the word list and the spoken word inside. Open it by double-clicking. It keeps your progress in a `data/` folder next to the executable (`data/words_to_learn.csv`); a `data/ms-da.csv` next to it replaces the word list inside. Add `--no-speech` to the build script to leave the speech packages out. The executable is a build output and is not committed; build it on the system you want to run it on (Windows, Linux or macOS).
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# ADR-0004: Windows executable built with PyInstaller
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | ADR-0004 |
|
||||
| CrossReference | [BC-001], [US-001], [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 | [768ca34] |
|
||||
|
||||
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The Product Owner (S01) wants an executable file, so that the program can be started without a Python installation (US-001.07). The program has images, a word list, a progress file that must survive restarts, and an optional speech extra. Options:
|
||||
|
||||
1. A one-directory build (a folder with the executable and its libraries): starts faster and the data can sit beside the files, but it is a folder of hundreds of files to hand over.
|
||||
2. A one-file build (a single `flash-card.exe` that unpacks itself to a temporary folder): one file to hand over, slower start; resources are in a temporary folder, so progress needs a place beside the executable.
|
||||
3. Another packager (Nuitka, cx_Freeze) or an installer: more tooling and a larger change for the same result.
|
||||
|
||||
## Decision
|
||||
|
||||
Build a one-file, windowed (no console) Windows executable with PyInstaller by `tools/build_executable.py`. The images and the word list are inside the executable. The progress file and an optional word list are in a `data/` folder beside the executable (found with `sys.executable` when `sys.frozen` is set), and `--data-dir` still overrides it. PyInstaller is only in the optional extra `build`, so `dependencies` stays empty (ADR-0001). The executable is a build output and is not committed.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
|
||||
- One file to hand over, and no Python, `.venv` or pip needed to run it.
|
||||
- The source run is unchanged; only the path rule differs when frozen, and it is tested without PyInstaller.
|
||||
- The word list stays editable beside the executable.
|
||||
|
||||
**Negative:**
|
||||
|
||||
- PyInstaller cannot cross-compile: the executable must be built on Windows (and on Linux or macOS for those systems).
|
||||
- A one-file executable unpacks to a temporary folder at each start and is larger with the speech packages; unsigned executables can trigger Windows SmartScreen or antivirus warnings.
|
||||
- The Windows Store Python can cause Tcl/Tk path problems with PyInstaller; a build with it must be checked by a real start.
|
||||
|
||||
## Affected Artifacts
|
||||
|
||||
- [BC-001] — scope: the executable is in scope; installer, signing and release upload are out
|
||||
- [US-001] — US-001.07 Start without Python
|
||||
- [DCD-001] — packaging support and the choice of the word list file in the composition root
|
||||
|
||||
---
|
||||
|
||||
[BC-001]: ../business-case.md
|
||||
[US-001]: ../user-stories.md
|
||||
[DCD-001]: ../dcd.md
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -14,7 +14,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
||||
| BC | Business Case | docs/business-case.md | 002 |
|
||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 009 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 010 |
|
||||
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
|
||||
| US | User Story | docs/user-stories.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 |
|
||||
| DCD | Design Class Diagram | docs/dcd.md | 002 |
|
||||
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 042 |
|
||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0005 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 051 |
|
||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||
|
||||
## Languages
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
| 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 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | In Scope: choice of level (new request of S01) | [0b983b7] |
|
||||
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | In Scope: choice of level (new request of S01) | [0b983b7] |
|
||||
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Scope: executable built with PyInstaller (new request of S01) | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
@@ -54,6 +55,7 @@ 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.
|
||||
- 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 single Windows executable file, built with PyInstaller by a script of this repository, that runs without an installed Python.
|
||||
- `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.
|
||||
- Repository description and topics on the git host.
|
||||
@@ -63,7 +65,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.
|
||||
- Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps.
|
||||
- Packaging for distribution on PyPI or as an installer.
|
||||
- Packaging for distribution on PyPI, an installer, a signed executable and an upload of the executable to a release (the executable built with PyInstaller is in scope; the file itself is not committed).
|
||||
- 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.
|
||||
|
||||
@@ -150,3 +152,4 @@ Proceed — the scope is small, the risks are mitigated, and the result serves t
|
||||
[SA-001]: ./stakeholder-analysis.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
|
||||
|
||||
+7
-4
@@ -4,19 +4,20 @@
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | DCD-001 |
|
||||
| CrossReference | [DM-001], [SD-001], [DICT-001], [ADR-0002] |
|
||||
| CrossReference | [DM-001], [SD-001], [DICT-001], [ADR-0002], [ADR-0004] |
|
||||
|
||||
## Version History
|
||||
| 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 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added Level, Deck.within, the level argument and the level screen | [0b983b7] |
|
||||
| 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 | Scope note: runtime path rules and the executable build are packaging support, not design classes | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose and Scope
|
||||
|
||||
The design classes of the program in `src/flash_card/`, in four layers (domain, application, infrastructure, presentation) with the presentation pattern MVVM ([ADR-0002]). The classes refine the concepts of [DM-001]; names are the IT terms of [DICT-001]. Methods trace to the operation contracts of [OC-001] and the sequence diagrams of [SD-001] (the SSD messages are in [SSD-001]).
|
||||
The design classes of the program in `src/flash_card/`, in four layers (domain, application, infrastructure, presentation) with the presentation pattern MVVM ([ADR-0002]). The classes refine the concepts of [DM-001]; names are the IT terms of [DICT-001]. Methods trace to the operation contracts of [OC-001] and the sequence diagrams of [SD-001] (the SSD messages are in [SSD-001]). The module `flash_card/runtime.py` (where the resources and the data folder are when the program is frozen by PyInstaller) and the scripts in `tools/` are packaging support ([ADR-0004]): they have no system operation and are not design classes; `app.main` is still the only place that names the adapters, and it chooses the word list file.
|
||||
|
||||
## Diagram
|
||||
|
||||
@@ -208,7 +209,7 @@ Main ..> FlashCardView : composes
|
||||
| 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 |
|
||||
| MVVM | `FlashCardView`, `FlashCardViewModel`, `LevelView`, `LevelViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) |
|
||||
| Composition root | `app.main` | The one place that names the adapters and shows the level screen |
|
||||
| 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) |
|
||||
| Value object | `Level`, `Card`, `LanguagePair` | Immutable data with its own rule (`Level.first`) |
|
||||
|
||||
## Dependency Check
|
||||
@@ -223,5 +224,7 @@ Imports point inward: `domain` imports nothing from the other packages; `applica
|
||||
[OC-001]: ./uc-001/oc.md
|
||||
[SD-001]: ./uc-001/sd.md
|
||||
[ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,86 @@
|
||||
# MIL-009: Executable with PyInstaller
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | MIL-009 |
|
||||
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
|
||||
| Language | en |
|
||||
| Domain | it |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-15 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
|
||||
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added task 9 (fix a flaky unit test found after the merge) | [061b4f4] |
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Decide whether the Learner can start the program from one executable file without a Python installation: the program is packaged with PyInstaller by a script of this repository, and behaves in the executable as it does from the source.
|
||||
|
||||
## Deliverable
|
||||
|
||||
`python tools/build_executable.py` builds `dist/flash-card.exe` (one file, no console window, an icon, the images and the word list inside, the speech extra included). The executable shows the level screen, keeps the progress in `data/words_to_learn.csv` next to it and prefers a `data/ms-da.csv` next to it over the word list inside. The executable itself is not committed to git; README and wiki (English, Danish, Bahasa Malaysia) explain how to build and use it.
|
||||
|
||||
## Go / No-Go Criteria
|
||||
|
||||
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `python tools/build_executable.py` ends with exit code 0 and produces `dist/flash-card.exe` | File exists and is larger than 1 MB | No file or a build error |
|
||||
| 2 | The executable starts on this machine with the virtual environment not activated and shows the level screen without a console window | A window titled "Flashy" exists after 5 seconds and the process is a Windows GUI program | The process ends or shows a console |
|
||||
| 3 | With a `data/ms-da.csv` copied next to the executable it uses that file; without it the word list inside the executable is used | Smoke test with a marked list of two words, then without | The wrong list is used |
|
||||
| 4 | Marking a word known in the executable writes `data/words_to_learn.csv` next to it and the next start continues from it | Smoke test reads the file after the run | No file or progress lost |
|
||||
| 5 | The four images and the icon are inside the executable and are found when frozen | Card and button images are visible in a captured screenshot of the executable | A missing image or an exception |
|
||||
| 6 | Path resolution when frozen (`sys.frozen`, `sys._MEIPASS`) is tested without PyInstaller | Unit tests pass | Not covered |
|
||||
| 7 | `dependencies` in `pyproject.toml` is still empty; PyInstaller is only in the extra `build` | File check | A runtime dependency was added |
|
||||
| 8 | BC-001, DCD-001 and ADR-0004 are updated and reviewed `Go`; README and wiki in three languages describe the build | Review records `Go`; `check-readme.sh` exit 0 | A document is missing |
|
||||
| 9 | 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-008 | The finished program, with the level screen, that is packaged |
|
||||
|
||||
## Traceability
|
||||
|
||||
| Business Case objective / KPI / user story | Reference |
|
||||
| --- | --- |
|
||||
| Objective 4 (a repository others can use), extended by the new request of S01; success criterion 4 (time to run) | [BC-001] |
|
||||
| US-001.07 Start without Python | [US-001] |
|
||||
|
||||
## Ownership
|
||||
|
||||
| Role | Stakeholder ID (SA) |
|
||||
| --- | --- |
|
||||
| Owner | S01 |
|
||||
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
|
||||
|
||||
## Target Date
|
||||
|
||||
2026-10-16 — 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 executable | Amend BC-001 (In Scope: an executable built with PyInstaller; Out of Scope: an installer and a signed release stay out), add ADR-0004 (PyInstaller one-file windowed build, word list inside and a `data` folder beside the executable for progress, Windows only), and add the runtime path resolution to DCD-001. Every changed document gets a Version History row and a review. | No | BC-001, DCD-001 |
|
||||
| 2 | Resolve resource and data paths when frozen | The images are found under `sys._MEIPASS` and the default data folder is `data/` next to the executable when `sys.frozen` is set; the source run keeps its paths. The rule is one function used by `constants.py`, tested by simulating the frozen state. | Yes | US-001.07 |
|
||||
| 3 | Prefer the word list next to the executable | When the data folder has no `ms-da.csv` the program uses the copy inside the executable; the progress file is always written to the data folder; `--data-dir` still overrides both. | Yes | US-001.07 |
|
||||
| 4 | Add the build script and the build extra | `tools/build_executable.py` calls PyInstaller (one file, windowed, name `flash-card`, the assets and the word list as data, the icon, the speech packages as hidden imports); `pyproject.toml` gets the extra `build`; `build/`, `dist/` and `*.spec` are ignored. | No | ADR-0004 |
|
||||
| 5 | Generate the application icon | `tools/generate_images.py` also writes `flash_card.ico` (the card with the tick) from the same drawing code, so no external asset is used. | No | |
|
||||
| 6 | Build and smoke-test the executable | Build it, start it with the virtual environment not active, check the window, the images, the level choice, the progress file next to it, the word list override and the speech import; record the result in the review record. | Yes | US-001.07 |
|
||||
| 7 | Test the path rules and the build arguments | Unit tests for the frozen and source path rules and for the argument list the build script gives PyInstaller (without running PyInstaller). | No | |
|
||||
| 8 | Update README and wiki | README and the Install page of the wiki in English, Danish and Bahasa Malaysia: how to build, where the executable and the data folder are, that Python is not needed to run it. The wiki is republished after the merge. | No | |
|
||||
| 9 | Fix a flaky unit test | After the merge a unit test of `app.main` failed in about 1 of 6 full runs because it created a real Tk root; it uses the module's fake root instead, so the unit tests never need a display. Found by running the suite repeatedly. | No | RC-049 |
|
||||
|
||||
---
|
||||
|
||||
[BC-001]: ../business-case.md
|
||||
[US-001]: ../user-stories.md
|
||||
[UC-001]: ../uc-001/uc.md
|
||||
[DCD-001]: ../dcd.md
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
[061b4f4]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/061b4f48b5d49e6800656c95d79df707e07ec0f3
|
||||
+10
-3
@@ -4,7 +4,7 @@
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| 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] |
|
||||
| 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] |
|
||||
| Language | en |
|
||||
| Domain | it |
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
| 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 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-008 Choose level (new request of S01) | [0b983b7] |
|
||||
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-008 Choose level (new request of S01) | [0b983b7] |
|
||||
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added gateway MIL-009 Executable with PyInstaller (new request of S01) | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
@@ -40,6 +41,7 @@ This plan schedules the seven phases (gateways) that deliver the flash card prog
|
||||
| Spoken Malay word | [MIL-006] | 2026-10-19 | 2026-10-19 | S01 | US-001.05 | Text to speech as optional extra | [Milestone MIL-006][MS-006] |
|
||||
| Documentation and publication | [MIL-007] | 2026-10-19 to 2026-10-21 | 2026-10-21 | S01 | none | README, screenshots, wiki in 3 languages, description and topics | [Milestone MIL-007][MS-007] |
|
||||
| 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 | |
|
||||
|
||||
```plantuml
|
||||
@startgantt
|
||||
@@ -60,6 +62,8 @@ Project starts 2026-10-14
|
||||
[Documentation Go/No-Go] happens 2026-10-21
|
||||
[Choose level] starts 2026-10-20 and ends 2026-10-20
|
||||
[Choose level Go/No-Go] happens 2026-10-20
|
||||
[Executable] starts 2026-10-16 and ends 2026-10-16
|
||||
[Executable Go/No-Go] happens 2026-10-16
|
||||
@endgantt
|
||||
```
|
||||
|
||||
@@ -75,12 +79,13 @@ Project starts 2026-10-14
|
||||
| README, screenshots, wiki in three languages | MIL-007 |
|
||||
| Repository description and topics | MIL-007 |
|
||||
| Choice of level (new request after MIL-007) | MIL-008 |
|
||||
| Executable built with PyInstaller (new request after MIL-008) | MIL-009 |
|
||||
| Framework documents of the baseline | Baseline pull request before MIL-001 |
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008
|
||||
Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 → MIL-008 → MIL-009
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -111,6 +116,7 @@ A No-Go on a phase moves every later date by the days the rework takes; MIL-008
|
||||
[MIL-006]: ./milestones/mil-006-malay-speech.md
|
||||
[MIL-007]: ./milestones/mil-007-documentation-and-publication.md
|
||||
[MIL-008]: ./milestones/mil-008-choose-level.md
|
||||
[MIL-009]: ./milestones/mil-009-executable.md
|
||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||
[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
|
||||
@@ -120,3 +126,4 @@ A No-Go on a phase moves every later date by the days the rework takes; MIL-008
|
||||
[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
|
||||
[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
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# RC-042: Review of BC-001
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-042 |
|
||||
| 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 | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## 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 executable is added to In Scope and the installer, signing and release upload are named in 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-009 (executable).
|
||||
|
||||
## 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
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -0,0 +1,70 @@
|
||||
# RC-043: Review of US-001
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-043 |
|
||||
| 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 | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## 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 seven stories and states one dependency exception. |
|
||||
| 2 | Written in "As a / I want / So that" form | Pass | All seven stories use `As a / I want / so that`. |
|
||||
| 3 | Clear, testable acceptance criteria are included | Pass | Every story, including US-001.07, 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-009 (executable).
|
||||
|
||||
## 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
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -0,0 +1,54 @@
|
||||
# RC-044: Review of PP-001
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-044 |
|
||||
| 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 | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## 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-009 on 2026-10-16 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-009 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
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -0,0 +1,55 @@
|
||||
# RC-045: Review of DCD-001
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-045 |
|
||||
| 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 | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## 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); view, view model, session, deck and adapters are separate. |
|
||||
| 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), 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 is unchanged; the packaging support (`runtime.py`, `tools/`) is stated in Purpose and Scope as having no system operation and not being a design class, so it needs no row. |
|
||||
| 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; no class was 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-009 (executable).
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| none | - | - |
|
||||
|
||||
---
|
||||
|
||||
[DCD-001]: ../../../docs/dcd.md
|
||||
[QC-DCD-001]: ../../../framework/qc/artifacts/qc-dcd.md
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -0,0 +1,55 @@
|
||||
# RC-046: Review of ADR-0004
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-046 |
|
||||
| CrossReference | [ADR-0004], [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 | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [ADR-0004]
|
||||
- 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-0004` matches the file name number. |
|
||||
| 2 | Context states the problem, the forces (cost, risk, constraints) and the options evaluated | Pass | Context states the need (US-001.07), the forces and three options. |
|
||||
| 3 | At least two options were considered, or the absence of alternatives is justified | Pass | Three 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 (Windows only, cross-compile limit), start-up time and size are named. |
|
||||
| 7 | Affected Artifacts are listed as links, and each is also in CrossReference | Pass | Affected Artifacts BC-001, US-001 and DCD-001 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; `sys.frozen`, `sys.executable` are code names. |
|
||||
| 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-0004]: ../../../docs/adr/adr-0004-pyinstaller-executable.md
|
||||
[QC-ADR-001]: ../../../framework/qc/artifacts/qc-adr.md
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -0,0 +1,66 @@
|
||||
# RC-047: Review of MIL-009
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-047 |
|
||||
| CrossReference | [MIL-009], [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 | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [MIL-009]
|
||||
- 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-009 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-009]: ../../../docs/milestones/mil-009-executable.md
|
||||
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
@@ -0,0 +1,64 @@
|
||||
# RC-048: Review of PY-MIL-009
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-048 |
|
||||
| CrossReference | [PY-MIL-009], [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 | [bc51381] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [PY-MIL-009]
|
||||
- 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 `runtime.py`, `build_executable.py` and the changed modules; 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 (`resource_root`, `executable_dir`, `build_arguments`, `resolve_data_files`). |
|
||||
| 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 71 files with no suppressions. |
|
||||
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 45 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 | `main` of the build tool returns 2 with a message when PyInstaller is not installed; no bare `except`; no swallowed exception. |
|
||||
| 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 | No new file handling beyond the existing context managers; the ICO file is written with `Path.write_bytes`. |
|
||||
| 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` in `src`; the tools write results with `sys.stdout.write`; no secret. |
|
||||
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | The program classes are unchanged (DCD-001); `runtime.py` and `tools/` are packaging support outside the design classes, as ADR-0004 and the Scope note of DCD-001 state; `resolve_data_files` is a function of the composition root. |
|
||||
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 216 tests (3 consecutive runs green): pure runtime helpers for the frozen and source cases, the argument list of the build, the ICO file, the bundled word list fallback, and the level screen window title; none starts PyInstaller or uses the network. The real executable was built and smoke-tested separately (note below). |
|
||||
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; `sys._MEIPASS` is read with `getattr` and no `Any` is used. |
|
||||
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` is still empty; PyInstaller is only in the extra `build` and Pillow only in `tools`. |
|
||||
| 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 frozen rule is one definition (`runtime.py`) used by `constants.py`; file names and sizes stay in `constants.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; `runtime.py` imports only the standard library. |
|
||||
| 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 build tool has one purpose and a pure `build_arguments` that is tested; the composition root chooses the word list file. |
|
||||
| 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/build_executable.py` and `runtime.py` sit outside the four layers as packaging support; layers and 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.
|
||||
|
||||
Milestone MIL-009 Go criteria: 1 `python tools/build_executable.py` ended with exit code 0 and wrote `dist/flash-card.exe` (25.8 MB) with PyInstaller 6.22.3 on the Windows Store Python 3.13.14; 2 started from a temporary folder without the virtual environment, the executable showed a window titled `Flashy` after about 2 seconds and has no console (windowed bootloader); 3 with `data/ms-da.csv` beside it (3 words) one tick left 2 cards in the progress file, without it the embedded list was used (1000 words); 4 one tick wrote `data/words_to_learn.csv` next to the executable with 999 cards at level 100; 5 a captured screenshot of the executable shows the card and both button images; 6 unit tests of the frozen rule pass; 7 `dependencies` empty, extra `build`; 8 BC-001, DCD-001, ADR-0004, PP-001, US-001 and MIL-009 were reviewed Go (RC-042 to RC-047), README and the Install page of the wiki in three languages describe the build; 9 this record, `pytest` 216 passed, ruff, mypy, lint-imports and doxygen clean. Defects found by the smoke test and fixed in this milestone: the level screen window had the default title `tk` and no padding because `LevelView` did not call `configure_window`. Notes: the one-file build starts a second process (the bootloader), so a tool that watches a window must not match the first process id; the first start after a build took over 20 seconds on this machine (unpacking and a virus scan) and later starts about 2 seconds; the spoken word was not tested in the executable (the speech packages are collected into it, the network call was not exercised). All Go.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| none | - | - |
|
||||
|
||||
---
|
||||
|
||||
[PY-MIL-009]: ../../../src/flash_card
|
||||
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||
[bc51381]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/bc513810ece2506d5ab63c1d4c603803c2ad415e
|
||||
@@ -0,0 +1,64 @@
|
||||
# RC-049: Review of PY-FIX-001
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-049 |
|
||||
| CrossReference | [PY-FIX-001], [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 | [061b4f4] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [PY-FIX-001]
|
||||
- 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 fixture and names follow PEP 8 (`fake_tk_root`). |
|
||||
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose. |
|
||||
| 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 | The fixture is annotated `-> None`; `mypy --strict` passes on 45 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 | N-A | No file handling in the change. |
|
||||
| 8 | Public modules, classes and functions have docstrings that say what, not how | N-A | Tests are excluded from the docstring rules; the fixture has a one-line docstring. |
|
||||
| 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 change is in a test of `app.main`. |
|
||||
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | The change makes the test independent of a real Tk root: 20 consecutive full runs (216 tests) were green after 2 failures in 12 runs before; real Tk stays covered by `tests/e2e`. |
|
||||
| 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 | No dependency change. |
|
||||
| 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 fake root is the module's existing `_FakeRoot`, used once by an autouse fixture instead of per test. |
|
||||
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | No source import changed; `lint-imports` 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 fixture with one responsibility. |
|
||||
| 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 | Test file stays 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 unchanged. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||
|
||||
Defect fixed: an unpatched `app.main` call created a real `tk.Tk()` inside a unit test, which failed in about 1 of 6 full runs (the only Tk process limit and Tcl handle problem seen in MIL-005). Found after the MIL-009 merge, outside any milestone task, so no issue is closed. Go.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| none | - | - |
|
||||
|
||||
---
|
||||
|
||||
[PY-FIX-001]: ../../../tests/unit/test_app.py
|
||||
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||
[061b4f4]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/061b4f48b5d49e6800656c95d79df707e07ec0f3
|
||||
@@ -0,0 +1,68 @@
|
||||
# RC-050: Review of MIL-009
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-050 |
|
||||
| CrossReference | [MIL-009], [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 | [061b4f4] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [MIL-009]
|
||||
- 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-009 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.
|
||||
|
||||
Re-review of MIL-009 after task 9 was added (a flaky unit test fix); Go/No-Go criteria, deliverable and dates are unchanged.
|
||||
|
||||
## 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-009]: ../../../docs/milestones/mil-009-executable.md
|
||||
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
|
||||
[061b4f4]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/061b4f48b5d49e6800656c95d79df707e07ec0f3
|
||||
@@ -10,7 +10,9 @@
|
||||
| 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 | Accepted | 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 | 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) | [061b4f4] |
|
||||
|
||||
---
|
||||
|
||||
@@ -24,17 +26,17 @@ updated whenever an artifact instance is created or reviewed.
|
||||
|
||||
| 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-030] |
|
||||
| BC-001 | Business Case | en | it | - | SA-001, PP-001 | [RC-042] |
|
||||
| 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-032] |
|
||||
| PP-001 | Project Plan | en | it | BC-001, SA-001 | MIL-001 to MIL-008 | [RC-044] |
|
||||
| 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-031] |
|
||||
| US-001 | User Story | en | it | UCD-001, BC-001 | MIL-002 to MIL-006, MIL-008 | [RC-043] |
|
||||
| UC-001 | Use Case | en | it | UCD-001, US-001 | DM-001, SSD-001 | [RC-033] |
|
||||
| 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] |
|
||||
| OC-001 | Operation Contract | - | - | SSD-001, DM-001 | SD-001 | [RC-037] |
|
||||
| SD-001 | Sequence Diagram | - | - | OC-001 | DCD-001 | [RC-038] |
|
||||
| DCD-001 | Design Class Diagram | - | - | DM-001, SD-001, DICT-001 | src/flash_card | [RC-039] |
|
||||
| DCD-001 | Design Class Diagram | - | - | DM-001, SD-001, DICT-001 | src/flash_card | [RC-045] |
|
||||
| DICT-001 | Domain Dictionary | en | it | BC-001, SA-001, DM-001 | DCD-001 | [RC-035] |
|
||||
| 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] |
|
||||
@@ -47,6 +49,8 @@ updated whenever an artifact instance is created or reviewed.
|
||||
| MIL-006 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-006 | [RC-021] |
|
||||
| MIL-007 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-007 | [RC-022] |
|
||||
| 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] |
|
||||
| ADR-0004 | Architecture Decision Record | - | - | BC-001 | - | [RC-046] |
|
||||
| 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-003 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-025] |
|
||||
@@ -55,6 +59,8 @@ updated whenever an artifact instance is created or reviewed.
|
||||
| PY-MIL-006 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-028] |
|
||||
| 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-009 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-048] |
|
||||
| PY-FIX-001 | Python Source Code | - | - | PY-MIL-008 | - | [RC-049] |
|
||||
|
||||
## Coverage Notes
|
||||
|
||||
@@ -138,3 +144,18 @@ updated whenever an artifact instance is created or reviewed.
|
||||
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
|
||||
[RC-041]: ./reviews/rc-041-py-mil-008.md
|
||||
[PY-MIL-008]: ../../src/flash_card
|
||||
[RC-042]: ./reviews/rc-042-business-case-exe.md
|
||||
[RC-043]: ./reviews/rc-043-user-stories-exe.md
|
||||
[RC-044]: ./reviews/rc-044-project-plan-exe.md
|
||||
[RC-045]: ./reviews/rc-045-design-class-diagram-exe.md
|
||||
[RC-046]: ./reviews/rc-046-adr-0004.md
|
||||
[RC-047]: ./reviews/rc-047-mil-009.md
|
||||
[MIL-009]: ../milestones/mil-009-executable.md
|
||||
[ADR-0004]: ../adr/adr-0004-pyinstaller-executable.md
|
||||
[768ca34]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/768ca344c95d4f21e565e37ac96661c2f8dc8f54
|
||||
[RC-048]: ./reviews/rc-048-py-mil-009.md
|
||||
[PY-MIL-009]: ../../src/flash_card
|
||||
[RC-049]: ./reviews/rc-049-py-fix-001.md
|
||||
[PY-FIX-001]: ../../tests/unit/test_app.py
|
||||
[RC-050]: ./reviews/rc-050-mil-009-task9.md
|
||||
[061b4f4]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/061b4f48b5d49e6800656c95d79df707e07ec0f3
|
||||
|
||||
+22
-4
@@ -4,7 +4,7 @@
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | US-001 |
|
||||
| CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-008] |
|
||||
| CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-008], [MIL-009] |
|
||||
| Language | en |
|
||||
| Domain | it |
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
| 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 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.06 Choose my level | [0b983b7] |
|
||||
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.06 Choose my level | [0b983b7] |
|
||||
| 2026-10-15 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added US-001.07 Start without Python | [768ca34] |
|
||||
|
||||
---
|
||||
|
||||
## 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 and the choice of level. 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 and the start without an installed Python. Each story is delivered by one milestone. The wireframe the stories follow is in `docs/uc-001/wireframe.md`.
|
||||
|
||||
## Story List
|
||||
|
||||
@@ -105,9 +106,24 @@ These stories cover the epic "Study Flash Cards": the four steps of the course a
|
||||
| --- | --- | --- |
|
||||
| [MIL-008], [UC-001] | fits one iteration | none |
|
||||
|
||||
### US-001.07 — Start without Python
|
||||
|
||||
**As a** Learner, **I want** to start the program by opening one executable file, **so that** I can learn without installing Python or a virtual environment.
|
||||
|
||||
**Acceptance Criteria**
|
||||
|
||||
- Given the executable was built, when I open it on a Windows computer without Python, then the level screen appears and no console window opens.
|
||||
- Given I mark a word as known in the executable, when I open it again, then the known word does not come up (my progress is kept in `data/words_to_learn.csv` next to the executable).
|
||||
- Given a `data/ms-da.csv` exists next to the executable, when the program starts, then that word list is used; otherwise the word list inside the executable is used.
|
||||
- Given the speech extra was included in the build and a network is available, when a card is shown, then the Malay word is spoken; without a network the program works silently.
|
||||
|
||||
| Traces to | Size | INVEST exceptions |
|
||||
| --- | --- | --- |
|
||||
| [MIL-009], [UC-001] | fits one iteration | none |
|
||||
|
||||
## INVEST Check
|
||||
|
||||
All six 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.06 depend on the window of US-001.01, which is stated in their milestones' Dependencies.
|
||||
All seven 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.07 depend on the window of US-001.01, which is stated in their milestones' Dependencies.
|
||||
|
||||
---
|
||||
|
||||
@@ -120,5 +136,7 @@ All six stories are Independent in delivery order (each builds on the one before
|
||||
[MIL-005]: ./milestones/mil-005-step4-save-progress.md
|
||||
[MIL-006]: ./milestones/mil-006-malay-speech.md
|
||||
[MIL-008]: ./milestones/mil-008-choose-level.md
|
||||
[MIL-009]: ./milestones/mil-009-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
|
||||
|
||||
@@ -56,6 +56,20 @@ python -m pytest
|
||||
|
||||
Vinduestesten i `tests/e2e/` springes over, når der ikke er en skærm.
|
||||
|
||||
## Byg en programfil (Windows)
|
||||
|
||||
```powershell
|
||||
python -m pip install -e ".[build,speech]"
|
||||
python tools/build_executable.py
|
||||
```
|
||||
|
||||
Scriptet bruger PyInstaller og skriver `dist/flash-card.exe`: én fil uden konsolvindue, med billeder, ordliste og udtalen indeni. Dobbeltklik for at starte; Python er ikke nødvendigt for at køre den.
|
||||
|
||||
- Din fremgang gemmes i en `data/`-mappe ved siden af programfilen (`data/words_to_learn.csv`).
|
||||
- En `data/ms-da.csv` ved siden af programfilen erstatter ordlisten indeni.
|
||||
- `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.
|
||||
|
||||
## Dokumentation af kildekoden
|
||||
|
||||
```bash
|
||||
|
||||
@@ -56,6 +56,20 @@ python -m pytest
|
||||
|
||||
Ujian tetingkap dalam `tests/e2e/` dilangkau apabila tiada paparan.
|
||||
|
||||
## Bina fail boleh laku (Windows)
|
||||
|
||||
```powershell
|
||||
python -m pip install -e ".[build,speech]"
|
||||
python tools/build_executable.py
|
||||
```
|
||||
|
||||
Skrip menggunakan PyInstaller dan menulis `dist/flash-card.exe`: satu fail tanpa tetingkap konsol, dengan imej, senarai perkataan dan suara di dalamnya. Klik dua kali untuk memulakan; Python tidak diperlukan untuk menjalankannya.
|
||||
|
||||
- Kemajuan anda disimpan dalam folder `data/` di sebelah fail boleh laku (`data/words_to_learn.csv`).
|
||||
- `data/ms-da.csv` di sebelah fail boleh laku menggantikan senarai perkataan di dalamnya.
|
||||
- `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.
|
||||
|
||||
## Dokumentasi kod sumber
|
||||
|
||||
```bash
|
||||
|
||||
@@ -56,6 +56,20 @@ python -m pytest
|
||||
|
||||
The window test in `tests/e2e/` is skipped when no display is available.
|
||||
|
||||
## Build an executable (Windows)
|
||||
|
||||
```powershell
|
||||
python -m pip install -e ".[build,speech]"
|
||||
python tools/build_executable.py
|
||||
```
|
||||
|
||||
The script uses PyInstaller and writes `dist/flash-card.exe`: one file, no console window, with the images, the word list and the spoken word inside. Double-click it to start; Python is not needed to run it.
|
||||
|
||||
- Your progress is kept in a `data/` folder next to the executable (`data/words_to_learn.csv`).
|
||||
- A `data/ms-da.csv` next to the executable replaces the word list inside it.
|
||||
- `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.
|
||||
|
||||
## Documentation of the source code
|
||||
|
||||
```bash
|
||||
|
||||
+3
-1
@@ -26,6 +26,8 @@ dependencies = []
|
||||
speech = ["gTTS>=2.5", "playsound==1.2.2"]
|
||||
# Only for tools/capture_screenshots.py; not needed to run the program.
|
||||
tools = ["pillow>=10"]
|
||||
# Only for tools/build_executable.py; not needed to run the program.
|
||||
build = ["pyinstaller>=6"]
|
||||
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "import-linter>=2.0"]
|
||||
|
||||
[project.scripts]
|
||||
@@ -35,7 +37,7 @@ flash-card = "flash_card.presentation.app:run"
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
flash_card = ["presentation/assets/*.png"]
|
||||
flash_card = ["presentation/assets/*.png", "presentation/assets/*.ico"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
@@ -9,6 +9,8 @@ from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from flash_card.runtime import executable_dir, is_frozen, resource_root
|
||||
|
||||
## @brief Window background colour.
|
||||
BACKGROUND_COLOR: Final[str] = "#B1DDC6"
|
||||
## @brief Window title.
|
||||
@@ -77,12 +79,24 @@ VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv"
|
||||
## @brief File name of the words still to learn.
|
||||
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
|
||||
|
||||
## @brief Root folder of the project.
|
||||
## @brief Root folder of the project, for a source run.
|
||||
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
|
||||
## @brief Default folder of the data files.
|
||||
DEFAULT_DATA_DIR: Final[Path] = PROJECT_ROOT / "data"
|
||||
## @brief Folder of the image assets.
|
||||
ASSETS_DIR: Final[Path] = Path(__file__).resolve().parent / "presentation" / "assets"
|
||||
## @brief Folder of the image assets, inside the bundle when frozen.
|
||||
ASSETS_DIR: Final[Path] = (
|
||||
resource_root() / "flash_card" / "presentation" / "assets"
|
||||
if is_frozen()
|
||||
else Path(__file__).resolve().parent / "presentation" / "assets"
|
||||
)
|
||||
## @brief Folder of the word list delivered with the program.
|
||||
BUNDLED_DATA_DIR: Final[Path] = resource_root() / "data"
|
||||
## @brief The word list delivered with the program.
|
||||
BUNDLED_VOCABULARY_PATH: Final[Path] = BUNDLED_DATA_DIR / VOCABULARY_FILE_NAME
|
||||
## @brief Default folder of the data files, next to the executable when frozen.
|
||||
DEFAULT_DATA_DIR: Final[Path] = executable_dir() / "data"
|
||||
## @brief File name of the program icon.
|
||||
ICON_FILE_NAME: Final[str] = "flash_card.ico"
|
||||
## @brief Name of the executable without extension.
|
||||
EXECUTABLE_NAME: Final[str] = "flash-card"
|
||||
## @brief File name of the card front image.
|
||||
CARD_FRONT_IMAGE_NAME: Final[str] = "card_front.png"
|
||||
## @brief File name of the card back image.
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import NoReturn
|
||||
from flash_card.application.ports.speech_port import SpeechPort
|
||||
from flash_card.application.use_cases.open_study_session import open_study_session
|
||||
from flash_card.constants import (
|
||||
BUNDLED_VOCABULARY_PATH,
|
||||
DEFAULT_DATA_DIR,
|
||||
LEVEL_ARGUMENT_VALUES,
|
||||
PROGRESS_FILE_NAME,
|
||||
@@ -151,6 +152,20 @@ class Application:
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
def resolve_data_files(data_dir: Path | None) -> tuple[Path, Path]:
|
||||
"""Find the word list and the progress file.
|
||||
|
||||
@param data_dir Folder given by the user, or None for the default folder.
|
||||
@return The vocabulary path and the progress path. Without a given folder,
|
||||
the delivered word list is used when the default folder has none.
|
||||
"""
|
||||
folder = DEFAULT_DATA_DIR if data_dir is None else data_dir
|
||||
vocabulary = folder / VOCABULARY_FILE_NAME
|
||||
if data_dir is None and not vocabulary.exists():
|
||||
vocabulary = BUNDLED_VOCABULARY_PATH
|
||||
return vocabulary, folder / PROGRESS_FILE_NAME
|
||||
|
||||
|
||||
def create_application(
|
||||
data_dir: Path | None = None,
|
||||
speech_enabled: bool = True,
|
||||
@@ -158,19 +173,20 @@ def create_application(
|
||||
) -> Application:
|
||||
"""Create the window and show the level screen, or start at the given level.
|
||||
|
||||
@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.
|
||||
@param speech_enabled False to run without spoken words.
|
||||
@param level The level to start at, or None to show the level screen.
|
||||
@return The application, its window in the root attribute.
|
||||
@throws VocabularyError When a level is given and the word list or saved
|
||||
progress is unusable.
|
||||
"""
|
||||
folder = DEFAULT_DATA_DIR if data_dir is None else data_dir
|
||||
vocabulary_path, progress_path = resolve_data_files(data_dir)
|
||||
root = tk.Tk()
|
||||
application = Application(
|
||||
root,
|
||||
CsvVocabularyRepository(folder / VOCABULARY_FILE_NAME),
|
||||
CsvProgressRepository(folder / PROGRESS_FILE_NAME),
|
||||
CsvVocabularyRepository(vocabulary_path),
|
||||
CsvProgressRepository(progress_path),
|
||||
build_speech(speech_enabled),
|
||||
)
|
||||
if level is None:
|
||||
@@ -209,8 +225,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser.add_argument(
|
||||
"--data-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_DATA_DIR,
|
||||
help="folder of the data files",
|
||||
default=None,
|
||||
help="folder of the data files (default: data next to the program)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-speech",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -21,6 +21,7 @@ from flash_card.presentation.features.level.level_view_model import (
|
||||
LevelChoice,
|
||||
LevelViewModel,
|
||||
)
|
||||
from flash_card.presentation.shared.window import configure_window
|
||||
|
||||
|
||||
class LevelView:
|
||||
@@ -40,6 +41,7 @@ class LevelView:
|
||||
"""
|
||||
## @brief Called with the level the user picked.
|
||||
self._on_choose = on_choose
|
||||
configure_window(root)
|
||||
## @brief Frame that holds the whole screen.
|
||||
self.frame = tk.Frame(
|
||||
root, bg=BACKGROUND_COLOR, width=CARD_WIDTH, height=LEVEL_SCREEN_HEIGHT
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Locate resources and the executable folder, frozen or from source.
|
||||
|
||||
@file runtime.py
|
||||
@brief Packaging support: tells where bundled files and the executable are.
|
||||
Imports only the standard library; it is not one of the layers.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
## @brief Number of folders from this file up to the project root.
|
||||
_ROOT_LEVELS = 2
|
||||
|
||||
|
||||
def is_frozen() -> bool:
|
||||
"""Tell whether the program runs as a PyInstaller executable.
|
||||
|
||||
@return True when frozen.
|
||||
"""
|
||||
return bool(getattr(sys, "frozen", False))
|
||||
|
||||
|
||||
def source_root() -> Path:
|
||||
"""Return the project root of a source run.
|
||||
|
||||
@return The folder that holds src.
|
||||
"""
|
||||
return Path(__file__).resolve().parents[_ROOT_LEVELS]
|
||||
|
||||
|
||||
def pick_resource_root(frozen: bool, bundle_dir: Path | None, root: Path) -> Path:
|
||||
"""Choose the folder that holds the bundled resources.
|
||||
|
||||
@param frozen True when running as an executable.
|
||||
@param bundle_dir The unpack folder of the executable, when frozen.
|
||||
@param root The project root of a source run.
|
||||
@return The bundle folder when frozen and known, else the project root.
|
||||
"""
|
||||
if frozen and bundle_dir is not None:
|
||||
return bundle_dir
|
||||
return root
|
||||
|
||||
|
||||
def pick_executable_dir(frozen: bool, executable: Path, root: Path) -> Path:
|
||||
"""Choose the folder next to the executable.
|
||||
|
||||
@param frozen True when running as an executable.
|
||||
@param executable Path of the running executable.
|
||||
@param root The project root of a source run.
|
||||
@return The folder of the executable when frozen, else the project root.
|
||||
"""
|
||||
if frozen:
|
||||
return executable.parent
|
||||
return root
|
||||
|
||||
|
||||
def resource_root() -> Path:
|
||||
"""Return the folder that holds the bundled resources.
|
||||
|
||||
@return The PyInstaller unpack folder when frozen, else the project root.
|
||||
"""
|
||||
bundle = getattr(sys, "_MEIPASS", None)
|
||||
bundle_dir = Path(bundle) if bundle else None
|
||||
return pick_resource_root(is_frozen(), bundle_dir, source_root())
|
||||
|
||||
|
||||
def executable_dir() -> Path:
|
||||
"""Return the folder of the running executable.
|
||||
|
||||
@return The folder of the executable when frozen, else the project root.
|
||||
"""
|
||||
return pick_executable_dir(is_frozen(), Path(sys.executable), source_root())
|
||||
@@ -105,6 +105,22 @@ def test_level_screen_shows_title_with_font_on_window_background(
|
||||
assert level_view.frame.cget("bg") == BACKGROUND_COLOR
|
||||
|
||||
|
||||
def test_level_screen_sets_window_title_background_and_padding(
|
||||
tk_root: tk.Tk,
|
||||
) -> None:
|
||||
tk_root.title("tk")
|
||||
tk_root.config(padx=0, pady=0, bg="white")
|
||||
|
||||
view = LevelView(tk_root, LevelViewModel(), lambda level: None)
|
||||
try:
|
||||
assert tk_root.title() == WINDOW_TITLE
|
||||
assert tk_root.cget("bg") == BACKGROUND_COLOR
|
||||
assert int(str(tk_root.cget("padx"))) == WINDOW_PADDING
|
||||
assert int(str(tk_root.cget("pady"))) == WINDOW_PADDING
|
||||
finally:
|
||||
view.destroy()
|
||||
|
||||
|
||||
def test_level_screen_frame_has_the_card_window_size(level_view: LevelView) -> None:
|
||||
assert level_view.frame.winfo_width() == CARD_WIDTH
|
||||
assert level_view.frame.winfo_height() == LEVEL_SCREEN_HEIGHT
|
||||
|
||||
@@ -92,6 +92,12 @@ def _first(cards: Sequence[Card]) -> Card:
|
||||
return cards[0]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_tk_root(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Keep every test in this module away from a real Tk root."""
|
||||
monkeypatch.setattr(app.tk, "Tk", _FakeRoot)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_views(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(app, "LevelView", _FakeLevelView)
|
||||
@@ -241,3 +247,49 @@ def test_start_raises_vocabulary_error_for_the_caller(fake_views: None) -> None:
|
||||
application = _application(_FakeRoot(), error=True)
|
||||
with pytest.raises(VocabularyNotFoundError):
|
||||
application.start(Level(100))
|
||||
|
||||
|
||||
def test_resolve_data_files_uses_bundled_list_when_default_folder_has_none(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
bundled = tmp_path / "bundle" / "ms-da.csv"
|
||||
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", tmp_path / "data")
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", bundled)
|
||||
vocabulary, progress = app.resolve_data_files(None)
|
||||
assert vocabulary == bundled
|
||||
assert progress == tmp_path / "data" / "words_to_learn.csv"
|
||||
|
||||
|
||||
def test_resolve_data_files_prefers_the_default_folder_list(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
(tmp_path / "ms-da.csv").write_text("x", encoding="utf-8")
|
||||
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", tmp_path / "other.csv")
|
||||
vocabulary, _ = app.resolve_data_files(None)
|
||||
assert vocabulary == tmp_path / "ms-da.csv"
|
||||
|
||||
|
||||
def test_resolve_data_files_uses_a_given_folder_alone(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", tmp_path / "other.csv")
|
||||
vocabulary, progress = app.resolve_data_files(tmp_path / "given")
|
||||
assert vocabulary == tmp_path / "given" / "ms-da.csv"
|
||||
assert progress == tmp_path / "given" / "words_to_learn.csv"
|
||||
|
||||
|
||||
def test_create_application_draws_first_card_from_bundled_list(
|
||||
fake_views: None, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
bundled = tmp_path / "bundle" / "ms-da.csv"
|
||||
bundled.parent.mkdir()
|
||||
bundled.write_text("ms;da\nsaya;jeg\n", encoding="utf-8")
|
||||
data = tmp_path / "data"
|
||||
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", data)
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", bundled)
|
||||
application = app.create_application(None, False, Level(None))
|
||||
view = cast(_FakeFlashCardView, application.view)
|
||||
assert view.view_model.text == "saya"
|
||||
view.view_model.is_known()
|
||||
assert (data / "words_to_learn.csv").is_file()
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Tests of the executable build tool.
|
||||
|
||||
@file test_build_executable.py
|
||||
@brief Checks the PyInstaller arguments and the missing PyInstaller message.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from flash_card.constants import EXECUTABLE_NAME, ICON_FILE_NAME
|
||||
|
||||
_TOOL_PATH = Path(__file__).resolve().parents[2] / "tools" / "build_executable.py"
|
||||
ROOT = Path("project")
|
||||
ASSETS = ROOT / "src" / "flash_card" / "presentation" / "assets"
|
||||
|
||||
|
||||
def _load_tool() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("build_executable", _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 _option(arguments: list[str], name: str) -> str:
|
||||
return arguments[arguments.index(name) + 1]
|
||||
|
||||
|
||||
def test_build_arguments_start_with_the_packaging_flags() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
assert arguments[:3] == ["--onefile", "--windowed", "--noconfirm"]
|
||||
|
||||
|
||||
def test_build_arguments_name_icon_and_folders() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
assert _option(arguments, "--name") == EXECUTABLE_NAME == "flash-card"
|
||||
assert _option(arguments, "--icon") == str(ASSETS / ICON_FILE_NAME)
|
||||
assert _option(arguments, "--paths") == str(ROOT / "src")
|
||||
assert _option(arguments, "--distpath") == str(ROOT / "dist")
|
||||
assert _option(arguments, "--workpath") == str(ROOT / "build")
|
||||
assert _option(arguments, "--specpath") == str(ROOT / "build")
|
||||
|
||||
|
||||
def test_build_arguments_add_assets_and_word_list() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
pairs = [
|
||||
arguments[i + 1] for i, value in enumerate(arguments) if value == "--add-data"
|
||||
]
|
||||
assert pairs == [
|
||||
f"{ASSETS}{os.pathsep}flash_card/presentation/assets",
|
||||
f"{ROOT / 'data' / 'ms-da.csv'}{os.pathsep}data",
|
||||
]
|
||||
|
||||
|
||||
def test_build_arguments_end_with_the_entry_script() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT, clean=True)
|
||||
assert arguments[-1] == str(ROOT / "src" / "flash_card" / "__main__.py")
|
||||
|
||||
|
||||
def test_build_arguments_include_speech_packages_by_default() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
assert arguments.count("--collect-all") == 2
|
||||
assert "gtts" in arguments
|
||||
assert "playsound" in arguments
|
||||
|
||||
|
||||
def test_build_arguments_leave_out_speech_packages_without_speech() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT, speech=False)
|
||||
assert "--collect-all" not in arguments
|
||||
assert "--hidden-import" not in arguments
|
||||
|
||||
|
||||
def test_build_arguments_clean_only_when_asked() -> None:
|
||||
tool = _load_tool()
|
||||
assert "--clean" not in tool.build_arguments(ROOT)
|
||||
assert "--clean" in tool.build_arguments(ROOT, clean=True)
|
||||
|
||||
|
||||
def test_main_returns_two_and_explains_when_pyinstaller_is_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
def refuse(name: str, package: str | None = None) -> ModuleType:
|
||||
raise ImportError(name)
|
||||
|
||||
tool = _load_tool()
|
||||
monkeypatch.setattr(importlib, "import_module", refuse)
|
||||
assert tool.main([]) == 2
|
||||
assert 'pip install -e ".[build]"' in capsys.readouterr().err
|
||||
@@ -35,3 +35,27 @@ def test_project_root_contains_pyproject() -> None:
|
||||
|
||||
def test_csv_encoding_drops_byte_order_mark() -> None:
|
||||
assert constants.CSV_ENCODING == "utf-8-sig"
|
||||
|
||||
|
||||
def test_assets_dir_holds_the_four_images_and_the_icon() -> None:
|
||||
names = [
|
||||
constants.CARD_FRONT_IMAGE_NAME,
|
||||
constants.CARD_BACK_IMAGE_NAME,
|
||||
constants.RIGHT_IMAGE_NAME,
|
||||
constants.WRONG_IMAGE_NAME,
|
||||
constants.ICON_FILE_NAME,
|
||||
]
|
||||
assert len(set(names)) == 5
|
||||
assert all((constants.ASSETS_DIR / name).is_file() for name in names)
|
||||
|
||||
|
||||
def test_bundled_vocabulary_is_the_word_list_of_the_bundled_data_dir() -> None:
|
||||
assert constants.BUNDLED_VOCABULARY_PATH == (
|
||||
constants.BUNDLED_DATA_DIR / constants.VOCABULARY_FILE_NAME
|
||||
)
|
||||
assert constants.BUNDLED_VOCABULARY_PATH.is_file()
|
||||
|
||||
|
||||
def test_executable_and_icon_names() -> None:
|
||||
assert constants.EXECUTABLE_NAME == "flash-card"
|
||||
assert constants.ICON_FILE_NAME == "flash_card.ico"
|
||||
|
||||
@@ -16,6 +16,7 @@ from flash_card.constants import (
|
||||
CARD_FRONT_IMAGE_NAME,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
ICON_FILE_NAME,
|
||||
RIGHT_IMAGE_NAME,
|
||||
WRONG_IMAGE_NAME,
|
||||
)
|
||||
@@ -56,3 +57,18 @@ def test_generate_images_writes_png_with_expected_size(
|
||||
assert data.startswith(_SIGNATURE)
|
||||
assert data[12:16] == b"IHDR"
|
||||
assert struct.unpack(">II", data[16:24]) == (width, height)
|
||||
|
||||
|
||||
def test_generate_images_writes_icon_with_one_256_pixel_png(generated: Path) -> None:
|
||||
data = (generated / ICON_FILE_NAME).read_bytes()
|
||||
reserved, kind, count = struct.unpack("<HHH", data[:6])
|
||||
width, height, _colors, _zero, planes, bpp, size, offset = struct.unpack(
|
||||
"<BBBBHHII", data[6:22]
|
||||
)
|
||||
assert (reserved, kind, count) == (0, 1, 1)
|
||||
assert (width, height, planes, bpp) == (0, 0, 1, 32)
|
||||
assert offset == 22
|
||||
assert size == len(data) - 22
|
||||
png = data[offset:]
|
||||
assert png.startswith(_SIGNATURE)
|
||||
assert struct.unpack(">II", png[16:24]) == (256, 256)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Tests of the runtime helpers.
|
||||
|
||||
@file test_runtime.py
|
||||
@brief Checks where resources and the executable are, frozen or from source.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from flash_card import runtime
|
||||
|
||||
BUNDLE = Path("bundle")
|
||||
EXECUTABLE = Path("apps") / "flash-card.exe"
|
||||
ROOT = Path("project")
|
||||
|
||||
|
||||
def test_pick_resource_root_returns_bundle_when_frozen() -> None:
|
||||
assert runtime.pick_resource_root(True, BUNDLE, ROOT) == BUNDLE
|
||||
|
||||
|
||||
def test_pick_resource_root_returns_root_when_not_frozen() -> None:
|
||||
assert runtime.pick_resource_root(False, BUNDLE, ROOT) == ROOT
|
||||
|
||||
|
||||
def test_pick_resource_root_returns_root_when_bundle_is_unknown() -> None:
|
||||
assert runtime.pick_resource_root(True, None, ROOT) == ROOT
|
||||
|
||||
|
||||
def test_pick_executable_dir_returns_executable_folder_when_frozen() -> None:
|
||||
assert runtime.pick_executable_dir(True, EXECUTABLE, ROOT) == EXECUTABLE.parent
|
||||
|
||||
|
||||
def test_pick_executable_dir_returns_root_when_not_frozen() -> None:
|
||||
assert runtime.pick_executable_dir(False, EXECUTABLE, ROOT) == ROOT
|
||||
|
||||
|
||||
def test_is_frozen_is_false_in_a_test_run() -> None:
|
||||
assert runtime.is_frozen() is False
|
||||
|
||||
|
||||
def test_resource_root_is_project_root_in_a_source_run() -> None:
|
||||
assert (runtime.resource_root() / "pyproject.toml").is_file()
|
||||
|
||||
|
||||
def test_executable_dir_is_project_root_in_a_source_run() -> None:
|
||||
assert runtime.executable_dir() == runtime.source_root()
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Build the single file Windows executable with PyInstaller.
|
||||
|
||||
@file build_executable.py
|
||||
@brief Runs PyInstaller with the arguments of the flash card program.
|
||||
Uses the standard library; PyInstaller is imported only when a build runs.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
_SRC_DIR = Path(__file__).resolve().parents[1] / "src"
|
||||
if str(_SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SRC_DIR))
|
||||
|
||||
from flash_card.constants import ( # noqa: E402
|
||||
EXECUTABLE_NAME,
|
||||
ICON_FILE_NAME,
|
||||
VOCABULARY_FILE_NAME,
|
||||
)
|
||||
|
||||
_ASSETS_RELATIVE = Path("src") / "flash_card" / "presentation" / "assets"
|
||||
_ASSETS_TARGET = "flash_card/presentation/assets"
|
||||
_DATA_RELATIVE = Path("data")
|
||||
_SPEECH_PACKAGES = ("gtts", "playsound")
|
||||
_INSTALL_HINT = 'python -m pip install -e ".[build]"'
|
||||
_EXIT_MISSING_PYINSTALLER = 2
|
||||
|
||||
|
||||
def _add_data(source: Path, target: str) -> list[str]:
|
||||
"""Return the --add-data option of one source and target.
|
||||
|
||||
@param source File or folder to bundle.
|
||||
@param target Folder inside the bundle.
|
||||
@return The option and its value.
|
||||
"""
|
||||
return ["--add-data", f"{source}{os.pathsep}{target}"]
|
||||
|
||||
|
||||
def build_arguments(
|
||||
root: Path, *, speech: bool = True, clean: bool = False
|
||||
) -> list[str]:
|
||||
"""Return the PyInstaller argument list.
|
||||
|
||||
@param root Project root folder.
|
||||
@param speech True to bundle the optional gtts and playsound packages.
|
||||
@param clean True to clear the PyInstaller cache first.
|
||||
@return The arguments, the entry script last.
|
||||
"""
|
||||
arguments = [
|
||||
"--onefile",
|
||||
"--windowed",
|
||||
"--noconfirm",
|
||||
"--name",
|
||||
EXECUTABLE_NAME,
|
||||
"--icon",
|
||||
str(root / _ASSETS_RELATIVE / ICON_FILE_NAME),
|
||||
"--paths",
|
||||
str(root / "src"),
|
||||
"--distpath",
|
||||
str(root / "dist"),
|
||||
"--workpath",
|
||||
str(root / "build"),
|
||||
"--specpath",
|
||||
str(root / "build"),
|
||||
]
|
||||
arguments += _add_data(root / _ASSETS_RELATIVE, _ASSETS_TARGET)
|
||||
arguments += _add_data(root / _DATA_RELATIVE / VOCABULARY_FILE_NAME, "data")
|
||||
if speech:
|
||||
for package in _SPEECH_PACKAGES:
|
||||
arguments += ["--hidden-import", package, "--collect-all", package]
|
||||
if clean:
|
||||
arguments.append("--clean")
|
||||
arguments.append(str(root / "src" / "flash_card" / "__main__.py"))
|
||||
return arguments
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
"""Build the executable from the command line.
|
||||
|
||||
@param argv Command line arguments, or None for sys.argv.
|
||||
@return 0 on success, 2 when PyInstaller is not installed.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Build the flash card executable.")
|
||||
parser.add_argument(
|
||||
"--no-speech", action="store_true", help="leave out gtts and playsound"
|
||||
)
|
||||
parser.add_argument("--clean", action="store_true", help="clear the cache first")
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
pyinstaller = importlib.import_module("PyInstaller.__main__")
|
||||
except ImportError:
|
||||
sys.stderr.write(
|
||||
f"PyInstaller is not installed; run {_INSTALL_HINT} and try again\n"
|
||||
)
|
||||
return _EXIT_MISSING_PYINSTALLER
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
pyinstaller.run(build_arguments(root, speech=not args.no_speech, clean=args.clean))
|
||||
sys.stdout.write(f"built {root / 'dist' / (EXECUTABLE_NAME + '.exe')}\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Generate the PNG assets of the flash card program.
|
||||
|
||||
@file generate_images.py
|
||||
@brief Draws card_front, card_back, right and wrong as antialiased RGBA PNGs.
|
||||
@brief Draws card_front, card_back, right and wrong as antialiased RGBA PNGs,
|
||||
plus the program icon flash_card.ico.
|
||||
Uses the standard library only; run it to regenerate the assets.
|
||||
"""
|
||||
|
||||
@@ -23,6 +24,7 @@ from flash_card.constants import ( # noqa: E402
|
||||
CARD_FRONT_IMAGE_NAME,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
ICON_FILE_NAME,
|
||||
RIGHT_IMAGE_NAME,
|
||||
WRONG_IMAGE_NAME,
|
||||
)
|
||||
@@ -57,6 +59,10 @@ _CROSS_SEGMENTS: tuple[Segment, ...] = (
|
||||
((68.0, 32.0), (32.0, 68.0)),
|
||||
)
|
||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
_ICON_SIZE = 256
|
||||
_ICON_HEADER_SIZE = 6
|
||||
_ICON_ENTRY_SIZE = 16
|
||||
_ICON_BITS_PER_PIXEL = 32
|
||||
|
||||
|
||||
class _Canvas:
|
||||
@@ -309,20 +315,27 @@ def _render_card(fill: Color) -> _Canvas:
|
||||
|
||||
|
||||
def _render_button(
|
||||
gradient: tuple[Color, Color], segments: Sequence[Segment]
|
||||
gradient: tuple[Color, Color],
|
||||
segments: Sequence[Segment],
|
||||
size: int = _BUTTON_SIZE,
|
||||
) -> _Canvas:
|
||||
"""Draw a rounded gradient square with a white symbol.
|
||||
|
||||
@param gradient Top and bottom colour.
|
||||
@param segments Line segments of the symbol.
|
||||
@param segments Line segments of the symbol, drawn for a 100 px button.
|
||||
@param size Side of the square in pixels; the design is scaled to it.
|
||||
@return The canvas.
|
||||
"""
|
||||
size = _BUTTON_SIZE
|
||||
scale = size / _BUTTON_SIZE
|
||||
scaled = [
|
||||
((a[0] * scale, a[1] * scale), (b[0] * scale, b[1] * scale))
|
||||
for a, b in segments
|
||||
]
|
||||
canvas = _Canvas(size, size)
|
||||
box: Box = (0.0, 0.0, float(size), float(size))
|
||||
square = _rounded_rect_mask(size, size, box, _BUTTON_RADIUS)
|
||||
square = _rounded_rect_mask(size, size, box, _BUTTON_RADIUS * scale)
|
||||
canvas.paint(square, _vertical_gradient(gradient[0], gradient[1], size))
|
||||
symbol = _stroke_mask(size, size, segments, _STROKE_WIDTH)
|
||||
symbol = _stroke_mask(size, size, scaled, _STROKE_WIDTH * scale)
|
||||
canvas.paint(symbol, _solid(_WHITE))
|
||||
return canvas
|
||||
|
||||
@@ -351,8 +364,24 @@ def encode_png(width: int, height: int, rgba: bytes) -> bytes:
|
||||
)
|
||||
|
||||
|
||||
def encode_ico(png: bytes, size: int) -> bytes:
|
||||
"""Wrap one PNG image in an ICO container.
|
||||
|
||||
@param png The PNG file content.
|
||||
@param size Image side in pixels; 256 is stored as 0.
|
||||
@return The ICO file content.
|
||||
"""
|
||||
stored = 0 if size >= _ICON_SIZE else size
|
||||
offset = _ICON_HEADER_SIZE + _ICON_ENTRY_SIZE
|
||||
header = struct.pack("<HHH", 0, 1, 1)
|
||||
entry = struct.pack(
|
||||
"<BBBBHHII", stored, stored, 0, 0, 1, _ICON_BITS_PER_PIXEL, len(png), offset
|
||||
)
|
||||
return header + entry + png
|
||||
|
||||
|
||||
def generate_images(output_dir: Path) -> list[Path]:
|
||||
"""Write the four PNG assets into a folder.
|
||||
"""Write the four PNG assets and the icon into a folder.
|
||||
|
||||
@param output_dir Folder to write into; created when missing.
|
||||
@return The paths of the written files.
|
||||
@@ -370,6 +399,11 @@ def generate_images(output_dir: Path) -> list[Path]:
|
||||
data = encode_png(canvas.width, canvas.height, canvas.to_rgba_bytes())
|
||||
path.write_bytes(data)
|
||||
written.append(path)
|
||||
icon = _render_button(_RIGHT_GRADIENT, _CHECK_SEGMENTS, _ICON_SIZE)
|
||||
png = encode_png(icon.width, icon.height, icon.to_rgba_bytes())
|
||||
icon_path = output_dir / ICON_FILE_NAME
|
||||
icon_path.write_bytes(encode_ico(png, _ICON_SIZE))
|
||||
written.append(icon_path)
|
||||
return written
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user