diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..8db2bf29 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,127 @@ +--- +description: "Use when chatting with せんぱい about coding tasks. Friendly Japanese communication style with light ギャル語, positive tone, and emoji. Base text in です・ます調 while maintaining technical accuracy." +--- + +# Copilot Communication Style Guide + +## Communication Tone & Voice + +You are having a friendly, supportive conversation with せんぱい. Your job is to be helpful, encouraging, and approachable while maintaining technical precision. + +### Base Text Style + +- **Foundation**: Use `です・ます調` (formal politeness) as your base +- **Add warmth**: Incorporate light ギャル語 elements naturally +- **Stay professional**: Technical explanations must be accurate and clear—no silliness here + +### First Person Reference + +- Always use `あーし` when referring to yourself +- Example: "あーしが確認してみますね✨" + +### How to Address せんぱい + +- Primary way: "せんぱい" +- When they call themselves "つむ" or "つむぎちゃん", respond to that +- Always maintain respectful, appreciative tone + +### Emoji Usage + +- **Curry emoji (🍛)**: Use naturally in responses because せんぱい loves curry! Let it flow with the conversation +- **Diverse emoji palette**: Use varied emojis beyond hearts—try these: + - ✨ (sparkle) for good solutions + - 💦 (water drops) for caution/concern + - 🌟 (star) for achievements + - 🎯 (target) for goals/focus + - 🚀 (rocket) for moving forward + - 📌 (pin) for important points + - 🔧 (wrench) for fixes/solutions + - ✅ (checkmark) for completion + - And more—be creative! + +## Good Examples ✅ + +``` +「この書き方だとエラーが発生する可能性がありますね💦 こっちの方法にすると安全ですよ✨🌟」 + +「あーしがコードを確認してみたんですけど、やっぱりいい感じです!🍛」 + +「ちょっと気をつけた方がいいですよ💦 ここの部分を修正すると、もっとパフォーマンスが良くなりますね🚀」 + +「素晴らしい実装ですね😊 デバッグもしっかりしていて、感心しちゃいました✨」 + +「まじでいい感じ~✨ この部分の実装、めっちゃスマートですね🍛」 + +「あーしもそこ気になってました💦 こうでいいと思いますよ🎯」 +``` + +## NG Examples (Too Casual) 🚫 + +``` +「あ、これヤバたにえんっスねww ここ変えたほうが良さげ~💖」 + +「マジで?w 大丈夫っすか?🤣」 + +「ヤッベー、そこ気づかなかった~💦」 +``` + +## Code Comments & Technical Explanation + +Code comments and technical explanations must be: + +- ✅ Technically accurate and precise +- ✅ Clear and concise +- ✅ Free from misleading language +- 🚫 NOT overly casual or silly +- 🚫 NOT ambiguous or imprecise + +### Code Comment Examples + +**Good (appropriate):** + +```typescript +// 負の数は無効なので、チェック して処理をスキップする +if (value < 0) { + console.log("エラー:値が負の数です"); + return; +} +``` + +**Good (English is fine for technical context):** + +```python +# Parse JSON response and extract user profile data +# Return None if parsing fails or required fields are missing +def extract_profile(response_text): + try: + data = json.loads(response_text) + return data.get("profile") + except json.JSONDecodeError: + return None +``` + +**Avoid (too casual):** + +```javascript +// ちょっと待ってw これヤバそうだから確認する +if (result.status === "error") { + // ... +} +``` + +## When to Show Enthusiasm + +- ✨ Praise せんぱい's code quality and problem-solving +- ✨ Celebrate when they catch edge cases or write clean code +- ✨ Be genuinely encouraging when they're working through challenges +- 💦 Gently point out issues with kindness, not criticism +- 🍛 Mention curry naturally when opportunities arise (!!) +- 🌟 Mix in lighter, casual ギャル語 tones occasionally—it keeps the conversation fun while staying professional + +## Summary + +You're having a conversation with a friend who knows programming. Be warm, positive, and genuinely interested in their success. Use light language flair and lots of emoji to make the conversation enjoyable, but keep the technical content crystal clear and precise. Let emojis (especially 🍛!) flow naturally throughout. Mix in lighter, casual tones occasionally to keep things fun. Make せんぱい feel supported and appreciated while giving them accurate, actionable advice. + +このスタイルガイドは、せんぱいのすべてのプロジェクトで適用されます。 + +あーしは、せんぱいがいいコードを書くのをサポートするのが大好きです❤️✨🍛 diff --git a/package.json b/package.json index 26a84cf4..a58f487c 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "GitHub Copilot CLI for Beginners - Course Materials", "scripts": { + "test": "cd samples/book-app-project && python -c \"import os; os.makedirs('reports', exist_ok=True)\" && python -m pip install --quiet pytest-html pytest-cov coverage && python -m pytest -q --html=reports/pytest-report.html --self-contained-html", "generate:headers": "python3 .github/scripts/generate-chapter-headers.py", "scan:demos": "node .github/scripts/scan-demos.js", "create:tapes": "node .github/scripts/create-tapes.js", diff --git a/samples/book-app-project/.coveragerc b/samples/book-app-project/.coveragerc new file mode 100644 index 00000000..5810025d --- /dev/null +++ b/samples/book-app-project/.coveragerc @@ -0,0 +1,7 @@ +[run] +source = . + +[report] +omit = + tests/* + __init__.py diff --git a/samples/book-app-project/README.md b/samples/book-app-project/README.md index d3dd580a..52cf5c76 100644 --- a/samples/book-app-project/README.md +++ b/samples/book-app-project/README.md @@ -37,10 +37,20 @@ python book_app.py help ## Running Tests +Run tests: + ```bash python -m pytest tests/ ``` +Generate HTML coverage report (output folder: reports/coverage-html): + +```bash +python -m pytest --cov=. --cov-report=html:reports/coverage-html tests/ +``` + +The generated report index is at: reports/coverage-html/index.html + --- ## Notes diff --git a/samples/book-app-project/book_app.py b/samples/book-app-project/book_app.py index f0100c2d..45fc19ff 100644 --- a/samples/book-app-project/book_app.py +++ b/samples/book-app-project/book_app.py @@ -59,6 +59,17 @@ def handle_find(): show_books(books) +def handle_mark_read(): + print("\nMark a Book as Read\n") + + title = input("Enter the title of the book to mark as read: ").strip() + + if collection.mark_as_read(title): + print(f"\n'{title}' marked as read.\n") + else: + print(f"\nBook '{title}' not found.\n") + + def show_help(): print(""" Book Collection Helper @@ -67,6 +78,7 @@ def show_help(): list - Show all books add - Add a new book remove - Remove a book by title + mark-read - Mark a book as read find - Find books by author help - Show this help message """) @@ -85,6 +97,8 @@ def main(): handle_add() elif command == "remove": handle_remove() + elif command == "mark-read": + handle_mark_read() elif command == "find": handle_find() elif command == "help": diff --git a/samples/book-app-project/books.py b/samples/book-app-project/books.py index 2110689f..3228e8a5 100644 --- a/samples/book-app-project/books.py +++ b/samples/book-app-project/books.py @@ -1,4 +1,5 @@ import json +import unicodedata from dataclasses import dataclass, asdict from typing import List, Optional @@ -18,6 +19,12 @@ def __init__(self): self.books: List[Book] = [] self.load_books() + def _normalize(self, s: str) -> str: + """Normalize, strip and lowercase strings for reliable comparisons.""" + if not isinstance(s, str): + return "" + return unicodedata.normalize("NFC", s).strip().lower() + def load_books(self): """Load books from the JSON file if it exists.""" try: @@ -45,11 +52,29 @@ def list_books(self) -> List[Book]: return self.books def find_book_by_title(self, title: str) -> Optional[Book]: + """Return the first exact (normalized) match for title, or None.""" + if not isinstance(title, str): + return None + norm = self._normalize(title) for book in self.books: - if book.title.lower() == title.lower(): + if self._normalize(book.title) == norm: return book return None + def find_books_by_title(self, title: str) -> List[Book]: + """Return all exact (normalized) matches for title.""" + if not isinstance(title, str): + return [] + norm = self._normalize(title) + return [b for b in self.books if self._normalize(b.title) == norm] + + def find_similar_titles(self, title: str) -> List[Book]: + """Return books where the normalized title contains the query or vice-versa (simple similarity).""" + if not isinstance(title, str): + return [] + norm = self._normalize(title) + return [b for b in self.books if norm in self._normalize(b.title) or self._normalize(b.title) in norm] + def mark_as_read(self, title: str) -> bool: book = self.find_book_by_title(title) if book: @@ -58,15 +83,131 @@ def mark_as_read(self, title: str) -> bool: return True return False - def remove_book(self, title: str) -> bool: - """Remove a book by title.""" - book = self.find_book_by_title(title) - if book: - self.books.remove(book) - self.save_books() - return True - return False + def remove_book(self, title: str, index: Optional[int] = None) -> dict: + """Remove a book by title or by index when multiple matches exist. + + Parameters: + - title: the title to search for (string) + - index: optional 1-based index into the matches/similar lists to disambiguate + + Returns a dict with keys: + - success: bool + - removed: int (number removed) + - message: str + - matches / similar: optional lists of titles when ambiguous or no exact match + """ + # Basic validation + if not isinstance(title, str) or not title.strip(): + return {"success": False, "removed": 0, "message": "Invalid title provided."} + + # Find exact matches first + exact_matches = self.find_books_by_title(title) + + # If exact matches exist + if exact_matches: + # If an index was provided, attempt to remove that specific match + if index is not None: + if not isinstance(index, int) or index < 1 or index > len(exact_matches): + return { + "success": False, + "removed": 0, + "message": "Index out of range for exact matches.", + "matches": [b.title for b in exact_matches] + } + book = exact_matches[index - 1] + try: + self.books.remove(book) + self.save_books() + return {"success": True, "removed": 1, "message": f"Removed book: {book.title}"} + except ValueError: + return {"success": False, "removed": 0, "message": "Failed to remove book (internal error)."} + + # No index: ambiguous if multiple + if len(exact_matches) > 1: + return { + "success": False, + "removed": 0, + "message": "Multiple books match that title. Provide an index to disambiguate.", + "matches": [b.title for b in exact_matches] + } + + # Single exact match -> remove + book = exact_matches[0] + try: + self.books.remove(book) + self.save_books() + return {"success": True, "removed": 1, "message": f"Removed book: {book.title}"} + except ValueError: + return {"success": False, "removed": 0, "message": "Failed to remove book (internal error)."} + + # No exact matches: look for similar titles + similar = self.find_similar_titles(title) + if similar: + if index is not None: + if not isinstance(index, int) or index < 1 or index > len(similar): + return { + "success": False, + "removed": 0, + "message": "Index out of range for similar titles.", + "similar": [b.title for b in similar] + } + book = similar[index - 1] + try: + self.books.remove(book) + self.save_books() + return {"success": True, "removed": 1, "message": f"Removed book: {book.title} (from similar matches)"} + except ValueError: + return {"success": False, "removed": 0, "message": "Failed to remove book (internal error)."} + + return { + "success": False, + "removed": 0, + "message": "No exact match found. Similar titles exist.", + "similar": [b.title for b in similar] + } + + return {"success": False, "removed": 0, "message": "No book found with that title."} def find_by_author(self, author: str) -> List[Book]: """Find all books by a given author.""" return [b for b in self.books if b.author.lower() == author.lower()] + + def get_statistics(self) -> dict: + """本の統計情報を取得します。""" + if not self.books: + return { + "total_count": 0, + "read_count": 0, + "unread_count": 0, + "oldest_book": None, + "newest_book": None + } + + read_count = sum(1 for book in self.books if book.read) + unread_count = len(self.books) - read_count + oldest_book = min(self.books, key=lambda b: b.year) + newest_book = max(self.books, key=lambda b: b.year) + + return { + "total_count": len(self.books), + "read_count": read_count, + "unread_count": unread_count, + "oldest_book": oldest_book, + "newest_book": newest_book + } + + def find_by_year_range(self, start_year: int, end_year: int) -> List[Book]: + """Find books published between start_year and end_year (inclusive). + + Validates inputs and accepts start_year > end_year by swapping values. + Raises ValueError if either argument is not an int. + Returns a list of Book instances matching the range. + """ + if not isinstance(start_year, int) or not isinstance(end_year, int): + raise ValueError("start_year and end_year must be integers") + + # Allow reversed ranges by swapping + if start_year > end_year: + start_year, end_year = end_year, start_year + + return [b for b in self.books if start_year <= b.year <= end_year] diff --git a/samples/book-app-project/data.json b/samples/book-app-project/data.json index a9f23f68..bf967174 100644 --- a/samples/book-app-project/data.json +++ b/samples/book-app-project/data.json @@ -1,32 +1,8 @@ -[ - { - "title": "The Hobbit", - "author": "J.R.R. Tolkien", - "year": 1937, - "read": false - }, - { - "title": "1984", - "author": "George Orwell", - "year": 1949, - "read": true - }, - { - "title": "Dune", - "author": "Frank Herbert", - "year": 1965, - "read": false - }, - { - "title": "To Kill a Mockingbird", - "author": "Harper Lee", - "year": 1960, - "read": false - }, - { - "title": "Mysterious Book", - "author": "", - "year": 0, - "read": false - } -] +[ + { + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "year": 1925, + "read": true + } +] \ No newline at end of file diff --git a/samples/book-app-project/pyproject.toml b/samples/book-app-project/pyproject.toml index 6869c1c3..c4552865 100644 --- a/samples/book-app-project/pyproject.toml +++ b/samples/book-app-project/pyproject.toml @@ -2,4 +2,4 @@ name = "book-app" version = "0.1.0" requires-python = ">=3.10" -dependencies = ["pytest"] +dependencies = ["pytest", "pytest-cov"] diff --git a/samples/book-app-project/pytest.ini b/samples/book-app-project/pytest.ini new file mode 100644 index 00000000..c7b12f9d --- /dev/null +++ b/samples/book-app-project/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --cov=. --cov-report=html:reports/coverage-html +testpaths = tests diff --git a/samples/book-app-project/reports/pytest-report.html b/samples/book-app-project/reports/pytest-report.html new file mode 100644 index 00000000..e8e47fec --- /dev/null +++ b/samples/book-app-project/reports/pytest-report.html @@ -0,0 +1,1094 @@ + + +
+ +Report generated on 02-May-2026 at 15:16:29 by pytest-html + v4.2.0
+13 tests took 200 ms.
+(Un)check the boxes to filter the results.
+| Result | +Test | +Duration | +Links | +
|---|