Compare commits

...
Author SHA1 Message Date
Tirsvad d37f165c19 Resolve pending commit links of the MIL-009 plan 2026-10-15 00:17:31 +08:00
Tirsvad 768ca344c9 MIL-009 plan and documents: executable with PyInstaller
Milestone MIL-009, story US-001.07, ADR-0004, and the amended BC-001,
DCD-001 and PP-001. Review records RC-042 to RC-047, all Go (autonomous
mode, not independent).

Task: MIL-009#1
2026-10-15 00:17:27 +08:00
Tirsvad d5873748cf Merge pull request 'MIL-008: Choose level' (#54) from mil-008-choose-level into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-14 17:58:52 +02:00
Tirsvad 6745f98c27 Resolve pending commit links of MIL-008 2026-10-14 23:58:46 +08:00
Tirsvad f45d6036e4 MIL-008: Choose level
Review record RC-041 (Go, autonomous mode, not independent).

Task: MIL-008#1
Task: MIL-008#2
Task: MIL-008#3
Task: MIL-008#4
Task: MIL-008#5
Task: MIL-008#6
Task: MIL-008#7
Task: MIL-008#8
2026-10-14 23:58:31 +08:00
Tirsvad ee9c531ea6 Merge pull request 'MIL-008 plan and design: choose level' (#45) from plan-mil-008-level into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 9s
2026-10-14 17:45:16 +02:00
Tirsvad f4d8267da1 Resolve pending commit links of the MIL-008 plan 2026-10-14 23:45:04 +08:00
Tirsvad 0b983b7fda MIL-008 plan and design: choose level (100, 1000 or all words)
New milestone MIL-008 with story US-001.06, and the design documents
updated for the level choice: BC-001, PP-001, US-001, UC-001, DM-001,
DICT-001, SSD-001, OC-001, SD-001, DCD-001 and the wireframe. Review
records RC-030 to RC-040, all Go (autonomous mode, not independent).

Task: MIL-008#1
2026-10-14 23:44:59 +08:00
Tirsvad 13673b2c48 Merge pull request 'MIL-007: Documentation and publication' (#44) from mil-007-documentation-and-publication into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 8s
2026-10-14 16:42:44 +02:00
Tirsvad ff4f2655c4 Resolve pending commit links of MIL-007 2026-10-14 22:42:37 +08:00
Tirsvad 92894268c5 MIL-007: Documentation and publication
Review record RC-029 (Go, autonomous mode, not independent).

Task: MIL-007#1
Task: MIL-007#2
Task: MIL-007#3
Task: MIL-007#4
Task: MIL-007#5
Task: MIL-007#6
Task: MIL-007#7
Task: MIL-007#8
2026-10-14 22:42:26 +08:00
Tirsvad b2cc709031 Merge pull request 'MIL-006: Spoken Malay word' (#43) from mil-006-malay-speech into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 9s
2026-10-14 16:39:51 +02:00
Tirsvad bc055e3c05 Resolve pending commit links of MIL-006 2026-10-14 22:39:44 +08:00
Tirsvad b3c5116110 MIL-006: Spoken Malay word
Review record RC-028 (Go, autonomous mode, not independent).

Task: MIL-006#1
Task: MIL-006#2
Task: MIL-006#3
Task: MIL-006#4
Task: MIL-006#5
2026-10-14 22:39:36 +08:00
Tirsvad 5900fda90a Merge pull request 'MIL-005: Step 4 - Save progress' (#42) from mil-005-step4-save-progress into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 9s
2026-10-14 16:30:13 +02:00
Tirsvad 9e56a6b787 Resolve pending commit links of MIL-005 2026-10-14 22:30:05 +08:00
Tirsvad 8dbaaa3059 MIL-005: Step 4 - Save progress
Review record RC-027 (Go, autonomous mode, not independent).

Task: MIL-005#1
Task: MIL-005#2
Task: MIL-005#3
Task: MIL-005#4
Task: MIL-005#5
2026-10-14 22:29:58 +08:00
Tirsvad da4e9382cf Merge pull request 'MIL-004: Step 3 - Flip the cards' (#41) from mil-004-step3-flip-cards into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 8s
2026-10-14 16:05:58 +02:00
Tirsvad 83fb0b1ae8 Resolve pending commit links of MIL-004 2026-10-14 22:05:52 +08:00
Tirsvad 02e0c2c55e MIL-004: Step 3 - Flip the cards
Review record RC-026 (Go, autonomous mode, not independent).

Task: MIL-004#1
Task: MIL-004#2
Task: MIL-004#3
2026-10-14 22:05:48 +08:00
Tirsvad 9ecae3e6b2 Merge pull request 'MIL-003: Step 2 - New flash cards' (#40) from mil-003-step2-new-flash-cards into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 7s
2026-10-14 15:59:43 +02:00
Tirsvad 3acb3ba03d Resolve pending commit links of MIL-003 2026-10-14 21:59:36 +08:00
Tirsvad 8d66c43eea MIL-003: Step 2 - New flash cards
Review record RC-025 (Go, autonomous mode, not independent).

Task: MIL-003#1
Task: MIL-003#2
Task: MIL-003#3
Task: MIL-003#4
Task: MIL-003#5
2026-10-14 21:59:29 +08:00
Tirsvad c6c737ce40 Merge pull request 'MIL-002: Step 1 - User interface' (#39) from mil-002-step1-user-interface into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-14 15:53:19 +02:00
Tirsvad 736729c99f Resolve pending commit links of MIL-002 2026-10-14 21:53:12 +08:00
Tirsvad 019565635d MIL-002: Step 1 - User interface
Review record RC-024 (Go, autonomous mode, not independent).

Task: MIL-002#1
Task: MIL-002#2
Task: MIL-002#3
Task: MIL-002#4
2026-10-14 21:53:07 +08:00
Tirsvad aecf7ad43c Merge pull request 'MIL-001: Project foundation' (#38) from mil-001-foundation into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-14 15:39:32 +02:00
125 changed files with 8458 additions and 1310 deletions
+5
View File
@@ -0,0 +1,5 @@
*.py text eol=lf
*.md text eol=lf
*.toml text eol=lf
*.csv text eol=lf
*.png binary
+44 -3
View File
@@ -15,18 +15,42 @@ A flash card program for learning the most frequent Malay words with their Danis
## 🧭 Overview ## 🧭 Overview
The program shows a Malay word on a card. After three seconds the card turns over and shows the Danish translation. You press the green tick when you know the word and the red cross when you do not. A known word is removed from the words to learn, and the words still to learn are saved, so the next start continues where you stopped. The program first asks for a level: the first 100 words, the first 1000 words or all words of the word list, which is ordered from the most to the least frequent word. Then it shows a Malay word on a card. After three seconds the card turns over and shows the Danish translation. You press the green tick when you know the word and the red cross when you do not. A known word is removed from the words to learn, and the words still to learn are saved, so the next start continues where you stopped.
| Part | What it is for | | Part | What it is for |
| --- | --- | | --- | --- |
| `src/flash_card/` | The program, in four layers: `domain`, `application`, `infrastructure`, `presentation` | | `src/flash_card/` | The program, in four layers: `domain`, `application`, `infrastructure`, `presentation` |
| `tests/` | `pytest` tests: `unit`, `integration`, `e2e` | | `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` | | `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/` | Project documents (business case, plan, design, review records), `docs/wiki/` and `docs/images/` |
| `tools/` | Helper scripts: `generate_images.py` and `capture_screenshots.py` |
| `Doxyfile` | Doxygen configuration for the source code documentation | | `Doxyfile` | Doxygen configuration for the source code documentation |
The core program uses only the Python standard library. Spoken Malay words are an optional extra. The core program uses only the Python standard library. Spoken Malay words are an optional extra.
### Screenshots
| Choose your level | Front: the Malay word | Back: the Danish translation after 3 seconds |
| --- | --- | --- |
| ![The level screen][screenshot-level] | ![The front of a card][screenshot-front] | ![The back of a card][screenshot-back] |
### Where is each assignment step?
| Assignment step | Code |
| --- | --- |
| 1. User interface | `FlashCardView.__init__` in `src/flash_card/presentation/features/flash_card/flash_card_view.py`; images made by `tools/generate_images.py` |
| 2. New flash cards | `CsvVocabularyRepository` (the standard library `csv` instead of `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
| 3. Flip the cards | `FlashCardView.flip_card` and `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
| 4. Save your progress | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository` |
| Extra: level choice | `LevelView`, `LevelViewModel`, `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session` |
| Extra: spoken word | `GttsSpeech`, `NullSpeech`, `build_speech` in `presentation/app.py` |
The wiki ([English][wiki-en], [Dansk][wiki-da], [Bahasa Malaysia][wiki-ms]) explains the same in three languages.
### Data
`data/ms-da.csv` holds 1000 frequent Malay words with a Danish machine translation; see [data/README.md][data-readme] for the format and origin.
## 📋 Requirements ## 📋 Requirements
| Need | Why | | Need | Why |
@@ -69,7 +93,15 @@ python -m pip install -e ".[dev]"
python -m flash_card python -m flash_card
``` ```
The program is run from the activated `.venv`. The window opens with the first card; the program is built step by step and the pull requests and milestones show each step. The program is run from the activated `.venv`. The window opens with the level screen: choose 100 words, 1000 words or All words. Then a card appears. Press the green tick for a word you know and the red cross for a word you do not. Options:
| Option | Meaning |
| --- | --- |
| `--data-dir <folder>` | Folder with `ms-da.csv` and `words_to_learn.csv` |
| `--level {100,1000,all}` | Start at that level and skip the level screen |
| `--no-speech` | Do not speak the Malay word |
Known words stay known at every level: the level only limits which of the words you do not know yet come up. To start again, delete `data/words_to_learn.csv`.
## 🧪 Tests ## 🧪 Tests
@@ -87,6 +119,8 @@ lint-imports
The window test in `tests/e2e/` is skipped when no display is available. The window test in `tests/e2e/` is skipped when no display is available.
Build the source code documentation (Doxygen comments) with `doxygen Doxyfile`; the HTML is written to `docs/doxygen/html/`. The screenshots are made with `python tools/capture_screenshots.py` after `python -m pip install -e ".[tools]"` (Pillow, tool only).
## 📄 License ## 📄 License
GNU Affero General Public License v3.0 or later. See [LICENSE][license]. GNU Affero General Public License v3.0 or later. See [LICENSE][license].
@@ -117,6 +151,13 @@ The documents a newcomer needs, in reading order.
--- ---
[license]: ./LICENSE [license]: ./LICENSE
[screenshot-level]: ./docs/images/screenshot-level.png
[screenshot-front]: ./docs/images/screenshot-front.png
[screenshot-back]: ./docs/images/screenshot-back.png
[wiki-en]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home
[wiki-da]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home-da
[wiki-ms]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki/Home-ms
[data-readme]: ./data/README.md
[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card [repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card
[documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki [documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki
[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/issues [issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/issues
@@ -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
+3 -3
View File
@@ -14,7 +14,7 @@ document of a type. `Primary File` may contain a glob (e.g.
| BC | Business Case | docs/business-case.md | 002 | | BC | Business Case | docs/business-case.md | 002 |
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 008 | | MIL | Milestone / Gateway | docs/milestones/*.md | 010 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| US | User Story | docs/user-stories.md | 002 | | US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-*/uc.md | 002 | | UC | Use Case | docs/uc-*/uc.md | 002 |
@@ -24,8 +24,8 @@ document of a type. `Primary File` may contain a glob (e.g.
| SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 | | SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
| DCD | Design Class Diagram | docs/dcd.md | 002 | | DCD | Design Class Diagram | docs/dcd.md | 002 |
| DICT | Domain Dictionary | docs/dictionary.md | 002 | | DICT | Domain Dictionary | docs/dictionary.md | 002 |
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0005 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 024 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 048 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
## Languages ## Languages
+8 -2
View File
@@ -11,7 +11,9 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 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] |
--- ---
@@ -52,6 +54,8 @@ A small, well-structured reference solution gives the Product Owner a language l
- Reading the Malay to Danish list from `data/ms-da.csv` (semicolon separated, header `ms;da`). - Reading the Malay to Danish list from `data/ms-da.csv` (semicolon separated, header `ms;da`).
- Saving the words still to learn to `data/words_to_learn.csv` and using that file on the next start. - Saving the words still to learn to `data/words_to_learn.csv` and using that file on the next start.
- Optional spoken Malay word (text to speech) as an extra that can be installed. - Optional spoken Malay word (text to speech) as an extra that can be installed.
- A choice of level (the first 100 words, the first 1000 words or all words of the frequency ordered word list) before learning starts.
- A 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`. - `pyproject.toml`, `.gitignore`, `Doxyfile`, virtual environment instructions, `pytest` tests and `constants.py`.
- README from the framework template, screenshots, and a wiki in English, Danish and Bahasa Malaysia. - README from the framework template, screenshots, and a wiki in English, Danish and Bahasa Malaysia.
- Repository description and topics on the git host. - Repository description and topics on the git host.
@@ -61,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. - Other languages: the user interface labels are Malay and Danish only; another word list works by changing the data file but is not delivered.
- Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps. - Spaced repetition scheduling, statistics, accounts, cloud synchronisation and mobile apps.
- 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. - Redistribution of the course's own image files; the card and button images are generated for this project.
- A translation quality review of the word list. - A translation quality review of the word list.
@@ -147,3 +151,5 @@ Proceed — the scope is small, the risks are mitigated, and the result serves t
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+46 -10
View File
@@ -4,18 +4,20 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | DCD-001 | | 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 ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 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 ## 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 ## Diagram
@@ -30,6 +32,10 @@ package domain {
+front_code : str +front_code : str
+back_code : str +back_code : str
} }
class Level <<frozen>> {
+limit : int | None
+first(cards : Sequence[Card]) : tuple[Card, ...]
}
class Vocabulary <<frozen>> { class Vocabulary <<frozen>> {
+language_pair : LanguagePair +language_pair : LanguagePair
+cards : tuple[Card, ...] +cards : tuple[Card, ...]
@@ -40,7 +46,8 @@ package domain {
+from_vocabulary(vocabulary : Vocabulary) : Deck +from_vocabulary(vocabulary : Vocabulary) : Deck
+cards : tuple[Card, ...] +cards : tuple[Card, ...]
+is_empty : bool +is_empty : bool
+draw(choose, exclude : Card | None) : Card +within(cards : Sequence[Card]) : tuple[Card, ...]
+draw(choose, exclude : Card | None, among : Sequence[Card] | None) : Card
+remove(card : Card) : None +remove(card : Card) : None
} }
interface VocabularyRepository { interface VocabularyRepository {
@@ -69,6 +76,7 @@ package application {
-_deck : Deck -_deck : Deck
-_progress : ProgressRepository -_progress : ProgressRepository
-_speech : SpeechPort -_speech : SpeechPort
-_level_cards : tuple[Card, ...]
+current_card : CardDto | None +current_card : CardDto | None
+remaining : int +remaining : int
+next_card() : CardDto | None +next_card() : CardDto | None
@@ -94,6 +102,17 @@ package infrastructure {
} }
} }
package presentation { package presentation {
class LevelChoice <<frozen>> {
+label : str
+level : Level
}
class LevelViewModel {
+title : str
+choices : tuple[LevelChoice, ...]
}
class LevelView {
+choose(level : Level) : None
}
class FlashCardViewModel { class FlashCardViewModel {
-_session : StudySession -_session : StudySession
+side : Side +side : Side
@@ -113,6 +132,8 @@ package presentation {
} }
Vocabulary "1" *-- "1..*" Card Vocabulary "1" *-- "1..*" Card
Vocabulary "1" --> "1" LanguagePair Vocabulary "1" --> "1" LanguagePair
Level ..> Card : selects the first
Deck ..> Level : filtered by the cards of
Deck "1" o-- "0..*" Card Deck "1" o-- "0..*" Card
Deck ..> Vocabulary : built from Deck ..> Vocabulary : built from
Deck ..> EmptyDeckError : raises Deck ..> EmptyDeckError : raises
@@ -125,6 +146,7 @@ StudySession "1" --> "1" SpeechPort
StudySession ..> CardDto : returns StudySession ..> CardDto : returns
open_study_session ..> VocabularyRepository open_study_session ..> VocabularyRepository
open_study_session ..> ProgressRepository open_study_session ..> ProgressRepository
open_study_session ..> Level
open_study_session ..> StudySession : creates open_study_session ..> StudySession : creates
CsvVocabularyRepository ..|> VocabularyRepository CsvVocabularyRepository ..|> VocabularyRepository
CsvProgressRepository ..|> ProgressRepository CsvProgressRepository ..|> ProgressRepository
@@ -132,6 +154,10 @@ GttsSpeech ..|> SpeechPort
NullSpeech ..|> SpeechPort NullSpeech ..|> SpeechPort
FlashCardViewModel "1" --> "1" StudySession FlashCardViewModel "1" --> "1" StudySession
FlashCardView "1" --> "1" FlashCardViewModel FlashCardView "1" --> "1" FlashCardViewModel
LevelViewModel "1" *-- "3" LevelChoice
LevelChoice "1" --> "1" Level
LevelView "1" --> "1" LevelViewModel
Main ..> LevelView : composes
Main ..> CsvVocabularyRepository : composes Main ..> CsvVocabularyRepository : composes
Main ..> CsvProgressRepository : composes Main ..> CsvProgressRepository : composes
Main ..> GttsSpeech : composes Main ..> GttsSpeech : composes
@@ -145,24 +171,30 @@ Main ..> FlashCardView : composes
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `Card` | Flash Card | Holds a word and its translation | `front`, `back` | none | | `Card` | Flash Card | Holds a word and its translation | `front`, `back` | none |
| `LanguagePair` | Word List (first and second language) | Names the two languages of a word list by code | `front_code`, `back_code` | none | | `LanguagePair` | Word List (first and second language) | Names the two languages of a word list by code | `front_code`, `back_code` | none |
| `Level` | Level | Names how many of the first words of the word list a session uses and selects them | `limit` | `first` |
| `Vocabulary` | Word List | Holds all cards of a language pair as delivered | `language_pair`, `cards` | none | | `Vocabulary` | Word List | Holds all cards of a language pair as delivered | `language_pair`, `cards` | none |
| `Deck` | Words To Learn | Holds the cards still to learn and picks and removes cards | `language_pair`, `_cards` | `from_vocabulary`, `draw`, `remove` | | `Deck` | Words To Learn | Holds the cards still to learn and picks and removes cards | `language_pair`, `_cards` | `from_vocabulary`, `within`, `draw`, `remove` |
| `VocabularyRepository` | Word List | Port: load the delivered word list | none | `load` | | `VocabularyRepository` | Word List | Port: load the delivered word list | none | `load` |
| `ProgressRepository` | Words To Learn | Port: load and save the words to learn | none | `load`, `save` | | `ProgressRepository` | Words To Learn | Port: load and save the words to learn | none | `load`, `save` |
| `StudySession` | Study Session | Runs one session: draws cards, marks cards known, saves progress, asks for speech | `_deck`, `_progress`, `_speech`, `_current` | `next_card`, `mark_known` | | `StudySession` | Study Session | Runs one session: draws cards of the level, marks cards known, saves progress, asks for speech | `_deck`, `_progress`, `_speech`, `_level_cards`, `_current` | `next_card`, `mark_known` |
| `CardDto` | Flash Card | Plain data that crosses to the presentation layer | four text fields | none | | `CardDto` | Flash Card | Plain data that crosses to the presentation layer | four text fields | none |
| `SpeechPort` | (none: external service) | Port: speak a text in a language | none | `speak` | | `SpeechPort` | (none: external service) | Port: speak a text in a language | none | `speak` |
| `CsvVocabularyRepository`, `CsvProgressRepository` | Word List, Words To Learn | Adapters that read and write the semicolon separated CSV file | `_path` | `load`, `save` | | `CsvVocabularyRepository`, `CsvProgressRepository` | Word List, Words To Learn | Adapters that read and write the semicolon separated CSV file | `_path` | `load`, `save` |
| `GttsSpeech`, `NullSpeech` | (none) | Adapters for speech: the gTTS service, and silence | none | `speak` | | `GttsSpeech`, `NullSpeech` | (none) | Adapters for speech: the gTTS service, and silence | none | `speak` |
| `FlashCardViewModel` | Study Session | Exposes the side, language name and text of the card shown, and the three commands | `_session`, `side`, `language_name`, `text` | `next_card`, `flip_card`, `is_known` | | `FlashCardViewModel` | Study Session | Exposes the side, language name and text of the card shown, and the three commands | `_session`, `side`, `language_name`, `text` | `next_card`, `flip_card`, `is_known` |
| `LevelChoice`, `LevelViewModel` | Level | Plain data and view model of the level screen: the title and the three choices with their labels | `title`, `choices` | none |
| `LevelView` | (none: user interface) | Draws the level screen and forwards the chosen level | buttons | `choose` |
| `FlashCardView` | (none: user interface) | Draws the window, forwards the buttons, owns the 3000 ms timer | `flip_timer`, widgets | `next_card`, `flip_card`, `is_known` | | `FlashCardView` | (none: user interface) | Draws the window, forwards the buttons, owns the 3000 ms timer | `flip_timer`, widgets | `next_card`, `flip_card`, `is_known` |
## Method Traceability ## Method Traceability
| Method signature | Operation Contract / SD message | | Method signature | Operation Contract / SD message |
| --- | --- | | --- | --- |
| `open_study_session(vocabulary_repository, progress_repository, speech, choose) -> StudySession` | [OC-001] `startStudySession`, [SD-001] sequence startStudySession (loads the words to learn) | | `open_study_session(vocabulary_repository, progress_repository, speech, choose, level) -> StudySession` | [OC-001] `chooseLevel`, [SD-001] sequence chooseLevel (loads the words to learn and the level's cards) |
| `StudySession.next_card() -> CardDto \| None` | [OC-001] `startStudySession` (first card), `markUnknown`; [SD-001] | | `Level.first(cards) -> tuple[Card, ...]`, `Deck.within(cards) -> tuple[Card, ...]` | [OC-001] `chooseLevel` postcondition (the level's cards); [SD-001] |
| `LevelViewModel.choices -> tuple[LevelChoice, ...]` | [OC-001] `startStudySession`; [SD-001] sequence startStudySession |
| `LevelView.choose(level) -> None` | [OC-001] `chooseLevel` (forwards the level to the composition root) |
| `StudySession.next_card() -> CardDto \| None` | [OC-001] `chooseLevel` (first card), `markUnknown`; [SD-001] |
| `StudySession.mark_known() -> None` | [OC-001] `markKnown` (remove and save); [SD-001] | | `StudySession.mark_known() -> None` | [OC-001] `markKnown` (remove and save); [SD-001] |
| `FlashCardViewModel.next_card() -> None` | [OC-001] `markUnknown`; [SD-001] | | `FlashCardViewModel.next_card() -> None` | [OC-001] `markUnknown`; [SD-001] |
| `FlashCardViewModel.is_known() -> None` | [OC-001] `markKnown`; [SD-001] | | `FlashCardViewModel.is_known() -> None` | [OC-001] `markKnown`; [SD-001] |
@@ -176,8 +208,9 @@ Main ..> FlashCardView : composes
| Repository | `VocabularyRepository`, `ProgressRepository`, CSV adapters | The domain does not know the CSV file | | Repository | `VocabularyRepository`, `ProgressRepository`, CSV adapters | The domain does not know the CSV file |
| Ports and adapters | `SpeechPort`, `GttsSpeech`, `NullSpeech` | Speech is an optional extra; the core runs without it | | Ports and adapters | `SpeechPort`, `GttsSpeech`, `NullSpeech` | Speech is an optional extra; the core runs without it |
| Null Object | `NullSpeech` | No `if speech:` checks in the use case | | Null Object | `NullSpeech` | No `if speech:` checks in the use case |
| MVVM | `FlashCardView`, `FlashCardViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) | | MVVM | `FlashCardView`, `FlashCardViewModel`, `LevelView`, `LevelViewModel` | Behaviour is tested without Tkinter ([ADR-0002]) |
| Composition root | `app.main` | The one place that names the adapters | | 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 ## Dependency Check
@@ -191,4 +224,7 @@ Imports point inward: `domain` imports nothing from the other packages; `applica
[OC-001]: ./uc-001/oc.md [OC-001]: ./uc-001/oc.md
[SD-001]: ./uc-001/sd.md [SD-001]: ./uc-001/sd.md
[ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.md [ADR-0002]: ./adr/adr-0002-clean-architecture-and-mvvm.md
[ADR-0004]: ./adr/adr-0004-pyinstaller-executable.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+4 -1
View File
@@ -11,7 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the term Level | [0b983b7] |
--- ---
@@ -27,6 +28,7 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
| Word list | en | `Vocabulary` | The complete set of flash cards of one language pair, as delivered with the program. | DM, UC, US | DCD | | Word list | en | `Vocabulary` | The complete set of flash cards of one language pair, as delivered with the program. | DM, UC, US | DCD |
| Words to learn | en | `Deck` | The flash cards the learner does not yet know; it starts as the word list and only gets smaller. | DM, UC, US | DCD | | Words to learn | en | `Deck` | The flash cards the learner does not yet know; it starts as the word list and only gets smaller. | DM, UC, US | DCD |
| Study session | en | `StudySession` | One run of the program in which the learner is shown cards one by one. | DM, UC, US | DCD | | Study session | en | `StudySession` | One run of the program in which the learner is shown cards one by one. | DM, UC, US | DCD |
| Level | en | `Level` | The number of the most frequent words of the word list that a study session uses: 100, 1000 or all. | DM, UC, US | DCD |
| Language pair | en | `LanguagePair` | The two languages of a word list, the language learned and the language of the translation. | DM, UC | DCD | | Language pair | en | `LanguagePair` | The two languages of a word list, the language learned and the language of the translation. | DM, UC | DCD |
| Progress file | en | `ProgressRepository` | The file `words_to_learn.csv` that keeps the words to learn between runs. | UC, US | DCD | | Progress file | en | `ProgressRepository` | The file `words_to_learn.csv` that keeps the words to learn between runs. | UC, US | DCD |
| Spoken word | en | `SpeechPort` | The Malay word read aloud when its card is shown. | UC, US | DCD | | Spoken word | en | `SpeechPort` | The Malay word read aloud when its card is shown. | UC, US | DCD |
@@ -43,3 +45,4 @@ Maps each Product Owner (PO) term to its professional IT term. PO language: Engl
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[DM-001]: ./domain-model.md [DM-001]: ./domain-model.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+12 -2
View File
@@ -11,13 +11,14 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the concept Level | [0b983b7] |
--- ---
## Purpose and Scope ## Purpose and Scope
Covers: [UC-001] Study Flash Cards. The model holds the concepts a Learner talks about: the flash card, the word list, the words to learn and the study session. The project-level model is this document; no other use case exists. Covers: [UC-001] Study Flash Cards. The model holds the concepts a Learner talks about: the flash card, the word list, the words to learn, the level and the study session. The project-level model is this document; no other use case exists.
## Diagram ## Diagram
@@ -34,6 +35,9 @@ class "Word List" as WordList {
second language second language
} }
class "Words To Learn" as WordsToLearn class "Words To Learn" as WordsToLearn
class "Level" as Level {
number of words, or all words
}
class "Study Session" as StudySession { class "Study Session" as StudySession {
card shown card shown
side shown side shown
@@ -43,6 +47,8 @@ WordsToLearn "1" o-- "0..*" FlashCard : holds
WordsToLearn "1" --> "1" WordList : starts as a copy of WordsToLearn "1" --> "1" WordList : starts as a copy of
StudySession "1" --> "1" WordsToLearn : works through StudySession "1" --> "1" WordsToLearn : works through
StudySession "1" --> "0..1" FlashCard : shows StudySession "1" --> "0..1" FlashCard : shows
StudySession "1" --> "1" Level : is limited to
Level "1" --> "1" WordList : takes the first words of
@enduml @enduml
``` ```
@@ -53,6 +59,7 @@ StudySession "1" --> "0..1" FlashCard : shows
| Flash Card | A word in one language with its translation in another | word, translation | UC-001 steps 3 and 4 ("card", "translation") | | Flash Card | A word in one language with its translation in another | word, translation | UC-001 steps 3 and 4 ("card", "translation") |
| Word List | The complete set of flash cards of a language pair, as delivered with the program | first language, second language | UC-001 step 2 ("word list") | | Word List | The complete set of flash cards of a language pair, as delivered with the program | first language, second language | UC-001 step 2 ("word list") |
| Words To Learn | The flash cards the Learner does not yet know; starts as the word list and only gets smaller | none | UC-001 steps 2 and 6 ("words to learn") | | Words To Learn | The flash cards the Learner does not yet know; starts as the word list and only gets smaller | none | UC-001 steps 2 and 6 ("words to learn") |
| Level | The part of the word list a study session uses: its first 100 or 1000 words, or all words | number of words, or all words | UC-001 steps 2, 3 and 5 ("level") |
| Study Session | One run of the program in which the Learner is shown cards | card shown, side shown | UC-001 main scenario | | Study Session | One run of the program in which the Learner is shown cards | card shown, side shown | UC-001 main scenario |
## Association Table ## Association Table
@@ -64,6 +71,8 @@ StudySession "1" --> "0..1" FlashCard : shows
| Words To Learn | starts as a copy of | Word List | 1 to 1 | | Words To Learn | starts as a copy of | Word List | 1 to 1 |
| Study Session | works through | Words To Learn | 1 to 1 | | Study Session | works through | Words To Learn | 1 to 1 |
| Study Session | shows | Flash Card | 1 to 0..1 | | Study Session | shows | Flash Card | 1 to 0..1 |
| Study Session | is limited to | Level | 1 to 1 |
| Level | takes the first words of | Word List | 1 to 1 |
## Generalizations ## Generalizations
@@ -78,3 +87,4 @@ StudySession "1" --> "0..1" FlashCard : shows
[SSD-001]: ./uc-001/ssd.md [SSD-001]: ./uc-001/ssd.md
[DICT-001]: ./dictionary.md [DICT-001]: ./dictionary.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+82
View File
@@ -0,0 +1,82 @@
# MIL-008: Choose level
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-008 |
| CrossReference | [BC-001], [US-001], [UC-001], [DCD-001] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Purpose
Decide whether the Learner can choose a level before learning: the program asks for 100 words, 1000 words or all words, and then uses the first N words of the word list, which is ordered from the most to the least frequent word.
## Deliverable
At start the window shows the choice "100 words", "1000 words" and "All words". After a choice the card window of the earlier milestones opens and every card comes from the first N words of the word list that the Learner does not yet know. Known words stay known across levels because there is still one progress file. The design documents, README, wiki (English, Danish, Bahasa Malaysia) and screenshots describe the choice.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | Choosing 100 draws only cards that are among the first 100 rows of `data/ms-da.csv`, for every card of a full run through the deck | Integration test passes on the real word list | A card from row 101 or later appears |
| 2 | Choosing 1000 words or All words uses every word of the list; with a list of fewer than N words all are used | Unit and integration tests pass | Any word is missing |
| 3 | Marking words known at level 100 and then choosing All words leaves those words known, and `words_to_learn.csv` still holds every word that is not known (also those outside level 100) | Integration test passes | A word outside the level is lost from the file |
| 4 | When every word of the chosen level is known the card shows the message that nothing is left to learn, and the program does not fail | Unit and e2e tests pass | Exception or empty window |
| 5 | The level screen holds no rule: the labels and the choice come from the level view model, which imports no Tkinter | View model unit tests pass | A rule is in the view |
| 6 | `python -m flash_card --level 100` (also `1000`, `all`) skips the screen and starts at that level; an unknown value is refused by the argument parser | Unit tests pass | The screen appears or the program starts |
| 7 | The updated UC-001, DM-001, DICT-001, SSD-001, OC-001, SD-001, DCD-001, BC-001, wireframe, README and the wiki in three languages mention the level choice, and each changed document has a new Version History row and a `Go` review | Review records `Go`; `check-readme.sh` exit 0 | A document is not updated |
| 8 | Code review against QC-PY-001 has no failed Mandatory criterion; `pytest` exits with 0 and `ruff`, `mypy --strict`, `lint-imports` and `doxygen` are clean | `RC-*` verdict Go and exit codes 0 | Otherwise |
## Dependencies
| Depends on | Reason |
| --- | --- |
| MIL-007 | The finished program, README, wiki and screenshots that this milestone extends |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| Objective 1 (learning with the word list) and success criterion 1, extended by the new request of S01 | [BC-001] |
| US-001.06 Choose my level | [US-001] |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S01 |
| Approving reviewer | S01 (AI as stand-in in autonomous mode) |
## Target Date
2026-10-20 — 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 design documents for the level choice | Add the level to UC-001 (a first step before the cards, and the finished-level extension), to DM-001 and DICT-001 (concept Level), to SSD-001, OC-001 and SD-001 (operation chooseLevel and the level in startStudySession) and to DCD-001; draw the wireframe of the level screen in wireframe.md; add the level choice to the In Scope list of BC-001. Every changed document gets a Version History row and a new review. | No | UC-001, DCD-001 |
| 2 | Add the Level value object and the level choices | `Level` (limit as a number of words, or none for all words) in the domain and the three choices 100, 1000 and all words in constants.py with their labels, so that the choices are defined once. | No | DCD-001 |
| 3 | Draw cards from the chosen level | `StudySession` draws only from the first N words of the word list that are still to learn, and removes a known card from the whole words to learn and saves the whole deck, so that words of other levels are never lost. | Yes | US-001.06 |
| 4 | Open the session with a level | `open_study_session` gets the level, loads the word list for its order and the progress file for what is left, and builds the session; a level with nothing left gives a finished session. | Yes | US-001.06 |
| 5 | Add the level screen | `LevelViewModel` (the choices and their labels, no Tkinter) and `LevelView` in `presentation/features/level/` with the title "Choose your level" and three buttons on the window background. | Yes | US-001.06 |
| 6 | Show the level screen before the cards | The composition root shows the level screen first and builds the card view on the same window after the choice; the option `--level 100`, `--level 1000` or `--level all` skips the screen. | Yes | US-001.06 |
| 7 | Test the level choice | Unit tests of the level, the filter, the session and the view model; integration tests on the real word list and the progress file across levels; e2e test of the level screen and the flow to the first card. | No | |
| 8 | Update the README, the wiki and the screenshots | README, Usage and Home pages of the wiki in English, Danish and Bahasa Malaysia, and a new screenshot of the level screen made by tools/capture_screenshots.py; republish the wiki after the merge. | No | |
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[DCD-001]: ../dcd.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
+83
View File
@@ -0,0 +1,83 @@
# 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 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [768ca34] |
---
## 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 | |
---
[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
+32 -11
View File
@@ -4,14 +4,16 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | PP-001 | | ID | PP-001 |
| CrossReference | [BC-001], [SA-001], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007] | | 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 | | Language | en |
| Domain | it | | Domain | it |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 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] |
--- ---
@@ -31,13 +33,15 @@ This plan schedules the seven phases (gateways) that deliver the flash card prog
| Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone | | Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone |
| --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- | --- |
| Project foundation | [MIL-001] | 2026-10-14 | 2026-10-14 | S01 | none | pyproject, gitignore, skeleton, Doxyfile, first README, word list | | | Project foundation | [MIL-001] | 2026-10-14 | 2026-10-14 | S01 | none | pyproject, gitignore, skeleton, Doxyfile, first README, word list | [Milestone MIL-001][MS-001] |
| Step 1 - User interface | [MIL-002] | 2026-10-15 | 2026-10-15 | S01 | US-001.01 | The window with card and buttons | | | Step 1 - User interface | [MIL-002] | 2026-10-15 | 2026-10-15 | S01 | US-001.01 | The window with card and buttons | [Milestone MIL-002][MS-002] |
| Step 2 - New flash cards | [MIL-003] | 2026-10-16 | 2026-10-16 | S01 | US-001.02 | Random cards from the word list | | | Step 2 - New flash cards | [MIL-003] | 2026-10-16 | 2026-10-16 | S01 | US-001.02 | Random cards from the word list | [Milestone MIL-003][MS-003] |
| Step 3 - Flip the cards | [MIL-004] | 2026-10-17 | 2026-10-17 | S01 | US-001.03 | Flip after 3000 ms | | | Step 3 - Flip the cards | [MIL-004] | 2026-10-17 | 2026-10-17 | S01 | US-001.03 | Flip after 3000 ms | [Milestone MIL-004][MS-004] |
| Step 4 - Save progress | [MIL-005] | 2026-10-18 | 2026-10-18 | S01 | US-001.04 | words_to_learn.csv | | | Step 4 - Save progress | [MIL-005] | 2026-10-18 | 2026-10-18 | S01 | US-001.04 | words_to_learn.csv | [Milestone MIL-005][MS-005] |
| Spoken Malay word | [MIL-006] | 2026-10-19 | 2026-10-19 | S01 | US-001.05 | Text to speech as optional extra | | | 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 | | | 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 ```plantuml
@startgantt @startgantt
@@ -56,6 +60,10 @@ Project starts 2026-10-14
[Step 4 Go/No-Go] happens 2026-10-18 [Step 4 Go/No-Go] happens 2026-10-18
[Speech Go/No-Go] happens 2026-10-19 [Speech Go/No-Go] happens 2026-10-19
[Documentation Go/No-Go] happens 2026-10-21 [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 @endgantt
``` ```
@@ -70,15 +78,17 @@ Project starts 2026-10-14
| `pyproject.toml`, `.gitignore`, `Doxyfile`, `.venv` instructions, `pytest`, `constants.py` | MIL-001 | | `pyproject.toml`, `.gitignore`, `Doxyfile`, `.venv` instructions, `pytest`, `constants.py` | MIL-001 |
| README, screenshots, wiki in three languages | MIL-007 | | README, screenshots, wiki in three languages | MIL-007 |
| Repository description and topics | 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 | | Framework documents of the baseline | Baseline pull request before MIL-001 |
## Dependencies ## Dependencies
``` ```
Baseline → MIL-001 → MIL-002 → MIL-003 → MIL-004 → MIL-005 → MIL-006 → MIL-007 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; the buffer is the 2026-10-20 working day, which has no phase of its own. 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.
## Plan Risks ## Plan Risks
@@ -105,4 +115,15 @@ A No-Go on a phase moves every later date by the days the rework takes; the buff
[MIL-005]: ./milestones/mil-005-step4-save-progress.md [MIL-005]: ./milestones/mil-005-step4-save-progress.md
[MIL-006]: ./milestones/mil-006-malay-speech.md [MIL-006]: ./milestones/mil-006-malay-speech.md
[MIL-007]: ./milestones/mil-007-documentation-and-publication.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 [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
[MS-003]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/167
[MS-004]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/168
[MS-005]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/169
[MS-006]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/milestone/170
[MS-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
+64
View File
@@ -0,0 +1,64 @@
# RC-024: Review of PY-MIL-002
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-024 |
| CrossReference | [PY-MIL-002], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0195656] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-002]
- 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 | Modules, classes and functions use snake_case and PascalCase; ruff rule set N passes on `src`, `tests` and `tools`. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`FlashCardView`, `language_text_id`, `generate_images`). |
| 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 | Every signature is annotated; `mypy --strict` passes on 13 source files and on `tools/generate_images.py`. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | The only `except` is `tk.TclError` in the e2e fixture, used to skip when no display exists; none is bare or swallowed silently. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
| 7 | Files, locks and connections are managed with context managers | Pass | Files are written with `Path.write_bytes`; the Tk root is destroyed in the fixture teardown. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, classes and functions 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`; no logging is needed in a view. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `FlashCardView` and `FlashCardViewModel` are classes of DCD-001 with the names and responsibilities given there; `create_application` is the composition root `app.main` of DCD-001. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 18 tests: view model, image generator, application entry, and 5 e2e tests of the real window (not skipped on this machine); none uses the network. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; the tool uses the standard library only. |
| 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 | Colours, sizes, fonts and file names come from `constants.py` once; the image generator reads sizes from it. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; the view model imports no Tkinter. |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | The view only builds widgets; the texts come from the view model (single responsibility); nothing is switched on type. |
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | `presentation/features/flash_card/` holds view and view model as the clean-architecture skill's default layout; tests are in `unit` and `e2e`. |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names are English snake_case; no space. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-002 Go criteria: 1 title, colour and padding asserted by the e2e test; 2 canvas 800 x 526 with one image and two texts (Arial 40 italic, Arial 60 bold) asserted; 3 buttons use `wrong.png`/`right.png`, bd 0, grid row 1 columns 0 and 1 asserted; 4 the view model supplies the texts and its tests import no Tkinter; 5 this record; 6 `pytest` 18 passed. All Go. The generated card images were inspected by eye.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-002]: ../../../src/flash_card/presentation
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[0195656]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/019565635df17077c937b9e54685cd3d7bdb1e3e
+64
View File
@@ -0,0 +1,64 @@
# RC-025: Review of PY-MIL-003
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-025 |
| CrossReference | [PY-MIL-003], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [8d66c43] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-003]
- 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 | Packages, modules, classes and functions follow PEP 8 casing; 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 in the dictionary's IT terms (`Deck`, `Vocabulary`, `StudySession`, `CardDto`). |
| 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 43 files with no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 31 source files. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `read_word_list` raises `VocabularyNotFoundError` with `from` to keep the cause; no bare or swallowed exception; `Deck.remove` raises `ValueError` (not in OC-001, a deviation recorded here). |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable default arguments; no builtin shadowed (`Card`, `Deck` do not shadow). |
| 7 | Files, locks and connections are managed with context managers | Pass | The CSV file is opened with `with` in `read_word_list`. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Public modules, classes and functions 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`; `app.main` logs errors with `logging`; no personal data logged. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | Classes and operations are those of DCD-001 (`Card`, `LanguagePair`, `Vocabulary`, `Deck`, `StudySession`, `CardDto`, `CsvVocabularyRepository`, view and view model). Deviations: `Deck.remove` raises `ValueError` when the card is absent; `read_word_list` is a shared function behind both CSV repositories, as MIL-005 task 1 requires. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 66 tests (unit by layer, integration on the real `data/ms-da.csv`, e2e on the real window) named for behaviour; none uses the network or depends on order. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; `csv`, `random` and `pathlib` are standard library (ADR-0001). |
| 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 CSV format, encoding and delimiter are defined once in `constants.py` and `read_word_list`; the draw rule is only in `Deck.draw`. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; a grep finds no `tkinter`, `csv` or `random` import in `domain` or `application`. |
| 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 reason to change per class; the choice function is injected into `StudySession` and `Deck.draw`, and the repository is a Protocol that the use case receives. |
| 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 | Source is in `domain`, `application`, `infrastructure`, `presentation` with the sub-folders of the clean-architecture skill; tests in `unit`, `integration`, `e2e`. |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names are English snake_case, no space. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-003 Go criteria: 1 integration test reads 1000 cards, pair ms/da; 2 missing header, bad row and missing file raise `VocabularyFormatError`/`VocabularyNotFoundError` (unit tests); 3 a press shows a different card when two or more exist and the same when one (unit tests); 4 blank lines are ignored (unit test); 5 no `tkinter`, `csv` or `random` import in domain/application (grep, empty); 6 this record and `pytest` 66 passed. A manual smoke run on the real file showed a new word after a press. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-003]: ../../../src/flash_card
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[8d66c43]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/8d66c43eea9673728e4f7497d7673498306e8b13
+64
View File
@@ -0,0 +1,64 @@
# RC-026: Review of PY-MIL-004
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-026 |
| CrossReference | [PY-MIL-004], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [02e0c2c] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-004]
- 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; 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 are the assignment's (`flip_card`, `flip_timer`, `next_card`) and the IT terms of the dictionary; `Side` states its 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 | Every signature is annotated; `mypy --strict` passes on 31 source files. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No exception handling was added; none is bare. |
| 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 file or connection is opened in this change. |
| 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`; no logging needed. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `Side`, `flip_card`, `flip_timer` and the view's timer follow DCD-001 and SD-001 (`flip_card` row of Method Traceability, timer step of SSD-001). |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 76 tests: view model sides, finished state, flip rules; e2e front and back state, recording scheduler, real 50 ms timer; no network, no order dependency (state reset in each test). |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty. |
| 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 side rule exists once in the view model; image and colour names come from `constants.py`; `_show_card` is the only place that draws a card. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; the view model imports no Tkinter. |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | The view model holds the side rule and the view only draws; a new side would extend `Side` and `_show_card`. |
| 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 | Files stay in `presentation/features/flash_card/`; tests in `unit` and `e2e`. |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | Names follow the rule; no new file contains a space. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-004 Go criteria: 1 `flip_card` changes side, language name and text (unit test); 2 `next_card` returns to the front (unit test); 3 `after(3000, flip_card)` is scheduled and the earlier id is cancelled first (recording scheduler e2e test); 4 back image and white text, front image and black text (e2e); 5 `PhotoImage` objects are attributes created in `__init__`, none in a callback (grep); 6 this record, `pytest` 76 passed. A manual run with a 100 ms delay showed `Danish` and white text. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-004]: ../../../src/flash_card/presentation
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[02e0c2c]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/02e0c2c55e2a62ca13e2dec9dbf3aa823cf16242
+64
View File
@@ -0,0 +1,64 @@
# RC-027: Review of PY-MIL-005
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-027 |
| CrossReference | [PY-MIL-005], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [8dbaaa3] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-005]
- 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; 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 are the dictionary's IT terms and the assignment's (`is_known`, `mark_known`, `ProgressRepository`). |
| 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 47 files with no suppressions; `[tool.ruff.format] line-ending = "lf"` and `.gitattributes` fix the line endings. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 33 source files. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | A malformed progress file raises `VocabularyFormatError` and is never overwritten; the temporary file is removed when a write fails (re-raise, no swallow); no bare `except`. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
| 7 | Files, locks and connections are managed with context managers | Pass | Reading uses `with`; the write goes through a temporary file that is replaced atomically. |
| 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`; no secret or personal data is logged. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `ProgressRepository`, `CsvProgressRepository`, `StudySession.mark_known` and `is_known` are in DCD-001 and OC-001 (`markKnown`); `open_study_session` takes no speech port until MIL-006, as DCD-001 shows for the final design. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 107 tests (6 consecutive full runs all green) include the assignment's 3 record example, restart, round trip, finished state and e2e on the real window over `tmp_path`; none uses the network. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty. |
| 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 | Reader and writer share `CSV_DELIMITER`, `CSV_ENCODING` and the row width in `csv_word_list.py`; the removal rule is only in `Deck.remove`. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; the `ProgressRepository` Protocol is in `domain`, its adapter in `infrastructure`. |
| 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 use case depends on the Protocol and receives the adapter; the view model sequences two session calls only. |
| 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 | New files are in the layer folders of the clean-architecture skill; tests in `unit`, `integration`, `e2e`. |
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names follow the rule. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-005 Go criteria: 1 the 3 record example leaves the other two cards in order with the header (integration test); 2 a start uses the progress file when it exists and the word list otherwise (integration test); 3 the wrong button never writes (unit and e2e); 4 the last card known gives a finished state (unit); 5 a malformed progress file raises `VocabularyFormatError` and stays untouched (unit); 6 round trip readable as a word list (integration); 7 this record, `pytest` 107 passed. A manual run on a copy of `ms-da.csv` removed the known word and wrote 999 cards. Defect found and fixed in this milestone: a flaky `TclError` caused by pytest's descriptor capture and Tcl file handles; the view now loads image bytes with `Path.read_bytes()` and `PhotoImage(data=...)`. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-005]: ../../../src/flash_card
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[8dbaaa3]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/8dbaaa30595a3650bc85006a2188f81ab903125e
+64
View File
@@ -0,0 +1,64 @@
# RC-028: Review of PY-MIL-006
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-028 |
| CrossReference | [PY-MIL-006], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [b3c5116] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-006]
- 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; 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 are the DCD-001 IT terms (`SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech`). |
| 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 55 files with no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 38 source files; third-party modules are imported with `importlib.import_module`, so no `type: ignore` is needed. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | The one broad `except Exception` in `GttsSpeech` is at the thread boundary, is commented, and logs with `exc_info`; it is not silent. No bare `except`. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
| 7 | Files, locks and connections are managed with context managers | Pass | Audio is written to a `tempfile.TemporaryDirectory` used as a context manager, so the file is always removed. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | New 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 | Failures are logged with `logging`, not printed; only the spoken text (a word) is in the log context, no personal data. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `open_study_session(vocabulary_repository, progress_repository, speech, choose)` and `build_speech` match DCD-001 and SD-001 (the `speak` message of `startStudySession` and `markUnknown`). |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 124 tests (3 consecutive runs green) with fakes for the synthesiser and player; no test uses the network or plays a sound. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` is still empty; the extra `speech` pins `playsound==1.2.2` and `gTTS>=2.5`, and no module imports them outside `GttsSpeech`. |
| 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 speak rule is only in `StudySession.next_card`; the unused `SPEECH_LANGUAGE_CODE` constant was removed. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; the port is in `application`, the adapters in `infrastructure`, the choice in the composition root. |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | `StudySession` depends on the `SpeechPort` Protocol and receives the adapter; `NullSpeech` honours the same contract (Null Object), so no `if speech` test exists. |
| 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 | `application/ports/` and `infrastructure/speech/` follow the folder structure of the clean-architecture skill. |
| 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-006 Go criteria: 1 `speak(text, front_code)` once per drawn card and never when empty (unit tests); 2 `gtts` and `playsound` imported only when speaking, temporary file removed (unit tests with fakes); 3 service or player failure is logged and not raised (unit tests with caplog); 4 `build_speech` returns `NullSpeech` when the extra is missing or `--no-speech` is given (unit tests); 5 `dependencies` empty, extra pins `playsound==1.2.2` (pyproject.toml); 6 `speak` returns before a blocked player is released (unit test with an Event); 7 this record, `pytest` 124 passed. A manual check with the extra installed synthesised an mp3 of `kata` in Malay (7872 bytes) and `build_speech(True)` returned `GttsSpeech`; the audio was not played on this machine. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-006]: ../../../src/flash_card
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[b3c5116]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/b3c51161107b4cdcbc4c5334e28608423a299638
+64
View File
@@ -0,0 +1,64 @@
# RC-029: Review of PY-MIL-007
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-029 |
| CrossReference | [PY-MIL-007], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [9289426] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-007]
- 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 `tools/capture_screenshots.py` and the changed view; 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 (`capture`, `_make_dpi_aware`, `SCREENSHOT_SEED`). |
| 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 56 files; the one `type: ignore` is for a Windows-only attribute and says which error it silences. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on `src` (38 files) and on both tool scripts. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No exception handling was added. |
| 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 | The data copy uses `tempfile.TemporaryDirectory` as a context manager; the Tk root is destroyed. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Module, functions and constants have Doxygen comments; `doxygen Doxyfile` ends with 0 warnings. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | The tool writes the two output paths with `sys.stdout.write`; no `print`, no secret. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | The only source change is the button background in `FlashCardView`, which follows `wireframe.md`; the tool is outside the design classes and is not part of the program. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 125 tests, 2 consecutive full runs green; one e2e assertion was added for the button background; the tool is checked by running it and reading both images. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue on the touched files; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; Pillow is the optional extra `tools` and is imported only by the tool. |
| 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 seed, output folder and delay come from one definition each; the delay is `FLIP_DELAY_MS`. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; the tool imports the composition root only. |
| 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 tool has one purpose (capture); the view change adds no rule. |
| 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/` holds helper scripts outside `src/`; the layers 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 (`docs/images`, `docs/wiki` pages are exceptions for wiki names). |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Milestone MIL-007 Go criteria: 1 `check-readme.sh` exit 0 and both screenshots linked from the README; 2 `docs/wiki/` holds Home, Install, Usage and Architecture as `<Page>.md`, `<Page>-da.md`, `<Page>-ms.md` (12 pages) and `_Sidebar.md` listing the three languages; 3 the screenshots are captured from the running program by `tools/capture_screenshots.py` and were inspected; 4 the README Setup and Run need 5 commands (create `.venv`, activate, upgrade pip, install, run); 5 the repository has a description and 10 topics (read back from the API); 6 `doxygen Doxyfile` 0 warnings; 7 `docs/sqa/traceability-matrix.md` has a row for every created artifact with its latest `RC-*`, and the plan links each milestone; 8 `check-languages.sh` exit 0 (`check-plan.sh` is a commit-message hook and was not run). The wiki is pushed to the host's wiki after this merge. The Danish and Malay wiki pages were written by the AI and have not been read by a native speaker; this is stated in the end-of-run summary. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-007]: ../../../tools
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[9289426]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/92894268c51ec77d56cff48d968566e6265795a2
@@ -0,0 +1,70 @@
# RC-030: Review of BC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-030 |
| CrossReference | [BC-001], [QC-BC-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## 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` subsections. |
| 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 level choice was added for MIL-008.
## 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
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,70 @@
# RC-031: Review of US-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-031 |
| CrossReference | [US-001], [QC-US-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## 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 six stories and states one dependency exception. |
| 2 | Written in "As a / I want / So that" form | Pass | All six stories use `As a / I want / so that`. |
| 3 | Clear, testable acceptance criteria are included | Pass | Every story, including US-001.06, 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 level choice was added for MIL-008.
## 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
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,54 @@
# RC-032: Review of PP-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-032 |
| CrossReference | [PP-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## 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-008 on 2026-10-20 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-008 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
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
+74
View File
@@ -0,0 +1,74 @@
# RC-033: Review of UC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-033 |
| CrossReference | [UC-001], [QC-UC-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [UC-001]
- Checklist used: [QC-UC-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Consists of a single, concise paragraph summarizing only the primary success scenario | N-A | The format is Fully Dressed; the Brief format criterion does not apply. |
| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | The format is Fully Dressed; the Casual format criterion does not apply. |
| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Scope, level, actor, stakeholders, pre- and postconditions, main success scenario and extensions are present. |
| 4 | Preconditions and postconditions are explicitly defined | Pass | Preconditions and postconditions are listed. |
| 5 | Primary actor is explicitly stated | Pass | Primary actor is `Learner`. |
| 6 | Stakeholders and their interests are stated | Pass | Stakeholders S01 and S02 and their interests are stated. |
| 7 | Main success scenario is written as clear, numbered steps | Pass | Main success scenario has 9 numbered steps, including the level choice (steps 2 and 3). |
| 8 | Alternative/exception flows correctly reference `<<include>>`/`<<extend>>` use cases where relevant, per UML 2.5.1 | Pass | UCD-001 has no include or extend relationship, so no extension needs to reference one; the extensions (3a, 4a, 5a, 7a, 7b, 9a) are plain alternative flows. |
| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | Pass | The Business Rules table gives a rule per step. |
| 10 | Naming of actors and use case title is consistent with the corresponding Use Case Diagram and User Stories | Pass | Title and actor match UCD-001 and US-001; the level step matches US-001.06. |
| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | `Level: user-goal` and `Scope: Flash Card program`. |
| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Steps are written as goals and answers; buttons, files and milliseconds are in the wireframe and milestones. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of UC-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the level choice was added for MIL-008.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[UC-001]: ../../../docs/uc-001/uc.md
[QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,69 @@
# RC-034: Review of DM-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-034 |
| CrossReference | [DM-001], [QC-DM-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [DM-001]
- Checklist used: [QC-DM-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 | Uses ubiquitous/business language throughout; no technical or implementation jargon (e.g. no "table", "class", "pointer") | Pass | Concepts are Flash Card, Word List, Words To Learn, Level and Study Session; no table or class names. |
| 2 | Multiplicities on associations are correct and complete (e.g. `1..*`, `0..1`) | Pass | Association table gives both multiplicities for all 7 associations. |
| 3 | No operation/method signatures shown — attributes and associations only | Pass | The diagram has attributes and associations only. |
| 4 | Associations are named with an unambiguous reading direction | Pass | Every association is named with a reading direction (`holds`, `works through`, `is limited to`, `takes the first words of`). |
| 5 | Generalization/specialization used correctly, reflecting true "is-a" relationships, not misused for code reuse | Pass | Generalization table says none exists, and why. |
| 6 | Every concept traces to a noun phrase found in the use cases or glossary | Pass | Concept table gives a source noun phrase in UC-001 for every concept. |
| 7 | Attributes are simple domain data (no foreign-key-like references or object pointers modeled as attributes) | Pass | Attributes are plain data (word, translation, first language, card shown). |
## 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 DM-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 level choice was added for MIL-008.
## 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 |
---
[DM-001]: ../../../docs/domain-model.md
[QC-DM-001]: ../../../framework/qc/artifacts/qc-domain-model.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,69 @@
# RC-035: Review of DICT-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-035 |
| CrossReference | [DICT-001], [QC-DICT-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [DICT-001]
- Checklist used: [QC-DICT-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Every row has a PO term, its language, an IT term and a definition | Pass | All 8 rows have PO term, language, IT term and definition. |
| 2 | Each PO term maps to exactly one IT term and the reverse (no synonyms) | Pass | One IT term per PO term; no synonym appears. |
| 3 | Every Domain Model concept has a row, and the Domain Model uses its PO term | Pass | The 5 concepts of DM-001 have a row (`Level` added) and DM-001 uses the PO term. |
| 4 | The Operation Contracts, Sequence Diagrams, Design Class Diagrams and ERD use the IT term, not the PO term | Pass | DCD-001, OC-001 and SD-001 use `Card`, `Vocabulary`, `Deck`, `StudySession`. |
| 5 | Definitions are written in the PO language and are one sentence | Pass | Definitions are one English sentence each. |
| 6 | "Used as PO term in" and "Used as IT term in" name artifact types that exist in the project | Pass | Artifact types named in both columns exist in the project. |
| 7 | The dictionary's `Language` and `Domain` rows, and the language of every row, match the PO language and domain in the project registry | Pass | `Language | en` and `Domain | it` match the registry; every row says `en`. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of DICT-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the level choice was added for MIL-008.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[DICT-001]: ../../../docs/dictionary.md
[QC-DICT-001]: ../../../framework/qc/artifacts/qc-dictionary.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,69 @@
# RC-036: Review of SSD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-036 |
| CrossReference | [SSD-001], [QC-SSD-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [SSD-001]
- Checklist used: [QC-SSD-001], and [QC-LANG-001] (language and domain)
- Scope: full review
- Language and domain: en / it
- Language reviewer: none (the stand-in for S01 reads the language and knows the domain)
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Input/output messages match the corresponding Use Case's main success scenario step-for-step | Pass | System Operations table maps each message to steps of the main success scenario (1 to 9); the timer step is the one stated deviation. |
| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | The system is `:System`; no internal object appears. |
| 3 | Object creation/destruction of the System instance handled explicitly where relevant | Pass | Lifecycle Notes state that the system lives for one run. |
| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed return arrows are drawn for `startStudySession`, `chooseLevel`, the turn-over, `markKnown` and `markUnknown`. |
| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | The Alternate Flows section names extensions 3a, 4a, 5a, 7b and 9a, states that they add no operation, and says they are out of scope. |
| 6 | Message names are verb phrases consistent with the use case's system responsibilities | Pass | `startStudySession`, `markKnown` and `markUnknown` are verb phrases. |
| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | Source Use Case section names UC-001 and the scenario. |
## Language and Domain Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The Metadata table has a `Language` row and a `Domain` row, and neither is a placeholder | Pass | Metadata of SSD-001 has `Language | en` and `Domain | it`; neither is a placeholder. |
| 2 | `Language` is a BCP 47 code and `Domain` is a value from the registry's domain list | Pass | `en` is a BCP 47 code and `it` is a value of the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | Prose and table cells are English; Malay and Danish words and file names appear only as quoted data. |
| 4 | The register matches the one the registry gives for the artifact type | Pass | The registry gives `IT Professional English` for this type; the text is written for that reader. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | The PO terms of DICT-001 (flash card, word list, words to learn, study session) are used and no synonym replaces them. |
| 6 | Metadata keys, section headings, IDs and statuses are in English | Pass | Metadata keys, headings, IDs and statuses are English. |
| 7 | No translated twin (`<name>.<language>.md`) exists beside the document | Pass | No translated twin exists beside the document (checked in `docs/`). |
| 8 | A change of language or domain since the previous accepted version has a Version History row and was reviewed again | Pass | First version; no change of language or domain since an earlier accepted version. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | The reviewer is the stand-in for S01, the Product Owner, who reads English and knows the IT domain; no second language reviewer is needed (review-checklist-process step 2). Open to challenge by S01 in the end-of-run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Fail | CSV, MVVM, UI, PO and similar abbreviations are not all spelled out at first use; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the level choice was added for MIL-008.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion LANG 10 (Optional) failed: Abbreviations are spelled out on first use, in the stated language; fix or accept the deferral | S01 | 2026-10-21 |
---
[SSD-001]: ../../../docs/uc-001/ssd.md
[QC-SSD-001]: ../../../framework/qc/artifacts/qc-ssd.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,53 @@
# RC-037: Review of OC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-037 |
| CrossReference | [OC-001], [QC-OC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [OC-001]
- Checklist used: [QC-OC-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Method signature is complete: operation name, parameter types, and return type | Pass | Each contract gives name, parameter list and return type (`startStudySession(): CardDto | None`). |
| 2 | Preconditions explicitly list required state before execution | Pass | Preconditions are listed per contract. |
| 3 | Postconditions explicitly describe resulting state using Larman's "instance created/associated/attribute modified" style | Pass | Postconditions use created / associated / destroyed / attribute set wording. |
| 4 | Exceptions and error conditions are documented, including the triggering precondition failure | Pass | Each contract has an Exceptions table with the failing precondition. |
| 5 | Operation is explicitly traceable to a single SSD message | Pass | Each of the four contracts (`startStudySession`, `chooseLevel`, `markKnown`, `markUnknown`) traces to exactly one SSD message; the timer step has none and says so. |
| 6 | Contract avoids specifying implementation/algorithmic details (declarative, not procedural) | Pass | Postconditions are declarative. |
| 7 | Cross-references the Domain Model classes/associations affected by pre/postconditions | Pass | Domain Model concepts and IT terms are cited per contract. |
## Overall Verdict
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the level choice was added for MIL-008.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[OC-001]: ../../../docs/uc-001/oc.md
[QC-OC-001]: ../../../framework/qc/artifacts/qc-operation-contract.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,54 @@
# RC-038: Review of SD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-038 |
| CrossReference | [SD-001], [QC-SD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [SD-001]
- Checklist used: [QC-SD-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Message passing strictly follows UML sync/async/return arrow syntax | Pass | Calls are solid arrows with dashed returns. |
| 2 | GRASP/GoF patterns applied and explicitly annotated where used (e.g. Controller, Observer, Mediator, Factory) | Pass | Each sequence has a Pattern Annotations table (Creator, Information Expert, Null Object, MVVM, Repository). |
| 3 | Lifelines show activation bars matching actual processing time/call nesting | Pass | Activations are drawn for the session and the view model. |
| 4 | Object creation and destruction shown with correct UML notation (`create`/`destroy` messages, X on lifeline) | Pass | `create` is used for `Deck` and `StudySession`; no object is destroyed during the program's life and that is stated by omission, the process ends. |
| 5 | Diagram realizes the postconditions of a specific Operation Contract | Pass | Each of the four sequences has a Postcondition Coverage table for its contract in OC-001. |
| 6 | Responsibility assignment favors low coupling/high cohesion (no god-object receiving all messages) | Pass | Responsibility Check notes state that no object receives all messages. |
| 7 | Loop, alt, and opt combined fragments used correctly for conditional/repeated behavior | Pass | `alt` and `opt` fragments are used for the progress file and the empty deck. |
| 8 | Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, or its absence is justified | Fail | The exceptions of OC-001 (missing file, malformed file) are raised before the sequence starts and are not drawn as `alt` fragments; Optional, deferred. |
## Overall Verdict
Go — every Mandatory criterion passes; the failed Optional criteria are deferred with an action item. Reviewed by AI as stand-in for S01; not independent of the author.
Full review (not a delta) after the level choice was added for MIL-008.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Criterion 8 (Optional) failed: Each exception of the realized Operation Contract is shown as an `alt` or `opt` fragment, ; fix or accept the deferral | S01 | 2026-10-21 |
---
[SD-001]: ../../../docs/uc-001/sd.md
[QC-SD-001]: ../../../framework/qc/artifacts/qc-sequence-diagram.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
@@ -0,0 +1,55 @@
# RC-039: Review of DCD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-039 |
| CrossReference | [DCD-001], [QC-DCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## 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), composition (LevelViewModel and LevelChoice), association, dependency and realization are distinguished. |
| 4 | Multiplicities and navigability specified on all associations | Pass | Multiplicities are on the associations; dependencies are directed. |
| 5 | Applied design patterns are annotated explicitly (e.g. Singleton, Factory, Strategy) | Pass | Pattern Annotations table lists Repository, Ports and adapters, Null Object, MVVM, Composition root. |
| 6 | Method signatures are traceable to Operation Contracts and/or design Sequence Diagrams | Pass | Method Traceability table maps every operation, including `Level.first`, `Deck.within`, `LevelViewModel.choices` and `LevelView.choose`, to OC-001 and SD-001. |
| 7 | Class names and structure remain consistent with the Domain Model concepts they refine | Pass | The Class Table has a `Refines` column; `Level` refines the concept Level of DM-001; names match the dictionary's IT terms. |
| 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 level choice was added for MIL-008.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[DCD-001]: ../../../docs/dcd.md
[QC-DCD-001]: ../../../framework/qc/artifacts/qc-dcd.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
+66
View File
@@ -0,0 +1,66 @@
# RC-040: Review of MIL-008
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-040 |
| CrossReference | [MIL-008], [QC-MIL-001], [QC-LANG-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [0b983b7] |
---
## Artifact Under Review
- Instance reviewed: [MIL-008]
- 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-008 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-008]: ../../../docs/milestones/mil-008-choose-level.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[QC-LANG-001]: ../../../framework/qc/artifacts/qc-language-domain.md
[0b983b7]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/0b983b7fdadf6958717c1d0227d0f2ebe037f7b1
+64
View File
@@ -0,0 +1,64 @@
# RC-041: Review of PY-MIL-008
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-041 |
| CrossReference | [PY-MIL-008], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [f45d603] |
---
## Artifact Under Review
- Instance reviewed: [PY-MIL-008]
- 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 all new modules (`level.py`, `level_view.py`, `window.py`); ruff rule set N passes. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose and are the dictionary's IT terms (`Level`, `Deck.within`, `LevelViewModel`, `LevelChoice`, `Application.start`). |
| 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 67 files with no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every signature is annotated; `mypy --strict` passes on 44 source files and on `tools/capture_screenshots.py`. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `Level` raises `ValueError` for a limit below 1; a failed session start after a click is logged, closes the window with exit code 1 and never swallows silently; no bare `except`. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
| 7 | Files, locks and connections are managed with context managers | Pass | No new file handling beyond the existing context managers. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | New and changed members have Doxygen docstrings; `doxygen Doxyfile` ends with 0 warnings. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`; failures are logged with `logging`; no personal data is logged. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `Level`, `Deck.within`, `Deck.draw(among)`, `StudySession(level_cards)`, `open_study_session(..., level)`, `LevelChoice`, `LevelViewModel`, `LevelView` and `Application` match the revised DCD-001, OC-001 `chooseLevel` and SD-001. Deviations: `Application` (root, repositories, speech) with `start`, `show_level_screen` and `exit_code` is the composition root detail that DCD-001 shows as `app.main`; `StudySession.remaining_in_level` and `LevelView.frame` are additions. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 191 tests (3 consecutive full runs green) cover `Level`, `Deck.within`, the level filter, the cross-level progress rules on the real word list, the view model, `--level` and the real level window flow; none uses the network. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` stays empty; no new package. |
| 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 level rule exists only in `Level.first` and `Deck.within`; the choices, labels and sizes exist once in `constants.py`; both views use `configure_window`. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports `Layers point inward` kept; a grep finds no `tkinter`, `csv` or `random` import in `domain` or `application`; the level view model imports no Tkinter. |
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | The view model owns the choices and the view draws them; the composition root only wires; the session depends on protocols and receives its collaborators. |
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | `presentation/features/level/` and `presentation/shared/` follow the clean-architecture folder structure; tests stay in `unit`, `integration`, `e2e` (with a session-scoped `tk_root` in `tests/e2e/conftest.py`). |
| 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-008 Go criteria: 1 level 100 draws exactly the first 100 rows over a full run of the real list (integration test); 2 levels 1000 and All use all 1000 words, and a shorter list is used whole (unit and integration tests); 3 words known at level 100 stay known at All and the progress file keeps every unknown word, also outside the level (integration test); 4 a level with nothing left gives the finished message and no failure (unit and e2e tests); 5 the level view model has no Tkinter import and its tests pass without a display; 6 `--level 100|1000|all` skips the screen and an unknown value exits with 2 (unit tests); 7 UC-001, DM-001, DICT-001, SSD-001, OC-001, SD-001, DCD-001, BC-001, US-001, PP-001 and MIL-008 were re-reviewed `Go` (RC-030 to RC-040), the wireframe has Screen 1, and the README and the wiki in en, da and ms describe the level; 8 this record, `pytest` 191 passed, ruff, mypy, lint-imports and doxygen clean. A real screenshot of the level screen was captured and inspected after fixing a too-small window. The Danish and Malay wiki additions were written by the AI and are not read by a native speaker. All Go.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| none | - | - |
---
[PY-MIL-008]: ../../../src/flash_card
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[f45d603]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/f45d6036e4210e1fbe52f6a582c540a6e1877056
@@ -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
+55
View File
@@ -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
+66
View File
@@ -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
+59 -11
View File
@@ -9,7 +9,9 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 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 | 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) | [768ca34] |
--- ---
@@ -23,18 +25,18 @@ updated whenever an artifact instance is created or reviewed.
| Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) | | Artifact Instance | Type | Language | Domain | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| BC-001 | Business Case | en | it | - | SA-001, PP-001 | [RC-001] | | 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] | | 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-007 | [RC-003] | | 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] | | 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 | [RC-005] | | 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-006] | | 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-007] | | 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-008] | | 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-009] | | OC-001 | Operation Contract | - | - | SSD-001, DM-001 | SD-001 | [RC-037] |
| SD-001 | Sequence Diagram | - | - | OC-001 | DCD-001 | [RC-010] | | 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-011] | | 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-012] | | 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-0001 | Architecture Decision Record | - | - | BC-001 | MIL-001, MIL-003, MIL-006 | [RC-013] |
| ADR-0002 | Architecture Decision Record | - | - | BC-001 | DCD-001 | [RC-014] | | ADR-0002 | Architecture Decision Record | - | - | BC-001 | DCD-001 | [RC-014] |
| ADR-0003 | Architecture Decision Record | - | - | BC-001 | - | [RC-015] | | ADR-0003 | Architecture Decision Record | - | - | BC-001 | - | [RC-015] |
@@ -45,7 +47,17 @@ updated whenever an artifact instance is created or reviewed.
| MIL-005 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-005 | [RC-020] | | MIL-005 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-005 | [RC-020] |
| MIL-006 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-006 | [RC-021] | | 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-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-047] |
| ADR-0004 | Architecture Decision Record | - | - | BC-001 | - | [RC-046] |
| PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] | | PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] |
| PY-MIL-002 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-024] |
| PY-MIL-003 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-025] |
| PY-MIL-004 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-026] |
| PY-MIL-005 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-027] |
| 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] |
## Coverage Notes ## Coverage Notes
@@ -102,3 +114,39 @@ updated whenever an artifact instance is created or reviewed.
[RC-022]: ./reviews/rc-022-mil-007.md [RC-022]: ./reviews/rc-022-mil-007.md
[RC-023]: ./reviews/rc-023-py-mil-001.md [RC-023]: ./reviews/rc-023-py-mil-001.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
[RC-024]: ./reviews/rc-024-py-mil-002.md
[PY-MIL-002]: ../../src/flash_card
[RC-025]: ./reviews/rc-025-py-mil-003.md
[PY-MIL-003]: ../../src/flash_card
[RC-026]: ./reviews/rc-026-py-mil-004.md
[PY-MIL-004]: ../../src/flash_card
[RC-027]: ./reviews/rc-027-py-mil-005.md
[PY-MIL-005]: ../../src/flash_card
[RC-028]: ./reviews/rc-028-py-mil-006.md
[PY-MIL-006]: ../../src/flash_card
[RC-029]: ./reviews/rc-029-py-mil-007.md
[PY-MIL-007]: ../../src/flash_card
[RC-030]: ./reviews/rc-030-business-case-level.md
[RC-031]: ./reviews/rc-031-user-stories-level.md
[RC-032]: ./reviews/rc-032-project-plan-level.md
[RC-033]: ./reviews/rc-033-use-case-level.md
[RC-034]: ./reviews/rc-034-domain-model-level.md
[RC-035]: ./reviews/rc-035-domain-dictionary-level.md
[RC-036]: ./reviews/rc-036-system-sequence-diagram-level.md
[RC-037]: ./reviews/rc-037-operation-contracts-level.md
[RC-038]: ./reviews/rc-038-sequence-diagrams-level.md
[RC-039]: ./reviews/rc-039-design-class-diagram-level.md
[RC-040]: ./reviews/rc-040-mil-008.md
[MIL-008]: ../milestones/mil-008-choose-level.md
[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
+40 -11
View File
@@ -9,7 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] |
--- ---
@@ -19,20 +20,46 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
| Item | Value | | Item | Value |
| --- | --- | | --- | --- |
| Operation | `startStudySession(): CardDto \| None` | | Operation | `startStudySession(): tuple[Level, ...]` |
| Traces to | `startStudySession` in [SSD-001] | | Traces to | `startStudySession` in [SSD-001] |
| Domain Model concepts | Study Session, Words To Learn, Word List, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Vocabulary`, `Card` ([DICT-001]) | | Domain Model concepts | Level ([DM-001]); IT term `Level` ([DICT-001]) |
**Preconditions**
- The program was started without a level.
**Postconditions**
- The levels 100 words, 1000 words and all words are returned, in that order.
- No `StudySession` or `Deck` was created and no file was read.
**Exceptions**
| Condition (failing precondition) | Outcome |
| --- | --- |
| The program was started with a level | The operation is not sent; `chooseLevel` is the first operation |
## Contract: chooseLevel
| Item | Value |
| --- | --- |
| Operation | `chooseLevel(level: Level): CardDto \| None` |
| Traces to | `chooseLevel` in [SSD-001] |
| Domain Model concepts | Study Session, Words To Learn, Word List, Level, Flash Card ([DM-001]); IT terms `StudySession`, `Deck`, `Vocabulary`, `Level`, `Card` ([DICT-001]) |
**Preconditions** **Preconditions**
- A `Vocabulary` exists in the word list file. - A `Vocabulary` exists in the word list file.
- `level` is one of the levels returned by `startStudySession`, or was given at start.
**Postconditions** **Postconditions**
- A `StudySession` instance was created. - A `StudySession` instance was created and associated with `level`.
- A `Deck` instance was created and associated with the `StudySession`. - A `Deck` instance was created and associated with the `StudySession`.
- The `Deck` holds the cards of the progress file when it exists, otherwise a copy of the cards of the `Vocabulary`. - The `Deck` holds the cards of the progress file when it exists, otherwise a copy of the cards of the `Vocabulary`; cards outside the level are kept in it.
- When the `Deck` is not empty, `StudySession.current_card` was set to a card of the `Deck` and a `CardDto` of it is returned; otherwise `current_card` is `None` and `None` is returned. - The level's cards are the first `level.limit` cards of the `Vocabulary` (all of them when the limit is none or larger than the list).
- When the `Deck` holds a card of the level, `StudySession.current_card` was set to such a card and a `CardDto` of it is returned; otherwise `current_card` is `None` and `None` is returned.
- The Malay word of the card was passed to the `SpeechPort`.
**Exceptions** **Exceptions**
@@ -40,6 +67,7 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
| --- | --- | | --- | --- |
| The word list file does not exist | `VocabularyNotFoundError` is raised; no session is created | | The word list file does not exist | `VocabularyNotFoundError` is raised; no session is created |
| The word list or progress file has no header of two cells, or a row with other than two cells | `VocabularyFormatError` is raised; no session is created and no file is changed | | The word list or progress file has no header of two cells, or a row with other than two cells | `VocabularyFormatError` is raised; no session is created and no file is changed |
| No card of the level is left in the `Deck` | `None` is returned; nothing is spoken |
## Contract: markKnown ## Contract: markKnown
@@ -55,8 +83,8 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
**Postconditions** **Postconditions**
- The `Card` of `current_card` was removed from the `Deck` (the association was destroyed). - The `Card` of `current_card` was removed from the `Deck` (the association was destroyed); cards outside the level are not touched.
- The `Deck` was stored in the progress file. - The whole `Deck` was stored in the progress file.
- `StudySession.current_card` was set to `None`. - `StudySession.current_card` was set to `None`.
**Exceptions** **Exceptions**
@@ -79,8 +107,8 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
**Postconditions** **Postconditions**
- `StudySession.current_card` was set to a card of the `Deck` that differs from the previous `current_card` when the `Deck` holds two or more cards, and a `CardDto` of it is returned. - `StudySession.current_card` was set to a card of the level that differs from the previous `current_card` when the level holds two or more cards still to learn, and a `CardDto` of it is returned.
- When the `Deck` is empty, `current_card` is `None` and `None` is returned. - When no card of the level is left, `current_card` is `None` and `None` is returned.
- The `Deck` and the progress file are unchanged. - The `Deck` and the progress file are unchanged.
- The Malay word of the new `current_card` was passed to the `SpeechPort`. - The Malay word of the new `current_card` was passed to the `SpeechPort`.
@@ -88,7 +116,7 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
| Condition (failing precondition) | Outcome | | Condition (failing precondition) | Outcome |
| --- | --- | | --- | --- |
| The `Deck` is empty | `None` is returned; nothing is spoken | | No card of the level is left | `None` is returned; nothing is spoken |
--- ---
@@ -97,3 +125,4 @@ The turn-over of the card after 3000 ms is started by the system's own timer, no
[DICT-001]: ../dictionary.md [DICT-001]: ../dictionary.md
[SD-001]: ./sd.md [SD-001]: ./sd.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+56 -14
View File
@@ -9,7 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the sequences startStudySession (levels) and chooseLevel (level filter) | [0b983b7] |
--- ---
@@ -19,29 +20,66 @@
### Diagram ### Diagram
```plantuml
@startuml
participant ":LevelView" as V
participant ":LevelViewModel" as VM
V -> VM : choices
VM --> V : tuple[Level, ...]
@enduml
```
### Pattern Annotations
| Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- |
| MVVM | `LevelView`, `LevelViewModel` | The view shows the choices; the labels and the order are in the view model |
### Postcondition Coverage
| Postcondition (from contract) | Satisfied by message |
| --- | --- |
| The three levels are returned in order | `choices` |
| No session or deck is created and no file is read | no other message is sent |
### Responsibility Check
The view model owns the choices; the view only draws buttons and forwards the click.
## Sequence: chooseLevel
**Realizes:** `chooseLevel` in [OC-001]
### Diagram
```plantuml ```plantuml
@startuml @startuml
participant "main : app" as M participant "main : app" as M
participant ":CsvVocabularyRepository" as VR participant ":CsvVocabularyRepository" as VR
participant ":CsvProgressRepository" as PR participant ":CsvProgressRepository" as PR
participant "level : Level" as L
participant ":StudySession" as S participant ":StudySession" as S
participant ":Deck" as D participant ":Deck" as D
participant ":SpeechPort" as SP participant ":SpeechPort" as SP
M -> VR : load()
VR --> M : vocabulary
M -> PR : load() M -> PR : load()
PR --> M : deck | None PR --> M : deck | None
alt deck is None alt deck is None
M -> VR : load()
VR --> M : vocabulary
create D create D
M -> D : from_vocabulary(vocabulary) M -> D : from_vocabulary(vocabulary)
D --> M : deck D --> M : deck
end end
M -> L : first(vocabulary.cards)
L --> M : level_cards
create S create S
M -> S : create(deck, progress, speech, choose) M -> S : create(deck, progress, speech, choose, level_cards)
M -> S : next_card() M -> S : next_card()
activate S activate S
opt deck is not empty S -> D : within(level_cards)
S -> D : draw(choose, exclude) D --> S : candidates
opt candidates is not empty
S -> D : draw(choose, exclude, among=candidates)
D --> S : card D --> S : card
S -> SP : speak(card.front, front_code) S -> SP : speak(card.front, front_code)
end end
@@ -55,20 +93,21 @@ deactivate S
| Pattern (GRASP / GoF) | Applied to | Rationale | | Pattern (GRASP / GoF) | Applied to | Rationale |
| --- | --- | --- | | --- | --- | --- |
| Creator | `app.main` creates `StudySession` | It holds the adapters the session needs | | Creator | `app.main` creates `StudySession` | It holds the adapters the session needs |
| Information Expert | `Deck` draws and removes its own cards | It owns the list of cards | | Information Expert | `Level.first`, `Deck.within`, `Deck.draw` | The level knows its limit, the deck knows its cards |
| Null Object | `NullSpeech` as `SpeechPort` | The session never tests for speech | | Null Object | `NullSpeech` as `SpeechPort` | The session never tests for speech |
### Postcondition Coverage ### Postcondition Coverage
| Postcondition (from contract) | Satisfied by message | | Postcondition (from contract) | Satisfied by message |
| --- | --- | | --- | --- |
| A `StudySession` instance was created | `create(deck, progress, speech, choose)` | | A `StudySession` was created and associated with the level | `create(deck, progress, speech, choose, level_cards)` |
| A `Deck` was created from the progress file or a copy of the `Vocabulary` | `PR.load()`, `alt` fragment, `from_vocabulary` | | A `Deck` was created from the progress file or a copy of the `Vocabulary`, cards outside the level kept | `PR.load()`, `alt` fragment, `from_vocabulary` |
| `current_card` set and a `CardDto` returned, or `None` | `next_card()` with the `opt` fragment | | The level's cards are the first N cards of the `Vocabulary` | `first(vocabulary.cards)` |
| `current_card` set and a `CardDto` returned, or `None`; the word was passed to the `SpeechPort` | `next_card()` with `within`, the `opt` fragment and `speak` |
### Responsibility Check ### Responsibility Check
The session coordinates, the deck holds the cards, the repositories hold the file format; no object receives all messages. The composition root only wires; the level selects, the deck holds, the session coordinates; no object receives all messages.
## Sequence: markKnown ## Sequence: markKnown
@@ -134,8 +173,10 @@ participant ":SpeechPort" as SP
V -> VM : next_card() V -> VM : next_card()
VM -> S : next_card() VM -> S : next_card()
activate S activate S
opt deck is not empty S -> D : within(level_cards)
S -> D : draw(choose, exclude=current) D --> S : candidates
opt candidates is not empty
S -> D : draw(choose, exclude=current, among=candidates)
D --> S : card D --> S : card
S -> SP : speak(card.front, front_code) S -> SP : speak(card.front, front_code)
end end
@@ -155,7 +196,7 @@ VM --> V : side, language name, text
| Postcondition (from contract) | Satisfied by message | | Postcondition (from contract) | Satisfied by message |
| --- | --- | | --- | --- |
| A different card was chosen when two or more exist | `draw(choose, exclude=current)` | | A different card of the level was chosen when two or more exist | `within(level_cards)`, `draw(choose, exclude=current, among=candidates)` |
| Deck and file unchanged | no `remove` or `save` message | | Deck and file unchanged | no `remove` or `save` message |
| The Malay word was passed to the `SpeechPort` | `speak(card.front, front_code)` | | The Malay word was passed to the `SpeechPort` | `speak(card.front, front_code)` |
@@ -169,3 +210,4 @@ Same as `markKnown`; the session is the only object that speaks and draws.
[DCD-001]: ../dcd.md [DCD-001]: ../dcd.md
[DICT-001]: ../dictionary.md [DICT-001]: ../dictionary.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+13 -8
View File
@@ -11,7 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added chooseLevel; startStudySession returns the levels | [0b983b7] |
--- ---
@@ -26,6 +27,8 @@ Study Flash Cards ([UC-001]) — scenario: main success scenario, with the unkno
actor Learner as L actor Learner as L
participant ":System" as S participant ":System" as S
L -> S : startStudySession() L -> S : startStudySession()
S --> L : levels (100 words, 1000 words, all words)
L -> S : chooseLevel(level)
S --> L : card (word, language name) S --> L : card (word, language name)
... 3000 ms pass, the system turns the card over ... ... 3000 ms pass, the system turns the card over ...
S --> L : card (translation, language name) S --> L : card (translation, language name)
@@ -40,20 +43,21 @@ S --> L : next card (word, language name)
| Step | Message | Parameters | Return | Use case step | | Step | Message | Parameters | Return | Use case step |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| 1 | startStudySession | none | card to show, or "nothing left to learn" | 1, 2, 3 | | 1 | startStudySession | none | the levels to choose from | 1, 2 |
| 2 | (timer) turn the card over | none | the same card, translation side | 4 | | 2 | chooseLevel | level | card to show, or "nothing left to learn" | 3, 4, 5 |
| 3 | markKnown | none | next card, or "nothing left to learn" | 5, 6, 7 | | 3 | (timer) turn the card over | none | the same card, translation side | 6 |
| 4 | markUnknown | none | next card | 5a, 7 | | 4 | markKnown | none | next card, or "nothing left to learn" | 7, 8, 9 |
| 5 | markUnknown | none | next card, or "nothing left to learn" | 7a, 9 |
The turn-over in step 2 is started by the system's own timer, not by a message from the Learner; it is shown as a return without a call, which is the one deviation from "every return answers a message". The turn-over in step 3 is started by the system's own timer, not by a message from the Learner; it is shown as a return without a call, which is the one deviation from "every return answers a message". When the level is given at start (extension 3a of [UC-001]) the Learner's first message is `chooseLevel` and `startStudySession` is not sent.
## Alternate Flows ## Alternate Flows
The extensions 2a, 3a, 5b and 7a of [UC-001] add no system operation of their own (they are errors, speech and timer handling inside the three operations above); their behaviour is in [OC-001] as exceptions. They are out of scope of this diagram, which shows the main success scenario with `markUnknown` as the one alternate answer. The extensions 3a, 4a, 5a, 7b and 9a of [UC-001] add no system operation of their own (they are errors, speech, timer and level handling inside the operations above); their behaviour is in [OC-001] as exceptions. They are out of scope of this diagram, which shows the main success scenario with `markUnknown` as the one alternate answer.
## Lifecycle Notes ## Lifecycle Notes
The system instance lives for one run of the program. The words to learn are created when the session starts and saved after each `markKnown`. The system instance lives for one run of the program. The words to learn are created when the level is chosen and saved after each `markKnown`.
--- ---
@@ -61,3 +65,4 @@ The system instance lives for one run of the program. The words to learn are cre
[DM-001]: ../domain-model.md [DM-001]: ../domain-model.md
[OC-001]: ./oc.md [OC-001]: ./oc.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+27 -18
View File
@@ -11,7 +11,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added the choice of level (US-001.06) | [0b983b7] |
--- ---
@@ -35,38 +36,45 @@
### Main Success Scenario ### Main Success Scenario
1. The Learner starts the program. 1. The Learner starts the program.
2. The system loads the words to learn: from the kept progress when it exists, otherwise from the word list. 2. The system offers the levels: 100 words, 1000 words and all words.
3. The system shows a random card with the word in Malay. 3. The Learner chooses a level.
4. After three seconds the system turns the card over and shows the translation in Danish. 4. The system loads the words to learn: from the kept progress when it exists, otherwise from the word list.
5. The Learner states that the word is known. 5. The system shows a random card, with the word in Malay, from the first words of the word list that fit the level and are still to learn.
6. The system removes the word from the words to learn and keeps the words to learn. 6. After three seconds the system turns the card over and shows the translation in Danish.
7. The system shows a new random card, and the Learner repeats from step 4 until no word is left. 7. The Learner states that the word is known.
8. The system removes the word from the words to learn and keeps the words to learn.
9. The system shows a new random card of the level, and the Learner repeats from step 6 until no word is left at the level.
### Extensions (Alternative / Exception Flows) ### Extensions (Alternative / Exception Flows)
- 3a. Spoken words are available on the Learner's machine: - 3a. The Learner has given the level when starting the program:
1. The system skips steps 2 and 3 and goes on at step 4.
- 5a. Spoken words are available on the Learner's machine:
1. The system speaks the Malay word. If speaking fails, the system logs it and goes on. 1. The system speaks the Malay word. If speaking fails, the system logs it and goes on.
- 5a. The Learner states that the word is not known: - 7a. The Learner states that the word is not known:
1. The system keeps the word in the words to learn and shows a new random card; the Learner goes on at step 4. 1. The system keeps the word in the words to learn and shows a new random card of the level; the Learner goes on at step 6.
- 5b. The Learner answers before three seconds have passed: - 7b. The Learner answers before three seconds have passed:
1. The system cancels the turn-over of the card being shown and starts a new one for the next card. 1. The system cancels the turn-over of the card being shown and starts a new one for the next card.
- 7a. No word is left: - 9a. No word is left at the level:
1. The system tells the Learner that there is nothing left to learn, and further answers change nothing. 1. The system tells the Learner that there is nothing left to learn at this level, and further answers change nothing.
- 2a. The kept progress or the word list is unreadable: - 4a. The kept progress or the word list is unreadable:
1. The system reports the problem to the Learner and ends; it does not overwrite the kept progress. 1. The system reports the problem to the Learner and ends; it does not overwrite the kept progress.
### Special Requirements / Business Rules ### Special Requirements / Business Rules
| Step | Rule | | Step | Rule |
| --- | --- | | --- | --- |
| 3, 7 | The next card is chosen at random from the words to learn | | 2, 3 | A level is a number of words (100 or 1000) or all words |
| 4 | The turn-over comes three seconds after the card appears | | 5 | The word list is ordered from the most to the least frequent word; a level of N words is its first N words; a list shorter than N is used whole |
| 6 | A word is identified by its Malay and Danish text; the first equal word is removed | | 5, 9 | The next card is chosen at random from the words of the level that are still to learn |
| 2 | The word list names its two languages and holds one word and one translation per card | | 6 | The turn-over comes three seconds after the card appears |
| 8 | A word is identified by its Malay and Danish text; the first equal word is removed; known words stay known at every level and words outside the level are kept |
| 4 | The word list names its two languages and holds one word and one translation per card |
### Open Issues ### Open Issues
- Starting again (resetting the kept progress) is not offered in the program; deleting the kept progress does it. - Starting again (resetting the kept progress) is not offered in the program; deleting the kept progress does it.
- Going back to the level choice without restarting the program is not offered.
--- ---
@@ -76,3 +84,4 @@
[DM-001]: ../domain-model.md [DM-001]: ../domain-model.md
[SSD-001]: ./ssd.md [SSD-001]: ./ssd.md
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3 [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
+25 -1
View File
@@ -1,8 +1,32 @@
# Wireframe: Study Flash Cards # Wireframe: Study Flash Cards
_Revision 2026-10-14: the level screen (screen 1) was added for US-001.06. It was drawn by the AI from the use case because the Product Owner gave no wireframe for it; S01 may change it._
The Product Owner gave this screen in the first prompt (the course's target screenshot). The presentation layer implements it and does not redesign it. Source use case: [UC-001](./uc.md). The Product Owner gave this screen in the first prompt (the course's target screenshot). The presentation layer implements it and does not redesign it. Source use case: [UC-001](./uc.md).
## Screen ## Screen 1: Choose your level
```text
+----------------------------------------------------------+ background #B1DDC6
| padding 50 px |
| |
| Choose your level (Arial 40 italic) |
| |
| [ 100 words ] |
| [ 1000 words ] |
| [ All words ] |
| |
+----------------------------------------------------------+
```
| Element | Position | Details |
| --- | --- | --- |
| Window | whole | Same title, padding and background as the card screen |
| Title | row 0 | "Choose your level", Arial 40 italic |
| Level buttons | rows 1 to 3, one column | Labels "100 words", "1000 words", "All words"; Arial 24 bold; white background, equal width |
| After a choice | whole | The screen is replaced by screen 2 on the same window |
## Screen 2: The card
```text ```text
+----------------------------------------------------------+ background #B1DDC6 +----------------------------------------------------------+ background #B1DDC6
+41 -4
View File
@@ -4,20 +4,22 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | US-001 | | ID | US-001 |
| CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006] | | CrossReference | [UCD-001], [BC-001], [UC-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-008], [MIL-009] |
| Language | en | | Language | en |
| Domain | it | | Domain | it |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [c3fbcbc] |
| 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 ## 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) and the spoken Malay word. 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 ## Story List
@@ -88,9 +90,40 @@ These stories cover the epic "Study Flash Cards": the four steps of the course a
| --- | --- | --- | | --- | --- | --- |
| [MIL-006], [UC-001] | fits one iteration | none | | [MIL-006], [UC-001] | fits one iteration | none |
### US-001.06 — Choose my level
**As a** Learner, **I want** to choose a level of 100 words, 1000 words or all words before I start, **so that** I begin with the most frequent words and widen the list when I am ready.
**Acceptance Criteria**
- Given the program is started, when the window opens, then I see the choices 100 words, 1000 words and All words, and no card yet.
- Given I choose a level of N words, when the first card is shown, then every card comes from the first N words of the word list, which is ordered from the most to the least frequent word.
- Given I choose 1000 words or All words and the word list has fewer words than that, when the cards are drawn, then all words of the word list are used.
- Given I have marked words as known at one level, when I choose another level, then the known words stay known and only the words of the chosen level that I do not know yet come up.
- Given every word of the chosen level is known, when the level is chosen, then the program tells me that there is nothing left to learn at this level.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [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 ## INVEST Check
All five 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.05 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.
--- ---
@@ -102,4 +135,8 @@ All five stories are Independent in delivery order (each builds on the one befor
[MIL-004]: ./milestones/mil-004-step3-flip-cards.md [MIL-004]: ./milestones/mil-004-step3-flip-cards.md
[MIL-005]: ./milestones/mil-005-step4-save-progress.md [MIL-005]: ./milestones/mil-005-step4-save-progress.md
[MIL-006]: ./milestones/mil-006-malay-speech.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 [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
+41
View File
@@ -0,0 +1,41 @@
# Arkitektur
🌐 [English](Architecture) · **Dansk** · [Bahasa Malaysia](Architecture-ms)
Programmet er bygget i fire lag under `src/flash_card/`. Importer peger indad; `lint-imports` kontrollerer det.
| Lag | Indeholder | Må importere |
| --- | --- | --- |
| `domain` | `Card`, `LanguagePair`, `Level`, `Vocabulary`, `Deck`, fejlene og repository-grænsefladerne | ingenting |
| `application` | `StudySession`, `open_study_session`, `CardDto` og `SpeechPort` | `domain` |
| `infrastructure` | CSV-repositories og tale-adaptere | `application`, `domain` |
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) og kompositionsroden `app.py` | alle lag |
Alle konstanter står i `src/flash_card/constants.py`.
## Hvor er hvert opgavetrin?
| Opgave | Kode |
| --- | --- |
| Trin 1: vindue, lærred, billede, tekster, knapper | `FlashCardView.__init__` i `presentation/features/flash_card/flash_card_view.py`; billederne laves af `tools/generate_images.py` |
| Trin 2: læs CSV, tilfældigt ord ved hvert tryk | `CsvVocabularyRepository` og `read_word_list` (erstatter `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
| Trin 3: vend efter 3 sekunder | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
| Trin 4: kendte ord, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
| Ekstra: niveauvalg | `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session`, `LevelView`, `LevelViewModel`, `Application.start` i `presentation/app.py` |
| Ekstra: tale | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` i `presentation/app.py` |
## Hvorfor en lagdelt løsning til en lille opgave?
Reglerne (hvilket kort der kommer næste gang, hvad et kendt ord gør) testes uden vindue og uden filsystem. Vinduet, CSV-filerne og taletjenesten kan udskiftes uden at røre reglerne. Se ADR-0001 og ADR-0002 i `docs/adr/`.
## Test
| Mappe | Hvad den tester |
| --- | --- |
| `tests/unit/` | Hvert lag for sig, med falske objekter til portene |
| `tests/integration/` | Den rigtige ordliste og fremgangsfilen i en midlertidig mappe |
| `tests/e2e/` | Det rigtige Tkinter-vindue (springes over uden skærm) |
## Projektdokumenter
Business case, plan, design og gennemgangsrapporter ligger i `docs/`. Arbejdet blev udført i syv milepæle, én pull request hver: se `docs/milestones/`.
+41
View File
@@ -0,0 +1,41 @@
# Seni bina
🌐 [English](Architecture) · [Dansk](Architecture-da) · **Bahasa Malaysia**
Program dibina dalam empat lapisan di bawah `src/flash_card/`. Import menuju ke dalam; `lint-imports` memeriksanya.
| Lapisan | Mengandungi | Boleh mengimport |
| --- | --- | --- |
| `domain` | `Card`, `LanguagePair`, `Level`, `Vocabulary`, `Deck`, ralat dan antara muka repositori | tiada |
| `application` | `StudySession`, `open_study_session`, `CardDto` dan `SpeechPort` | `domain` |
| `infrastructure` | Repositori CSV dan penyesuai suara | `application`, `domain` |
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) dan akar komposisi `app.py` | semua lapisan |
Semua pemalar terdapat dalam `src/flash_card/constants.py`.
## Di manakah setiap langkah tugasan?
| Tugasan | Kod |
| --- | --- |
| Langkah 1: tetingkap, kanvas, imej, teks, butang | `FlashCardView.__init__` dalam `presentation/features/flash_card/flash_card_view.py`; imej dibuat oleh `tools/generate_images.py` |
| Langkah 2: baca CSV, perkataan rawak setiap tekanan | `CsvVocabularyRepository` dan `read_word_list` (menggantikan `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
| Langkah 3: terbalik selepas 3 saat | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
| Langkah 4: perkataan diketahui, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
| Tambahan: pilihan tahap | `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session`, `LevelView`, `LevelViewModel`, `Application.start` dalam `presentation/app.py` |
| Tambahan: suara | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` dalam `presentation/app.py` |
## Mengapa program berlapis untuk tugasan kecil?
Peraturan (kad mana yang seterusnya, apa yang berlaku apabila perkataan diketahui) diuji tanpa tetingkap atau sistem fail. Tetingkap, fail CSV dan perkhidmatan suara boleh diganti tanpa menyentuh peraturan. Lihat ADR-0001 dan ADR-0002 dalam `docs/adr/`.
## Ujian
| Folder | Apa yang diuji |
| --- | --- |
| `tests/unit/` | Setiap lapisan sendiri, dengan objek palsu untuk port |
| `tests/integration/` | Senarai perkataan sebenar dan fail kemajuan dalam folder sementara |
| `tests/e2e/` | Tetingkap Tkinter sebenar (dilangkau tanpa paparan) |
## Dokumen projek
Kes perniagaan, pelan, reka bentuk dan rekod semakan terdapat dalam `docs/`. Kerja dilakukan dalam tujuh pencapaian, satu pull request setiap satu: lihat `docs/milestones/`.
+41
View File
@@ -0,0 +1,41 @@
# Architecture
🌐 **English** · [Dansk](Architecture-da) · [Bahasa Malaysia](Architecture-ms)
The program is built in four layers under `src/flash_card/`. Imports point inward; `lint-imports` checks this.
| Layer | Holds | May import |
| --- | --- | --- |
| `domain` | `Card`, `LanguagePair`, `Level`, `Vocabulary`, `Deck`, the errors and the repository interfaces | nothing |
| `application` | `StudySession`, `open_study_session`, `CardDto` and `SpeechPort` | `domain` |
| `infrastructure` | The CSV repositories and the speech adapters | `application`, `domain` |
| `presentation` | `FlashCardView`, `FlashCardViewModel` (MVVM) and the composition root `app.py` | all layers |
All constants are in `src/flash_card/constants.py`.
## Where is each assignment step?
| Assignment | Code |
| --- | --- |
| Step 1: window, canvas, image, texts, buttons | `FlashCardView.__init__` in `presentation/features/flash_card/flash_card_view.py`; images made by `tools/generate_images.py` |
| Step 2: read the CSV, random word on each press | `CsvVocabularyRepository` and `read_word_list` (replace `pandas`), `Deck.draw`, `StudySession.next_card`, `FlashCardView.next_card` |
| Step 3: flip after 3 seconds | `FlashCardView.flip_card`, `FlashCardView.flip_timer` (`after`, `after_cancel`), `FlashCardViewModel.flip_card` |
| Step 4: known words, `words_to_learn.csv` | `FlashCardView.is_known`, `StudySession.mark_known`, `CsvProgressRepository`, `open_study_session` |
| Extra: level choice | `Level`, `Deck.within`, `StudySession.next_card`, `open_study_session`, `LevelView`, `LevelViewModel`, `Application.start` in `presentation/app.py` |
| Extra: speech | `SpeechPort`, `GttsSpeech`, `NullSpeech`, `build_speech` in `presentation/app.py` |
## Why a layered program for a small assignment?
The rules (which card comes next, what a known word does) are tested without a window or a file system. The window, the CSV files and the speech service can be replaced without touching the rules. See ADR-0001 and ADR-0002 in `docs/adr/`.
## Tests
| Folder | What it tests |
| --- | --- |
| `tests/unit/` | Each layer alone, with fakes for the ports |
| `tests/integration/` | The real word list and the progress file in a temporary folder |
| `tests/e2e/` | The real Tkinter window (skipped when no display exists) |
## Project documents
The business case, plan, design and review records are in `docs/`. The work was done in seven milestones, one pull request each: see `docs/milestones/`.
+32
View File
@@ -0,0 +1,32 @@
# Flash Card
🌐 [English](Home) · **Dansk** · [Bahasa Malaysia](Home-ms)
Et flashkortprogram til at lære de mest almindelige malajiske ord med danske oversættelser. Det er løsningen på dag 31 i Udemys kursus *100 Days of Code: The Complete Python Pro Bootcamp*.
![Niveauskærmen: vælg 100 ord, 1000 ord eller alle ord](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-level.png)
![Forsiden af et kort: det malajiske ord](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-front.png)
![Bagsiden af kortet efter tre sekunder: den danske oversættelse](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-back.png)
| Side | Læs den, når du vil |
| --- | --- |
| [Installation](Install-da) | Oprette `.venv`, installere programmet og køre testene |
| [Brug](Usage-da) | Øve kortene, forstå fremgangsfilen og bruge indstillingerne |
| [Arkitektur](Architecture-da) | Se lagene og finde koden til hvert opgavetrin |
## De fire opgavetrin
| Trin | Hvad programmet gør | Milepæl |
| --- | --- | --- |
| 1. Brugerflade | Et vindue med et kort og to knapper | MIL-002 |
| 2. Nye flashkort | Et tilfældigt ord fra ordlisten ved hvert tryk på en knap | MIL-003 |
| 3. Vend kortene | Efter 3 sekunder viser kortet den danske oversættelse | MIL-004 |
| 4. Gem din fremgang | Kendte ord fjernes, og resten gemmes i `words_to_learn.csv` | MIL-005 |
| Ekstra | Det malajiske ord bliver udtalt (valgfrit) | MIL-006 |
| Ekstra | Du vælger først et niveau: de første 100 ord, de første 1000 ord eller alle ord | MIL-008 |
## Til kursister
Opgavens funktionsnavne er bevaret: `next_card`, `flip_card`, `is_known` og attributterne `flip_timer` og `current_card`. Data læses med standardbibliotekets `csv`-modul i stedet for `pandas`, så kerneprogrammet ikke har nogen afhængigheder ved kørsel.
Repository: <https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card>
+32
View File
@@ -0,0 +1,32 @@
# Flash Card
🌐 [English](Home) · [Dansk](Home-da) · **Bahasa Malaysia**
Program kad imbas untuk mempelajari perkataan Melayu yang paling kerap digunakan beserta terjemahan Denmark. Ia ialah penyelesaian hari ke-31 kursus Udemy *100 Days of Code: The Complete Python Pro Bootcamp*.
![Skrin tahap: pilih 100 perkataan, 1000 perkataan atau semua perkataan](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-level.png)
![Bahagian depan kad: perkataan Melayu](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-front.png)
![Bahagian belakang kad selepas tiga saat: terjemahan Denmark](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-back.png)
| Halaman | Baca apabila anda mahu |
| --- | --- |
| [Pemasangan](Install-ms) | Mencipta `.venv`, memasang program dan menjalankan ujian |
| [Penggunaan](Usage-ms) | Berlatih dengan kad, memahami fail kemajuan dan menggunakan pilihan |
| [Seni bina](Architecture-ms) | Melihat lapisan dan mencari kod bagi setiap langkah tugasan |
## Empat langkah tugasan
| Langkah | Apa yang program lakukan | Pencapaian |
| --- | --- | --- |
| 1. Antara muka pengguna | Tetingkap dengan satu kad dan dua butang | MIL-002 |
| 2. Kad imbas baharu | Satu perkataan rawak daripada senarai perkataan setiap kali butang ditekan | MIL-003 |
| 3. Terbalikkan kad | Selepas 3 saat kad menunjukkan terjemahan Denmark | MIL-004 |
| 4. Simpan kemajuan anda | Perkataan yang diketahui dibuang dan selebihnya disimpan dalam `words_to_learn.csv` | MIL-005 |
| Tambahan | Perkataan Melayu disebut (pilihan) | MIL-006 |
| Tambahan | Anda memilih tahap dahulu: 100 perkataan pertama, 1000 perkataan pertama atau semua perkataan | MIL-008 |
## Untuk peserta kursus
Nama fungsi tugasan dikekalkan: `next_card`, `flip_card`, `is_known`, serta atribut `flip_timer` dan `current_card`. Data dibaca dengan modul `csv` pustaka standard dan bukan `pandas`, jadi program teras tidak mempunyai kebergantungan semasa larian.
Repositori: <https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card>
+32
View File
@@ -0,0 +1,32 @@
# Flash Card
🌐 **English** · [Dansk](Home-da) · [Bahasa Malaysia](Home-ms)
A flash card program for learning the most frequent Malay words with their Danish translations. It is the solution of day 31 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*.
![The level screen: choose 100 words, 1000 words or all words](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-level.png)
![The front of a card: the Malay word](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-front.png)
![The back of the card after three seconds: the Danish translation](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/raw/branch/main/docs/images/screenshot-back.png)
| Page | Read it when you want to |
| --- | --- |
| [Install](Install) | Create the `.venv`, install the program and run the tests |
| [Usage](Usage) | Study the cards, understand the progress file, use the options |
| [Architecture](Architecture) | See the layers and find the code of each assignment step |
## The four assignment steps
| Step | What the program does | Milestone |
| --- | --- | --- |
| 1. User interface | A window with a card and two buttons | MIL-002 |
| 2. New flash cards | A random word from the word list on every button press | MIL-003 |
| 3. Flip the cards | After 3 seconds the card shows the Danish translation | MIL-004 |
| 4. Save your progress | Known words are removed and the rest is saved in `words_to_learn.csv` | MIL-005 |
| Extra | The Malay word is spoken (optional) | MIL-006 |
| Extra | You choose a level first: the first 100 words, the first 1000 words or all words | MIL-008 |
## For course participants
The function names of the assignment are kept: `next_card`, `flip_card`, `is_known`, and the attributes `flip_timer` and `current_card`. The data is read with the standard library `csv` module instead of `pandas`, so the core program has no runtime dependency.
Repository: <https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card>
+65
View File
@@ -0,0 +1,65 @@
# Installation
🌐 [English](Install) · **Dansk** · [Bahasa Malaysia](Install-ms)
## Krav
- Python 3.13 eller nyere med Tkinter (på Debian eller Ubuntu: `sudo apt install python3-tk`).
- `git`.
- Valgfrit: en internetforbindelse og tilvalget `speech`, hvis du vil høre det malajiske ord.
## Hent koden
```bash
git clone https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card.git
cd 031-flash-card
```
## Opret og brug et lokalt virtuelt miljø
Windows (PowerShell):
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
Linux og macOS:
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
- `.[dev]` tilføjer `pytest`, `ruff`, `mypy` og `import-linter`. Brug `.`, hvis du kun vil køre programmet.
- Hvis PowerShell nægter at køre `Activate.ps1`, så kør `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` én gang.
- Forlad miljøet med `deactivate`; slet mappen `.venv` for at starte forfra.
## Udtalt malajisk ord (valgfrit)
```bash
python -m pip install -e ".[speech]"
```
Det installerer `gTTS` og `playsound==1.2.2`. Uden tilvalget virker programmet uden lyd.
## Kør og test
```bash
python -m flash_card
python -m pytest
```
Vinduestesten i `tests/e2e/` springes over, når der ikke er en skærm.
## Dokumentation af kildekoden
```bash
doxygen Doxyfile
```
HTML-filerne skrives til `docs/doxygen/html/index.html`.
+65
View File
@@ -0,0 +1,65 @@
# Pemasangan
🌐 [English](Install) · [Dansk](Install-da) · **Bahasa Malaysia**
## Keperluan
- Python 3.13 atau lebih baharu dengan Tkinter (pada Debian atau Ubuntu: `sudo apt install python3-tk`).
- `git`.
- Pilihan: sambungan internet dan tambahan `speech` jika anda mahu mendengar perkataan Melayu.
## Dapatkan kod
```bash
git clone https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card.git
cd 031-flash-card
```
## Cipta dan gunakan persekitaran maya setempat
Windows (PowerShell):
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
Linux dan macOS:
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
- `.[dev]` menambah `pytest`, `ruff`, `mypy` dan `import-linter`. Gunakan `.` jika anda hanya mahu menjalankan program.
- Jika PowerShell enggan menjalankan `Activate.ps1`, jalankan `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` sekali.
- Keluar daripada persekitaran dengan `deactivate`; padam folder `.venv` untuk bermula semula.
## Perkataan Melayu bersuara (pilihan)
```bash
python -m pip install -e ".[speech]"
```
Ini memasang `gTTS` dan `playsound==1.2.2`. Tanpa tambahan ini program berfungsi tanpa bunyi.
## Jalankan dan uji
```bash
python -m flash_card
python -m pytest
```
Ujian tetingkap dalam `tests/e2e/` dilangkau apabila tiada paparan.
## Dokumentasi kod sumber
```bash
doxygen Doxyfile
```
HTML ditulis ke `docs/doxygen/html/index.html`.
+65
View File
@@ -0,0 +1,65 @@
# Install
🌐 **English** · [Dansk](Install-da) · [Bahasa Malaysia](Install-ms)
## Requirements
- Python 3.13 or later with Tkinter (on Debian or Ubuntu: `sudo apt install python3-tk`).
- `git`.
- Optional: an internet connection and the `speech` extra to hear the Malay word.
## Get the code
```bash
git clone https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card.git
cd 031-flash-card
```
## Create and use a local virtual environment
Windows (PowerShell):
```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
Linux and macOS:
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```
- `.[dev]` adds `pytest`, `ruff`, `mypy` and `import-linter`. Use `.` if you only want to run the program.
- If PowerShell refuses to run `Activate.ps1`, run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once.
- Leave the environment with `deactivate`; delete the `.venv` folder to start over.
## Spoken Malay word (optional)
```bash
python -m pip install -e ".[speech]"
```
This installs `gTTS` and `playsound==1.2.2`. Without it the program works silently.
## Run and test
```bash
python -m flash_card
python -m pytest
```
The window test in `tests/e2e/` is skipped when no display is available.
## Documentation of the source code
```bash
doxygen Doxyfile
```
The HTML is written to `docs/doxygen/html/index.html`.
+53
View File
@@ -0,0 +1,53 @@
# Brug
🌐 [English](Usage) · **Dansk** · [Bahasa Malaysia](Usage-ms)
## Øv kortene
1. Start programmet med `python -m flash_card`.
2. Vælg et niveau: **100 words**, **1000 words** eller **All words**. Ordlisten er sorteret fra det hyppigste til det sjældneste ord, så niveau 100 er de 100 hyppigste ord.
3. Et kort viser et malajisk ord. Prøv at huske den danske oversættelse.
4. Efter tre sekunder vender kortet og viser oversættelsen.
5. Tryk på det grønne flueben, hvis du kendte ordet, eller på det røde kryds, hvis du ikke gjorde.
6. Et nyt tilfældigt ord fra niveauet vises. Gentag.
Et ord, du har markeret som kendt, fjernes og kommer ikke igen. Når der ikke er flere ord på niveauet, står der "Nothing left to learn" på kortet; start programmet igen og vælg et højere niveau.
## Din fremgang
- Første gang du trykker på det grønne flueben, skriver programmet `data/words_to_learn.csv` med de ord, du endnu ikke kan.
- Næste start bruger den fil. Findes den ikke, bruger programmet hele ordlisten `data/ms-da.csv`.
- For at starte forfra skal du slette `data/words_to_learn.csv`.
- Det røde kryds ændrer aldrig filen.
- Kendte ord forbliver kendte på alle niveauer. Niveauet begrænser kun, hvilke af de ord, du endnu ikke kender, der kommer frem, og filen beholder altid alle ord, du ikke kender, også dem uden for niveauet.
- Denne liste har 1000 ord, så "1000 words" og "All words" giver de samme kort.
## Filformat
Begge filer er UTF-8-tekst med ét kort pr. linje og cellerne adskilt af semikolon. Første linje har de to sprogkoder.
```text
ms;da
aku;mig
yang;hvilke
```
## Indstillinger
| Indstilling | Betydning |
| --- | --- |
| `--data-dir <mappe>` | Mappen med `ms-da.csv` og `words_to_learn.csv` (standard: `data/` i repositoryet) |
| `--level {100,1000,all}` | Start på det niveau og spring niveauskærmen over |
| `--no-speech` | Udtal ikke det malajiske ord, selv om tilvalget til tale er installeret |
## Brug et andet sprogpar
Læg en fil med samme format i datamappen og kald den `ms-da.csv`, eller ret `VOCABULARY_FILE_NAME` og `LANGUAGE_NAMES` i `src/flash_card/constants.py`. Kodene i overskriften vises som sprognavne fra `LANGUAGE_NAMES`; ukendte koder vises med store bogstaver.
## Fejlfinding
| Problem | Hvad du skal gøre |
| --- | --- |
| `No module named tkinter` | Installer Tkinter (`python3-tk` på Debian og Ubuntu) |
| Ingen lyd | Installer tilvalget `speech` og tjek netværket; programmet logger en advarsel, når udtalen mislykkes |
| Programmet stopper med en meddelelse om filen | CSV-filen er forkert opbygget; meddelelsen nævner filen og linjen. Filen bliver ikke overskrevet |
+53
View File
@@ -0,0 +1,53 @@
# Penggunaan
🌐 [English](Usage) · [Dansk](Usage-da) · **Bahasa Malaysia**
## Berlatih dengan kad
1. Mulakan program dengan `python -m flash_card`.
2. Pilih tahap: **100 words**, **1000 words** atau **All words**. Senarai perkataan disusun daripada perkataan paling kerap kepada paling jarang, jadi tahap 100 ialah 100 perkataan paling kerap.
3. Kad menunjukkan satu perkataan Melayu. Cuba ingat terjemahan Denmarknya.
4. Selepas tiga saat kad terbalik dan menunjukkan terjemahan.
5. Tekan tanda semak hijau jika anda tahu perkataan itu, atau pangkah merah jika tidak.
6. Satu perkataan rawak baharu daripada tahap itu muncul. Ulang.
Perkataan yang anda tandakan sebagai diketahui dibuang dan tidak muncul lagi. Apabila tiada perkataan lagi pada tahap itu, kad menunjukkan "Nothing left to learn"; mulakan program semula dan pilih tahap yang lebih tinggi.
## Kemajuan anda
- Kali pertama anda menekan tanda semak hijau, program menulis `data/words_to_learn.csv` dengan perkataan yang anda belum tahu.
- Permulaan seterusnya menggunakan fail itu. Jika ia tiada, program menggunakan senarai penuh `data/ms-da.csv`.
- Untuk bermula semula, padam `data/words_to_learn.csv`.
- Pangkah merah tidak pernah mengubah fail.
- Perkataan yang diketahui kekal diketahui pada semua tahap. Tahap hanya mengehadkan perkataan yang belum anda ketahui yang muncul, dan fail sentiasa menyimpan semua perkataan yang anda belum tahu, termasuk yang di luar tahap.
- Senarai ini mempunyai 1000 perkataan, jadi "1000 words" dan "All words" memberikan kad yang sama.
## Format fail
Kedua-dua fail ialah teks UTF-8 dengan satu kad setiap baris dan sel dipisahkan oleh koma bertitik. Baris pertama mengandungi dua kod bahasa.
```text
ms;da
aku;mig
yang;hvilke
```
## Pilihan
| Pilihan | Maksud |
| --- | --- |
| `--data-dir <folder>` | Folder yang mengandungi `ms-da.csv` dan `words_to_learn.csv` (lalai: `data/` dalam repositori) |
| `--level {100,1000,all}` | Mulakan pada tahap itu dan langkau skrin tahap |
| `--no-speech` | Jangan sebut perkataan Melayu, walaupun tambahan suara dipasang |
## Gunakan pasangan bahasa lain
Letakkan fail dengan format yang sama dalam folder data dan namakannya `ms-da.csv`, atau ubah `VOCABULARY_FILE_NAME` dan `LANGUAGE_NAMES` dalam `src/flash_card/constants.py`. Kod pada baris pertama dipaparkan sebagai nama bahasa daripada `LANGUAGE_NAMES`; kod yang tidak dikenali dipaparkan dengan huruf besar.
## Penyelesaian masalah
| Masalah | Apa yang perlu dilakukan |
| --- | --- |
| `No module named tkinter` | Pasang Tkinter (`python3-tk` pada Debian dan Ubuntu) |
| Tiada bunyi | Pasang tambahan `speech` dan periksa rangkaian; program mencatat amaran apabila penyebutan gagal |
| Program berhenti dengan mesej tentang fail | Fail CSV tidak betul; mesej menyebut fail dan barisnya. Fail tidak ditulis ganti |
+53
View File
@@ -0,0 +1,53 @@
# Usage
🌐 **English** · [Dansk](Usage-da) · [Bahasa Malaysia](Usage-ms)
## Study the cards
1. Start the program with `python -m flash_card`.
2. Choose a level: **100 words**, **1000 words** or **All words**. The word list is ordered from the most to the least frequent word, so level 100 is the 100 most frequent words.
3. A card shows a Malay word. Try to remember its Danish translation.
4. After three seconds the card turns over and shows the translation.
5. Press the green tick when you knew the word, or the red cross when you did not.
6. A new random word of the level appears. Repeat.
A word you marked as known is removed and does not come up again. When no word of the level is left, the card says "Nothing left to learn"; restart the program and choose a higher level.
## Your progress
- The first time you press the green tick, the program writes `data/words_to_learn.csv` with the words you do not know yet.
- The next start uses that file. If it does not exist, the program uses the full word list `data/ms-da.csv`.
- To start again, delete `data/words_to_learn.csv`.
- The red cross never changes the file.
- Known words stay known at every level. The level only limits which of the words you do not know yet come up, and the file always keeps every word you do not know, also those outside the level.
- This list has 1000 words, so "1000 words" and "All words" give the same cards.
## File format
Both files are UTF-8 text with one card per line and the cells separated by a semicolon. The first line holds the two language codes.
```text
ms;da
aku;mig
yang;hvilke
```
## Options
| Option | Meaning |
| --- | --- |
| `--data-dir <folder>` | Folder that holds `ms-da.csv` and `words_to_learn.csv` (default: `data/` of the repository) |
| `--level {100,1000,all}` | Start at that level and skip the level screen |
| `--no-speech` | Do not speak the Malay word, even when the speech extra is installed |
## Use another language pair
Put a file with the same format in the data folder and name it `ms-da.csv`, or change `VOCABULARY_FILE_NAME` and `LANGUAGE_NAMES` in `src/flash_card/constants.py`. The header codes are shown as language names from `LANGUAGE_NAMES`; unknown codes are shown in upper case.
## Troubleshooting
| Problem | What to do |
| --- | --- |
| `No module named tkinter` | Install Tkinter (`python3-tk` on Debian and Ubuntu) |
| No sound | Install the `speech` extra and check the network; the program logs a warning when speaking fails |
| The program stops with a message about the file | The CSV file is malformed; the message names the file and the line. The file is not overwritten |
+12
View File
@@ -0,0 +1,12 @@
**Flash Card**
- [Home](Home)
- [Install](Install)
- [Usage](Usage)
- [Architecture](Architecture)
**Languages**
- English: [Home](Home) · [Install](Install) · [Usage](Usage) · [Architecture](Architecture)
- Dansk: [Hjem](Home-da) · [Installation](Install-da) · [Brug](Usage-da) · [Arkitektur](Architecture-da)
- Bahasa Malaysia: [Utama](Home-ms) · [Pemasangan](Install-ms) · [Penggunaan](Usage-ms) · [Seni bina](Architecture-ms)
+5
View File
@@ -24,6 +24,8 @@ dependencies = []
[project.optional-dependencies] [project.optional-dependencies]
# Spoken Malay word (text to speech). Optional; the program works without it. # Spoken Malay word (text to speech). Optional; the program works without it.
speech = ["gTTS>=2.5", "playsound==1.2.2"] speech = ["gTTS>=2.5", "playsound==1.2.2"]
# Only for tools/capture_screenshots.py; not needed to run the program.
tools = ["pillow>=10"]
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "import-linter>=2.0"] dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "import-linter>=2.0"]
[project.scripts] [project.scripts]
@@ -45,6 +47,9 @@ line-length = 88
target-version = "py313" target-version = "py313"
src = ["src", "tests"] src = ["src", "tests"]
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "N", "D"] select = ["E", "F", "I", "UP", "B", "SIM", "N", "D"]
ignore = ["D203", "D213"] ignore = ["D203", "D213"]
@@ -0,0 +1,5 @@
"""Application data transfer objects.
@file __init__.py
@brief Holds the plain data that crosses to the presentation layer.
"""
@@ -0,0 +1,21 @@
"""Card data for the presentation layer.
@file card_dto.py
@brief The CardDto: a card as plain text.
"""
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class CardDto:
"""A card as plain data."""
## @brief Word on the card front.
front_text: str
## @brief Translation on the card back.
back_text: str
## @brief Language code of the front.
front_language: str
## @brief Language code of the back.
back_language: str
@@ -0,0 +1,5 @@
"""Ports of the application layer.
@file __init__.py
@brief Holds the interfaces the use cases need from outside.
"""
@@ -0,0 +1,22 @@
"""Port for spoken words.
@file speech_port.py
@brief The SpeechPort interface the study session uses to say a word aloud.
"""
from typing import Protocol
class SpeechPort(Protocol):
"""Speaks a text aloud in a language."""
def speak(self, text: str, language_code: str) -> None:
"""Speak a text without blocking the caller for long.
An implementation does not raise: a failure is logged and ignored.
@param text The text to speak.
@param language_code Language of the text, for example "ms".
@return Nothing.
"""
...
@@ -0,0 +1,5 @@
"""Application use cases.
@file __init__.py
@brief Holds the use cases of the flash card program.
"""
@@ -0,0 +1,43 @@
"""Start a study session.
@file open_study_session.py
@brief The open_study_session use case.
"""
from collections.abc import Callable, Sequence
from flash_card.application.ports.speech_port import SpeechPort
from flash_card.application.use_cases.study_session import StudySession
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
from flash_card.domain.repositories.progress_repository import ProgressRepository
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
from flash_card.domain.value_objects.level import Level
def open_study_session(
vocabulary_repository: VocabularyRepository,
progress_repository: ProgressRepository,
speech: SpeechPort,
choose: Callable[[Sequence[Card]], Card],
level: Level,
) -> StudySession:
"""Build a session over the saved words to learn, else the whole word list.
The word list is always loaded: its order defines which words a level holds.
@param vocabulary_repository Source of the delivered word list.
@param progress_repository Store of the words still to learn.
@param speech Speaks the front text of each drawn card.
@param choose Picks one card from a non-empty sequence.
@param level The level: the first words of the list that may be drawn.
@return The new session.
@throws VocabularyError When the word list or the saved progress cannot be
loaded.
"""
vocabulary = vocabulary_repository.load()
deck = progress_repository.load()
if deck is None:
deck = Deck.from_vocabulary(vocabulary)
level_cards = level.first(vocabulary.cards)
return StudySession(deck, choose, progress_repository, speech, level_cards)
@@ -0,0 +1,117 @@
"""One study session.
@file study_session.py
@brief The StudySession use case: draws the cards the user studies.
"""
from collections.abc import Callable, Sequence
from flash_card.application.dto.card_dto import CardDto
from flash_card.application.ports.speech_port import SpeechPort
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
from flash_card.domain.repositories.progress_repository import ProgressRepository
class StudySession:
"""Draws cards from a deck, one after another."""
def __init__(
self,
deck: Deck,
choose: Callable[[Sequence[Card]], Card],
progress: ProgressRepository,
speech: SpeechPort,
level_cards: Sequence[Card] | None = None,
) -> None:
"""Create a session.
@param deck The cards still to learn.
@param choose Picks one card from a non-empty sequence.
@param progress Store of the words still to learn.
@param speech Speaks the front text of each drawn card.
@param level_cards The cards of the chosen level, or None for no limit.
"""
## @brief The cards of the chosen level, or None for no limit.
self._level_cards = None if level_cards is None else tuple(level_cards)
## @brief Speaks the front text of each drawn card.
self._speech = speech
## @brief Store of the words still to learn.
self._progress = progress
## @brief The cards still to learn.
self._deck = deck
## @brief Picks one card from a non-empty sequence.
self._choose = choose
## @brief The card shown now, or None.
self._current: Card | None = None
@property
def current_card(self) -> CardDto | None:
"""Return the card shown now.
@return The card, or None before the first draw or when none is left.
"""
return self._to_dto(self._current)
@property
def remaining(self) -> int:
"""Return the number of cards still to learn, of every level.
@return The card count.
"""
return len(self._deck)
@property
def remaining_in_level(self) -> int:
"""Return the number of cards still to learn in the chosen level.
@return The card count.
"""
return len(self._candidates())
def next_card(self) -> CardDto | None:
"""Draw the next card, different from the current one when possible.
@return The new card, or None when no card of the level is left.
"""
candidates = self._candidates()
if not candidates:
self._current = None
return None
card = self._deck.draw(self._choose, self._current, candidates)
self._current = card
self._speech.speak(card.front, self._deck.language_pair.front_code)
return self._to_dto(card)
def mark_known(self) -> None:
"""Remove the current card from the whole deck and save the whole deck.
Does nothing and saves nothing when no card is shown.
@return Nothing.
"""
if self._current is None:
return
self._deck.remove(self._current)
self._progress.save(self._deck)
self._current = None
def _candidates(self) -> tuple[Card, ...]:
"""Return the cards that may be drawn.
@return The deck cards of the level, or all deck cards without a level.
"""
if self._level_cards is None:
return self._deck.cards
return self._deck.within(self._level_cards)
def _to_dto(self, card: Card | None) -> CardDto | None:
"""Map a card to its DTO.
@param card The card, or None.
@return The DTO, or None when there is no card.
"""
if card is None:
return None
pair = self._deck.language_pair
return CardDto(card.front, card.back, pair.front_code, pair.back_code)
+27 -8
View File
@@ -20,6 +20,8 @@ WINDOW_PADDING: Final[int] = 50
CARD_WIDTH: Final[int] = 800 CARD_WIDTH: Final[int] = 800
## @brief Card height in pixels. ## @brief Card height in pixels.
CARD_HEIGHT: Final[int] = 526 CARD_HEIGHT: Final[int] = 526
## @brief Level screen height in pixels: card plus the card window button row.
LEVEL_SCREEN_HEIGHT: Final[int] = 526 + 100
## @brief Horizontal centre of the card in pixels. ## @brief Horizontal centre of the card in pixels.
CARD_CENTER_X: Final[int] = 400 CARD_CENTER_X: Final[int] = 400
## @brief Vertical position of the language text in pixels. ## @brief Vertical position of the language text in pixels.
@@ -42,21 +44,38 @@ BACK_TEXT_COLOR: Final[str] = "white"
LANGUAGE_NAMES: Final[Mapping[str, str]] = {"ms": "Malay", "da": "Danish"} LANGUAGE_NAMES: Final[Mapping[str, str]] = {"ms": "Malay", "da": "Danish"}
## @brief Message shown when no word is left to learn. ## @brief Message shown when no word is left to learn.
FINISHED_MESSAGE: Final[str] = "Nothing left to learn" FINISHED_MESSAGE: Final[str] = "Nothing left to learn"
## @brief Language name shown before the first card.
PLACEHOLDER_LANGUAGE_NAME: Final[str] = "Malay" ## @brief Title of the level screen.
## @brief Word shown before the first card. LEVEL_TITLE: Final[str] = "Choose your level"
PLACEHOLDER_WORD: Final[str] = "kata" ## @brief Level choices in display order: label and word limit (None = all words).
LEVEL_CHOICES: Final[tuple[tuple[str, int | None], ...]] = (
("100 words", 100),
("1000 words", 1000),
("All words", None),
)
## @brief Values of the --level option and the word limit each stands for.
LEVEL_ARGUMENT_VALUES: Final[Mapping[str, int | None]] = {
"100": 100,
"1000": 1000,
"all": None,
}
## @brief Font of the level screen title: family, size, style.
LEVEL_TITLE_FONT: Final[tuple[str, int, str]] = ("Arial", 40, "italic")
## @brief Font of the level buttons: family, size, style.
LEVEL_BUTTON_FONT: Final[tuple[str, int, str]] = ("Arial", 24, "bold")
## @brief Width of every level button in characters.
LEVEL_BUTTON_WIDTH: Final[int] = 14
## @brief Background colour of the level buttons.
LEVEL_BUTTON_COLOR: Final[str] = "white"
## @brief Delimiter of the CSV files. ## @brief Delimiter of the CSV files.
CSV_DELIMITER: Final[str] = ";" CSV_DELIMITER: Final[str] = ";"
## @brief Encoding of the CSV files. ## @brief Encoding of the CSV files; drops a leading byte order mark.
CSV_ENCODING: Final[str] = "utf-8" CSV_ENCODING: Final[str] = "utf-8-sig"
## @brief File name of the vocabulary. ## @brief File name of the vocabulary.
VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv" VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv"
## @brief File name of the words still to learn. ## @brief File name of the words still to learn.
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv" PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
## @brief Language code used for speech.
SPEECH_LANGUAGE_CODE: Final[str] = "ms"
## @brief Root folder of the project. ## @brief Root folder of the project.
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2] PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
@@ -0,0 +1,5 @@
"""Domain entities.
@file __init__.py
@brief Holds the entities of the flash card program.
"""
+17
View File
@@ -0,0 +1,17 @@
"""A flash card.
@file card.py
@brief The Card entity: a word and its translation.
"""
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Card:
"""A word and its translation."""
## @brief The word in the front language.
front: str
## @brief The translation in the back language.
back: str
+115
View File
@@ -0,0 +1,115 @@
"""The cards still to learn.
@file deck.py
@brief The Deck entity: holds cards, draws one and removes one.
"""
from collections import Counter
from collections.abc import Callable, Iterable, Sequence
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.vocabulary import Vocabulary
from flash_card.domain.errors.vocabulary_errors import EmptyDeckError
from flash_card.domain.value_objects.language_pair import LanguagePair
class Deck:
"""The cards still to learn, of one language pair."""
def __init__(self, language_pair: LanguagePair, cards: Iterable[Card]) -> None:
"""Create a deck.
@param language_pair The languages of the cards.
@param cards The cards of the deck.
"""
## @brief The languages of the cards.
self._language_pair = language_pair
## @brief The cards still in the deck.
self._cards = list(cards)
@classmethod
def from_vocabulary(cls, vocabulary: Vocabulary) -> "Deck":
"""Create a deck holding every card of a vocabulary.
@param vocabulary The delivered word list.
@return A new deck.
"""
return cls(vocabulary.language_pair, vocabulary.cards)
@property
def language_pair(self) -> LanguagePair:
"""Return the languages of the cards.
@return The language pair.
"""
return self._language_pair
@property
def cards(self) -> tuple[Card, ...]:
"""Return a copy of the cards.
@return The cards in deck order.
"""
return tuple(self._cards)
@property
def is_empty(self) -> bool:
"""Tell whether the deck has no card.
@return True when no card is left.
"""
return not self._cards
def __len__(self) -> int:
"""Return the number of cards.
@return The card count.
"""
return len(self._cards)
def within(self, cards: Sequence[Card]) -> tuple[Card, ...]:
"""Select the cards of the deck that also appear in the given cards.
A card counts as often as it appears in both.
@param cards The cards to match against, for example a level.
@return The matching cards in deck order.
"""
allowed = Counter(cards)
selected: list[Card] = []
for card in self._cards:
if allowed[card] > 0:
allowed[card] -= 1
selected.append(card)
return tuple(selected)
def draw(
self,
choose: Callable[[Sequence[Card]], Card],
exclude: Card | None = None,
among: Sequence[Card] | None = None,
) -> Card:
"""Pick a card without removing it.
The pick avoids the excluded card when another candidate exists.
@param choose Picks one card from a non-empty sequence.
@param exclude A card to avoid, usually the one shown now.
@param among The candidates, or None for every card of the deck.
@return The picked card.
@throws EmptyDeckError When there is no candidate.
"""
candidates = self._cards if among is None else list(among)
if not candidates:
raise EmptyDeckError("cannot draw from an empty deck")
others = [card for card in candidates if card != exclude]
return choose(others or candidates)
def remove(self, card: Card) -> None:
"""Remove the first card equal to the given one.
@param card The card to remove.
@return Nothing.
@throws ValueError When the deck holds no such card.
"""
self._cards.remove(card)
@@ -0,0 +1,20 @@
"""The delivered word list.
@file vocabulary.py
@brief The Vocabulary entity: all cards of a language pair.
"""
from dataclasses import dataclass
from flash_card.domain.entities.card import Card
from flash_card.domain.value_objects.language_pair import LanguagePair
@dataclass(frozen=True, slots=True)
class Vocabulary:
"""All cards of a language pair as delivered."""
## @brief The languages of the cards.
language_pair: LanguagePair
## @brief The cards in file order.
cards: tuple[Card, ...]
+5
View File
@@ -0,0 +1,5 @@
"""Domain errors.
@file __init__.py
@brief Holds the errors the domain rules raise.
"""
@@ -0,0 +1,21 @@
"""Errors of the vocabulary and the deck.
@file vocabulary_errors.py
@brief The VocabularyError hierarchy.
"""
class VocabularyError(Exception):
"""Base of every error about the word list or the deck."""
class VocabularyFormatError(VocabularyError):
"""The word list is not in the expected format."""
class VocabularyNotFoundError(VocabularyError):
"""The word list does not exist."""
class EmptyDeckError(VocabularyError):
"""A card was drawn from a deck with no card."""
@@ -0,0 +1,5 @@
"""Domain repository ports.
@file __init__.py
@brief Holds the repository interfaces of the domain.
"""
@@ -0,0 +1,29 @@
"""Port to the words still to learn.
@file progress_repository.py
@brief The ProgressRepository interface.
"""
from typing import Protocol
from flash_card.domain.entities.deck import Deck
class ProgressRepository(Protocol):
"""Store of the words still to learn."""
def load(self) -> Deck | None:
"""Load the saved words to learn.
@return The deck, or None when no progress was saved.
@throws VocabularyError When the saved progress is unusable.
"""
...
def save(self, deck: Deck) -> None:
"""Save the words still to learn.
@param deck The cards still to learn.
@return Nothing.
"""
...
@@ -0,0 +1,21 @@
"""Port to the delivered word list.
@file vocabulary_repository.py
@brief The VocabularyRepository interface.
"""
from typing import Protocol
from flash_card.domain.entities.vocabulary import Vocabulary
class VocabularyRepository(Protocol):
"""Source of the delivered word list."""
def load(self) -> Vocabulary:
"""Load the delivered word list.
@return The vocabulary.
@throws VocabularyError When the word list cannot be loaded.
"""
...
@@ -0,0 +1,5 @@
"""Domain value objects.
@file __init__.py
@brief Holds the immutable value objects of the domain.
"""
@@ -0,0 +1,17 @@
"""The two languages of a word list.
@file language_pair.py
@brief The LanguagePair value object.
"""
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class LanguagePair:
"""The language codes of the front and the back of the cards."""
## @brief Language code of the card front.
front_code: str
## @brief Language code of the card back.
back_code: str
@@ -0,0 +1,38 @@
"""The level of a study session.
@file level.py
@brief The Level value object: how many of the first words a session uses.
"""
from collections.abc import Sequence
from dataclasses import dataclass
from flash_card.domain.entities.card import Card
@dataclass(frozen=True, slots=True)
class Level:
"""A limit on the words of the list, counted from the most frequent."""
## @brief Number of first words used, or None for all words.
limit: int | None
def __post_init__(self) -> None:
"""Check the limit.
@return Nothing.
@throws ValueError When the limit is set and not above zero.
"""
if self.limit is not None and self.limit <= 0:
raise ValueError(f"level limit must be above zero, got {self.limit}")
def first(self, cards: Sequence[Card]) -> tuple[Card, ...]:
"""Select the cards of this level.
@param cards The word list in order from the most frequent word.
@return The first limit cards, or all cards when there is no limit or
the list is shorter.
"""
if self.limit is None:
return tuple(cards)
return tuple(cards[: self.limit])
@@ -0,0 +1,5 @@
"""Persistence adapters.
@file __init__.py
@brief Holds the adapters that read and write files.
"""
@@ -0,0 +1,44 @@
"""CSV implementation of the progress port.
@file csv_progress_repository.py
@brief The CsvProgressRepository adapter.
"""
from pathlib import Path
from flash_card.domain.entities.deck import Deck
from flash_card.infrastructure.persistence.csv_word_list import (
read_word_list,
write_word_list,
)
class CsvProgressRepository:
"""Keeps the words still to learn in a CSV file."""
def __init__(self, path: Path) -> None:
"""Create the repository.
@param path The words to learn file.
"""
## @brief The words to learn file.
self._path = path
def load(self) -> Deck | None:
"""Load the saved words to learn.
@return The deck, possibly empty, or None when the file does not exist.
@throws VocabularyFormatError When the file is malformed; it is left as is.
"""
if not self._path.exists():
return None
pair, cards = read_word_list(self._path)
return Deck(pair, cards)
def save(self, deck: Deck) -> None:
"""Save the words still to learn.
@param deck The cards still to learn.
@return Nothing.
"""
write_word_list(self._path, deck.language_pair, deck.cards)
@@ -0,0 +1,35 @@
"""CSV implementation of the vocabulary port.
@file csv_vocabulary_repository.py
@brief The CsvVocabularyRepository adapter.
"""
from pathlib import Path
from flash_card.domain.entities.vocabulary import Vocabulary
from flash_card.domain.errors.vocabulary_errors import VocabularyFormatError
from flash_card.infrastructure.persistence.csv_word_list import read_word_list
class CsvVocabularyRepository:
"""Loads the delivered word list from a CSV file."""
def __init__(self, path: Path) -> None:
"""Create the repository.
@param path The word list file.
"""
## @brief The word list file.
self._path = path
def load(self) -> Vocabulary:
"""Load the word list.
@return The vocabulary.
@throws VocabularyNotFoundError When the file does not exist.
@throws VocabularyFormatError When the file is malformed or has no card.
"""
pair, cards = read_word_list(self._path)
if not cards:
raise VocabularyFormatError(f"{self._path}: word list has no card")
return Vocabulary(pair, cards)
@@ -0,0 +1,116 @@
"""Reading and writing of the word list CSV file.
@file csv_word_list.py
@brief Reads and writes a semicolon separated word list with a language code
header; the format is defined once here.
"""
import csv
from collections.abc import Iterable
from pathlib import Path
from flash_card.constants import CSV_DELIMITER, CSV_ENCODING
from flash_card.domain.entities.card import Card
from flash_card.domain.errors.vocabulary_errors import (
VocabularyFormatError,
VocabularyNotFoundError,
)
from flash_card.domain.value_objects.language_pair import LanguagePair
## @brief Number of cells in the header and in every card row.
_CELLS_PER_ROW = 2
def _format_error(path: Path, line: int, reason: str) -> VocabularyFormatError:
"""Build a format error naming the file and line.
@param path The file read.
@param line The line number of the fault.
@param reason What is wrong.
@return The error to raise.
"""
return VocabularyFormatError(f"{path}, line {line}: {reason}")
def _parse_header(path: Path, line: int, row: list[str]) -> LanguagePair:
"""Parse the header row.
@param path The file read.
@param line The line number of the row.
@param row The cells of the row.
@return The language pair.
@throws VocabularyFormatError When the header is not two codes.
"""
cells = [cell.strip() for cell in row]
if len(cells) != _CELLS_PER_ROW or not all(cells):
raise _format_error(path, line, "header must hold two language codes")
return LanguagePair(cells[0], cells[1])
def _parse_card(path: Path, line: int, row: list[str]) -> Card:
"""Parse a card row.
@param path The file read.
@param line The line number of the row.
@param row The cells of the row.
@return The card.
@throws VocabularyFormatError When the row is not two cells.
"""
if len(row) != _CELLS_PER_ROW:
raise _format_error(path, line, "row must hold exactly two cells")
return Card(row[0].strip(), row[1].strip())
def read_word_list(path: Path) -> tuple[LanguagePair, tuple[Card, ...]]:
"""Read a word list file.
@param path The CSV file: a header of two language codes, then one card a row.
@return The language pair and the cards in file order.
@throws VocabularyNotFoundError When the file does not exist.
@throws VocabularyFormatError When the file is empty or a row is malformed.
"""
pair: LanguagePair | None = None
cards: list[Card] = []
try:
with path.open(encoding=CSV_ENCODING, newline="") as file:
reader = csv.reader(file, delimiter=CSV_DELIMITER)
for row in reader:
if not any(cell.strip() for cell in row):
continue
if pair is None:
pair = _parse_header(path, reader.line_num, row)
else:
cards.append(_parse_card(path, reader.line_num, row))
except FileNotFoundError as error:
raise VocabularyNotFoundError(f"word list not found: {path}") from error
if pair is None:
raise _format_error(path, 1, "file is empty")
return pair, tuple(cards)
def write_word_list(
path: Path, language_pair: LanguagePair, cards: Iterable[Card]
) -> None:
"""Write a word list file atomically.
The rows go to a temporary file in the same folder, which then replaces the
target, so a crash never leaves a half written file.
@param path The CSV file to create or replace.
@param language_pair The languages, written as the header row.
@param cards The cards, written one row each in the given order.
@return Nothing.
@throws OSError When the file cannot be written.
"""
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
rows = [(language_pair.front_code, language_pair.back_code)]
rows.extend((card.front, card.back) for card in cards)
try:
with temporary.open("w", encoding=CSV_ENCODING, newline="") as file:
writer = csv.writer(file, delimiter=CSV_DELIMITER, lineterminator="\n")
writer.writerows(rows)
temporary.replace(path)
except BaseException:
temporary.unlink(missing_ok=True)
raise
@@ -0,0 +1,5 @@
"""Speech adapters.
@file __init__.py
@brief Holds the implementations of the speech port.
"""
@@ -0,0 +1,108 @@
"""Speech adapter over the gTTS service.
@file gtts_speech.py
@brief GttsSpeech, the SpeechPort that synthesises with gTTS and plays the file.
The packages gtts and playsound are optional and imported only when used.
"""
import importlib
import logging
import tempfile
import threading
from collections.abc import Callable
from pathlib import Path
## @brief Logger of the speech adapter.
_logger = logging.getLogger(__name__)
## @brief File name of the synthesised speech inside the temporary folder.
_SPEECH_FILE_NAME = "speech.mp3"
## @brief Writes an mp3 of a text and language to a path.
Synthesize = Callable[[str, str, Path], None]
## @brief Plays an audio file.
Play = Callable[[Path], None]
def _default_synthesize(text: str, language_code: str, path: Path) -> None:
"""Write an mp3 of the text with gTTS.
@param text The text to speak.
@param language_code Language of the text.
@param path File to write.
@return Nothing.
@throws ImportError When the gtts package is not installed.
"""
# Imported by name so the program starts without the optional extra.
module = importlib.import_module("gtts")
module.gTTS(text=text, lang=language_code).save(str(path))
def _default_play(path: Path) -> None:
"""Play an audio file and wait until it has been played.
@param path File to play.
@return Nothing.
@throws ImportError When the playsound package is not installed.
"""
# Imported by name so the program starts without the optional extra.
module = importlib.import_module("playsound")
module.playsound(str(path))
class GttsSpeech:
"""Speaks a text in a background thread; never raises."""
def __init__(
self,
synthesize: Synthesize | None = None,
play: Play | None = None,
) -> None:
"""Create the adapter.
@param synthesize Writes an mp3 of a text and language to a path, or
None for gTTS.
@param play Plays an audio file, or None for playsound.
"""
## @brief Writes the audio file.
self._synthesize = _default_synthesize if synthesize is None else synthesize
## @brief Plays the audio file.
self._play = _default_play if play is None else play
## @brief The thread of the latest speak call, or None.
self._thread: threading.Thread | None = None
def speak(self, text: str, language_code: str) -> None:
"""Start speaking and return at once.
@param text The text to speak.
@param language_code Language of the text.
@return Nothing.
"""
thread = threading.Thread(
target=self._run, args=(text, language_code), daemon=True
)
self._thread = thread
thread.start()
def wait(self) -> None:
"""Block until the latest speech has ended.
@return Nothing.
"""
if self._thread is not None:
self._thread.join()
def _run(self, text: str, language_code: str) -> None:
"""Synthesise and play in a temporary folder, then remove the file.
@param text The text to speak.
@param language_code Language of the text.
@return Nothing.
"""
try:
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / _SPEECH_FILE_NAME
self._synthesize(text, language_code, path)
self._play(path)
except Exception: # thread boundary: speech must never crash the program
_logger.warning("Speech failed for %r", text, exc_info=True)
@@ -0,0 +1,18 @@
"""Silent speech adapter.
@file null_speech.py
@brief NullSpeech, the SpeechPort used when speech is off or unavailable.
"""
class NullSpeech:
"""Speaks nothing."""
def speak(self, text: str, language_code: str) -> None:
"""Do nothing.
@param text The text, ignored.
@param language_code The language, ignored.
@return Nothing.
"""
return None
+223 -8
View File
@@ -1,22 +1,237 @@
"""Entry point of the flash card program. """Entry point and composition root of the flash card program.
@file app.py @file app.py
@brief Starts the program; the window arrives in MIL-002. @brief Creates the window, shows the level screen and then the card screen.
""" """
from collections.abc import Sequence import argparse
import importlib.util
import logging
import random
import tkinter as tk
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import NoReturn 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 (
DEFAULT_DATA_DIR,
LEVEL_ARGUMENT_VALUES,
PROGRESS_FILE_NAME,
VOCABULARY_FILE_NAME,
)
from flash_card.domain.entities.card import Card
from flash_card.domain.errors.vocabulary_errors import VocabularyError
from flash_card.domain.repositories.progress_repository import ProgressRepository
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
from flash_card.domain.value_objects.level import Level
from flash_card.infrastructure.persistence.csv_progress_repository import (
CsvProgressRepository,
)
from flash_card.infrastructure.persistence.csv_vocabulary_repository import (
CsvVocabularyRepository,
)
from flash_card.infrastructure.speech.gtts_speech import GttsSpeech
from flash_card.infrastructure.speech.null_speech import NullSpeech
from flash_card.presentation.features.flash_card.flash_card_view import FlashCardView
from flash_card.presentation.features.flash_card.flash_card_view_model import (
FlashCardViewModel,
)
from flash_card.presentation.features.level.level_view import LevelView
from flash_card.presentation.features.level.level_view_model import LevelViewModel
## @brief Logger of the entry point.
_logger = logging.getLogger(__name__)
## @brief Optional packages the spoken words need.
_SPEECH_PACKAGES = ("gtts", "playsound")
def build_speech(enabled: bool) -> SpeechPort:
"""Choose the speech adapter.
@param enabled False when the user switched speech off.
@return GttsSpeech when enabled and gtts and playsound are installed,
else NullSpeech.
"""
if enabled and all(
importlib.util.find_spec(name) is not None for name in _SPEECH_PACKAGES
):
return GttsSpeech()
return NullSpeech()
class Application:
"""The running program: one window that shows the level screen, then the card."""
def __init__(
self,
root: tk.Tk,
vocabulary_repository: VocabularyRepository,
progress_repository: ProgressRepository,
speech: SpeechPort,
choose: Callable[[Sequence[Card]], Card] = random.choice,
) -> None:
"""Create the application on a window; no screen is shown yet.
@param root The Tk root window.
@param vocabulary_repository Source of the delivered word list.
@param progress_repository Store of the words still to learn.
@param speech Speaks the front text of each drawn card.
@param choose Picks one card from a non-empty sequence.
"""
## @brief The Tk root window.
self.root = root
## @brief Source of the delivered word list.
self._vocabulary_repository = vocabulary_repository
## @brief Store of the words still to learn.
self._progress_repository = progress_repository
## @brief Speaks the front text of each drawn card.
self._speech = speech
## @brief Picks one card from a non-empty sequence.
self._choose = choose
## @brief The screen shown now, or None before the first one.
self._view: LevelView | FlashCardView | None = None
## @brief The exit code of the program.
self._exit_code = 0
@property
def view(self) -> LevelView | FlashCardView | None:
"""Return the screen shown now.
@return The level screen, the card screen, or None before the first.
"""
return self._view
@property
def exit_code(self) -> int:
"""Return the exit code the program ends with.
@return 0, or 1 when a session could not be started after a choice.
"""
return self._exit_code
def show_level_screen(self) -> None:
"""Show the level screen; a choice starts the session.
@return Nothing.
"""
self._view = LevelView(self.root, LevelViewModel(), self._choose_level)
def start(self, level: Level) -> None:
"""Open the session of a level and show the card screen on the window.
@param level The level the user chose.
@return Nothing.
@throws VocabularyError When the word list or saved progress is unusable.
"""
session = open_study_session(
self._vocabulary_repository,
self._progress_repository,
self._speech,
self._choose,
level,
)
if isinstance(self._view, LevelView):
self._view.destroy()
self._view = FlashCardView(self.root, FlashCardViewModel(session))
def _choose_level(self, level: Level) -> None:
"""Start the session after a click; close the window when it fails.
@param level The level the user chose.
@return Nothing.
"""
try:
self.start(level)
except VocabularyError as error:
_logger.error("%s", error)
self._exit_code = 1
self.root.destroy()
def create_application(
data_dir: Path | None = None,
speech_enabled: bool = True,
level: Level | None = None,
) -> 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 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
root = tk.Tk()
application = Application(
root,
CsvVocabularyRepository(folder / VOCABULARY_FILE_NAME),
CsvProgressRepository(folder / PROGRESS_FILE_NAME),
build_speech(speech_enabled),
)
if level is None:
application.show_level_screen()
return application
try:
application.start(level)
except VocabularyError:
root.destroy()
raise
return application
def _level_argument(value: str) -> Level:
"""Convert a --level value to a level.
@param value One of the keys of LEVEL_ARGUMENT_VALUES.
@return The level.
@throws argparse.ArgumentTypeError When the value is unknown.
"""
if value not in LEVEL_ARGUMENT_VALUES:
choices = ", ".join(LEVEL_ARGUMENT_VALUES)
raise argparse.ArgumentTypeError(
f"invalid level {value!r} (choose from {choices})"
)
return Level(LEVEL_ARGUMENT_VALUES[value])
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Sequence[str] | None = None) -> int:
"""Run the program and return its exit code. """Run the program and return its exit code.
The window arrives in MIL-002; for now nothing happens. @param argv Command line arguments, or None for those of the process.
@return The exit code, 0 on success and 1 when the word list is unusable.
@param argv Command line arguments, or None for none.
@return The exit code, 0 on success.
""" """
return 0 parser = argparse.ArgumentParser(description="Flash card program.")
parser.add_argument(
"--data-dir",
type=Path,
default=DEFAULT_DATA_DIR,
help="folder of the data files",
)
parser.add_argument(
"--no-speech",
action="store_true",
help="do not speak the words",
)
parser.add_argument(
"--level",
type=_level_argument,
default=None,
metavar="{" + ",".join(LEVEL_ARGUMENT_VALUES) + "}",
help="start at a level (100, 1000 or all words) and skip the level screen",
)
args = parser.parse_args(argv)
try:
application = create_application(args.data_dir, not args.no_speech, args.level)
except VocabularyError as error:
_logger.error("%s", error)
return 1
application.root.mainloop()
return application.exit_code
def run() -> NoReturn: def run() -> NoReturn:
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
"""Features of the presentation layer, one folder each."""
@@ -0,0 +1 @@
"""The flash card feature: its view and view model."""
@@ -0,0 +1,210 @@
"""View of the flash card screen.
@file flash_card_view.py
@brief Builds the window of the wireframe: card canvas and two buttons.
"""
import tkinter as tk
from flash_card.constants import (
ASSETS_DIR,
BACK_TEXT_COLOR,
BACKGROUND_COLOR,
CARD_BACK_IMAGE_NAME,
CARD_CENTER_X,
CARD_FRONT_IMAGE_NAME,
CARD_HEIGHT,
CARD_WIDTH,
FLIP_DELAY_MS,
FRONT_TEXT_COLOR,
LANGUAGE_FONT,
LANGUAGE_TEXT_Y,
RIGHT_IMAGE_NAME,
WORD_FONT,
WORD_TEXT_Y,
WRONG_IMAGE_NAME,
)
from flash_card.presentation.features.flash_card.flash_card_view_model import (
FlashCardViewModel,
Side,
)
from flash_card.presentation.shared.window import configure_window
def _load_image(root: tk.Tk, name: str) -> tk.PhotoImage:
"""Load an asset image.
The bytes are read here and handed to Tk as data, so the image does not
depend on Tcl opening the file: Tcl keeps the standard handles it saw at
start, and a process that swaps them (pytest fd capture) makes the first
Tcl file open fail with an empty error.
@param root The Tk root window that owns the image.
@param name The file name in the assets folder.
@return The image; the caller must keep a reference.
"""
return tk.PhotoImage(master=root, data=(ASSETS_DIR / name).read_bytes())
class FlashCardView:
"""The flash card window: a card and the wrong and right buttons."""
def __init__(self, root: tk.Tk, view_model: FlashCardViewModel) -> None:
"""Build the window on a root.
@param root The Tk root window.
@param view_model The data to show.
"""
## @brief The Tk root window.
self._root = root
## @brief The data source of the texts.
self._view_model = view_model
configure_window(root)
# Kept as attributes: Tkinter drops an image nothing refers to.
## @brief Card front image, kept so Tk does not drop it.
self._card_front_image = _load_image(root, CARD_FRONT_IMAGE_NAME)
## @brief Card back image, kept so Tk does not drop it.
self._card_back_image = _load_image(root, CARD_BACK_IMAGE_NAME)
## @brief Wrong button image, kept so Tk does not drop it.
self._wrong_image = _load_image(root, WRONG_IMAGE_NAME)
## @brief Right button image, kept so Tk does not drop it.
self._right_image = _load_image(root, RIGHT_IMAGE_NAME)
## @brief Canvas that draws the card.
self.canvas = tk.Canvas(
root,
width=CARD_WIDTH,
height=CARD_HEIGHT,
bg=BACKGROUND_COLOR,
highlightthickness=0,
)
## @brief Canvas item id of the card image.
self.image_id = self.canvas.create_image(
CARD_CENTER_X, CARD_HEIGHT / 2, image=self._card_front_image
)
## @brief Canvas item id of the language name text.
self.language_text_id = self.canvas.create_text(
CARD_CENTER_X,
LANGUAGE_TEXT_Y,
text=view_model.language_name,
font=LANGUAGE_FONT,
fill=FRONT_TEXT_COLOR,
)
## @brief Canvas item id of the word text.
self.word_text_id = self.canvas.create_text(
CARD_CENTER_X,
WORD_TEXT_Y,
text=view_model.text,
font=WORD_FONT,
fill=FRONT_TEXT_COLOR,
)
self.canvas.grid(row=0, column=0, columnspan=2)
## @brief Button for a word the user does not know.
self.wrong_button = tk.Button(
root,
image=self._wrong_image,
bg=BACKGROUND_COLOR,
activebackground=BACKGROUND_COLOR,
relief="flat",
highlightthickness=0,
bd=0,
command=self.next_card,
)
self.wrong_button.grid(row=1, column=0)
## @brief Button for a word the user knows.
self.right_button = tk.Button(
root,
image=self._right_image,
bg=BACKGROUND_COLOR,
activebackground=BACKGROUND_COLOR,
relief="flat",
highlightthickness=0,
bd=0,
command=self.is_known,
)
self.right_button.grid(row=1, column=1)
## @brief Id of the pending flip timer, or None when none is pending.
self.flip_timer: str | None = None
self._start_flip_timer()
@property
def card_front_image(self) -> tk.PhotoImage:
"""Return the card front image.
@return The image shown while the front side is up.
"""
return self._card_front_image
@property
def card_back_image(self) -> tk.PhotoImage:
"""Return the card back image.
@return The image shown while the back side is up.
"""
return self._card_back_image
def next_card(self) -> None:
"""Show the next card front and restart the flip timer.
@return Nothing.
"""
self._cancel_flip_timer()
self._view_model.next_card()
self._show_card()
self._start_flip_timer()
def is_known(self) -> None:
"""Mark the card as known, show the next card front, restart the timer.
@return Nothing.
"""
self._cancel_flip_timer()
self._view_model.is_known()
self._show_card()
self._start_flip_timer()
def flip_card(self) -> None:
"""Show the back of the current card.
@return Nothing.
"""
self._cancel_flip_timer()
self._view_model.flip_card()
self._show_card()
def _cancel_flip_timer(self) -> None:
"""Cancel the pending flip timer, if any.
@return Nothing.
"""
if self.flip_timer is not None:
self._root.after_cancel(self.flip_timer)
self.flip_timer = None
def _start_flip_timer(self) -> None:
"""Schedule the flip unless no card is left.
@return Nothing.
"""
if not self._view_model.is_finished:
self.flip_timer = self._root.after(FLIP_DELAY_MS, self.flip_card)
def _show_card(self) -> None:
"""Draw image and texts of the side the view model shows.
@return Nothing.
"""
if self._view_model.side is Side.BACK:
image, color = self._card_back_image, BACK_TEXT_COLOR
else:
image, color = self._card_front_image, FRONT_TEXT_COLOR
self.canvas.itemconfig(self.image_id, image=image)
self.canvas.itemconfig(
self.language_text_id, text=self._view_model.language_name, fill=color
)
self.canvas.itemconfig(
self.word_text_id, text=self._view_model.text, fill=color
)
@@ -0,0 +1,119 @@
"""View model of the flash card screen.
@file flash_card_view_model.py
@brief Exposes the texts shown on the card; free of any Tkinter import.
"""
import enum
from collections.abc import Mapping
from flash_card.application.dto.card_dto import CardDto
from flash_card.application.use_cases.study_session import StudySession
from flash_card.constants import FINISHED_MESSAGE, LANGUAGE_NAMES
class Side(enum.Enum):
"""The side of a card that is shown."""
## @brief The front: the word to learn.
FRONT = enum.auto()
## @brief The back: the translation.
BACK = enum.auto()
class FlashCardViewModel:
"""Data the flash card view shows."""
def __init__(
self,
session: StudySession,
language_names: Mapping[str, str] = LANGUAGE_NAMES,
) -> None:
"""Create the view model and draw the first card.
@param session The study session that supplies the cards.
@param language_names Display names by language code.
"""
## @brief The study session that supplies the cards.
self._session = session
## @brief Display names by language code.
self._language_names = language_names
## @brief The card shown now, or None when none is left.
self._card: CardDto | None = session.next_card()
## @brief The side of the card shown.
self._side = Side.FRONT
@property
def is_finished(self) -> bool:
"""Tell whether no card is left to show.
@return True when there is no card.
"""
return self._card is None
@property
def side(self) -> Side:
"""Return the side of the card shown.
@return FRONT or BACK; FRONT when no card is left.
"""
return self._side
@property
def language_name(self) -> str:
"""Return the language name shown on the card.
@return The display name of the language of the side shown, upper case
code when unknown, or an empty string when no card is left.
"""
if self._card is None:
return ""
if self._side is Side.BACK:
code = self._card.back_language
else:
code = self._card.front_language
return self._language_names.get(code, code.upper())
@property
def text(self) -> str:
"""Return the word shown on the card.
@return The text of the side shown, or the finished message when no
card is left.
"""
if self._card is None:
return FINISHED_MESSAGE
if self._side is Side.BACK:
return self._card.back_text
return self._card.front_text
def next_card(self) -> None:
"""Draw the next card and show its front; does nothing when finished.
@return Nothing.
"""
if self._card is None:
return
self._card = self._session.next_card()
self._side = Side.FRONT
def is_known(self) -> None:
"""Mark the card as known, save the progress and show the next card.
Does nothing when finished.
@return Nothing.
"""
if self._card is None:
return
self._session.mark_known()
self._card = self._session.next_card()
self._side = Side.FRONT
def flip_card(self) -> None:
"""Show the back of the card; does nothing when finished or flipped.
@return Nothing.
"""
if self._card is not None:
self._side = Side.BACK
@@ -0,0 +1 @@
"""The level feature: its view and view model."""
@@ -0,0 +1,97 @@
"""View of the level screen.
@file level_view.py
@brief Builds the level screen of the wireframe: a title and one button per level.
"""
import tkinter as tk
from collections.abc import Callable
from flash_card.constants import (
BACKGROUND_COLOR,
CARD_WIDTH,
LEVEL_BUTTON_COLOR,
LEVEL_BUTTON_FONT,
LEVEL_BUTTON_WIDTH,
LEVEL_SCREEN_HEIGHT,
LEVEL_TITLE_FONT,
)
from flash_card.domain.value_objects.level import Level
from flash_card.presentation.features.level.level_view_model import (
LevelChoice,
LevelViewModel,
)
class LevelView:
"""The level screen: a title and a button for each level."""
def __init__(
self,
root: tk.Tk,
view_model: LevelViewModel,
on_choose: Callable[[Level], None],
) -> None:
"""Build the screen on a root.
@param root The Tk root window.
@param view_model The data to show.
@param on_choose Called with the level the user picked.
"""
## @brief Called with the level the user picked.
self._on_choose = on_choose
## @brief Frame that holds the whole screen.
self.frame = tk.Frame(
root, bg=BACKGROUND_COLOR, width=CARD_WIDTH, height=LEVEL_SCREEN_HEIGHT
)
self.frame.grid(row=0, column=0)
self.frame.grid_propagate(False)
self.frame.grid_columnconfigure(0, weight=1)
for row in range(len(view_model.choices) + 1):
self.frame.grid_rowconfigure(row, weight=1)
## @brief The title text.
self.title_label = tk.Label(
self.frame,
text=view_model.title,
font=LEVEL_TITLE_FONT,
bg=BACKGROUND_COLOR,
)
self.title_label.grid(row=0, column=0)
## @brief One button per choice, in display order.
self.buttons = tuple(
self._build_button(row, choice)
for row, choice in enumerate(view_model.choices, start=1)
)
def choose(self, level: Level) -> None:
"""Forward the chosen level.
@param level The level the user picked.
@return Nothing.
"""
self._on_choose(level)
def destroy(self) -> None:
"""Remove the screen from the window.
@return Nothing.
"""
self.frame.destroy()
def _build_button(self, row: int, choice: LevelChoice) -> tk.Button:
"""Create the button of one choice.
@param row Grid row of the button.
@param choice The choice the button stands for.
@return The button, placed in the frame.
"""
button = tk.Button(
self.frame,
text=choice.label,
font=LEVEL_BUTTON_FONT,
width=LEVEL_BUTTON_WIDTH,
bg=LEVEL_BUTTON_COLOR,
command=lambda: self.choose(choice.level),
)
button.grid(row=row, column=0)
return button
@@ -0,0 +1,53 @@
"""View model of the level screen.
@file level_view_model.py
@brief Exposes the title and the level choices; free of any Tkinter import.
"""
from collections.abc import Sequence
from dataclasses import dataclass
from flash_card.constants import LEVEL_CHOICES, LEVEL_TITLE
from flash_card.domain.value_objects.level import Level
@dataclass(frozen=True, slots=True)
class LevelChoice:
"""One choice on the level screen."""
## @brief Text of the choice.
label: str
## @brief The level the choice stands for.
level: Level
class LevelViewModel:
"""Data the level view shows."""
def __init__(self, choices: Sequence[tuple[str, int | None]] | None = None) -> None:
"""Create the view model.
@param choices Label and word limit of each choice, or None for the
choices of the program.
"""
source = LEVEL_CHOICES if choices is None else choices
## @brief The choices in display order.
self._choices = tuple(
LevelChoice(label, Level(limit)) for label, limit in source
)
@property
def title(self) -> str:
"""Return the title of the screen.
@return The title text.
"""
return LEVEL_TITLE
@property
def choices(self) -> tuple[LevelChoice, ...]:
"""Return the choices.
@return The choices in display order.
"""
return self._choices
@@ -0,0 +1 @@
"""Code shared by the screens of the presentation layer."""

Some files were not shown because too many files have changed in this diff Show More