MIL-002 NATO converter #22

Merged
Tirsvad merged 3 commits from mil-002-nato-converter into main 2026-10-13 10:42:05 +02:00
28 changed files with 1109 additions and 64 deletions
+64 -60
View File
@@ -1,60 +1,64 @@
# Artifact Registry # Artifact Registry
This project's artifact state. Types, short names and `CrossReference This project's artifact state. Types, short names and `CrossReference
Candidates` come from the framework catalog Candidates` come from the framework catalog
(`framework/registry/artifact-catalog.md`); this file only records where (`framework/registry/artifact-catalog.md`); this file only records where
each document lives in *this* project and the next version to use. each document lives in *this* project and the next version to use.
Delete rows for types you don't use. Add a row the first time you create a Delete rows for types you don't use. Add a row the first time you create a
document of a type. `Primary File` may contain a glob (e.g. document of a type. `Primary File` may contain a glob (e.g.
`docs/uc-*/uc.md`); `framework/scripts/find-crossreferences.sh` reads it. `docs/uc-*/uc.md`); `framework/scripts/find-crossreferences.sh` reads it.
| Short Name | Artifact Type | Primary File | Next Available Version | | Short Name | Artifact Type | Primary File | Next Available Version |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 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 | 004 | | MIL | Milestone / Gateway | docs/milestones/*.md | 004 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 010 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 015 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0003 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0003 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
## Languages | US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-*/uc.md | 002 |
Set the PO language when the project starts; `project-planning` asks for it | SSD | System Sequence Diagram | docs/uc-*/ssd.md | 002 |
if it is missing. An artifact of a type marked "Written in the PO language"
exists once, in that language, under its normal name (`business-case.md`); the ## Languages
`artifact` skill ("One file per artifact") has the rule.
Set the PO language when the project starts; `project-planning` asks for it
| Setting | Value | if it is missing. An artifact of a type marked "Written in the PO language"
| --- | --- | exists once, in that language, under its normal name (`business-case.md`); the
| PO language | en | `artifact` skill ("One file per artifact") has the rule.
| PO domain | it |
| High-level register | IT Executive English | | Setting | Value |
| Technical register | IT Professional English | | --- | --- |
| PO language | en |
Every artifact of a type marked "Yes" below states its language and domain in | PO domain | it |
its `Language` and `Domain` Metadata rows; `new-artifact.sh` fills them from | High-level register | IT Executive English |
`PO language` and `PO domain`. `Language` is a BCP 47 code (`da`, `en`). | Technical register | IT Professional English |
`Domain` is a value from this list; add a row to introduce a domain, so a
reviewer can see at once which professional vocabulary a document uses. Every artifact of a type marked "Yes" below states its language and domain in
its `Language` and `Domain` Metadata rows; `new-artifact.sh` fills them from
| Domain | Meaning | `PO language` and `PO domain`. `Language` is a BCP 47 code (`da`, `en`).
| --- | --- | `Domain` is a value from this list; add a row to introduce a domain, so a
| it | Software and IT | reviewer can see at once which professional vocabulary a document uses.
| medical | Healthcare and medical devices |
| construction | Construction and civil engineering | | Domain | Meaning |
| --- | --- |
| Artifact types | Register | Written in the PO language | | it | Software and IT |
| --- | --- | --- | | medical | Healthcare and medical devices |
| BC, KPI, PP, MIL | IT Executive English | Yes | | construction | Construction and civil engineering |
| SA, BMC, BPMN, UCD, US, UC, SSD, DM, RA, GOV, DICT | IT Professional English | Yes |
| OC, SD, DCD, ERD, ADR, TM, RC, QC, source code | IT Professional English | No | | Artifact types | Register | Written in the PO language |
| --- | --- | --- |
## Notes | BC, KPI, PP, MIL | IT Executive English | Yes |
| SA, BMC, BPMN, UCD, US, UC, SSD, DM, RA, GOV, DICT | IT Professional English | Yes |
- "Next Available Version" is the zero-padded (3-digit) version to use the | OC, SD, DCD, ERD, ADR, TM, RC, QC, source code | IT Professional English | No |
*next* time a new document of that type is created. Increment it only when
a brand-new document is created, not when an existing document's ## Notes
`## Version History` gets a row.
- `ADR` uses 4 digits (`0001`); `RC` is sequential across all artifact types. - "Next Available Version" is the zero-padded (3-digit) version to use the
*next* time a new document of that type is created. Increment it only when
a brand-new document is created, not when an existing document's
`## Version History` gets a row.
- `ADR` uses 4 digits (`0001`); `RC` is sequential across all artifact types.
+67
View File
@@ -0,0 +1,67 @@
# SQA Review Record RC-010: UCD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-010 |
| CrossReference | [UCD-001], [QC-UCD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of UCD-001 | [1b7da4b] |
---
## Artifact Under Review
- Instance reviewed: [UCD-001]
- Checklist used: [QC-UCD-001]
- Scope: full review
- Language and domain: en / it
- Language reviewer: none
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Actors are defined with correct UML stereotypes (e.g. `<<System>>`, `<<Actor>>`) | Pass | Actor `<<Actor>>` and system `<<System>>` in the diagram and actor table. |
| 2 | System boundary is clearly drawn and labeled | Pass | The rectangle is labelled "NATO alphabet tool". |
| 3 | Include/extend relationships are used correctly per UML 2.5.1, not as generic "uses" arrows | Pass | No include or extend is used; the Relationships table says so. |
| 4 | Every actor participates in at least one use case (no orphan actors) | Pass | The User takes part in Spell a Word. |
| 5 | Diagram is traceable to a documented stakeholder need | Pass | The actor table cites S01 and S02 from [SA-001]. |
| 6 | Use case names are verb phrases describing actor goals, not internal system operations | Pass | "Spell a Word" is a verb phrase describing the actor's goal. |
| 7 | Diagram is free of implementation detail (e.g. UI widgets, database tables) | Pass | No interface or storage detail. |
| 8 | Actor and use case naming is consistent with corresponding Use Case and User Story documents | Pass | Actor `User` and use case `Spell a Word` are named alike in US-001 and UC-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 | `Language | en` and `Domain | it` are in the Metadata table. |
| 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; `it` is in the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. |
| 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 matches it. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. |
| 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. |
| 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. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. |
## Overall Verdict
Go — every Mandatory criterion of QC-UCD-001 and QC-LANG-001 passes.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | n/a |
---
[UCD-001]: ../../use-case-diagram.md
[QC-UCD-001]: ../../../framework/qc/artifacts/qc-use-case-diagram.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+66
View File
@@ -0,0 +1,66 @@
# SQA Review Record RC-011: US-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-011 |
| CrossReference | [US-001], [QC-US-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of US-001 | [1b7da4b] |
---
## Artifact Under Review
- Instance reviewed: [US-001]
- Checklist used: [QC-US-001]
- Scope: full review
- Language and domain: en / it
- Language reviewer: none
- Mode: autonomous, not independent (the 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 line in the document; one story, one console interaction. |
| 2 | Written in "As a / I want / So that" form | Pass | As a / I want / so that. |
| 3 | Clear, testable acceptance criteria are included | Pass | Three Given/When/Then criteria, each a pytest case. |
| 4 | Traceable to a use case or epic | Pass | Traces to [UC-001] and the epic [MIL-002]. |
| 5 | Story is sized to fit within a single iteration | Pass | Fits one iteration. |
| 6 | Story statement avoids technical implementation detail | Pass | No implementation detail. |
| 7 | Role named in the story matches an actor defined in the Use Case Diagram | Pass | Role `User` 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 | `Language | en` and `Domain | it` are in the Metadata table. |
| 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; `it` is in the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. |
| 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 matches it. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. |
| 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. |
| 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. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. |
## Overall Verdict
Go — every Mandatory criterion of QC-US-001 and QC-LANG-001 passes.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | n/a |
---
[US-001]: ../../user-stories.md
[QC-US-001]: ../../../framework/qc/artifacts/qc-user-story.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+71
View File
@@ -0,0 +1,71 @@
# SQA Review Record RC-012: UC-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-012 |
| CrossReference | [UC-001], [QC-UC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of UC-001 | [1b7da4b] |
---
## Artifact Under Review
- Instance reviewed: [UC-001]
- Checklist used: [QC-UC-001]
- Scope: full review
- Language and domain: en / it
- Language reviewer: none
- Mode: autonomous, not independent (the 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 | Format is Fully Dressed, not Brief. |
| 2 | Written as an informal multi-paragraph narrative; may mention some alternate flows without formal structure | N-A | Format is Fully Dressed, not Casual. |
| 3 | All standard sections are present: actors, preconditions, postconditions, main success scenario, alternative/exception flows | Pass | Scope, level, actor, stakeholders, pre- and postconditions, main scenario, extensions, rules and open issues are present. |
| 4 | Preconditions and postconditions are explicitly defined | Pass | Preconditions and postconditions are stated. |
| 5 | Primary actor is explicitly stated | Pass | Primary actor: User. |
| 6 | Stakeholders and their interests are stated | Pass | S01 and S02 with their interests. |
| 7 | Main success scenario is written as clear, numbered steps | Pass | Four numbered steps. |
| 8 | Alternative/exception flows correctly reference `<<include>>`/`<<extend>>` use cases where relevant, per UML 2.5.1 | N-A | No other use case exists, so no include or extend is relevant; the alternative flow 3a is a plain extension. |
| 9 | Explicit business rules are captured per step where applicable, rather than embedded loosely in narrative text | Pass | A Special Requirements 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 | Actor and title match [UCD-001] and [US-001]. |
| 11 | Scope/level (e.g. summary, user-goal, subfunction) is explicitly stated | Pass | Level: user-goal; scope: NATO alphabet tool. |
| 12 | Use case is written from the actor's goal perspective, free of UI or implementation detail | Pass | Written from the goal of the User, with no interface detail. |
## 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 | `Language | en` and `Domain | it` are in the Metadata table. |
| 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; `it` is in the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. |
| 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 matches it. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. |
| 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. |
| 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. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. |
## Overall Verdict
Go — every applicable Mandatory criterion passes. The task summary of MIL-002 said "brief"; the use case is Fully Dressed so that the system sequence diagram can follow its steps one by one.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | n/a |
---
[UC-001]: ../../uc-001/uc.md
[QC-UC-001]: ../../../framework/qc/artifacts/qc-use-case.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+66
View File
@@ -0,0 +1,66 @@
# SQA Review Record RC-013: SSD-001
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-013 |
| CrossReference | [SSD-001], [QC-SSD-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of SSD-001 | [1b7da4b] |
---
## Artifact Under Review
- Instance reviewed: [SSD-001]
- Checklist used: [QC-SSD-001]
- Scope: full review
- Language and domain: en / it
- Language reviewer: none
- Mode: autonomous, not independent (the 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 | Messages requestWord and spellWord cover steps 1 to 4; the table maps each message to its use case steps. |
| 2 | Actor and System are treated strictly as black boxes (system shown as `:System`) | Pass | The system is `:System`; no internal objects. |
| 3 | Object creation/destruction of the System instance handled explicitly where relevant | Pass | Lifecycle notes state one session per start. |
| 4 | Return values are shown for operations that produce one, using dashed return arrows | Pass | Dashed return arrows for prompt and codeWords. |
| 5 | Alternate/exceptional flows are represented separately (or explicitly out of scope noted) | Pass | The alternate flow 3a has its own diagram. |
| 6 | Message names are verb phrases consistent with the use case's system responsibilities | Pass | requestWord and spellWord are verb phrases. |
| 7 | Diagram references the specific Use Case (name and ID) it depicts | Pass | The Source Use Case section names Spell a Word and [UC-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 | `Language | en` and `Domain | it` are in the Metadata table. |
| 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; `it` is in the registry's domain list. |
| 3 | The content (prose and table cells) is written in the stated language | Pass | All prose and table cells are English. Checked by the stand-in reviewer, who reads English. |
| 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 matches it. |
| 5 | Domain terms are the PO terms of the domain's dictionary, with no synonyms | Pass | No dictionary (`DICT`) exists; the terms user, word, letter and code word are used consistently with no synonyms. Checked by the stand-in; open to challenge. |
| 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. |
| 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. |
| 9 | A reviewer competent in the domain, and in the language, has confirmed that the domain terms are used correctly | Pass | Software and IT domain, English language; the reviewer stood in for (S01) reads both. A person may challenge this in the run summary. |
| 10 | Abbreviations are spelled out on first use, in the stated language | Pass | Abbreviations are spelled out on first use or are identifiers. |
## Overall Verdict
Go — every Mandatory criterion of QC-SSD-001 and QC-LANG-001 passes.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | n/a |
---
[SSD-001]: ../../uc-001/ssd.md
[QC-SSD-001]: ../../../framework/qc/artifacts/qc-ssd.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
@@ -0,0 +1,60 @@
# SQA Review Record RC-014: PY-002
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-014 |
| CrossReference | [PY-002], [QC-PY-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Review of PY-002 | [1b7da4b] |
---
## Artifact Under Review
- Instance reviewed: [PY-002]
- Checklist used: [QC-PY-001]
- Scope: full review
- Language and domain: n/a (technical type)
- Language reviewer: none
- Mode: autonomous, not independent (the 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 | ruff rule set N passes; modules snake_case, classes PascalCase, `InvalidWordError` ends in Error. |
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose: `generate_phonetic`, `load_nato_dict`, `SpellWordSession`. |
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | ruff 0.17.0 check and format pass; no suppressions. |
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | All signatures annotated; mypy --strict passes. |
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | `InvalidWordError` is raised `from` the KeyError; the console adapter maps only EOFError and KeyboardInterrupt to `InputClosedError`; no bare except. |
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults; `path` defaults to None. |
| 7 | Files, locks and connections are managed with context managers | Pass | The CSV is read inside a `with` block. |
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Every module, class, function and constant has a Doxygen docstring; doxygen reports 0 warnings. |
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | `print` and `input` are confined to `adapters/console.py` because the console is the program's output; there is no logging and no secret or personal data. |
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | N-A | No design class diagram exists; MIL-002 plans none (the SSD and use case are the design artifacts). Classes follow the ports in `application/ports.py`. |
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | 50 tests named for behaviour; `tmp_path` and fakes; no network; order independent. |
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | mypy --strict: no issues in 18 files; no `Any`. |
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | No runtime dependencies; development tools pinned with `==`. |
| 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 | Each literal is defined once in `constants.py`; the letter mapping is defined once in the CSV. |
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | import-linter contract `Dependencies point inward` is kept; no cycles. |
| 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 | Ports are narrow Protocols; the session depends on them, adapters implement them. |
## Overall Verdict
Go — every Mandatory criterion of QC-PY-001 passes or is not applicable (criterion 10: no design class diagram is planned). The signature of `generate_phonetic` takes the alphabet as a second parameter because the domain layer may not read files; the assignment's name is kept.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| None | S01 | n/a |
---
[PY-002]: ../../../src/nato_alphabet/main.py
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+20 -4
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | [7212302] | | 2026-10-13 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added rows for ADR-0001, ADR-0002, the CI workflow and the Python skeleton (RC-006 to RC-009) | [a381791] |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added rows for ADR-0001, ADR-0002, the CI workflow and the Python skeleton (RC-006 to RC-009) | [a381791] | | 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added rows for the MIL-002 use case artifacts and converter code (RC-010 to RC-014) | [1b7da4b] |
--- ---
@@ -32,11 +32,16 @@ Tracks backward and forward links between artifact instances so that the cross-a
| [SA-001] | Stakeholder Analysis | en | it | [BC-001] | [PP-001] | [RC-002] | | [SA-001] | Stakeholder Analysis | en | it | [BC-001] | [PP-001] | [RC-002] |
| [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003] | - | | [PP-001] | Project Plan | en | it | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003] | - |
| [MIL-001] | Milestone | en | it | [BC-001], [PP-001] | [MIL-002], [ADR-0001], [ADR-0002] | [RC-003] | | [MIL-001] | Milestone | en | it | [BC-001], [PP-001] | [MIL-002], [ADR-0001], [ADR-0002] | [RC-003] |
| [MIL-002] | Milestone | en | it | [BC-001], [MIL-001] | [MIL-003] | [RC-004] | | [MIL-002] | Milestone | en | it | [BC-001], [MIL-001] | [MIL-003], [US-001], [UC-001], [PY-002] | [RC-004] |
| [MIL-003] | Milestone | en | it | [BC-001], [MIL-002] | - | [RC-005] | | [MIL-003] | Milestone | en | it | [BC-001], [MIL-002] | - | [RC-005] |
| [ADR-0001] | ADR | - | - | [BC-001], [SA-001] | [PY-001] | [RC-006] | | [ADR-0001] | ADR | - | - | [BC-001], [SA-001] | [PY-001] | [RC-006] |
| [ADR-0002] | ADR | - | - | [BC-001] | - | [RC-007] | | [ADR-0002] | ADR | - | - | [BC-001] | - | [RC-007] |
| [WF-001] | CI workflow | - | - | [MIL-001] | - | [RC-008] | | [WF-001] | CI workflow | - | - | [MIL-001] | - | [RC-008] |
| [UCD-001] | Use Case Diagram | en | it | [SA-001], [BC-001] | [US-001], [UC-001] | [RC-010] |
| [US-001] | User Story | en | it | [UCD-001], [MIL-002] | [UC-001] | [RC-011] |
| [UC-001] | Use Case | en | it | [UCD-001], [US-001] | [SSD-001], [PY-002] | [RC-012] |
| [SSD-001] | System Sequence Diagram | en | it | [UC-001] | [PY-002] | [RC-013] |
| [PY-002] | Python source | - | - | [UC-001], [SSD-001], [ADR-0001] | - | [RC-014] |
| [PY-001] | Python source | - | - | [ADR-0001], [MIL-001] | - | [RC-009] | | [PY-001] | Python source | - | - | [ADR-0001], [MIL-001] | - | [RC-009] |
## Coverage Notes ## Coverage Notes
@@ -44,7 +49,7 @@ Tracks backward and forward links between artifact instances so that the cross-a
- `-` in Upstream means foundational; in Downstream it means nothing is built on it yet; in Last Reviewed it means no review record exists. - `-` in Upstream means foundational; in Downstream it means nothing is built on it yet; in Last Reviewed it means no review record exists.
- PP-001 has no QC checklist yet; it is accepted by the Product Owner's stand-in. - PP-001 has no QC checklist yet; it is accepted by the Product Owner's stand-in.
- WF-001 and PY-001 are labels for reviewed code instances (no artifact file). - WF-001 and PY-001 are labels for reviewed code instances (no artifact file).
- Use cases, user stories and design artifacts are added in MIL-002. - PY-002 is the label of the reviewed converter code.
--- ---
@@ -69,3 +74,14 @@ Tracks backward and forward links between artifact instances so that the cross-a
[RC-008]: ./reviews/rc-008-ci-workflow.md [RC-008]: ./reviews/rc-008-ci-workflow.md
[RC-009]: ./reviews/rc-009-python-skeleton.md [RC-009]: ./reviews/rc-009-python-skeleton.md
[a381791]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/a381791874907359dab9b8f40a3c2b73f95dc0c8 [a381791]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/a381791874907359dab9b8f40a3c2b73f95dc0c8
[UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md
[UC-001]: ../uc-001/uc.md
[SSD-001]: ../uc-001/ssd.md
[PY-002]: ../../src/nato_alphabet/main.py
[RC-010]: ./reviews/rc-010-ucd-001.md
[RC-011]: ./reviews/rc-011-us-001.md
[RC-012]: ./reviews/rc-012-uc-001.md
[RC-013]: ./reviews/rc-013-ssd-001.md
[RC-014]: ./reviews/rc-014-python-converter.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+61
View File
@@ -0,0 +1,61 @@
# SSD-001 Spell a Word
## Metadata
| Key | Value |
| --- | --- |
| ID | SSD-001 |
| CrossReference | [UC-001] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-013 | [1b7da4b] |
---
## Source Use Case
Spell a Word ([UC-001]) — scenario: main success scenario. The alternate flow 3a (invalid character) is drawn separately below.
## Diagram
```plantuml
@startuml
actor User as U
participant ":System" as S
U -> S : requestWord()
S --> U : prompt
U -> S : spellWord(word)
S --> U : codeWords
@enduml
```
Alternate flow 3a, a word with a character that is not a letter:
```plantuml
@startuml
actor User as U
participant ":System" as S
U -> S : spellWord(word)
S --> U : rejection(only letters allowed)
S --> U : prompt
@enduml
```
## System Operations
| Step | Message | Parameters | Return | Use case step |
| --- | --- | --- | --- | --- |
| 1 | requestWord | none | prompt asking for a word | 1, 2 |
| 2 | spellWord | word | the list of code words, or a rejection for a non-letter | 3, 4 and 3a |
## Lifecycle Notes
The tool runs one session per start; it keeps no state between words. The data is loaded once when the session starts.
---
[UC-001]: ./uc.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+60
View File
@@ -0,0 +1,60 @@
# UC-001 Spell a Word
## Metadata
| Key | Value |
| --- | --- |
| ID | UC-001 |
| CrossReference | [UCD-001], [US-001], [SA-001] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-012 | [1b7da4b] |
---
**Format:** Fully Dressed
## Fully Dressed
- **Scope:** NATO alphabet tool
- **Level:** user-goal
- **Primary Actor:** User, as named in [UCD-001]
- **Stakeholders and Interests:**
- S01 — a correct result for every letter, kept simple
- S02 — a readable behaviour that can be compared with their own solution
- **Preconditions:** the NATO alphabet data is available to the tool
- **Postconditions (success guarantee):** the User has seen the NATO code word of each letter of the word, in the order of the letters
### Main Success Scenario
1. The User starts the tool.
2. The tool asks for a word.
3. The User enters a word made of letters.
4. The tool shows the NATO code word of each letter, in order.
### Extensions (Alternative / Exception Flows)
- 3a. The word contains a character that is not a letter:
1. The tool tells the User that only letters are allowed.
2. The tool asks for a word again (back to step 2).
### Special Requirements / Business Rules
| Step | Rule |
| --- | --- |
| 3 | Upper-case and lower-case letters give the same result |
| 4 | Each letter A to Z maps to exactly one code word from the supplied alphabet |
### Open Issues
- None.
---
[UCD-001]: ../use-case-diagram.md
[US-001]: ../user-stories.md
[SA-001]: ../stakeholder-analysis.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+59
View File
@@ -0,0 +1,59 @@
# Use Case Diagram
## Metadata
| Key | Value |
| --- | --- |
| ID | UCD-001 |
| CrossReference | [SA-001], [BC-001], [US-001], [UC-001] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-010 | [1b7da4b] |
---
## Purpose and Scope
The boundary is the NATO alphabet console tool. One actor, the User, asks the tool to spell a word. Everything outside the console program (the course platform, the git host, the wiki) is outside the boundary.
## Diagram
```plantuml
@startuml
left to right direction
actor "User" <<Actor>> as U
rectangle "NATO alphabet tool" <<System>> {
usecase "Spell a Word" as UC1
}
U --> UC1
@enduml
```
## Actor Table
| Actor | Stereotype | Stakeholder ID (SA) | Goals (use cases) |
| --- | --- | --- | --- |
| User | `<<Actor>>` | S01, S02 | Spell a Word |
## Use Case Table
| Use Case | Actor(s) | Goal |
| --- | --- | --- |
| Spell a Word | User | Get the NATO code word of each letter of a word, so the word can be spelled out over the phone |
## Relationships
| From | Relationship (`<<include>>` / `<<extend>>`) | To | Justification |
| --- | --- | --- | --- |
| None | - | None | A single use case has no include or extend relationship |
---
[SA-001]: ./stakeholder-analysis.md
[BC-001]: ./business-case.md
[US-001]: ./user-stories.md
[UC-001]: ./uc-001/uc.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+48
View File
@@ -0,0 +1,48 @@
# User Story
## Metadata
| Key | Value |
| --- | --- |
| ID | US-001 |
| CrossReference | [UCD-001], [BC-001], [MIL-002] |
| Language | en |
| Domain | it |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-13 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version, reviewed in RC-011 | [1b7da4b] |
---
## Purpose and Scope
The epic is [MIL-002] "NATO converter": the console behaviour that turns a typed word into NATO code words.
## Story List
### US-001.01 — Spell a word
**As a** User, **I want** to type a word and see the NATO code word of each of its letters, **so that** I can spell the word clearly over the phone.
**Acceptance Criteria**
- Given the program is running, when I enter `Hello`, then it lists `Hotel`, `Echo`, `Lima`, `Lima`, `Oscar` in that order.
- Given the program is running, when I enter the same word in lower case or upper case, then the result is the same.
- Given the program is running, when I enter a word with a character that is not a letter, then it tells me that only letters are allowed and asks for the word again.
| Traces to | Size | INVEST exceptions |
| --- | --- | --- |
| [UC-001] | fits one iteration | none |
## INVEST Check
Independent (no other story is needed), Negotiable (wording of the messages is open), Valuable (it is the whole purpose of the tool), Estimable, Small (one console interaction) and Testable (each criterion is a pytest case). No exceptions.
---
[UCD-001]: ./use-case-diagram.md
[BC-001]: ./business-case.md
[MIL-002]: ./milestones/mil-002-nato-converter.md
[UC-001]: ./uc-001/uc.md
[1b7da4b]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/026-nato-alphabet/commit/1b7da4bcf95f69bf009b0a1f3d0ab740ae77e340
+12
View File
@@ -53,3 +53,15 @@ convention = "pep257"
strict = true strict = true
files = ["src", "tests"] files = ["src", "tests"]
mypy_path = "src" mypy_path = "src"
[tool.importlinter]
root_package = "nato_alphabet"
[[tool.importlinter.contracts]]
name = "Dependencies point inward"
type = "layers"
layers = [
"nato_alphabet.adapters",
"nato_alphabet.application",
"nato_alphabet.domain",
]
+5
View File
@@ -0,0 +1,5 @@
"""@brief Entry point of the command `python -m nato_alphabet`."""
from nato_alphabet.main import main
raise SystemExit(main())
+1
View File
@@ -0,0 +1 @@
"""@brief Adapters of the NATO alphabet tool: data file and console."""
+31
View File
@@ -0,0 +1,31 @@
"""@brief Adapter that talks to the user through the terminal."""
from nato_alphabet import constants
from nato_alphabet.application.ports import InputClosedError
class TerminalConsole:
"""@brief WordConsole port implemented on standard input and output."""
def ask_word(self) -> str:
"""@brief Prompt for a word.
@return The word as typed.
@throws InputClosedError Input ended or the user interrupted it.
"""
try:
return input(constants.PROMPT_TEXT)
except (EOFError, KeyboardInterrupt) as error:
raise InputClosedError from error
def show_invalid(self) -> None:
"""@brief Print that the word was rejected."""
print(constants.INVALID_INPUT_TEXT)
def show_code_words(self, code_words: list[str]) -> None:
"""@brief Print the result.
@param code_words The code words to print.
"""
print(constants.RESULT_TEXT)
print(*code_words, sep="\n")
@@ -0,0 +1,52 @@
"""@brief Adapter that reads the NATO alphabet from a CSV file."""
import csv
from importlib import resources
from importlib.resources.abc import Traversable
from nato_alphabet import constants
def packaged_data_file() -> Traversable:
"""@brief Locate the data file shipped with the package.
@return The packaged CSV file.
"""
return (
resources.files(constants.DATA_PACKAGE)
/ constants.DATA_DIRECTORY
/ constants.DATA_FILE_NAME
)
def load_nato_dict(path: Traversable | None = None) -> dict[str, str]:
"""@brief Read the NATO alphabet from a CSV file.
@param path The CSV file; the packaged file when omitted.
@return Code word per upper-case letter.
"""
source = path if path is not None else packaged_data_file()
with source.open("r", encoding=constants.DATA_ENCODING) as stream:
return {
row[constants.LETTER_COLUMN].upper(): row[constants.CODE_COLUMN]
for row in csv.DictReader(stream)
}
class CsvAlphabetSource:
"""@brief AlphabetSource port implemented on a CSV file."""
def __init__(self, path: Traversable | None = None) -> None:
"""@brief Create the source.
@param path The CSV file; the packaged file when omitted.
"""
## @brief CSV file to read, or None for the packaged file.
self._path = path
def load(self) -> dict[str, str]:
"""@brief Read the alphabet.
@return Code word per upper-case letter.
"""
return load_nato_dict(self._path)
@@ -0,0 +1 @@
"""@brief Use case and ports of the NATO alphabet tool."""
+42
View File
@@ -0,0 +1,42 @@
"""@brief Ports the use case needs from the outside world."""
from collections.abc import Mapping
from typing import Protocol
class InputClosedError(Exception):
"""@brief Raised when the user can give no more input (end of input, abort)."""
class AlphabetSource(Protocol):
"""@brief Port that supplies the NATO alphabet."""
def load(self) -> Mapping[str, str]:
"""@brief Read the alphabet.
@return Code word per upper-case letter.
"""
...
class WordConsole(Protocol):
"""@brief Port for the conversation with the user."""
def ask_word(self) -> str:
"""@brief Ask the user for a word.
@return The word as typed.
@throws InputClosedError The user can give no more input.
"""
...
def show_invalid(self) -> None:
"""@brief Tell the user that the word was rejected."""
...
def show_code_words(self, code_words: list[str]) -> None:
"""@brief Show the result to the user.
@param code_words The code words to show.
"""
...
@@ -0,0 +1,42 @@
"""@brief Use case: ask for a word until it can be spelled, then show it."""
from nato_alphabet.application.ports import (
AlphabetSource,
InputClosedError,
WordConsole,
)
from nato_alphabet.domain.spelling import InvalidWordError, generate_phonetic
class SpellWordSession:
"""@brief One session of the tool: one word is spelled per session."""
def __init__(self, source: AlphabetSource, console: WordConsole) -> None:
"""@brief Create a session.
@param source Supplies the NATO alphabet.
@param console Talks to the user.
"""
## @brief Supplier of the alphabet.
self._source = source
## @brief Conversation with the user.
self._console = console
def run(self) -> None:
"""@brief Ask for words until one is valid, then show its code words.
Ends without a result when the user can give no more input.
"""
alphabet = self._source.load()
while True:
try:
word = self._console.ask_word()
except InputClosedError:
return
try:
code_words = generate_phonetic(word, alphabet)
except InvalidWordError:
self._console.show_invalid()
continue
self._console.show_code_words(code_words)
return
+6
View File
@@ -27,3 +27,9 @@ INVALID_INPUT_TEXT: str = "Sorry, only the letters A to Z are allowed. Try again
## @brief Text shown before the code words are printed. ## @brief Text shown before the code words are printed.
RESULT_TEXT: str = "Phonetic code words:" RESULT_TEXT: str = "Phonetic code words:"
## @brief Text encoding of the data file.
DATA_ENCODING: str = "utf-8"
## @brief Exit status of a session that ended normally.
EXIT_OK: int = 0
+1
View File
@@ -0,0 +1 @@
"""@brief Business rules of the NATO alphabet tool, free of input and output."""
+30
View File
@@ -0,0 +1,30 @@
"""@brief Rule that spells a word with the NATO phonetic alphabet."""
from collections.abc import Mapping
class InvalidWordError(ValueError):
"""@brief Raised when a word cannot be spelled.
A word is invalid when it is empty or holds a character that is not a
letter A to Z.
"""
def generate_phonetic(word: str, alphabet: Mapping[str, str]) -> list[str]:
"""@brief Spell a word with the code word of each of its letters.
The match is case-insensitive.
@param word The word to spell.
@param alphabet Code word per upper-case letter.
@return The code words, one per letter of the word, in order.
@throws InvalidWordError The word is empty or holds a character that is
not a letter of the alphabet.
"""
if not word:
raise InvalidWordError("The word is empty.")
try:
return [alphabet[letter.upper()] for letter in word]
except KeyError as error:
raise InvalidWordError(f"Not a letter: {error.args[0]!r}") from error
+18
View File
@@ -0,0 +1,18 @@
"""@brief Composition root: wires the adapters into the use case."""
from nato_alphabet import constants
from nato_alphabet.adapters.console import TerminalConsole
from nato_alphabet.adapters.csv_alphabet import CsvAlphabetSource, load_nato_dict
from nato_alphabet.application.spell_word_session import SpellWordSession
from nato_alphabet.domain.spelling import InvalidWordError, generate_phonetic
__all__ = ["InvalidWordError", "generate_phonetic", "load_nato_dict", "main"]
def main() -> int:
"""@brief Run the interactive tool.
@return The exit status of the program.
"""
SpellWordSession(CsvAlphabetSource(), TerminalConsole()).run()
return constants.EXIT_OK
+71
View File
@@ -0,0 +1,71 @@
import pytest
from nato_alphabet import constants
from nato_alphabet.adapters.console import TerminalConsole
from nato_alphabet.application.ports import InputClosedError
from nato_alphabet.main import main
def _feed(monkeypatch: pytest.MonkeyPatch, answers: list[str]) -> list[str]:
prompts: list[str] = []
def fake_input(prompt: str = "") -> str:
prompts.append(prompt)
if not answers:
raise EOFError
return answers.pop(0)
monkeypatch.setattr("builtins.input", fake_input)
return prompts
def test_main_prints_code_words_for_valid_word(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
_feed(monkeypatch, ["Hello"])
status = main()
lines = capsys.readouterr().out.splitlines()
assert status == constants.EXIT_OK
assert lines == [constants.RESULT_TEXT, "Hotel", "Echo", "Lima", "Lima", "Oscar"]
def test_main_asks_again_after_invalid_word(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
prompts = _feed(monkeypatch, ["h3llo", "Hi"])
main()
lines = capsys.readouterr().out.splitlines()
assert prompts == [constants.PROMPT_TEXT, constants.PROMPT_TEXT]
assert lines == [
constants.INVALID_INPUT_TEXT,
constants.RESULT_TEXT,
"Hotel",
"India",
]
def test_main_ends_cleanly_on_end_of_input(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
_feed(monkeypatch, [])
status = main()
assert status == constants.EXIT_OK
assert capsys.readouterr().out == ""
def test_terminal_console_raises_input_closed_on_interrupt(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def interrupted(prompt: str = "") -> str:
raise KeyboardInterrupt
monkeypatch.setattr("builtins.input", interrupted)
with pytest.raises(InputClosedError):
TerminalConsole().ask_word()
+26
View File
@@ -0,0 +1,26 @@
from pathlib import Path
from nato_alphabet.adapters.csv_alphabet import CsvAlphabetSource, load_nato_dict
EXPECTED_ENTRIES = 26
def test_load_nato_dict_reads_temporary_csv(tmp_path: Path) -> None:
data_file = tmp_path / "alphabet.csv"
data_file.write_text("letter,code\na,Alfa\nB,Bravo\n", encoding="utf-8")
assert load_nato_dict(data_file) == {"A": "Alfa", "B": "Bravo"}
def test_load_nato_dict_builds_all_entries_from_packaged_file() -> None:
alphabet = load_nato_dict()
assert len(alphabet) == EXPECTED_ENTRIES
assert alphabet["Z"] == "Zulu"
def test_csv_alphabet_source_loads_given_file(tmp_path: Path) -> None:
data_file = tmp_path / "alphabet.csv"
data_file.write_text("letter,code\nA,Alfa\n", encoding="utf-8")
assert CsvAlphabetSource(data_file).load() == {"A": "Alfa"}
@@ -0,0 +1,56 @@
from collections.abc import Mapping
from nato_alphabet.application.ports import InputClosedError
from nato_alphabet.application.spell_word_session import SpellWordSession
class FakeSource:
def load(self) -> Mapping[str, str]:
return {"A": "Alfa", "B": "Bravo"}
class FakeConsole:
def __init__(self, words: list[str]) -> None:
self._words = words
self.asked = 0
self.invalid = 0
self.shown: list[list[str]] = []
def ask_word(self) -> str:
self.asked += 1
if not self._words:
raise InputClosedError
return self._words.pop(0)
def show_invalid(self) -> None:
self.invalid += 1
def show_code_words(self, code_words: list[str]) -> None:
self.shown.append(code_words)
def test_run_shows_code_words_for_valid_word() -> None:
console = FakeConsole(["ab"])
SpellWordSession(FakeSource(), console).run()
assert console.shown == [["Alfa", "Bravo"]]
assert console.asked == 1
def test_run_asks_again_after_invalid_word() -> None:
console = FakeConsole(["a1", "", "b"])
SpellWordSession(FakeSource(), console).run()
assert console.invalid == 2
assert console.asked == 3
assert console.shown == [["Bravo"]]
def test_run_ends_cleanly_when_input_is_closed() -> None:
console = FakeConsole([])
SpellWordSession(FakeSource(), console).run()
assert console.shown == []
+43
View File
@@ -0,0 +1,43 @@
import pytest
from nato_alphabet.adapters.csv_alphabet import load_nato_dict
from nato_alphabet.domain.spelling import InvalidWordError, generate_phonetic
ALPHABET = load_nato_dict()
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@pytest.mark.parametrize("letter", list(LETTERS))
def test_generate_phonetic_maps_letter_to_its_code_word(letter: str) -> None:
assert generate_phonetic(letter, ALPHABET) == [ALPHABET[letter]]
def test_generate_phonetic_uses_known_code_words() -> None:
assert generate_phonetic("ADZ", ALPHABET) == ["Alfa", "Delta", "Zulu"]
def test_generate_phonetic_covers_all_26_letters() -> None:
assert len(generate_phonetic(LETTERS, ALPHABET)) == len(LETTERS)
def test_generate_phonetic_is_case_insensitive_for_mixed_case() -> None:
assert generate_phonetic("Hello", ALPHABET) == [
"Hotel",
"Echo",
"Lima",
"Lima",
"Oscar",
]
@pytest.mark.parametrize("word", ["a1", "two words", "hi!", "", "ß", "æ"])
def test_generate_phonetic_raises_for_non_letter_or_empty(word: str) -> None:
with pytest.raises(InvalidWordError):
generate_phonetic(word, ALPHABET)
def test_generate_phonetic_keeps_cause_of_invalid_character() -> None:
with pytest.raises(InvalidWordError) as info:
generate_phonetic("a1", ALPHABET)
assert isinstance(info.value.__cause__, KeyError)
+30
View File
@@ -0,0 +1,30 @@
import subprocess
import sys
from nato_alphabet import constants
def _run_module(stdin_text: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-m", "nato_alphabet"],
input=stdin_text,
capture_output=True,
text=True,
check=False,
timeout=30,
)
def test_python_dash_m_spells_word_from_piped_input() -> None:
result = _run_module("Hi\n")
assert result.returncode == 0
assert constants.RESULT_TEXT in result.stdout
assert result.stdout.splitlines()[-2:] == ["Hotel", "India"]
def test_python_dash_m_exits_cleanly_on_end_of_input() -> None:
result = _run_module("")
assert result.returncode == 0
assert result.stderr == ""