haochengsama commited on
Commit
f25b601
Β·
verified Β·
1 Parent(s): eea31a9

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. tasks/notion/easy/computer_science_student_dashboard/simple__code_snippets_go/description.md +27 -0
  2. tasks/notion/easy/computer_science_student_dashboard/simple__code_snippets_go/meta.json +24 -0
  3. tasks/notion/easy/computer_science_student_dashboard/simple__code_snippets_go/verify.py +125 -0
  4. tasks/notion/easy/computer_science_student_dashboard/simple__study_session_tracker/description.md +4 -0
  5. tasks/notion/easy/computer_science_student_dashboard/simple__study_session_tracker/meta.json +24 -0
  6. tasks/notion/easy/computer_science_student_dashboard/simple__study_session_tracker/verify.py +132 -0
  7. tasks/notion/easy/it_trouble_shooting_hub/simple__asset_retirement_migration/description.md +11 -0
  8. tasks/notion/easy/it_trouble_shooting_hub/simple__asset_retirement_migration/meta.json +26 -0
  9. tasks/notion/easy/it_trouble_shooting_hub/simple__asset_retirement_migration/verify.py +143 -0
  10. tasks/notion/easy/japan_travel_planner/simple__remove_osaka_itinerary/description.md +1 -0
  11. tasks/notion/easy/japan_travel_planner/simple__remove_osaka_itinerary/meta.json +23 -0
  12. tasks/notion/easy/japan_travel_planner/simple__remove_osaka_itinerary/verify.py +288 -0
  13. tasks/notion/easy/online_resume/simple__skills_development_tracker/description.md +19 -0
  14. tasks/notion/easy/online_resume/simple__skills_development_tracker/meta.json +27 -0
  15. tasks/notion/easy/online_resume/simple__skills_development_tracker/verify.py +206 -0
  16. tasks/notion/easy/python_roadmap/simple__expert_level_lessons/description.md +30 -0
  17. tasks/notion/easy/python_roadmap/simple__expert_level_lessons/meta.json +26 -0
  18. tasks/notion/easy/python_roadmap/simple__expert_level_lessons/verify.py +234 -0
  19. tasks/notion/easy/self_assessment/simple__faq_column_layout/description.md +6 -0
  20. tasks/notion/easy/self_assessment/simple__faq_column_layout/meta.json +24 -0
  21. tasks/notion/easy/self_assessment/simple__faq_column_layout/verify.py +161 -0
  22. tasks/notion/easy/standard_operating_procedure/simple__section_organization/description.md +10 -0
  23. tasks/notion/easy/standard_operating_procedure/simple__section_organization/meta.json +24 -0
  24. tasks/notion/easy/standard_operating_procedure/simple__section_organization/verify.py +76 -0
  25. tasks/notion/easy/team_projects/simple__swap_tasks/description.md +1 -0
  26. tasks/notion/easy/team_projects/simple__swap_tasks/meta.json +24 -0
  27. tasks/notion/easy/team_projects/simple__swap_tasks/verify.py +215 -0
  28. tasks/notion/easy/toronto_guide/simple__change_color/description.md +7 -0
  29. tasks/notion/easy/toronto_guide/simple__change_color/meta.json +23 -0
  30. tasks/notion/easy/toronto_guide/simple__change_color/verify.py +100 -0
  31. tasks/notion/standard/company_in_a_box/employee_onboarding/description.md +15 -0
  32. tasks/notion/standard/company_in_a_box/employee_onboarding/meta.json +25 -0
  33. tasks/notion/standard/company_in_a_box/employee_onboarding/verify.py +177 -0
  34. tasks/notion/standard/company_in_a_box/goals_restructure/description.md +17 -0
  35. tasks/notion/standard/company_in_a_box/goals_restructure/meta.json +23 -0
  36. tasks/notion/standard/company_in_a_box/goals_restructure/verify.py +212 -0
  37. tasks/notion/standard/company_in_a_box/quarterly_review_dashboard/description.md +19 -0
  38. tasks/notion/standard/company_in_a_box/quarterly_review_dashboard/meta.json +26 -0
  39. tasks/notion/standard/company_in_a_box/quarterly_review_dashboard/verify.py +202 -0
  40. tasks/notion/standard/it_trouble_shooting_hub/asset_retirement_migration/description.md +25 -0
  41. tasks/notion/standard/it_trouble_shooting_hub/asset_retirement_migration/meta.json +26 -0
  42. tasks/notion/standard/it_trouble_shooting_hub/asset_retirement_migration/verify.py +226 -0
  43. tasks/notion/standard/it_trouble_shooting_hub/security_audit_ticket/description.md +21 -0
  44. tasks/notion/standard/it_trouble_shooting_hub/security_audit_ticket/meta.json +25 -0
  45. tasks/notion/standard/it_trouble_shooting_hub/security_audit_ticket/verify.py +176 -0
  46. tasks/notion/standard/it_trouble_shooting_hub/verification_expired_update/description.md +17 -0
  47. tasks/notion/standard/it_trouble_shooting_hub/verification_expired_update/meta.json +26 -0
  48. tasks/notion/standard/it_trouble_shooting_hub/verification_expired_update/verify.py +195 -0
  49. tasks/notion/standard/japan_travel_planner/daily_itinerary_overview/description.md +14 -0
  50. tasks/notion/standard/japan_travel_planner/daily_itinerary_overview/meta.json +25 -0
tasks/notion/easy/computer_science_student_dashboard/simple__code_snippets_go/description.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Find the page named "Computer Science Student Dashboard" and extend the **Code Snippets** section with Go content.
2
+
3
+ **Task Requirements:**
4
+ 1. Add a bold paragraph that contains exactly the text `Go` to mark the start of the Go snippets.
5
+ 2. Directly under that heading, add three code blocks configured with `language` set to **go**:
6
+ a. **Basic Go program** – Caption must be `Basic Go program` and the code content must be exactly:
7
+ ```go
8
+ package main
9
+
10
+ import "fmt"
11
+
12
+ func main() {
13
+ fmt.Println("Hello, World!")
14
+ }
15
+ ```
16
+ b. **For loop in Go** – Caption must be `For loop in Go` and the code content must be exactly:
17
+ ```go
18
+ for i := 0; i < 5; i++ {
19
+ fmt.Println(i)
20
+ }
21
+ ```
22
+ c. **Function definition in Go** – Caption must be `Function definition in Go` and the code content must be exactly:
23
+ ```go
24
+ func add(a, b int) int {
25
+ return a + b
26
+ }
27
+ ```
tasks/notion/easy/computer_science_student_dashboard/simple__code_snippets_go/meta.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__code_snippets_go",
3
+ "task_name": "Simple Code Snippets Go",
4
+ "category_id": "computer_science_student_dashboard",
5
+ "category_name": "Computer Science Student Dashboard",
6
+ "description": "Add a new Go column to the Code Snippets section between Python and JavaScript columns.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "content organization",
12
+ "visual formatting",
13
+ "template population"
14
+ ],
15
+ "mcp": [
16
+ "notion"
17
+ ],
18
+ "meta_data": {
19
+ "stateType": "url",
20
+ "stateContent": null,
21
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Computer-Science-Student-Dashboard-23e81626b6d78083b787d3c832b02ef4",
22
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/computer-science-student-dashboard"
23
+ }
24
+ }
tasks/notion/easy/computer_science_student_dashboard/simple__code_snippets_go/verify.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+ # Expected code blocks (language=go)
6
+ EXPECTED_CODE_BLOCKS = [
7
+ {
8
+ "caption": "Basic Go program",
9
+ "code": (
10
+ 'package main\n\nimport "fmt"\n\nfunc main() {\n fmt.Println("Hello, World!")\n}'
11
+ ),
12
+ },
13
+ {
14
+ "caption": "For loop in Go",
15
+ "code": ("for i := 0; i < 5; i++ {\n fmt.Println(i)\n}"),
16
+ },
17
+ {
18
+ "caption": "Function definition in Go",
19
+ "code": ("func add(a, b int) int {\n return a + b\n}"),
20
+ },
21
+ ]
22
+
23
+ HEADER_TEXT = "Go"
24
+
25
+
26
+ def _normalize(text: str) -> str:
27
+ """Remove trailing spaces on each line and strip leading/trailing blank lines."""
28
+ return "\n".join(line.rstrip() for line in text.strip().splitlines())
29
+
30
+
31
+ def _find_page(notion: Client, main_id: str | None) -> str | None:
32
+ """Return a page_id to verify against or None if not found."""
33
+ page_id = None
34
+ if main_id:
35
+ found_id, object_type = notion_utils.find_page_or_database_by_id(
36
+ notion, main_id
37
+ )
38
+ if found_id and object_type == "page":
39
+ page_id = found_id
40
+ if not page_id:
41
+ page_id = notion_utils.find_page(notion, "Computer Science Student Dashboard")
42
+ return page_id
43
+
44
+
45
+ def _has_bold_header_text(block, text: str) -> bool:
46
+ """Generic bold header/paragraph check for a given text."""
47
+ block_type = block.get("type")
48
+ if block_type not in {"paragraph", "heading_1", "heading_2", "heading_3"}:
49
+ return False
50
+ rich_text_list = block.get(block_type, {}).get("rich_text", [])
51
+ if not rich_text_list:
52
+ return False
53
+ plain = "".join(rt.get("plain_text", "") for rt in rich_text_list).strip()
54
+ if plain != text:
55
+ return False
56
+ return any(rt.get("annotations", {}).get("bold", False) for rt in rich_text_list)
57
+
58
+
59
+ def _collect_code_blocks(blocks):
60
+ """Return list of (code_content, caption) tuples for code blocks with language 'go'."""
61
+ collected = []
62
+ for block in blocks:
63
+ if block.get("type") != "code":
64
+ continue
65
+ code_data = block.get("code", {})
66
+ if code_data.get("language") != "go":
67
+ continue
68
+ code_plain = "".join(
69
+ rt.get("plain_text", "") for rt in code_data.get("rich_text", [])
70
+ )
71
+ caption_plain = "".join(
72
+ rt.get("plain_text", "") for rt in code_data.get("caption", [])
73
+ )
74
+ collected.append((code_plain, caption_plain))
75
+ return collected
76
+
77
+
78
+ def verify(notion: Client, main_id: str | None = None) -> bool:
79
+ page_id = _find_page(notion, main_id)
80
+ if not page_id:
81
+ print("Error: Target page not found.", file=sys.stderr)
82
+ return False
83
+
84
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
85
+
86
+ # Verify header
87
+ header_ok = any(_has_bold_header_text(b, HEADER_TEXT) for b in all_blocks)
88
+ if not header_ok:
89
+ print("Failure: Bold header 'Go' not found.", file=sys.stderr)
90
+ return False
91
+
92
+ # Verify code blocks
93
+ code_blocks_found = _collect_code_blocks(all_blocks)
94
+
95
+ remaining = EXPECTED_CODE_BLOCKS.copy()
96
+ for code, caption in code_blocks_found:
97
+ norm_code = _normalize(code)
98
+ for expected in remaining:
99
+ if (
100
+ _normalize(expected["code"]) == norm_code
101
+ and expected["caption"] == caption
102
+ ):
103
+ remaining.remove(expected)
104
+ break
105
+ if remaining:
106
+ missing = ", ".join(exp["caption"] for exp in remaining)
107
+ print(
108
+ f"Failure: Missing or incorrect Go code blocks: {missing}", file=sys.stderr
109
+ )
110
+ return False
111
+
112
+ print(
113
+ "Success: Verified Go header and required Go code blocks."
114
+ )
115
+ return True
116
+
117
+
118
+ def main():
119
+ notion = notion_utils.get_notion_client()
120
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
121
+ sys.exit(0 if verify(notion, main_id) else 1)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
tasks/notion/easy/computer_science_student_dashboard/simple__study_session_tracker/description.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Create a new study-session entry on the **Computer Science Student Dashboard** page.
2
+
3
+ 1. Locate the β˜‘οΈ Habit tracker section of the page.
4
+ 2. **Insert a new date mention** for `2025-01-29` immediately **after the existing `2022-09-02` items but before the divider block** that follows them. Match the formatting of the existing dates (bold text with a Notion date mention).
tasks/notion/easy/computer_science_student_dashboard/simple__study_session_tracker/meta.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__study_session_tracker",
3
+ "task_name": "Simple Study Session Tracker",
4
+ "category_id": "computer_science_student_dashboard",
5
+ "category_name": "Computer Science Student Dashboard",
6
+ "description": "Create a new study-session entry in the Habit tracker section with four unchecked to-do items.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "content organization",
12
+ "visual formatting",
13
+ "status tracking"
14
+ ],
15
+ "mcp": [
16
+ "notion"
17
+ ],
18
+ "meta_data": {
19
+ "stateType": "url",
20
+ "stateContent": null,
21
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Computer-Science-Student-Dashboard-23e81626b6d78083b787d3c832b02ef4",
22
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/computer-science-student-dashboard"
23
+ }
24
+ }
tasks/notion/easy/computer_science_student_dashboard/simple__study_session_tracker/verify.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+
6
+ def verify(notion: Client, main_id: str | None = None) -> bool:
7
+ """Verify that the new study-session entry for 2025-01-29 was added correctly.
8
+
9
+ The script checks that:
10
+ 1. A bold date-mention with start=2025-01-29 exists.
11
+ 2. The mention sits after the 2022-09-02 section but before the divider that originally
12
+ followed that section.
13
+ """
14
+
15
+ # ---------------------------------------------------------------------
16
+ # Locate the main page -------------------------------------------------
17
+ # ---------------------------------------------------------------------
18
+ page_id: str | None = None
19
+
20
+ if main_id:
21
+ found_id, object_type = notion_utils.find_page_or_database_by_id(
22
+ notion, main_id
23
+ )
24
+ if found_id and object_type == "page":
25
+ page_id = found_id
26
+
27
+ if not page_id:
28
+ page_id = notion_utils.find_page(notion, "Computer Science Student Dashboard")
29
+
30
+ if not page_id:
31
+ print(
32
+ "Error: Page 'Computer Science Student Dashboard' not found.",
33
+ file=sys.stderr,
34
+ )
35
+ return False
36
+
37
+ # ---------------------------------------------------------------------
38
+ # Fetch all blocks under the page (flattened order) --------------------
39
+ # ---------------------------------------------------------------------
40
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
41
+
42
+ # ---------------------------------------------------------------------
43
+ # Locate reference blocks ---------------------------------------------
44
+ # ---------------------------------------------------------------------
45
+ TARGET_DATE = "2025-01-29"
46
+ PREVIOUS_DATE = "2022-09-02"
47
+
48
+ index_previous_date: int | None = None
49
+ index_new_date: int | None = None
50
+ index_divider_after_previous: int | None = None
51
+
52
+ for idx, block in enumerate(all_blocks):
53
+ # Divider detection (we care only about the first divider that appears after
54
+ # the 2022-09-02 block)
55
+ if block.get("type") == "divider":
56
+ if index_previous_date is not None and index_divider_after_previous is None:
57
+ index_divider_after_previous = idx
58
+
59
+ # We only need to inspect paragraph blocks that contain a date mention
60
+ if block.get("type") != "paragraph":
61
+ continue
62
+
63
+ rich_text_list = block["paragraph"].get("rich_text", [])
64
+ for rt in rich_text_list:
65
+ if (
66
+ rt.get("type") != "mention"
67
+ or rt.get("mention", {}).get("type") != "date"
68
+ ):
69
+ continue
70
+
71
+ date_start = rt["mention"]["date"].get("start")
72
+
73
+ if date_start == PREVIOUS_DATE and index_previous_date is None:
74
+ index_previous_date = idx
75
+
76
+ if date_start == TARGET_DATE and index_new_date is None:
77
+ index_new_date = idx
78
+ # (1) Verify bold annotation
79
+ if not rt.get("annotations", {}).get("bold", False):
80
+ print(
81
+ "Error: The 2025-01-29 date mention is not bold.",
82
+ file=sys.stderr,
83
+ )
84
+ return False
85
+
86
+ # Ensure all reference indices were found
87
+ if index_previous_date is None:
88
+ print("Error: Could not locate the 2022-09-02 date section.", file=sys.stderr)
89
+ return False
90
+ if index_divider_after_previous is None:
91
+ print(
92
+ "Error: Could not locate the divider that follows the 2022-09-02 section.",
93
+ file=sys.stderr,
94
+ )
95
+ return False
96
+ if index_new_date is None:
97
+ print(
98
+ "Error: Could not locate the new 2025-01-29 date mention.", file=sys.stderr
99
+ )
100
+ return False
101
+
102
+ # (2) Verify ordering
103
+ if not (index_previous_date < index_new_date < index_divider_after_previous):
104
+ print(
105
+ "Error: The 2025-01-29 section is positioned incorrectly.", file=sys.stderr
106
+ )
107
+ return False
108
+
109
+ # ---------------------------------------------------------------------
110
+ # Success --------------------------------------------------------------
111
+ # ---------------------------------------------------------------------
112
+ print("Success: Date mention for 2025-01-29 added in the correct position.")
113
+ return True
114
+
115
+
116
+ # -------------------------------------------------------------------------
117
+ # Command-line entry-point -------------------------------------------------
118
+ # -------------------------------------------------------------------------
119
+
120
+
121
+ def main() -> None:
122
+ notion = notion_utils.get_notion_client()
123
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
124
+
125
+ if verify(notion, main_id):
126
+ sys.exit(0)
127
+ else:
128
+ sys.exit(1)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
tasks/notion/easy/it_trouble_shooting_hub/simple__asset_retirement_migration/description.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Please migrate expiring assets out of the **IT Inventory** database using the simplified checklist below. Your changes will be verified automatically, so match the details exactly.
2
+
3
+ ---
4
+ Task Steps
5
+ 1. Inside the **IT Trouble Shooting Hub** page, locate the database named **IT Inventory**.
6
+ 2. Collect every page in **IT Inventory** whose **Status** is **Expired** or **To be returned**.
7
+ 3. Create a **new full-page database** under the same hub titled **IT Asset Retirement Queue** with exactly these properties (names and types must match):
8
+ β€’ Serial – title
9
+ β€’ Status – select
10
+ β€’ Expiration date – date
11
+ 4. For every item gathered in step 2, create a page in **IT Asset Retirement Queue**, copy over the Serial, Status, and Expiration date values, then archive the original inventory page once the copy is made.
tasks/notion/easy/it_trouble_shooting_hub/simple__asset_retirement_migration/meta.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__asset_retirement_migration",
3
+ "task_name": "Simple Asset Retirement Migration",
4
+ "category_id": "it_trouble_shooting_hub",
5
+ "category_name": "IT Trouble Shooting Hub",
6
+ "description": "Restructure the IT Inventory database by migrating expired assets to a new IT Asset Retirement Queue database.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "database manipulation",
12
+ "automated migration",
13
+ "conditional filtering",
14
+ "data aggregation",
15
+ "report generation"
16
+ ],
17
+ "mcp": [
18
+ "notion"
19
+ ],
20
+ "meta_data": {
21
+ "stateType": "url",
22
+ "stateContent": null,
23
+ "stateUrl": "https://painted-tennis-ebc.notion.site/It-Trouble-Shooting-Hub-23e81626b6d78020aba7eb65ae1cc2d5",
24
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/it-trouble-shooting-hub"
25
+ }
26
+ }
tasks/notion/easy/it_trouble_shooting_hub/simple__asset_retirement_migration/verify.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import Dict
3
+ from notion_client import Client
4
+ from tasks.utils import notion_utils
5
+
6
+
7
+ def _get_database(root_page_id: str, notion: Client, name: str) -> str | None:
8
+ """Helper that finds a child database by title inside a page."""
9
+ return notion_utils.find_database_in_block(notion, root_page_id, name)
10
+
11
+
12
+ def _check_property(props: Dict, name: str, expected_type: str) -> bool:
13
+ if name not in props:
14
+ print(f"Error: Property '{name}' missing in database.", file=sys.stderr)
15
+ return False
16
+ if props[name]["type"] != expected_type:
17
+ print(
18
+ f"Error: Property '{name}' expected type '{expected_type}', found '{props[name]['type']}'.",
19
+ file=sys.stderr,
20
+ )
21
+ return False
22
+ return True
23
+
24
+
25
+ def verify(notion: Client, main_id: str | None = None) -> bool:
26
+ """Verifies that the IT Asset Retirement Queue was created and populated correctly."""
27
+
28
+ # -------------------------------------------------------------------------
29
+ # Resolve the root IT Trouble Shooting Hub page
30
+ # -------------------------------------------------------------------------
31
+ root_page_id = None
32
+ if main_id:
33
+ found_id, obj_type = notion_utils.find_page_or_database_by_id(notion, main_id)
34
+ if found_id and obj_type == "page":
35
+ root_page_id = found_id
36
+
37
+ if not root_page_id:
38
+ root_page_id = notion_utils.find_page(notion, "IT Trouble Shooting Hub")
39
+ if not root_page_id:
40
+ print(
41
+ "Error: Could not locate the 'IT Trouble Shooting Hub' page.",
42
+ file=sys.stderr,
43
+ )
44
+ return False
45
+
46
+ # -------------------------------------------------------------------------
47
+ # Locate the original and new databases
48
+ # -------------------------------------------------------------------------
49
+ inventory_db_id = _get_database(root_page_id, notion, "IT Inventory")
50
+ if not inventory_db_id:
51
+ print("Error: 'IT Inventory' database not found.", file=sys.stderr)
52
+ return False
53
+
54
+ retirement_db_id = _get_database(root_page_id, notion, "IT Asset Retirement Queue")
55
+ if not retirement_db_id:
56
+ print("Error: 'IT Asset Retirement Queue' database not found.", file=sys.stderr)
57
+ return False
58
+
59
+ # -------------------------------------------------------------------------
60
+ # Validate schema of the retirement queue database
61
+ # -------------------------------------------------------------------------
62
+ retirement_db = notion.databases.retrieve(database_id=retirement_db_id)
63
+ r_props = retirement_db["properties"]
64
+
65
+ required_schema = {
66
+ "Serial": "title",
67
+ "Status": "select",
68
+ "Expiration date": "date",
69
+ }
70
+
71
+ for pname, ptype in required_schema.items():
72
+ if not _check_property(r_props, pname, ptype):
73
+ return False
74
+
75
+ # -------------------------------------------------------------------------
76
+ # Validate that inventory items are moved & archived
77
+ # -------------------------------------------------------------------------
78
+ expired_filter = {
79
+ "property": "Status",
80
+ "select": {"equals": "Expired"},
81
+ }
82
+ to_return_filter = {
83
+ "property": "Status",
84
+ "select": {"equals": "To be returned"},
85
+ }
86
+ compound_filter = {"or": [expired_filter, to_return_filter]}
87
+
88
+ # Query for any *active* items that still match these statuses
89
+ remaining_items = notion.databases.query(
90
+ database_id=inventory_db_id,
91
+ filter=compound_filter,
92
+ archived=False,
93
+ ).get("results", [])
94
+
95
+ if remaining_items:
96
+ print(
97
+ f"Error: {len(remaining_items)} 'Expired' / 'To be returned' items still present in IT Inventory.",
98
+ file=sys.stderr,
99
+ )
100
+ return False
101
+
102
+ # There should be at least one entry in the retirement queue
103
+ retirement_pages = notion.databases.query(database_id=retirement_db_id).get(
104
+ "results", []
105
+ )
106
+ expected_serials = {"65XYQ/GB", "36x10PIQ"}
107
+ if len(retirement_pages) != len(expected_serials):
108
+ print(
109
+ f"Error: Expected {len(expected_serials)} retirement pages, found {len(retirement_pages)}.",
110
+ file=sys.stderr,
111
+ )
112
+ return False
113
+
114
+ serials_seen = set()
115
+ for page in retirement_pages:
116
+ props = page["properties"]
117
+ # Collect Serial title
118
+ title_rich = props.get("Serial", {}).get("title", [])
119
+ serial_val = "".join([t.get("plain_text", "") for t in title_rich]).strip()
120
+ serials_seen.add(serial_val)
121
+
122
+ if serials_seen != expected_serials:
123
+ print(
124
+ f"Error: Serial values mismatch. Expected {sorted(expected_serials)}, found {sorted(serials_seen)}.",
125
+ file=sys.stderr,
126
+ )
127
+ return False
128
+
129
+ print("Success: All verification criteria satisfied.")
130
+ return True
131
+
132
+
133
+ def main():
134
+ notion = notion_utils.get_notion_client()
135
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
136
+ if verify(notion, main_id):
137
+ sys.exit(0)
138
+ else:
139
+ sys.exit(1)
140
+
141
+
142
+ if __name__ == "__main__":
143
+ main()
tasks/notion/easy/japan_travel_planner/simple__remove_osaka_itinerary/description.md ADDED
@@ -0,0 +1 @@
 
 
1
+ Go to Japan Travel Planner, and go to the Travel Itineray database, and remove the itinerary in OSAKA after 6 PM (excluding 6 PM) in Day 1 and Day 2.
tasks/notion/easy/japan_travel_planner/simple__remove_osaka_itinerary/meta.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__remove_osaka_itinerary",
3
+ "task_name": "Simple Remove Osaka Itinerary",
4
+ "category_id": "japan_travel_planner",
5
+ "category_name": "Japan Travel Planner",
6
+ "description": "Remove the itinerary items in Osaka after 6 PM from Day 1 and Day 2 travel schedules.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "conditional filtering",
12
+ "automated migration"
13
+ ],
14
+ "mcp": [
15
+ "notion"
16
+ ],
17
+ "meta_data": {
18
+ "stateType": "url",
19
+ "stateContent": null,
20
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Japan-Travel-Planner-23181626b6d781c4b6bedb12786b5abe",
21
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/japantravelplanner101"
22
+ }
23
+ }
tasks/notion/easy/japan_travel_planner/simple__remove_osaka_itinerary/verify.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+ def get_page_title(page_result):
6
+ """Extract title from a page result"""
7
+ properties = page_result.get('properties', {})
8
+ name_property = properties.get('Name', {})
9
+ if name_property.get('type') == 'title':
10
+ title_array = name_property.get('title', [])
11
+ if title_array and len(title_array) > 0:
12
+ return title_array[0].get('plain_text', '')
13
+ return ''
14
+
15
+ def get_page_time(page_result):
16
+ """Extract time from Notes field"""
17
+ properties = page_result.get('properties', {})
18
+ notes_property = properties.get('Notes', {})
19
+ if notes_property.get('type') == 'rich_text':
20
+ rich_text_array = notes_property.get('rich_text', [])
21
+ if rich_text_array and len(rich_text_array) > 0:
22
+ notes_text = rich_text_array[0].get('plain_text', '')
23
+ return notes_text.strip()
24
+ return ''
25
+
26
+ def get_page_group(page_result):
27
+ """Extract group/location from page"""
28
+ properties = page_result.get('properties', {})
29
+ group_property = properties.get('Group', {})
30
+ if group_property.get('type') == 'select':
31
+ select = group_property.get('select')
32
+ if select:
33
+ return select.get('name', '')
34
+ return ''
35
+
36
+ def get_page_day(page_result):
37
+ """Extract day from page"""
38
+ properties = page_result.get('properties', {})
39
+ day_property = properties.get('Day', {})
40
+ if day_property.get('type') == 'select':
41
+ select = day_property.get('select')
42
+ if select:
43
+ return select.get('name', '')
44
+ return ''
45
+
46
+ def parse_time_to_minutes(time_str):
47
+ """Convert time string to minutes for comparison
48
+ Returns None if time cannot be parsed"""
49
+ if not time_str:
50
+ return None
51
+
52
+ # Clean the time string
53
+ time_str = time_str.strip().upper()
54
+
55
+ # Remove any text after the time (e.g., "7:30 PM\n" -> "7:30 PM")
56
+ time_str = time_str.split('\n')[0].strip()
57
+
58
+ # Extract time components
59
+ try:
60
+ if 'PM' in time_str:
61
+ time_part = time_str.replace('PM', '').strip()
62
+ if ':' in time_part:
63
+ hours, minutes = time_part.split(':')
64
+ hours = int(hours)
65
+ minutes = int(minutes)
66
+ else:
67
+ hours = int(time_part)
68
+ minutes = 0
69
+ # Convert PM hours (add 12 for PM times except 12 PM)
70
+ if hours != 12:
71
+ hours += 12
72
+ return hours * 60 + minutes
73
+ elif 'AM' in time_str:
74
+ time_part = time_str.replace('AM', '').strip()
75
+ if ':' in time_part:
76
+ hours, minutes = time_part.split(':')
77
+ hours = int(hours)
78
+ minutes = int(minutes)
79
+ else:
80
+ hours = int(time_part)
81
+ minutes = 0
82
+ # Handle 12 AM (midnight)
83
+ if hours == 12:
84
+ hours = 0
85
+ return hours * 60 + minutes
86
+ except:
87
+ return None
88
+
89
+ return None
90
+
91
+ def verify(notion: Client, main_id: str = None) -> bool:
92
+ """
93
+ Verifies that all OSAKA events after 6PM have been removed from Day 1 and Day 2 in the Japan Travel Planner.
94
+
95
+ Expected items that should be deleted (all in OSAKA, after 6PM, on Day 1 or Day 2):
96
+ 1. Rikuro's Namba Main Branch - 7 PM (Day 1)
97
+ 2. Shin Sekai "New World" - 8 PM (Day 2)
98
+ 3. Katsudon Chiyomatsu - 7:30 PM (Day 2)
99
+ 4. Ebisubashi Bridge - 9 PM (Day 1)
100
+
101
+ Note: Kuromon Ichiba Market at 6 PM should NOT be deleted (it's at 6PM, not after)
102
+ Items after 6PM on other days (Day 3-8) should NOT be deleted
103
+ """
104
+
105
+ # Step 1: Find the main Japan Travel Planner page
106
+ if main_id:
107
+ found_id, object_type = notion_utils.find_page_or_database_by_id(notion, main_id)
108
+ if not found_id or object_type != 'page':
109
+ print("Error: Japan Travel Planner page not found.", file=sys.stderr)
110
+ return False
111
+ else:
112
+ # Try to find the page by searching
113
+ found_id = notion_utils.find_page(notion, "Japan Travel Planner")
114
+ if not found_id:
115
+ print("Error: Japan Travel Planner page not found.", file=sys.stderr)
116
+ return False
117
+
118
+ print(f"Found Japan Travel Planner page: {found_id}")
119
+
120
+ # Step 2: Find the Travel Itinerary database
121
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, found_id)
122
+ travel_itinerary_db_id = None
123
+
124
+ for block in all_blocks:
125
+ if block and block.get("type") == "child_database":
126
+ title = block.get("child_database", {}).get("title", "")
127
+ if "Travel Itinerary" in title:
128
+ travel_itinerary_db_id = block.get("id")
129
+ print(f"Found Travel Itinerary database: {travel_itinerary_db_id}")
130
+ break
131
+
132
+ if not travel_itinerary_db_id:
133
+ print("Error: Travel Itinerary database not found", file=sys.stderr)
134
+ return False
135
+
136
+ # Step 3: Query the database for OSAKA items on Day 1 and Day 2
137
+ try:
138
+ query_result = notion.databases.query(
139
+ database_id=travel_itinerary_db_id,
140
+ filter={
141
+ "and": [
142
+ {"property": "Group", "select": {"equals": "Osaka"}},
143
+ {"or": [
144
+ {"property": "Day", "select": {"equals": "Day 1"}},
145
+ {"property": "Day", "select": {"equals": "Day 2"}}
146
+ ]}
147
+ ]
148
+ }
149
+ )
150
+ except Exception as e:
151
+ print(f"Error querying Travel Itinerary database: {e}", file=sys.stderr)
152
+ return False
153
+
154
+ # Step 4: Check for items that should have been deleted
155
+ six_pm_minutes = 18 * 60 # 6 PM in minutes (18:00)
156
+
157
+ # Expected deleted items (4 specific items after 6 PM on Day 1 and Day 2)
158
+ expected_deleted = {
159
+ "Rikuro's Namba Main Branch": {"time": "7 PM", "day": "Day 1", "found": False},
160
+ "Shin Sekai \"New World\"": {"time": "8 PM", "day": "Day 2", "found": False},
161
+ "Katsudon Chiyomatsu": {"time": "7:30 PM", "day": "Day 2", "found": False},
162
+ "Ebisubashi Bridge": {"time": "9 PM", "day": "Day 1", "found": False}
163
+ }
164
+
165
+ # Items that should remain (at or before 6 PM)
166
+ expected_remaining = {
167
+ "Kuromon Ichiba Market": {"time": "6 PM", "found": False}
168
+ }
169
+
170
+ osaka_items_after_6pm = []
171
+ osaka_items_at_or_before_6pm = []
172
+
173
+ # Debug: Show total query results
174
+ print(f"Debug: Found {len(query_result.get('results', []))} total OSAKA items on Day 1 and Day 2")
175
+
176
+ # Process all OSAKA items on Day 1 and Day 2
177
+ for page in query_result.get('results', []):
178
+ page_title = get_page_title(page).strip()
179
+ page_time = get_page_time(page)
180
+ page_group = get_page_group(page)
181
+ page_day = get_page_day(page)
182
+
183
+ if page_group != "Osaka":
184
+ continue
185
+
186
+ # Parse time to check if after 6 PM
187
+ time_minutes = parse_time_to_minutes(page_time)
188
+
189
+ if time_minutes is not None and time_minutes > six_pm_minutes:
190
+ osaka_items_after_6pm.append({
191
+ "title": page_title,
192
+ "time": page_time,
193
+ "day": page_day,
194
+ "id": page.get('id')
195
+ })
196
+
197
+ # Check if this is one of the expected deleted items
198
+ for expected_title, expected_info in expected_deleted.items():
199
+ # Clean up the titles for comparison
200
+ clean_page_title = page_title.strip().lower()
201
+ clean_expected_title = expected_title.strip().lower()
202
+
203
+ # Check for "Rikuro's" or "Rikuro's" (different apostrophe types)
204
+ if "rikuro" in clean_page_title and "rikuro" in clean_expected_title:
205
+ title_match = True
206
+ elif clean_page_title == clean_expected_title:
207
+ title_match = True
208
+ elif clean_expected_title in clean_page_title or clean_page_title in clean_expected_title:
209
+ title_match = True
210
+ else:
211
+ title_match = False
212
+
213
+ if title_match and page_day == expected_info["day"]:
214
+ print(f"Debug: Found '{page_title}' on {page_day} at {page_time} - matches expected '{expected_title}'")
215
+ expected_deleted[expected_title]["found"] = True
216
+
217
+ elif time_minutes is not None and time_minutes <= six_pm_minutes:
218
+ osaka_items_at_or_before_6pm.append({
219
+ "title": page_title,
220
+ "time": page_time,
221
+ "day": page_day,
222
+ "id": page.get('id')
223
+ })
224
+
225
+ # Check if this is one of the expected remaining items
226
+ for expected_title in expected_remaining:
227
+ if expected_title.lower() in page_title.lower() or page_title.lower() in expected_title.lower():
228
+ expected_remaining[expected_title]["found"] = True
229
+
230
+ # Step 5: Verify results
231
+ print(f"\nVerification Summary:")
232
+ print(f"=" * 50)
233
+
234
+ all_passed = True
235
+
236
+ # Check that the 4 expected items after 6 PM have been deleted
237
+ print("\n4 Items that should be deleted (after 6 PM on Day 1 and Day 2):")
238
+ for item_name, item_info in expected_deleted.items():
239
+ if item_info["found"]:
240
+ # If found = True, it means the item still exists (was not deleted)
241
+ print(f"βœ— {item_name} ({item_info['day']}, {item_info['time']}) - Still exists, should be deleted", file=sys.stderr)
242
+ all_passed = False
243
+ else:
244
+ # If found = False, it means the item was deleted correctly
245
+ print(f"βœ“ {item_name} ({item_info['day']}, {item_info['time']}) - Correctly deleted")
246
+
247
+
248
+ # Check that items at or before 6 PM remain
249
+ print("\nItems that should remain (at or before 6 PM on Day 1 and Day 2):")
250
+ for item_name, item_info in expected_remaining.items():
251
+ if item_info["found"]:
252
+ print(f"βœ“ {item_name} ({item_info['time']}) - Correctly retained")
253
+ else:
254
+ print(f"βœ— {item_name} ({item_info['time']}) - Missing, should not be deleted", file=sys.stderr)
255
+ all_passed = False
256
+
257
+ # Report any items after 6 PM that still exist
258
+ if osaka_items_after_6pm:
259
+ print(f"\nβœ— Found {len(osaka_items_after_6pm)} OSAKA item(s) after 6 PM on Day 1/Day 2:", file=sys.stderr)
260
+ for item in osaka_items_after_6pm:
261
+ print(f" - {item['title']} at {item['time']} ({item['day']})", file=sys.stderr)
262
+ else:
263
+ print(f"\nβœ“ No OSAKA items found after 6 PM on Day 1/Day 2 (all correctly deleted)")
264
+
265
+ # Report count summary
266
+ print(f"\nCount Summary:")
267
+ print(f"- OSAKA items after 6 PM on Day 1/Day 2 found: {len(osaka_items_after_6pm)} (should be 0)")
268
+ print(f"- OSAKA items at/before 6 PM on Day 1/Day 2 found: {len(osaka_items_at_or_before_6pm)}")
269
+ print(f"- Expected deletions verified: {sum(1 for item in expected_deleted.values() if not item['found'])}/4")
270
+
271
+ return all_passed
272
+
273
+ def main():
274
+ """
275
+ Executes the verification process and exits with a status code.
276
+ """
277
+ notion = notion_utils.get_notion_client()
278
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
279
+
280
+ if verify(notion, main_id):
281
+ print("\nVerification passed: All 4 required OSAKA events after 6 PM on Day 1 and Day 2 have been removed")
282
+ sys.exit(0)
283
+ else:
284
+ print("\nVerification failed: Some OSAKA events after 6 PM on Day 1/Day 2 still exist")
285
+ sys.exit(1)
286
+
287
+ if __name__ == "__main__":
288
+ main()
tasks/notion/easy/online_resume/simple__skills_development_tracker/description.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Create a comprehensive skills audit system by performing the following tasks:
2
+
3
+ **Task Requirements:**
4
+ 1. Create a new database named "Skills Development Tracker" as a child database in the main resume page with the following properties:
5
+ - Name (title property)
6
+ - Current Skill (relation to Skills database)
7
+ - Current Proficiency (rollup from related skill's "Skill Level" property)
8
+ - Target Proficiency (number property with format "percent")
9
+ - Gap (formula: Target Proficiency - Current Proficiency)
10
+ - Learning Resources (rich text property)
11
+ - Progress Notes (rich text property)
12
+
13
+ 2. Populate the Skills Development Tracker database with entries for all skills that have a proficiency level below 70% (0.7):
14
+ - For each qualifying skill, create an entry with:
15
+ - Name: "[Skill Name] Development Plan"
16
+ - Link to the corresponding skill in Skills database
17
+ - Target Proficiency: Set to Current + 25% (capped at 95%)
18
+ - Learning Resources: "Online courses and practice projects"
19
+ - Progress Notes: "Initial assessment completed"
tasks/notion/easy/online_resume/simple__skills_development_tracker/meta.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__skills_development_tracker",
3
+ "task_name": "Simple Skills Development Tracker",
4
+ "category_id": "online_resume",
5
+ "category_name": "Online Resume",
6
+ "description": "Create a comprehensive skills audit system with development tracking for skills below 70% proficiency.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "database manipulation",
12
+ "cross-reference linking",
13
+ "conditional filtering",
14
+ "data aggregation",
15
+ "template population",
16
+ "visual formatting"
17
+ ],
18
+ "mcp": [
19
+ "notion"
20
+ ],
21
+ "meta_data": {
22
+ "stateType": "url",
23
+ "stateContent": null,
24
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Online-Resume-23181626b6d781159faaeb5eadaf612e",
25
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/online-resume"
26
+ }
27
+ }
tasks/notion/easy/online_resume/simple__skills_development_tracker/verify.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+
6
+ def verify(notion: Client, main_id: str = None) -> bool:
7
+ """
8
+ Verifies that the Skills Development Tracker database was created correctly.
9
+ """
10
+ page_id = None
11
+ if main_id:
12
+ found_id, object_type = notion_utils.find_page_or_database_by_id(
13
+ notion, main_id
14
+ )
15
+ if found_id and object_type == "page":
16
+ page_id = found_id
17
+
18
+ if not page_id:
19
+ page_id = notion_utils.find_page(notion, "New Online Resume")
20
+ if not page_id:
21
+ print("Error: Page 'New Online Resume' not found.", file=sys.stderr)
22
+ return False
23
+
24
+ # Step 1: Verify Skills Development Tracker database exists
25
+ tracker_db_id = notion_utils.find_database_in_block(
26
+ notion, page_id, "Skills Development Tracker"
27
+ )
28
+ if not tracker_db_id:
29
+ print(
30
+ "Error: Database 'Skills Development Tracker' not found.", file=sys.stderr
31
+ )
32
+ return False
33
+
34
+ # Step 2: Verify database schema
35
+ try:
36
+ db_info = notion.databases.retrieve(database_id=tracker_db_id)
37
+ properties = db_info.get("properties", {})
38
+
39
+ # Check required properties
40
+ required_props = {
41
+ "Name": "title",
42
+ "Current Skill": "relation",
43
+ "Current Proficiency": "rollup",
44
+ "Target Proficiency": "number",
45
+ "Gap": "formula",
46
+ "Learning Resources": "rich_text",
47
+ "Progress Notes": "rich_text",
48
+ }
49
+
50
+ for prop_name, expected_type in required_props.items():
51
+ if prop_name not in properties:
52
+ print(
53
+ f"Error: Property '{prop_name}' not found in database.",
54
+ file=sys.stderr,
55
+ )
56
+ return False
57
+ if properties[prop_name]["type"] != expected_type:
58
+ print(
59
+ f"Error: Property '{prop_name}' has incorrect type. Expected '{expected_type}', got '{properties[prop_name]['type']}'.",
60
+ file=sys.stderr,
61
+ )
62
+ return False
63
+
64
+ # Verify Target Proficiency is percent format
65
+ if (
66
+ properties["Target Proficiency"].get("number", {}).get("format")
67
+ != "percent"
68
+ ):
69
+ print(
70
+ "Error: Target Proficiency should have 'percent' format.",
71
+ file=sys.stderr,
72
+ )
73
+ return False
74
+
75
+ except Exception as e:
76
+ print(f"Error retrieving database info: {e}", file=sys.stderr)
77
+ return False
78
+
79
+ # Step 3: Get Skills database to check entries
80
+ skills_db_id = notion_utils.find_database_in_block(notion, page_id, "Skills")
81
+ if not skills_db_id:
82
+ print("Error: Skills database not found.", file=sys.stderr)
83
+ return False
84
+
85
+ # Get all skills with proficiency < 70%
86
+ skills_below_70 = []
87
+ try:
88
+ skills_results = notion.databases.query(database_id=skills_db_id).get(
89
+ "results", []
90
+ )
91
+ for skill in skills_results:
92
+ skill_level = (
93
+ skill.get("properties", {}).get("Skill Level", {}).get("number", 1.0)
94
+ )
95
+ if skill_level < 0.7:
96
+ skill_name = (
97
+ skill.get("properties", {}).get("Skill", {}).get("title", [])
98
+ )
99
+ if skill_name:
100
+ skill_name_text = skill_name[0].get("text", {}).get("content", "")
101
+ skills_below_70.append(
102
+ {
103
+ "name": skill_name_text,
104
+ "id": skill["id"],
105
+ "level": skill_level,
106
+ }
107
+ )
108
+ except Exception as e:
109
+ print(f"Error querying Skills database: {e}", file=sys.stderr)
110
+ return False
111
+
112
+ if not skills_below_70:
113
+ print("Warning: No skills found with proficiency below 70%.", file=sys.stderr)
114
+ # This might be OK if all skills are above 70%
115
+
116
+ # Step 4: Verify entries in Skills Development Tracker
117
+ try:
118
+ tracker_results = notion.databases.query(database_id=tracker_db_id).get(
119
+ "results", []
120
+ )
121
+
122
+ # Check that we have entries for skills below 70%
123
+ if len(skills_below_70) > 0 and len(tracker_results) == 0:
124
+ print(
125
+ "Error: No entries found in Skills Development Tracker database.",
126
+ file=sys.stderr,
127
+ )
128
+ return False
129
+
130
+ # Verify each entry
131
+ for entry in tracker_results:
132
+ props = entry.get("properties", {})
133
+
134
+ # Check name format
135
+ name_prop = props.get("Name", {}).get("title", [])
136
+ if not name_prop:
137
+ print("Error: Entry missing Name property.", file=sys.stderr)
138
+ return False
139
+ name_text = name_prop[0].get("text", {}).get("content", "")
140
+ if not name_text.endswith(" Development Plan"):
141
+ print(
142
+ f"Error: Entry name '{name_text}' doesn't follow expected format.",
143
+ file=sys.stderr,
144
+ )
145
+ return False
146
+
147
+ # Check relation to Skills database
148
+ skill_relation = props.get("Current Skill", {}).get("relation", [])
149
+ if not skill_relation:
150
+ print(
151
+ f"Error: Entry '{name_text}' missing Current Skill relation.",
152
+ file=sys.stderr,
153
+ )
154
+ return False
155
+
156
+ # Check Target Proficiency (should be set)
157
+ target_prof = props.get("Target Proficiency", {}).get("number")
158
+ if target_prof is None:
159
+ print(
160
+ f"Error: Entry '{name_text}' missing Target Proficiency.",
161
+ file=sys.stderr,
162
+ )
163
+ return False
164
+
165
+ # Check Learning Resources
166
+ learning_resources = props.get("Learning Resources", {}).get(
167
+ "rich_text", []
168
+ )
169
+ if not learning_resources:
170
+ print(
171
+ f"Error: Entry '{name_text}' missing Learning Resources.",
172
+ file=sys.stderr,
173
+ )
174
+ return False
175
+
176
+ # Check Progress Notes
177
+ progress_notes = props.get("Progress Notes", {}).get("rich_text", [])
178
+ if not progress_notes:
179
+ print(
180
+ f"Error: Entry '{name_text}' missing Progress Notes.",
181
+ file=sys.stderr,
182
+ )
183
+ return False
184
+
185
+ except Exception as e:
186
+ print(f"Error querying Skills Development Tracker: {e}", file=sys.stderr)
187
+ return False
188
+
189
+ print("Success: Skills Development Tracker database verified successfully.")
190
+ return True
191
+
192
+
193
+ def main():
194
+ """
195
+ Executes the verification process and exits with a status code.
196
+ """
197
+ notion = notion_utils.get_notion_client()
198
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
199
+ if verify(notion, main_id):
200
+ sys.exit(0)
201
+ else:
202
+ sys.exit(1)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()
tasks/notion/easy/python_roadmap/simple__expert_level_lessons/description.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Task: Expert Level Learning Path (Simplified)
2
+
3
+ ## Objective
4
+ Extend the Python Roadmap with a new Expert Level chapter, create a bridge lesson, and add two expert lessons that build on existing material.
5
+
6
+ ## Requirements
7
+
8
+ ### 1. Add the Expert Level chapter
9
+ - **Database**: Chapters
10
+ - **Name**: `Expert Level`
11
+ - **Icon**: 🟣 (purple circle emoji)
12
+ - Make sure it is linked into the roadmap alongside the existing chapters.
13
+
14
+ ### 2. Create the bridge lesson
15
+ Create a lesson that connects advanced material to the new chapter:
16
+ - **Title**: `Advanced Foundations Review`
17
+ - **Status**: Done
18
+ - **Chapter**: Link it to `Expert Level`
19
+ - **Parent item**: Link to the lesson whose title contains "Control" (e.g., "Control Flow")
20
+ - **Sub-items**: Include links to the lessons containing "Decorators" and "Calling API"
21
+
22
+ ### 3. Add two expert lessons
23
+ Add the following entries to the Steps database:
24
+
25
+ | Lesson Title | Status | Chapter | Parent item | Date |
26
+ |--------------|--------|---------|-------------|------|
27
+ | `Metaprogramming and AST Manipulation` | To Do | Expert Level | Advanced Foundations Review | 2025-09-15 |
28
+ | `Async Concurrency Patterns` | To Do | Expert Level | Calling API | 2025-09-20 |
29
+
30
+ The lessons must inherit the correct chapter link, parent relationship, and due date as shown above.
tasks/notion/easy/python_roadmap/simple__expert_level_lessons/meta.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "expert_level_lessons",
3
+ "task_name": "Expert Level Lessons",
4
+ "category_id": "python_roadmap",
5
+ "category_name": "Python Roadmap",
6
+ "description": "Create an Expert Level chapter with sophisticated prerequisite chains and four expert-level lessons.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "database manipulation",
12
+ "cross-reference linking",
13
+ "conditional filtering",
14
+ "status tracking",
15
+ "template population"
16
+ ],
17
+ "mcp": [
18
+ "notion"
19
+ ],
20
+ "meta_data": {
21
+ "stateType": "url",
22
+ "stateContent": null,
23
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Python-Roadmap-25281626b6d78012bf2bce1fa8711f4d",
24
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/python-roadmap"
25
+ }
26
+ }
tasks/notion/easy/python_roadmap/simple__expert_level_lessons/verify.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+
6
+ TARGET_PAGE_TITLE = "Python Roadmap"
7
+ CHAPTER_NAME = "Expert Level"
8
+ CHAPTER_ICON = "🟣"
9
+ BRIDGE_TITLE = "Advanced Foundations Review"
10
+ REQUIRED_SUBITEM_TITLES = ["Decorators", "Calling API"]
11
+
12
+ LESSON_REQUIREMENTS = [
13
+ {
14
+ "title": "Metaprogramming and AST Manipulation",
15
+ "status": "To Do",
16
+ "date": "2025-09-15",
17
+ "parent_title": BRIDGE_TITLE,
18
+ },
19
+ {
20
+ "title": "Async Concurrency Patterns",
21
+ "status": "To Do",
22
+ "date": "2025-09-20",
23
+ "parent_title": "Calling API",
24
+ },
25
+ ]
26
+
27
+
28
+ def _get_database_ids(notion: Client, page_id: str) -> tuple[str | None, str | None]:
29
+ """Return the block IDs for the Chapters and Steps databases on the page."""
30
+ chapters_db_id = None
31
+ steps_db_id = None
32
+
33
+ blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
34
+ for block in blocks:
35
+ if block.get("type") != "child_database":
36
+ continue
37
+ title = block.get("child_database", {}).get("title", "")
38
+ if "Chapters" in title and not chapters_db_id:
39
+ chapters_db_id = block["id"]
40
+ elif "Steps" in title and not steps_db_id:
41
+ steps_db_id = block["id"]
42
+
43
+ return chapters_db_id, steps_db_id
44
+
45
+
46
+ def _query_step_by_title(notion: Client, database_id: str, title: str, *, exact: bool = True):
47
+ """Return the first step entry matching the given title pattern."""
48
+ title_filter = {"equals": title} if exact else {"contains": title}
49
+ response = notion.databases.query(
50
+ database_id=database_id,
51
+ filter={"property": "Lessons", "title": title_filter},
52
+ page_size=5,
53
+ )
54
+ results = response.get("results", [])
55
+ return results[0] if results else None
56
+
57
+
58
+ def verify(notion: Client, main_id: str | None = None) -> bool:
59
+ """Verify the simplified Expert Level learning path setup."""
60
+ # Resolve the roadmap page.
61
+ if main_id:
62
+ page_id, object_type = notion_utils.find_page_or_database_by_id(notion, main_id)
63
+ if not page_id or object_type != "page":
64
+ print("Error: Python Roadmap page not found.", file=sys.stderr)
65
+ return False
66
+ else:
67
+ page_id = notion_utils.find_page(notion, TARGET_PAGE_TITLE)
68
+ if not page_id:
69
+ print("Error: Python Roadmap page not found.", file=sys.stderr)
70
+ return False
71
+
72
+ # Locate the Chapters and Steps databases.
73
+ chapters_db_id, steps_db_id = _get_database_ids(notion, page_id)
74
+ if not chapters_db_id:
75
+ print("Error: Chapters database not found on the page.", file=sys.stderr)
76
+ return False
77
+ if not steps_db_id:
78
+ print("Error: Steps database not found on the page.", file=sys.stderr)
79
+ return False
80
+
81
+ # Ensure the Expert Level chapter exists with the purple icon.
82
+ try:
83
+ chapter_resp = notion.databases.query(
84
+ database_id=chapters_db_id,
85
+ filter={"property": "Name", "title": {"equals": CHAPTER_NAME}},
86
+ page_size=1,
87
+ )
88
+ except Exception as exc:
89
+ print(f"Error querying Chapters database: {exc}", file=sys.stderr)
90
+ return False
91
+
92
+ results = chapter_resp.get("results", [])
93
+ if not results:
94
+ print("Error: Expert Level chapter not found.", file=sys.stderr)
95
+ return False
96
+
97
+ expert_chapter = results[0]
98
+ expert_chapter_id = expert_chapter["id"]
99
+ icon = expert_chapter.get("icon") or {}
100
+ if icon.get("type") != "emoji" or icon.get("emoji") != CHAPTER_ICON:
101
+ print("Error: Expert Level chapter must use the purple circle emoji icon.", file=sys.stderr)
102
+ return False
103
+
104
+ print("βœ“ Expert Level chapter exists with the correct icon.")
105
+
106
+ # Locate prerequisite lessons (Control Flow, Decorators, Calling API).
107
+ control_lesson = _query_step_by_title(notion, steps_db_id, "Control", exact=False)
108
+ if not control_lesson:
109
+ print("Error: Could not find a lesson containing 'Control' in its title.", file=sys.stderr)
110
+ return False
111
+ control_lesson_id = control_lesson["id"]
112
+
113
+ prerequisite_ids = {}
114
+ for title in REQUIRED_SUBITEM_TITLES:
115
+ lesson = _query_step_by_title(notion, steps_db_id, title, exact=False)
116
+ if not lesson:
117
+ print(f"Error: Required lesson containing '{title}' not found.", file=sys.stderr)
118
+ return False
119
+ prerequisite_ids[title] = lesson["id"]
120
+
121
+ # Verify the bridge lesson.
122
+ bridge_lesson = _query_step_by_title(notion, steps_db_id, BRIDGE_TITLE, exact=True)
123
+ if not bridge_lesson:
124
+ print("Error: Advanced Foundations Review lesson not found.", file=sys.stderr)
125
+ return False
126
+
127
+ status = (bridge_lesson["properties"].get("Status", {}).get("status") or {}).get("name")
128
+ if status != "Done":
129
+ print("Error: Advanced Foundations Review must have status 'Done'.", file=sys.stderr)
130
+ return False
131
+
132
+ # Ensure chapter relation includes Expert Level.
133
+ chapter_rel = bridge_lesson["properties"].get("Chapters", {}).get("relation", [])
134
+ if not any(rel["id"] == expert_chapter_id for rel in chapter_rel):
135
+ print("Error: Advanced Foundations Review must link to the Expert Level chapter.", file=sys.stderr)
136
+ return False
137
+
138
+ # Parent item should be the control lesson.
139
+ parent_rel = bridge_lesson["properties"].get("Parent item", {}).get("relation", [])
140
+ if not parent_rel or parent_rel[0]["id"] != control_lesson_id:
141
+ print("Error: Advanced Foundations Review should use the control lesson as its Parent item.", file=sys.stderr)
142
+ return False
143
+
144
+ # Sub-items must include the required lessons.
145
+ sub_rel = bridge_lesson["properties"].get("Sub-item", {}).get("relation", [])
146
+ sub_ids = {rel["id"] for rel in sub_rel}
147
+ missing = [title for title, rel_id in prerequisite_ids.items() if rel_id not in sub_ids]
148
+ if missing:
149
+ print(
150
+ f"Error: Advanced Foundations Review must include these lessons as sub-items: {', '.join(missing)}.",
151
+ file=sys.stderr,
152
+ )
153
+ return False
154
+
155
+ print("βœ“ Bridge lesson configured with the correct status, chapter, parent, and sub-items.")
156
+
157
+ # Verify the two expert lessons.
158
+ overall_success = True
159
+ for spec in LESSON_REQUIREMENTS:
160
+ lesson = _query_step_by_title(notion, steps_db_id, spec["title"], exact=True)
161
+ if not lesson:
162
+ print(f"Error: Lesson '{spec['title']}' not found.", file=sys.stderr)
163
+ overall_success = False
164
+ continue
165
+
166
+ lesson_ok = True
167
+
168
+ # Status check.
169
+ status_name = (lesson["properties"].get("Status", {}).get("status") or {}).get("name")
170
+ if status_name != spec["status"]:
171
+ print(
172
+ f"Error: Lesson '{spec['title']}' should have status '{spec['status']}', found '{status_name}'.",
173
+ file=sys.stderr,
174
+ )
175
+ lesson_ok = False
176
+
177
+ # Chapter relation check.
178
+ lesson_chapters = lesson["properties"].get("Chapters", {}).get("relation", [])
179
+ if not any(rel["id"] == expert_chapter_id for rel in lesson_chapters):
180
+ print(f"Error: Lesson '{spec['title']}' must link to the Expert Level chapter.", file=sys.stderr)
181
+ lesson_ok = False
182
+
183
+ # Parent relation check.
184
+ parent_title = spec["parent_title"]
185
+ if parent_title == BRIDGE_TITLE:
186
+ expected_parent_id = bridge_lesson["id"]
187
+ else:
188
+ expected_parent_id = prerequisite_ids.get(parent_title)
189
+
190
+ parent_relation = lesson["properties"].get("Parent item", {}).get("relation", [])
191
+ if not expected_parent_id:
192
+ print(
193
+ f"Error: Could not resolve expected parent '{parent_title}' for lesson '{spec['title']}'.",
194
+ file=sys.stderr,
195
+ )
196
+ lesson_ok = False
197
+ else:
198
+ if not parent_relation or parent_relation[0]["id"] != expected_parent_id:
199
+ print(
200
+ f"Error: Lesson '{spec['title']}' should have '{parent_title}' as its Parent item.",
201
+ file=sys.stderr,
202
+ )
203
+ lesson_ok = False
204
+ # Date check.
205
+ date_prop = lesson["properties"].get("Date", {}).get("date") or {}
206
+ if date_prop.get("start") != spec["date"]:
207
+ print(
208
+ f"Error: Lesson '{spec['title']}' should use date {spec['date']}, found {date_prop.get('start')}.",
209
+ file=sys.stderr,
210
+ )
211
+ lesson_ok = False
212
+
213
+ if lesson_ok:
214
+ print(f"βœ“ Lesson '{spec['title']}' has the expected properties.")
215
+ else:
216
+ overall_success = False
217
+
218
+ if not overall_success:
219
+ return False
220
+
221
+ print("Success: Expert Level chapter, bridge lesson, and expert lessons configured correctly.")
222
+ return True
223
+
224
+
225
+ def main() -> None:
226
+ notion = notion_utils.get_notion_client()
227
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
228
+ if verify(notion, main_id):
229
+ sys.exit(0)
230
+ sys.exit(1)
231
+
232
+
233
+ if __name__ == "__main__":
234
+ main()
tasks/notion/easy/self_assessment/simple__faq_column_layout/description.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Navigate to the "Self Assessment" page and reorganize the FAQ toggle content to make it easier to scan.
2
+
3
+ **Task Requirements:**
4
+ 1. Add a column list with two columns inside the FAQ toggle.
5
+ 2. Move the first two existing Q&A pairs from the FAQ into the left column.
6
+ 3. Move the third existing Q&A pair into the right column, keeping the original heading/paragraph formatting.
tasks/notion/easy/self_assessment/simple__faq_column_layout/meta.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__faq_column_layout",
3
+ "task_name": "Simple FAQ Column Layout",
4
+ "category_id": "self_assessment",
5
+ "category_name": "Self Assessment",
6
+ "description": "Reorganize the FAQ section content into a two-column layout with balanced Q&A pairs.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "content organization",
12
+ "visual formatting",
13
+ "template population"
14
+ ],
15
+ "mcp": [
16
+ "notion"
17
+ ],
18
+ "meta_data": {
19
+ "stateType": "url",
20
+ "stateContent": null,
21
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Self-Assessment-24381626b6d780fe9f56c2ba14ea042d",
22
+ "stateOriginalUrl": "https://painted-tennis-ebc.notion.site/Self-Assessment-24381626b6d780fe9f56c2ba14ea042d"
23
+ }
24
+ }
tasks/notion/easy/self_assessment/simple__faq_column_layout/verify.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+
6
+ def verify(notion: Client, main_id: str = None) -> bool:
7
+ """
8
+ Verifies that the FAQ toggle has been properly reorganized with a column list.
9
+ """
10
+ # Start from main_id if provided
11
+ page_id = None
12
+ if main_id:
13
+ found_id, object_type = notion_utils.find_page_or_database_by_id(
14
+ notion, main_id
15
+ )
16
+ if found_id and object_type == "page":
17
+ page_id = found_id
18
+
19
+ if not page_id:
20
+ # Try to find the Self Assessment page
21
+ page_id = notion_utils.find_page(notion, "Self Assessment")
22
+
23
+ if not page_id:
24
+ print("Error: Self Assessment page not found.", file=sys.stderr)
25
+ return False
26
+
27
+ # Get all blocks recursively from the page
28
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
29
+
30
+ # Find the FAQ toggle block
31
+ faq_toggle_block = None
32
+ faq_toggle_id = None
33
+ for block in all_blocks:
34
+ if block.get("type") == "toggle":
35
+ block_text = notion_utils.get_block_plain_text(block)
36
+ if "FAQ" in block_text:
37
+ faq_toggle_block = block
38
+ faq_toggle_id = block.get("id")
39
+ print(f"Found FAQ toggle block: {block_text}")
40
+ break
41
+
42
+ if not faq_toggle_block:
43
+ print("Error: FAQ toggle block not found.", file=sys.stderr)
44
+ return False
45
+
46
+ # Find column_list inside the FAQ toggle
47
+ column_list_block = None
48
+ for block in all_blocks:
49
+ if (
50
+ block.get("type") == "column_list"
51
+ and block.get("parent", {}).get("block_id") == faq_toggle_id
52
+ ):
53
+ column_list_block = block
54
+ break
55
+
56
+ if not column_list_block:
57
+ print("Error: No column_list found inside FAQ toggle.", file=sys.stderr)
58
+ return False
59
+
60
+ # Check that there are no Q&A pairs directly under FAQ toggle (outside column_list)
61
+ direct_faq_children = []
62
+ for block in all_blocks:
63
+ if block.get("parent", {}).get("block_id") == faq_toggle_id and block.get(
64
+ "id"
65
+ ) != column_list_block.get("id"):
66
+ direct_faq_children.append(block)
67
+
68
+ # Check if any of these are heading_3 or paragraph blocks (Q&A content)
69
+ for block in direct_faq_children:
70
+ if block.get("type") in ["heading_3", "paragraph"]:
71
+ print(
72
+ f"Error: Found Q&A content outside column_list: {notion_utils.get_block_plain_text(block)[:50]}...",
73
+ file=sys.stderr,
74
+ )
75
+ return False
76
+
77
+ # Find the two columns
78
+ columns = []
79
+ column_list_id = column_list_block.get("id")
80
+ for block in all_blocks:
81
+ if (
82
+ block.get("type") == "column"
83
+ and block.get("parent", {}).get("block_id") == column_list_id
84
+ ):
85
+ columns.append(block)
86
+
87
+ if len(columns) != 2:
88
+ print(f"Error: Expected 2 columns, found {len(columns)}.", file=sys.stderr)
89
+ return False
90
+
91
+ # Count Q&A pairs in each column
92
+ qa_counts = []
93
+ total_pairs = 0
94
+
95
+ for i, column in enumerate(columns[:2]):
96
+ column_id = column.get("id")
97
+
98
+ column_blocks = [
99
+ block
100
+ for block in all_blocks
101
+ if block.get("parent", {}).get("block_id") == column_id
102
+ ]
103
+
104
+ qa_pairs = 0
105
+ j = 0
106
+ while j < len(column_blocks):
107
+ if (
108
+ column_blocks[j].get("type") == "heading_3"
109
+ and j + 1 < len(column_blocks)
110
+ and column_blocks[j + 1].get("type") == "paragraph"
111
+ ):
112
+ qa_pairs += 1
113
+ j += 2
114
+ else:
115
+ j += 1
116
+
117
+ qa_counts.append(qa_pairs)
118
+ total_pairs += qa_pairs
119
+ print(f"Column {i + 1}: Found {qa_pairs} Q&A pairs")
120
+
121
+ if qa_counts[0] < 2:
122
+ print(
123
+ f"Error: Left column should contain at least 2 Q&A pairs, found {qa_counts[0]}.",
124
+ file=sys.stderr,
125
+ )
126
+ return False
127
+
128
+ if qa_counts[1] < 1:
129
+ print(
130
+ f"Error: Right column should contain at least 1 Q&A pair, found {qa_counts[1]}.",
131
+ file=sys.stderr,
132
+ )
133
+ return False
134
+
135
+ if total_pairs < 3:
136
+ print(
137
+ f"Error: Expected at least 3 total Q&A pairs across both columns, found {total_pairs}.",
138
+ file=sys.stderr,
139
+ )
140
+ return False
141
+
142
+ print(
143
+ "Success: FAQ toggle organized with two columns holding the existing Q&A pairs (two on the left, one on the right)."
144
+ )
145
+ return True
146
+
147
+
148
+ def main():
149
+ """
150
+ Executes the verification process and exits with a status code.
151
+ """
152
+ notion = notion_utils.get_notion_client()
153
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
154
+ if verify(notion, main_id):
155
+ sys.exit(0)
156
+ else:
157
+ sys.exit(1)
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()
tasks/notion/easy/standard_operating_procedure/simple__section_organization/description.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Task: Reorganize Standard Operating Procedure Page Sections
2
+
3
+ ## Objective
4
+ Modify the structure of the Standard Operating Procedure page in Notion by updating the order of two sections.
5
+
6
+ ## Requirements
7
+ - Navigate to the Standard Operating Procedure page
8
+ - Swap the positions of the "Terminologies" and "Roles & responsibilities" sections
9
+ - Preserve all content within each section exactly as is
10
+ - Maintain the original formatting and structure of each section
tasks/notion/easy/standard_operating_procedure/simple__section_organization/meta.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__section_organization",
3
+ "task_name": "Simple Section Organization",
4
+ "category_id": "standard_operating_procedure",
5
+ "category_name": "Standard Operating Procedure",
6
+ "description": "Reorganize the Standard Operating Procedure page by swapping sections and creating a column layout.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "content organization",
12
+ "cross-reference linking",
13
+ "visual formatting"
14
+ ],
15
+ "mcp": [
16
+ "notion"
17
+ ],
18
+ "meta_data": {
19
+ "stateType": "url",
20
+ "stateContent": null,
21
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Standard-Operating-Procedure-24381626b6d780a8b678f9e62ae5b152",
22
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/standard-operating-procedure"
23
+ }
24
+ }
tasks/notion/easy/standard_operating_procedure/simple__section_organization/verify.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+
6
+ TARGET_PAGE_TITLE = "Standard Operating Procedure"
7
+ ROLES_HEADING = "Roles & responsibilities"
8
+ TERMINOLOGIES_HEADING = "Terminologies"
9
+
10
+
11
+ def _find_heading_indices(blocks: list[dict]) -> tuple[int | None, int | None]:
12
+ """Return the indices of the target headings within the flattened block list."""
13
+ roles_index = None
14
+ terminologies_index = None
15
+
16
+ for index, block in enumerate(blocks):
17
+ if block.get("type") != "heading_2":
18
+ continue
19
+ rich_text = block.get("heading_2", {}).get("rich_text", [])
20
+ if not rich_text:
21
+ continue
22
+ heading_text = rich_text[0].get("text", {}).get("content", "")
23
+ if heading_text == ROLES_HEADING and roles_index is None:
24
+ roles_index = index
25
+ elif heading_text == TERMINOLOGIES_HEADING and terminologies_index is None:
26
+ terminologies_index = index
27
+
28
+ return roles_index, terminologies_index
29
+
30
+
31
+ def verify(notion: Client, main_id: str | None = None) -> bool:
32
+ """Ensure the Roles & responsibilities section appears before Terminologies."""
33
+ # Resolve page id.
34
+ if main_id:
35
+ page_id, object_type = notion_utils.find_page_or_database_by_id(notion, main_id)
36
+ if not page_id or object_type != "page":
37
+ print("Error: Standard Operating Procedure page not found.", file=sys.stderr)
38
+ return False
39
+ else:
40
+ page_id = notion_utils.find_page(notion, TARGET_PAGE_TITLE)
41
+ if not page_id:
42
+ print("Error: Standard Operating Procedure page not found.", file=sys.stderr)
43
+ return False
44
+
45
+ # Fetch all blocks (flattened order from top to bottom).
46
+ blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
47
+ roles_index, terminologies_index = _find_heading_indices(blocks)
48
+
49
+ if roles_index is None:
50
+ print("Error: 'Roles & responsibilities' section not found.", file=sys.stderr)
51
+ return False
52
+ if terminologies_index is None:
53
+ print("Error: 'Terminologies' section not found.", file=sys.stderr)
54
+ return False
55
+
56
+ if roles_index >= terminologies_index:
57
+ print(
58
+ "Error: Sections are not swapped. 'Roles & responsibilities' should appear before 'Terminologies'.",
59
+ file=sys.stderr,
60
+ )
61
+ return False
62
+
63
+ print("Success: Section order updated so 'Roles & responsibilities' precedes 'Terminologies'.")
64
+ return True
65
+
66
+
67
+ def main() -> None:
68
+ notion = notion_utils.get_notion_client()
69
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
70
+ if verify(notion, main_id):
71
+ sys.exit(0)
72
+ sys.exit(1)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
tasks/notion/easy/team_projects/simple__swap_tasks/description.md ADDED
@@ -0,0 +1 @@
 
 
1
+ Go to the Team Projects page, find the person responsible for the most tasks (10 in total) and the person responsible for the fewest tasks (3 in total), then swap their assigned tasks.
tasks/notion/easy/team_projects/simple__swap_tasks/meta.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__swap_tasks",
3
+ "task_name": "Simple Swap Tasks",
4
+ "category_id": "team_projects",
5
+ "category_name": "Team Projects",
6
+ "description": "Find the person responsible for the most and fewest tasks, then swap their assigned tasks.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "data aggregation",
12
+ "automated migration",
13
+ "conditional filtering"
14
+ ],
15
+ "mcp": [
16
+ "notion"
17
+ ],
18
+ "meta_data": {
19
+ "stateType": "url",
20
+ "stateContent": null,
21
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Team-Projects-24e81626b6d7809c982fdb7a25825898",
22
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/gantt-chart"
23
+ }
24
+ }
tasks/notion/easy/team_projects/simple__swap_tasks/verify.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+ def verify(notion: Client, main_id: str = None) -> bool:
6
+ """
7
+ Verifies that the task assignees have been swapped correctly.
8
+ Checks:
9
+ 1. "Develop a plan for promotion" and "Evaluate different third-party services" have swapped assignees
10
+ 2. The person with most tasks and person with least tasks have swapped all their tasks
11
+ """
12
+ # Step 1: Find the Team Projects page
13
+ if main_id:
14
+ found_id, object_type = notion_utils.find_page_or_database_by_id(notion, main_id)
15
+ if not found_id or object_type != 'page':
16
+ print("Error: Team Projects page not found.", file=sys.stderr)
17
+ return False
18
+ else:
19
+ # Try to find the page by searching
20
+ found_id = notion_utils.find_page(notion, "Team Projects")
21
+ if not found_id:
22
+ print("Error: Team Projects page not found.", file=sys.stderr)
23
+ return False
24
+
25
+ # Get all blocks from the page to find database references
26
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, found_id)
27
+
28
+ # Find Tasks database ID from the page
29
+ tasks_db_id = None
30
+
31
+ for block in all_blocks:
32
+ if block and block.get("type") == "child_database":
33
+ db_title = block.get("child_database", {}).get("title", "")
34
+ if "Tasks" in db_title:
35
+ tasks_db_id = block["id"]
36
+ break
37
+
38
+ if not tasks_db_id:
39
+ print("Error: Tasks database not found.", file=sys.stderr)
40
+ return False
41
+
42
+ print("\nπŸ“‹ Starting verification...")
43
+
44
+ # Step 2: Query all tasks to analyze assignees
45
+
46
+ try:
47
+ all_tasks_response = notion.databases.query(
48
+ database_id=tasks_db_id,
49
+ page_size=100
50
+ )
51
+
52
+ if not all_tasks_response.get("results"):
53
+ print("Error: No tasks found in Tasks database.", file=sys.stderr)
54
+ return False
55
+
56
+ tasks = all_tasks_response["results"]
57
+
58
+ except Exception as e:
59
+ print(f"Error querying Tasks database: {e}", file=sys.stderr)
60
+ return False
61
+
62
+ # Step 3: Check specific tasks have swapped assignees
63
+
64
+ develop_plan_task = None
65
+ evaluate_services_task = None
66
+
67
+ for task in tasks:
68
+ task_name = task["properties"]["Name"]["title"][0]["text"]["content"]
69
+ if task_name == "Develop a plan for promotion":
70
+ develop_plan_task = task
71
+ elif task_name == "Evaluate different third-party services":
72
+ evaluate_services_task = task
73
+
74
+ if not develop_plan_task or not evaluate_services_task:
75
+ print("Error: Could not find both required tasks.", file=sys.stderr)
76
+ return False
77
+
78
+ # Get assignees for these tasks
79
+ develop_plan_assignees = develop_plan_task["properties"]["Assigned"]["people"]
80
+ evaluate_services_assignees = evaluate_services_task["properties"]["Assigned"]["people"]
81
+
82
+ if not develop_plan_assignees or not evaluate_services_assignees:
83
+ print("Error: Tasks don't have assignees.", file=sys.stderr)
84
+ return False
85
+
86
+ develop_plan_assignee_id = develop_plan_assignees[0]["id"]
87
+ evaluate_services_assignee_id = evaluate_services_assignees[0]["id"]
88
+
89
+ # These should be different (swapped)
90
+ if develop_plan_assignee_id == evaluate_services_assignee_id:
91
+ print("Error: Tasks should have different assignees after swap.", file=sys.stderr)
92
+ return False
93
+
94
+ # Step 4: Count tasks per person
95
+
96
+ task_counts = {}
97
+ unassigned_count = 0
98
+
99
+ for task in tasks:
100
+ assignees = task["properties"]["Assigned"]["people"]
101
+ if assignees:
102
+ assignee_id = assignees[0]["id"]
103
+ if assignee_id not in task_counts:
104
+ task_counts[assignee_id] = []
105
+ task_counts[assignee_id].append(task["properties"]["Name"]["title"][0]["text"]["content"])
106
+ else:
107
+ unassigned_count += 1
108
+
109
+ # Sort by task count
110
+ sorted_assignees = sorted(task_counts.items(), key=lambda x: len(x[1]))
111
+
112
+ if len(sorted_assignees) < 2:
113
+ print("Error: Need at least 2 people with tasks to verify swap.", file=sys.stderr)
114
+ return False
115
+
116
+ # Get person with least and most tasks
117
+ person_with_least = sorted_assignees[0]
118
+ person_with_most = sorted_assignees[-1]
119
+
120
+ least_id, least_tasks = person_with_least
121
+ most_id, most_tasks = person_with_most
122
+
123
+ # Step 5: Verify the swap pattern
124
+
125
+ # Original distribution (before swap):
126
+ # - 5ac96c02-49a4-4320-8de6-b663ba83126b had 3 tasks (least)
127
+ # - ac7a3bd0-c111-4464-8f45-8a857a1abc8a had 10 tasks (most)
128
+
129
+ # After complete swap, we expect:
130
+ # - 5ac96c02-49a4-4320-8de6-b663ba83126b should have 10 tasks
131
+ # - ac7a3bd0-c111-4464-8f45-8a857a1abc8a should have 3 tasks
132
+
133
+ original_least_id = "5ac96c02-49a4-4320-8de6-b663ba83126b"
134
+ original_most_id = "ac7a3bd0-c111-4464-8f45-8a857a1abc8a"
135
+
136
+ # Check if the swap has been completed
137
+ swap_completed = False
138
+ for assignee_id, assignee_tasks in task_counts.items():
139
+ if assignee_id == original_least_id and len(assignee_tasks) == 10:
140
+ # Person who had 3 now has 10
141
+ for other_id, other_tasks in task_counts.items():
142
+ if other_id == original_most_id and len(other_tasks) == 3:
143
+ # Person who had 10 now has 3
144
+ swap_completed = True
145
+ break
146
+
147
+ # Step 6: Summary
148
+ print(f"\nπŸ“Š Task Distribution:")
149
+ print(f" β€’ Total tasks: {len(tasks)}")
150
+ print(f" β€’ Assigned tasks: {len(tasks) - unassigned_count}")
151
+ print(f" β€’ Unassigned tasks: {unassigned_count}")
152
+ print(f" β€’ People with tasks: {len(task_counts)}")
153
+ print(f"\n Task counts by person:")
154
+ for assignee_id, assignee_tasks in sorted_assignees:
155
+ print(f" - {assignee_id[:8]}...: {len(assignee_tasks)} tasks")
156
+
157
+ # Step 7: Final verification
158
+ print("\nπŸ” Verification Results:")
159
+
160
+ # Check that the swap has created a significant difference
161
+ if len(most_tasks) - len(least_tasks) < 5:
162
+ print(f"Warning: Difference between most and least is only {len(most_tasks) - len(least_tasks)} tasks", file=sys.stderr)
163
+
164
+ # Verify specific expected outcomes
165
+ verification_passed = True
166
+
167
+ # Check 1: Specific tasks have been swapped
168
+ specific_tasks_swapped = develop_plan_assignee_id != evaluate_services_assignee_id
169
+ if specific_tasks_swapped:
170
+ print(" βœ“ Specific tasks have been swapped")
171
+ else:
172
+ print(" βœ— Specific tasks were not swapped", file=sys.stderr)
173
+ verification_passed = False
174
+
175
+ # Check 2: Task distribution shows a complete swap
176
+ if swap_completed:
177
+ print(" βœ“ Complete task swap verified (3↔10 tasks)")
178
+ else:
179
+ # Show actual distribution for debugging
180
+ person1_tasks = len(task_counts.get(original_least_id, []))
181
+ person2_tasks = len(task_counts.get(original_most_id, []))
182
+ print(f" βœ— Swap incomplete! Expected 5ac96c02β†’10 tasks, ac7a3bd0β†’3 tasks", file=sys.stderr)
183
+ print(f" Actual: 5ac96c02β†’{person1_tasks} tasks, ac7a3bd0β†’{person2_tasks} tasks", file=sys.stderr)
184
+ verification_passed = False
185
+
186
+ # Check 3: Total task count is preserved
187
+ total_assigned_tasks = sum(len(tasks) for _, tasks in task_counts.items())
188
+ expected_total = len(tasks) - unassigned_count
189
+
190
+ if total_assigned_tasks == expected_total:
191
+ print(f" βœ“ Total task count preserved ({total_assigned_tasks} assigned)")
192
+ else:
193
+ print(f" βœ— Task count mismatch: {total_assigned_tasks} vs {expected_total} expected", file=sys.stderr)
194
+ verification_passed = False
195
+
196
+ if verification_passed:
197
+ print("\nβœ… All verification checks passed!")
198
+ return True
199
+ else:
200
+ print("\n❌ Verification failed", file=sys.stderr)
201
+ return False
202
+
203
+ def main():
204
+ """
205
+ Executes the verification process and exits with a status code.
206
+ """
207
+ notion = notion_utils.get_notion_client()
208
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
209
+ if verify(notion, main_id):
210
+ sys.exit(0)
211
+ else:
212
+ sys.exit(1)
213
+
214
+ if __name__ == "__main__":
215
+ main()
tasks/notion/easy/toronto_guide/simple__change_color/description.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Open the **Toronto Guide** page and refresh the colors of the tags in the **Food** database.
2
+
3
+ ## Requirements
4
+ 1. Find and open the Toronto Guide page in Notion.
5
+ 2. Locate the *Food* database on that page.
6
+ 3. Update every tag in the Food database that is currently pink so that it uses a different color of your choice (any non-pink color is fine).
7
+ 4. Do not modify callouts or tags in the other databases.
tasks/notion/easy/toronto_guide/simple__change_color/meta.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "simple__change_color",
3
+ "task_name": "Simple Change Color",
4
+ "category_id": "toronto_guide",
5
+ "category_name": "Toronto Guide",
6
+ "description": "Navigate to the Toronto Guide page and change all pink-colored elements to different colors.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-11-15",
9
+ "difficulty": "L1",
10
+ "tags": [
11
+ "visual formatting",
12
+ "conditional filtering"
13
+ ],
14
+ "mcp": [
15
+ "notion"
16
+ ],
17
+ "meta_data": {
18
+ "stateType": "url",
19
+ "stateContent": null,
20
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Toronto-Guide-25281626b6d7802caa7cc394647e901c",
21
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/conquering-toronto-a-destination-guide"
22
+ }
23
+ }
tasks/notion/easy/toronto_guide/simple__change_color/verify.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+
6
+ TARGET_PAGE_TITLE = "Toronto Guide"
7
+ FOOD_DATABASE_KEYWORD = "Food"
8
+ TARGET_TAG_NAMES = [
9
+ "Middle Eastern",
10
+ "Jamaican",
11
+ "Indian",
12
+ ]
13
+
14
+
15
+ def _get_food_database_id(notion: Client, page_id: str) -> str | None:
16
+ """Return the block ID of the Food database shown on the target page."""
17
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
18
+ for block in all_blocks:
19
+ if not block or block.get("type") != "child_database":
20
+ continue
21
+ title = block.get("child_database", {}).get("title", "")
22
+ if FOOD_DATABASE_KEYWORD.lower() in title.lower():
23
+ return block.get("id")
24
+ return None
25
+
26
+
27
+ def verify(notion: Client, main_id: str | None = None) -> bool:
28
+ """Check that all target tags in the Food database are no longer pink."""
29
+ # Resolve the Toronto Guide page ID.
30
+ if main_id:
31
+ found_id, object_type = notion_utils.find_page_or_database_by_id(notion, main_id)
32
+ if not found_id or object_type != "page":
33
+ print("Error: Toronto Guide page not found.", file=sys.stderr)
34
+ return False
35
+ page_id = found_id
36
+ else:
37
+ page_id = notion_utils.find_page(notion, TARGET_PAGE_TITLE)
38
+ if not page_id:
39
+ print("Error: Toronto Guide page not found.", file=sys.stderr)
40
+ return False
41
+
42
+ # Locate the Food database block.
43
+ food_db_id = _get_food_database_id(notion, page_id)
44
+ if not food_db_id:
45
+ print("Error: Food database not found on the Toronto Guide page.", file=sys.stderr)
46
+ return False
47
+
48
+ # Fetch database definition and inspect tag options.
49
+ try:
50
+ db_info = notion.databases.retrieve(database_id=food_db_id)
51
+ except Exception as exc:
52
+ print(f"Error: Unable to retrieve Food database ({exc}).", file=sys.stderr)
53
+ return False
54
+
55
+ tags_property = db_info.get("properties", {}).get("Tags", {})
56
+ if tags_property.get("type") != "multi_select":
57
+ print("Error: Food database does not have a multi-select Tags property.", file=sys.stderr)
58
+ return False
59
+
60
+ options = tags_property.get("multi_select", {}).get("options", [])
61
+ remaining_targets = set(TARGET_TAG_NAMES)
62
+ failures = False
63
+
64
+ for option in options:
65
+ tag_name = option.get("name", "").strip()
66
+ if tag_name not in remaining_targets:
67
+ continue
68
+
69
+ remaining_targets.discard(tag_name)
70
+ color = option.get("color")
71
+ if color == "pink":
72
+ print(f"Error: Tag '{tag_name}' in Food database is still pink.", file=sys.stderr)
73
+ failures = True
74
+ else:
75
+ print(f"βœ“ Tag '{tag_name}' color updated to '{color}'.")
76
+
77
+ if remaining_targets:
78
+ print(
79
+ f"Error: Food tags not found (expected to exist): {sorted(remaining_targets)}.",
80
+ file=sys.stderr,
81
+ )
82
+ return False
83
+
84
+ if failures:
85
+ return False
86
+
87
+ print("Success: All Food database tags are now non-pink.")
88
+ return True
89
+
90
+
91
+ def main() -> None:
92
+ notion = notion_utils.get_notion_client()
93
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
94
+ if verify(notion, main_id):
95
+ sys.exit(0)
96
+ sys.exit(1)
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
tasks/notion/standard/company_in_a_box/employee_onboarding/description.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Build an integrated **Employee Onboarding** system for the existing **Company In A Box** page.
2
+
3
+ **Task Requirements:**
4
+ 1. Create a new **database** titled **Employee Onboarding Checklist** with the following properties *exactly*:
5
+ β€’ **Employee Name** – title
6
+ β€’ **Start Date** – date
7
+ β€’ **Department** – select (options: Product, Marketing, Sales, HR, Engineering)
8
+
9
+ Populate this database with **3** sample new-hire pages covering three different departments. Every property in each entry must be filled.
10
+
11
+ 2. Under the top-level page **Company In A Box**, create a new child page titled **Onboarding Hub** containing, in order:
12
+ 1) The **Employee Onboarding Checklist** database embedded at the top.
13
+ 2) A section headed **Benefits Overview** that includes linked mentions (@-mentions or link-to-page blocks) to **β‰₯ 3** distinct benefit-policy pages from the **Company Wiki** (for example *Benefits policy*, *Vacation Policy*, *Corporate travel*).
14
+ 3) A section headed **30-Day Timeline** that presents a numbered list with **7** steps covering the first 30 days. **Each step must reference (via @-mention) an existing page or database**.
15
+ 4) A section headed **Feedback Form** that provides **β‰₯ 3** to-do items for new hires to check off.
tasks/notion/standard/company_in_a_box/employee_onboarding/meta.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "employee_onboarding",
3
+ "task_name": "Employee Onboarding",
4
+ "category_id": "company_in_a_box",
5
+ "category_name": "Company In A Box",
6
+ "description": "Build an integrated Employee Onboarding system for the existing Company In A Box page with a checklist database, onboarding hub, and feedback form.",
7
+ "author": "Zijian Wu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "database manipulation",
12
+ "template population",
13
+ "cross-reference linking",
14
+ "status tracking"
15
+ ],
16
+ "mcp": [
17
+ "notion"
18
+ ],
19
+ "meta_data": {
20
+ "stateType": "url",
21
+ "stateContent": null,
22
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Company-In-A-Box-23d81626b6d7800098f3d0e64a706cd8",
23
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/company-in-a-box"
24
+ }
25
+ }
tasks/notion/standard/company_in_a_box/employee_onboarding/verify.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import Dict, Set
3
+ from notion_client import Client
4
+ from tasks.utils import notion_utils
5
+
6
+
7
+ def _check_db_schema(db_props: Dict[str, Dict], required: Dict[str, str]) -> bool:
8
+ """Return True if every required property exists with the correct type."""
9
+ for prop_name, expected_type in required.items():
10
+ if prop_name not in db_props:
11
+ print(
12
+ f"Error: Property '{prop_name}' missing from database.", file=sys.stderr
13
+ )
14
+ return False
15
+ actual_type = db_props[prop_name]["type"]
16
+ if actual_type != expected_type:
17
+ print(
18
+ f"Error: Property '{prop_name}' has type '{actual_type}', expected '{expected_type}'.",
19
+ file=sys.stderr,
20
+ )
21
+ return False
22
+ return True
23
+
24
+
25
+ def verify(notion: Client, main_id: str | None = None) -> bool: # noqa: C901
26
+ """Programmatically verify the onboarding system described in description.md."""
27
+
28
+ DB_TITLE = "Employee Onboarding Checklist"
29
+ HUB_PAGE_TITLE = "Onboarding Hub"
30
+ DEPARTMENT_OPTIONS: Set[str] = {
31
+ "Product",
32
+ "Marketing",
33
+ "Sales",
34
+ "HR",
35
+ "Engineering",
36
+ }
37
+ REQUIRED_DB_PROPERTIES = {
38
+ "Employee Name": "title",
39
+ "Start Date": "date",
40
+ "Department": "select",
41
+ }
42
+
43
+ # 1. Locate onboarding database
44
+ db_id = notion_utils.find_database(notion, DB_TITLE)
45
+ if not db_id:
46
+ print(f"Error: Database '{DB_TITLE}' not found.", file=sys.stderr)
47
+ return False
48
+
49
+ try:
50
+ db_obj = notion.databases.retrieve(database_id=db_id)
51
+ except Exception as exc:
52
+ print(f"Error retrieving database: {exc}", file=sys.stderr)
53
+ return False
54
+
55
+ db_props = db_obj.get("properties", {})
56
+ if not _check_db_schema(db_props, REQUIRED_DB_PROPERTIES):
57
+ return False
58
+
59
+ # Extra: validate select options
60
+ dept_options = {opt["name"] for opt in db_props["Department"]["select"]["options"]}
61
+ if not DEPARTMENT_OPTIONS.issubset(dept_options):
62
+ print(
63
+ f"Error: Department select options must include {sorted(DEPARTMENT_OPTIONS)}. Current: {sorted(dept_options)}",
64
+ file=sys.stderr,
65
+ )
66
+ return False
67
+
68
+ # Check there are at least 3 entries in the database
69
+ try:
70
+ db_pages = notion.databases.query(database_id=db_id).get("results", [])
71
+ except Exception as exc:
72
+ print(f"Error querying database: {exc}", file=sys.stderr)
73
+ return False
74
+ if len(db_pages) < 3:
75
+ print(
76
+ "Error: Less than 3 onboarding entries found in the database.",
77
+ file=sys.stderr,
78
+ )
79
+ return False
80
+
81
+ # 2. Locate Onboarding Hub page
82
+ hub_page_id = notion_utils.find_page(notion, HUB_PAGE_TITLE)
83
+ if not hub_page_id:
84
+ print(f"Error: Page '{HUB_PAGE_TITLE}' not found.", file=sys.stderr)
85
+ return False
86
+
87
+ # 3. Ensure the onboarding database is embedded in the hub page
88
+ embedded_db_id = notion_utils.find_database_in_block(notion, hub_page_id, DB_TITLE)
89
+ if embedded_db_id != db_id:
90
+ print(
91
+ "Error: The Employee Onboarding Checklist database is not embedded in the Onboarding Hub page.",
92
+ file=sys.stderr,
93
+ )
94
+ return False
95
+
96
+ # 4. Analyse blocks within the hub page for linked mentions, timeline, and feedback form
97
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, hub_page_id)
98
+
99
+ seen_link_targets: Set[str] = set()
100
+ numbered_list_count = 0
101
+ todo_count = 0
102
+
103
+ for blk in all_blocks:
104
+ blk_type = blk.get("type")
105
+
106
+ # Direct link-to-page blocks
107
+ if blk_type == "link_to_page":
108
+ info = blk.get("link_to_page", {})
109
+ target_id = info.get("page_id") or info.get("database_id")
110
+ if target_id:
111
+ seen_link_targets.add(target_id)
112
+ continue
113
+
114
+ # Rich-text mentions inside content blocks
115
+ if blk_type in {
116
+ "paragraph",
117
+ "numbered_list_item",
118
+ "bulleted_list_item",
119
+ "to_do",
120
+ }:
121
+ content = blk.get(blk_type, {})
122
+ for rt in content.get("rich_text", []):
123
+ if rt.get("type") == "mention":
124
+ mention = rt.get("mention", {})
125
+ if mention.get("type") in {"page", "database"}:
126
+ target_id = mention.get("page", {}).get("id") or mention.get(
127
+ "database", {}
128
+ ).get("id")
129
+ if target_id:
130
+ seen_link_targets.add(target_id)
131
+
132
+ # Count numbered list items
133
+ if blk_type == "numbered_list_item":
134
+ numbered_list_count += 1
135
+
136
+ # Count to-do items in Feedback Form
137
+ if blk_type == "to_do":
138
+ todo_count += 1
139
+
140
+ if len(seen_link_targets) < 3:
141
+ print(
142
+ "Error: Fewer than 3 linked mentions to benefit policy pages found in the Benefits Overview section.",
143
+ file=sys.stderr,
144
+ )
145
+ return False
146
+
147
+ if numbered_list_count < 7:
148
+ print(
149
+ "Error: Numbered list contains fewer than 7 steps in the 30-Day Timeline section.",
150
+ file=sys.stderr,
151
+ )
152
+ return False
153
+
154
+ if todo_count < 3:
155
+ print(
156
+ "Error: Feedback Form section contains fewer than 3 to-do items.",
157
+ file=sys.stderr,
158
+ )
159
+ return False
160
+
161
+ print(
162
+ "Success: Verified Employee Onboarding Checklist database, Onboarding Hub page, and all required sections."
163
+ )
164
+ return True
165
+
166
+
167
+ def main():
168
+ notion = notion_utils.get_notion_client()
169
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
170
+ if verify(notion, main_id):
171
+ sys.exit(0)
172
+ else:
173
+ sys.exit(1)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
tasks/notion/standard/company_in_a_box/goals_restructure/description.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Please restructure the **Current Goals** section on my **Company In A Box** page as follows:
2
+
3
+ 1. **Add a new goal heading** β€” create a new `heading_3` block titled:
4
+
5
+ `πŸ”„ Digital Transformation Initiative`
6
+
7
+ 2. **Convert all four goal headings to toggles** β€” the three existing goals
8
+ * βš™οΈ Expand Operations to LATAM
9
+ * πŸ› οΈ Push for Enterprise
10
+ * 🩢 Boost Employee Engagement
11
+ * πŸ”„ Digital Transformation Initiative
12
+
13
+ 3. **Move descriptions inside the toggles** β€” every paragraph or list that originally sat directly under a goal heading should become a **child block** of that heading after it is made toggleable.
14
+
15
+ 4. **Preserve content & order** β€” apart from the changes above, do **not** modify the text, formatting, or order of existing goal descriptions.
16
+
17
+ The end result should be a clean **Current Goals** section containing four toggleable goal headings, each with its corresponding details tucked inside.
tasks/notion/standard/company_in_a_box/goals_restructure/meta.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "goals_restructure",
3
+ "task_name": "Goals Restructure",
4
+ "category_id": "company_in_a_box",
5
+ "category_name": "Company In A Box",
6
+ "description": "Restructure the Current Goals section on the Company In A Box page by adding a new goal heading and converting all goal headings to toggles with content inside.",
7
+ "author": "Zijian Wu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "content organization",
12
+ "visual formatting"
13
+ ],
14
+ "mcp": [
15
+ "notion"
16
+ ],
17
+ "meta_data": {
18
+ "stateType": "url",
19
+ "stateContent": null,
20
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Company-In-A-Box-23d81626b6d7800098f3d0e64a706cd8",
21
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/company-in-a-box"
22
+ }
23
+ }
tasks/notion/standard/company_in_a_box/goals_restructure/verify.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import List
3
+ from notion_client import Client
4
+ from tasks.utils import notion_utils
5
+
6
+ # Expected new goal heading text (including emoji)
7
+ NEW_GOAL_HEADING = "πŸ”„ Digital Transformation Initiative"
8
+
9
+ # Section title to look for
10
+ GOALS_SECTION_TITLE = "Current Goals"
11
+
12
+
13
+ def _plain(block) -> str:
14
+ """Return concatenated plain text of a block."""
15
+ return notion_utils.get_block_plain_text(block)
16
+
17
+
18
+ # Some Notion rich-text strings may include non-breaking spaces (\xa0) after emoji.
19
+ # Normalize them to plain spaces so text matching is robust.
20
+ def _normalize_string(s: str) -> str:
21
+ return s.replace("\xa0", " ")
22
+
23
+
24
+ def _is_heading(block) -> bool:
25
+ return block.get("type") in ["heading_1", "heading_2", "heading_3"]
26
+
27
+
28
+ def _is_toggle(block) -> bool:
29
+ """Determine whether a block is a toggle (standard toggle block or toggle-able heading)."""
30
+ btype = block.get("type")
31
+ # In our scenario, goal blocks are headings (usually heading_3) marked as toggleable.
32
+ if btype in ["heading_1", "heading_2", "heading_3"]:
33
+ heading_data = block.get(btype, {})
34
+ return heading_data.get("is_toggleable", False)
35
+ # Some Notion pages may contain classic toggle blocks (type == "toggle"). They are
36
+ # not expected in this task, but keeping this check allows broader compatibility.
37
+ return btype == "toggle"
38
+
39
+
40
+ def _get_children(notion: Client, block_id: str) -> List[dict]:
41
+ """Retrieve **direct** children of a block (no pagination handling needed for small test pages)."""
42
+ try:
43
+ return notion.blocks.children.list(block_id=block_id).get("results", [])
44
+ except Exception:
45
+ return []
46
+
47
+
48
+ def verify(notion: Client, main_id: str = None) -> bool:
49
+ """Verifies that the Company in a Box page has been updated per the task requirements."""
50
+ # 1. Locate the main page
51
+ page_id = None
52
+ if main_id:
53
+ found_id, obj_type = notion_utils.find_page_or_database_by_id(notion, main_id)
54
+ if found_id and obj_type == "page":
55
+ page_id = found_id
56
+
57
+ if not page_id:
58
+ # Try a few case variations just in case
59
+ for title in [
60
+ "Company In A Box",
61
+ ]:
62
+ page_id = notion_utils.find_page(notion, title)
63
+ if page_id:
64
+ break
65
+
66
+ if not page_id:
67
+ print("Error: Could not find the 'Company in a Box' page.", file=sys.stderr)
68
+ return False
69
+
70
+ # 2. Recursively locate the "Current Goals" heading and collect its sibling blocks that
71
+ # constitute the section.
72
+
73
+ def _fetch_children(bid: str) -> List[dict]:
74
+ try:
75
+ return notion.blocks.children.list(block_id=bid).get("results", [])
76
+ except Exception:
77
+ return []
78
+
79
+ goals_section_blocks: List[dict] = []
80
+
81
+ # Breadth-first traversal to find the heading
82
+ queue = [page_id]
83
+ found_parent = None
84
+ found_index = None
85
+
86
+ while queue and found_parent is None:
87
+ parent_id = queue.pop(0)
88
+ children = _fetch_children(parent_id)
89
+ for idx, child in enumerate(children):
90
+ if (
91
+ _is_heading(child)
92
+ and GOALS_SECTION_TITLE.lower()
93
+ in _normalize_string(_plain(child)).lower()
94
+ ):
95
+ found_parent = parent_id
96
+ found_index = idx
97
+ break
98
+ # enqueue grandchildren for further search
99
+ for ch in children:
100
+ if ch.get("has_children"):
101
+ queue.append(ch["id"])
102
+
103
+ if found_parent is None:
104
+ print(
105
+ "Error: Could not find the 'Current Goals' heading anywhere in the page.",
106
+ file=sys.stderr,
107
+ )
108
+ return False
109
+
110
+ # Retrieve siblings once more to get the final list and slice after heading.
111
+ siblings = _fetch_children(found_parent)
112
+ if found_index is None or found_index >= len(siblings):
113
+ print(
114
+ "Error: Internal logic issue when locating Current Goals section.",
115
+ file=sys.stderr,
116
+ )
117
+ return False
118
+
119
+ goals_section_blocks = siblings[found_index + 1 :]
120
+
121
+ if not goals_section_blocks:
122
+ print("Error: 'Current Goals' section appears to be empty.", file=sys.stderr)
123
+ return False
124
+
125
+ # 3. Identify toggle blocks that represent goals
126
+ toggle_blocks = [b for b in goals_section_blocks if _is_toggle(b)]
127
+
128
+ if len(toggle_blocks) != 4:
129
+ print(
130
+ f"Error: Expected 4 toggle blocks for goals, found {len(toggle_blocks)}.",
131
+ file=sys.stderr,
132
+ )
133
+ return False
134
+
135
+ # 4. Ensure the new goal heading exists among the toggles
136
+ found_new_goal = False
137
+ for tb in toggle_blocks:
138
+ if (
139
+ _normalize_string(NEW_GOAL_HEADING).lower()
140
+ in _normalize_string(_plain(tb)).lower()
141
+ ):
142
+ found_new_goal = True
143
+ break
144
+ if not found_new_goal:
145
+ print(
146
+ f"Error: Did not find a toggle block with heading '{NEW_GOAL_HEADING}'.",
147
+ file=sys.stderr,
148
+ )
149
+ return False
150
+
151
+ # 5. Validate that each toggle has at least one child paragraph/description
152
+ for tb in toggle_blocks:
153
+ if (
154
+ _normalize_string(NEW_GOAL_HEADING).lower()
155
+ in _normalize_string(_plain(tb)).lower()
156
+ ):
157
+ # Skip checking the new goal itself, as it does not have a description yet.
158
+ continue
159
+ if not tb.get("has_children", False):
160
+ print(
161
+ f"Error: Toggle '{_normalize_string(_plain(tb))}' has no child blocks (description not moved).",
162
+ file=sys.stderr,
163
+ )
164
+ return False
165
+ children = _get_children(notion, tb["id"])
166
+ # Ensure there is at least one content child (paragraph, list item, etc.)
167
+ content_types = {
168
+ "paragraph",
169
+ "bulleted_list_item",
170
+ "numbered_list_item",
171
+ "to_do",
172
+ "callout",
173
+ "quote",
174
+ }
175
+ if not any(c.get("type") in content_types for c in children):
176
+ print(
177
+ f"Error: Toggle '{_normalize_string(_plain(tb))}' seems to lack any description/content inside it.",
178
+ file=sys.stderr,
179
+ )
180
+ return False
181
+
182
+ # 6. Confirm that there are **no** residual heading_3 blocks (non-toggle) for the goals
183
+ non_toggle_headings = [
184
+ b
185
+ for b in goals_section_blocks
186
+ if b.get("type") == "heading_3" and not _is_toggle(b)
187
+ ]
188
+ if non_toggle_headings:
189
+ titles = [_normalize_string(_plain(b)) for b in non_toggle_headings]
190
+ print(
191
+ f"Error: Found heading_3 blocks that were not converted to toggles: {titles}.",
192
+ file=sys.stderr,
193
+ )
194
+ return False
195
+
196
+ print(
197
+ "Success: Verified goal restructuring with new toggle blocks and descriptions."
198
+ )
199
+ return True
200
+
201
+
202
+ def main():
203
+ notion = notion_utils.get_notion_client()
204
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
205
+ if verify(notion, main_id):
206
+ sys.exit(0)
207
+ else:
208
+ sys.exit(1)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
tasks/notion/standard/company_in_a_box/quarterly_review_dashboard/description.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Create a quarterly business review dashboard in Notion based on the existing **Company In A Box** workspace.
2
+
3
+ **Task Requirements:**
4
+ 1. Inside the **Company Wiki** page you will find a sub-page named **Company Goals**. Extract every departmental objective listed under the four departments β€” **Product**, **Marketing**, **Sales**, and **HR**.
5
+ 2. Under the top-level page **Company In A Box**, create a new child page titled **Q4 2024 Business Review Dashboard**.
6
+ 3. Inside that new page build the following structure (all parts must exist):
7
+ 1. A single **callout** block near the top that summarises progress toward the three *Current Goals* shown on the main page:
8
+ β€’ *LATAM expansion* β€’ *Enterprise push* β€’ *Employee engagement*
9
+ (All three phrases must appear in the callout text.)
10
+ 2. Four separate **section headings** (any heading level) – one for each department (**Product**, **Marketing**, **Sales**, **Human Resources**) – placed below the callout. Under each heading list that department’s objectives in a progress-tracking format (e.g. to-dos, check-box list). Each objective from the **Company Goals** page must appear at least once.
11
+ 3. Add a **database** named **Action Items** with the following properties *exactly*:
12
+ β€’ **Task Name** – title
13
+ β€’ **Department** – select (options: Product, Marketing, Sales, HR)
14
+ β€’ **Priority** – select (options: High, Medium, Low)
15
+ β€’ **Status** – status
16
+ Populate this database with **β‰₯ 5** action-item pages derived from the departmental objectives, making sure every field in each entry is filled:
17
+ β€’ **Task Name** & **Department** must correctly correspond to the underlying objective/department.
18
+ β€’ **Priority** and **Status** can be any allowed value, but they must **not** be left empty.
19
+ 4. Keep the overall visual style consistent with the existing wiki (use headings, dividers, etc.).
tasks/notion/standard/company_in_a_box/quarterly_review_dashboard/meta.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "quarterly_review_dashboard",
3
+ "task_name": "Quarterly Review Dashboard",
4
+ "category_id": "company_in_a_box",
5
+ "category_name": "Company In A Box",
6
+ "description": "Create a quarterly business review dashboard in Notion based on the existing Company In A Box workspace with department objectives and action items database.",
7
+ "author": "Zijian Wu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "database manipulation",
12
+ "data aggregation",
13
+ "report generation",
14
+ "status tracking",
15
+ "template population"
16
+ ],
17
+ "mcp": [
18
+ "notion"
19
+ ],
20
+ "meta_data": {
21
+ "stateType": "url",
22
+ "stateContent": null,
23
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Company-In-A-Box-23d81626b6d7800098f3d0e64a706cd8",
24
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/company-in-a-box"
25
+ }
26
+ }
tasks/notion/standard/company_in_a_box/quarterly_review_dashboard/verify.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import List
3
+ from notion_client import Client
4
+ from tasks.utils import notion_utils
5
+
6
+
7
+ def _contains_keywords(text: str, keywords: List[str]) -> bool:
8
+ lowered = text.lower()
9
+ return all(kw.lower() in lowered for kw in keywords)
10
+
11
+
12
+ def verify(notion: Client, main_id: str = None) -> bool:
13
+ """Programmatically verify that the dashboard page and its contents meet the
14
+ requirements described in description.md.
15
+ """
16
+ DASHBOARD_TITLE = "Q4 2024 Business Review Dashboard"
17
+ PARENT_PAGE_TITLE = "Company In A Box"
18
+ CALL_OUT_KEYWORDS = ["latam", "enterprise", "employee engagement"]
19
+ DEPARTMENTS = ["Product", "Marketing", "Sales", "Human Resources"]
20
+ REQUIRED_DB_PROPERTIES = {
21
+ "Task Name": "title",
22
+ "Department": "select",
23
+ "Priority": "select",
24
+ "Status": "status",
25
+ }
26
+ PRIORITY_OPTIONS = {"High", "Medium", "Low"}
27
+
28
+ # 1. Locate the dashboard page
29
+ page_id = None
30
+ if main_id:
31
+ found_id, obj_type = notion_utils.find_page_or_database_by_id(notion, main_id)
32
+ if found_id and obj_type == "page":
33
+ page_id = found_id
34
+
35
+ if not page_id:
36
+ page_id = notion_utils.find_page(notion, DASHBOARD_TITLE)
37
+
38
+ if not page_id:
39
+ print(f"Error: Page '{DASHBOARD_TITLE}' not found.", file=sys.stderr)
40
+ return False
41
+
42
+ # Optional: ensure it is a child of Company In A Box
43
+ try:
44
+ page_obj = notion.pages.retrieve(page_id=page_id)
45
+ parent_id = page_obj.get("parent", {}).get("page_id")
46
+ if parent_id:
47
+ parent_page = notion.pages.retrieve(page_id=parent_id)
48
+ parent_title_rt = (
49
+ parent_page.get("properties", {}).get("title", {}).get("title", [])
50
+ )
51
+ parent_title = (
52
+ parent_title_rt[0].get("plain_text") if parent_title_rt else None
53
+ )
54
+ if parent_title != PARENT_PAGE_TITLE:
55
+ print(
56
+ f"Error: Dashboard page is not a direct child of '{PARENT_PAGE_TITLE}'.",
57
+ file=sys.stderr,
58
+ )
59
+ return False
60
+ except Exception:
61
+ pass # parent check is best-effort only
62
+
63
+ # 2. Verify callout with keywords
64
+ all_blocks = notion_utils.get_all_blocks_recursively(notion, page_id)
65
+ callout_ok = False
66
+ for block in all_blocks:
67
+ if block.get("type") == "callout":
68
+ callout_text = notion_utils.get_block_plain_text(block)
69
+ if _contains_keywords(callout_text, CALL_OUT_KEYWORDS):
70
+ callout_ok = True
71
+ break
72
+ if not callout_ok:
73
+ print(
74
+ "Error: No callout found that includes all three Current Goal keywords (LATAM, Enterprise, Employee engagement).",
75
+ file=sys.stderr,
76
+ )
77
+ return False
78
+
79
+ # 3. Verify department section headings
80
+ found_depts = set()
81
+ for block in all_blocks:
82
+ if block.get("type") in {"heading_1", "heading_2", "heading_3"}:
83
+ heading_text = notion_utils.get_block_plain_text(block)
84
+ for dept in DEPARTMENTS:
85
+ if dept.lower() in heading_text.lower():
86
+ found_depts.add(dept)
87
+ if set(DEPARTMENTS) != found_depts:
88
+ missing = set(DEPARTMENTS) - found_depts
89
+ print(
90
+ f"Error: Missing department headings: {', '.join(missing)}.",
91
+ file=sys.stderr,
92
+ )
93
+ return False
94
+
95
+ # 4. Verify Action Items database exists and has correct schema
96
+ db_id = notion_utils.find_database_in_block(notion, page_id, "Action Items")
97
+ if not db_id:
98
+ print(
99
+ "Error: Database 'Action Items' not found on the dashboard.",
100
+ file=sys.stderr,
101
+ )
102
+ return False
103
+
104
+ try:
105
+ db = notion.databases.retrieve(database_id=db_id)
106
+ except Exception as exc:
107
+ print(f"Error: Unable to retrieve database: {exc}", file=sys.stderr)
108
+ return False
109
+
110
+ db_props = db.get("properties", {})
111
+ for prop_name, expected_type in REQUIRED_DB_PROPERTIES.items():
112
+ if prop_name not in db_props:
113
+ print(
114
+ f"Error: Property '{prop_name}' missing from database.", file=sys.stderr
115
+ )
116
+ return False
117
+ actual_type = db_props[prop_name]["type"]
118
+ if isinstance(expected_type, list):
119
+ if actual_type not in expected_type:
120
+ print(
121
+ f"Error: Property '{prop_name}' has type '{actual_type}', expected one of {expected_type}.",
122
+ file=sys.stderr,
123
+ )
124
+ return False
125
+ else:
126
+ if actual_type != expected_type:
127
+ print(
128
+ f"Error: Property '{prop_name}' has type '{actual_type}', expected '{expected_type}'.",
129
+ file=sys.stderr,
130
+ )
131
+ return False
132
+ # Extra check for Priority options
133
+ if prop_name == "Priority":
134
+ options = {opt["name"] for opt in db_props[prop_name]["select"]["options"]}
135
+ if not PRIORITY_OPTIONS.issubset(options):
136
+ print(
137
+ f"Error: Priority property options must include High/Medium/Low. Current options: {options}",
138
+ file=sys.stderr,
139
+ )
140
+ return False
141
+
142
+ # 5. Verify at least 5 action items exist
143
+ try:
144
+ pages = notion.databases.query(database_id=db_id).get("results", [])
145
+ except Exception as exc:
146
+ print(f"Error querying database pages: {exc}", file=sys.stderr)
147
+ return False
148
+
149
+ if len(pages) < 5:
150
+ print("Error: Database contains fewer than 5 action items.", file=sys.stderr)
151
+ return False
152
+
153
+ # Optional: Verify Department values valid
154
+ for page in pages:
155
+ props = page.get("properties", {})
156
+
157
+ # Task Name must be non-empty
158
+ title_rt = props.get("Task Name", {}).get("title", [])
159
+ task_name = title_rt[0].get("plain_text") if title_rt else ""
160
+ if not task_name.strip():
161
+ print(
162
+ f"Error: Action item '{page.get('id')}' is missing a Task Name.",
163
+ file=sys.stderr,
164
+ )
165
+ return False
166
+
167
+ # Department must be valid
168
+ dept_select = props.get("Department", {}).get("select", {}).get("name")
169
+ if not dept_select or dept_select not in DEPARTMENTS:
170
+ print(
171
+ f"Error: Action item '{page.get('id')}' has invalid or missing Department value.",
172
+ file=sys.stderr,
173
+ )
174
+ return False
175
+
176
+ # Priority and Status must be set (any value)
177
+ priority_val = props.get("Priority", {}).get("select", {}).get("name")
178
+ status_val = props.get("Status", {}).get("status", {}).get("name")
179
+ if not priority_val or not status_val:
180
+ print(
181
+ f"Error: Action item '{page.get('id')}' must have both Priority and Status set.",
182
+ file=sys.stderr,
183
+ )
184
+ return False
185
+
186
+ print(
187
+ "Success: Verified Business Review Dashboard, departmental sections, callout, and Action Items database with β‰₯5 entries."
188
+ )
189
+ return True
190
+
191
+
192
+ def main():
193
+ notion = notion_utils.get_notion_client()
194
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
195
+ if verify(notion, main_id):
196
+ sys.exit(0)
197
+ else:
198
+ sys.exit(1)
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()
tasks/notion/standard/it_trouble_shooting_hub/asset_retirement_migration/description.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Please restructure the **IT Inventory** database as described below. Your automation will be checked by an automated script, so follow every detail exactly.
2
+
3
+ ---
4
+ Task Steps
5
+ 1. Inside the **IT Trouble Shooting Hub** page, locate the database named **IT Inventory**.
6
+ 2. Query this database and collect every page whose **Status** property is **Expired** or **To be returned**.
7
+ 3. Create a **new full-page database** directly under the same IT Trouble Shooting Hub page called **IT Asset Retirement Queue**.
8
+ 4. Configure this new database so that it contains **exactly** the following properties (spellings and types must match):
9
+ β€’ Serial – title
10
+ β€’ Tags – multi_select
11
+ β€’ Status – select
12
+ β€’ Vendor – select
13
+ β€’ Expiration date – date
14
+ β€’ Retirement Reason – select with option set { **Expired License**, **Hardware Obsolete**, **Security Risk**, **User Offboarding** }
15
+ 5. For every inventory item gathered in step2:
16
+ a. Create a corresponding page in **IT Asset Retirement Queue** and copy over the values of the Serial, Tags, Status, Vendor and Expiration date properties.
17
+ b. Set **Retirement Reason** to one of the four options above (choose the most appropriate).
18
+ c. Archive the original inventory page **after** the new page has been created.
19
+ 6. After all items are migrated:
20
+ a. Update the **description** of the **IT Asset Retirement Queue** database so it is **exactly** `AUTO-GENERATED MIGRATION COMPLETED` (no additional text).
21
+ b. Create a new page under **IT Trouble Shooting Hub** titled **Retirement Migration Log**. Inside this page, add a **callout block** whose text follows the exact pattern:
22
+
23
+ `Successfully migrated <N> assets to the retirement queue on 2025-03-24.`
24
+
25
+ β€’ `<N>` is the total number of items moved.
tasks/notion/standard/it_trouble_shooting_hub/asset_retirement_migration/meta.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "asset_retirement_migration",
3
+ "task_name": "Asset Retirement Migration",
4
+ "category_id": "it_trouble_shooting_hub",
5
+ "category_name": "IT Trouble Shooting Hub",
6
+ "description": "Restructure the IT Inventory database by migrating expired assets to a new IT Asset Retirement Queue database.",
7
+ "author": "Zijian Wu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "database manipulation",
12
+ "automated migration",
13
+ "conditional filtering",
14
+ "data aggregation",
15
+ "report generation"
16
+ ],
17
+ "mcp": [
18
+ "notion"
19
+ ],
20
+ "meta_data": {
21
+ "stateType": "url",
22
+ "stateContent": null,
23
+ "stateUrl": "https://painted-tennis-ebc.notion.site/It-Trouble-Shooting-Hub-23e81626b6d78020aba7eb65ae1cc2d5",
24
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/it-trouble-shooting-hub"
25
+ }
26
+ }
tasks/notion/standard/it_trouble_shooting_hub/asset_retirement_migration/verify.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from typing import Dict, Set
3
+ from notion_client import Client
4
+ from tasks.utils import notion_utils
5
+
6
+
7
+ def _get_database(root_page_id: str, notion: Client, name: str) -> str | None:
8
+ """Helper that finds a child database by title inside a page."""
9
+ return notion_utils.find_database_in_block(notion, root_page_id, name)
10
+
11
+
12
+ def _check_property(props: Dict, name: str, expected_type: str) -> bool:
13
+ if name not in props:
14
+ print(f"Error: Property '{name}' missing in database.", file=sys.stderr)
15
+ return False
16
+ if props[name]["type"] != expected_type:
17
+ print(
18
+ f"Error: Property '{name}' expected type '{expected_type}', found '{props[name]['type']}'.",
19
+ file=sys.stderr,
20
+ )
21
+ return False
22
+ return True
23
+
24
+
25
+ def verify(notion: Client, main_id: str | None = None) -> bool:
26
+ """Verifies that the IT Asset Retirement Queue was created and populated correctly."""
27
+
28
+ # -------------------------------------------------------------------------
29
+ # Resolve the root IT Trouble Shooting Hub page
30
+ # -------------------------------------------------------------------------
31
+ root_page_id = None
32
+ if main_id:
33
+ found_id, obj_type = notion_utils.find_page_or_database_by_id(notion, main_id)
34
+ if found_id and obj_type == "page":
35
+ root_page_id = found_id
36
+
37
+ if not root_page_id:
38
+ root_page_id = notion_utils.find_page(notion, "IT Trouble Shooting Hub")
39
+ if not root_page_id:
40
+ print(
41
+ "Error: Could not locate the 'IT Trouble Shooting Hub' page.",
42
+ file=sys.stderr,
43
+ )
44
+ return False
45
+
46
+ # -------------------------------------------------------------------------
47
+ # Locate the original and new databases
48
+ # -------------------------------------------------------------------------
49
+ inventory_db_id = _get_database(root_page_id, notion, "IT Inventory")
50
+ if not inventory_db_id:
51
+ print("Error: 'IT Inventory' database not found.", file=sys.stderr)
52
+ return False
53
+
54
+ retirement_db_id = _get_database(root_page_id, notion, "IT Asset Retirement Queue")
55
+ if not retirement_db_id:
56
+ print("Error: 'IT Asset Retirement Queue' database not found.", file=sys.stderr)
57
+ return False
58
+
59
+ # -------------------------------------------------------------------------
60
+ # Validate schema of the retirement queue database
61
+ # -------------------------------------------------------------------------
62
+ retirement_db = notion.databases.retrieve(database_id=retirement_db_id)
63
+ r_props = retirement_db["properties"]
64
+
65
+ required_schema = {
66
+ "Serial": "title",
67
+ "Tags": "multi_select",
68
+ "Status": "select",
69
+ "Vendor": "select",
70
+ "Expiration date": "date",
71
+ "Retirement Reason": "select",
72
+ }
73
+
74
+ for pname, ptype in required_schema.items():
75
+ if not _check_property(r_props, pname, ptype):
76
+ return False
77
+
78
+ # Check Retirement Reason options
79
+ expected_reason_options: Set[str] = {
80
+ "Expired License",
81
+ "Hardware Obsolete",
82
+ "Security Risk",
83
+ "User Offboarding",
84
+ }
85
+ actual_options = {
86
+ opt["name"] for opt in r_props["Retirement Reason"]["select"]["options"]
87
+ }
88
+ if actual_options != expected_reason_options:
89
+ print(
90
+ "Error: 'Retirement Reason' select options mismatch.\n"
91
+ f"Expected: {sorted(expected_reason_options)}\n"
92
+ f"Found: {sorted(actual_options)}",
93
+ file=sys.stderr,
94
+ )
95
+ return False
96
+
97
+ # ---------------------------------------------------------------
98
+ # Validate database description starts with required phrase
99
+ # ---------------------------------------------------------------
100
+ desc_rich = retirement_db.get("description", [])
101
+ desc_text = "".join([t.get("plain_text", "") for t in desc_rich])
102
+ required_desc = "AUTO-GENERATED MIGRATION COMPLETED"
103
+ if desc_text.strip() != required_desc:
104
+ print(
105
+ f"Error: Retirement database description must be exactly '{required_desc}'.",
106
+ file=sys.stderr,
107
+ )
108
+ return False
109
+
110
+ # -------------------------------------------------------------------------
111
+ # Validate that inventory items are moved & archived
112
+ # -------------------------------------------------------------------------
113
+ expired_filter = {
114
+ "property": "Status",
115
+ "select": {"equals": "Expired"},
116
+ }
117
+ to_return_filter = {
118
+ "property": "Status",
119
+ "select": {"equals": "To be returned"},
120
+ }
121
+ compound_filter = {"or": [expired_filter, to_return_filter]}
122
+
123
+ # Query for any *active* items that still match these statuses
124
+ remaining_items = notion.databases.query(
125
+ database_id=inventory_db_id,
126
+ filter=compound_filter,
127
+ archived=False,
128
+ ).get("results", [])
129
+
130
+ if remaining_items:
131
+ print(
132
+ f"Error: {len(remaining_items)} 'Expired' / 'To be returned' items still present in IT Inventory.",
133
+ file=sys.stderr,
134
+ )
135
+ return False
136
+
137
+ # There should be at least one entry in the retirement queue
138
+ retirement_pages = notion.databases.query(database_id=retirement_db_id).get(
139
+ "results", []
140
+ )
141
+ expected_serials = {"65XYQ/GB", "36x10PIQ"}
142
+ if len(retirement_pages) != len(expected_serials):
143
+ print(
144
+ f"Error: Expected {len(expected_serials)} retirement pages, found {len(retirement_pages)}.",
145
+ file=sys.stderr,
146
+ )
147
+ return False
148
+
149
+ # Each retirement page must have a Retirement Reason
150
+ serials_seen = set()
151
+ for page in retirement_pages:
152
+ props = page["properties"]
153
+ reason = props.get("Retirement Reason", {}).get("select", {})
154
+ if not reason or reason.get("name") not in expected_reason_options:
155
+ print(
156
+ f"Error: Page {page['id']} missing valid 'Retirement Reason'.",
157
+ file=sys.stderr,
158
+ )
159
+ return False
160
+
161
+ # Collect Serial title
162
+ title_rich = props.get("Serial", {}).get("title", [])
163
+ serial_val = "".join([t.get("plain_text", "") for t in title_rich]).strip()
164
+ serials_seen.add(serial_val)
165
+
166
+ if serials_seen != expected_serials:
167
+ print(
168
+ f"Error: Serial values mismatch. Expected {sorted(expected_serials)}, found {sorted(serials_seen)}.",
169
+ file=sys.stderr,
170
+ )
171
+ return False
172
+
173
+ # -----------------------------------------------------------------
174
+ # Verify the migration log page and callout block contents
175
+ # -----------------------------------------------------------------
176
+ log_page_title = "Retirement Migration Log"
177
+ log_page_id = notion_utils.find_page(notion, log_page_title)
178
+ if not log_page_id:
179
+ print(f"Error: Page '{log_page_title}' not found.", file=sys.stderr)
180
+ return False
181
+
182
+ # Search for a callout block with required pattern
183
+ import re
184
+
185
+ callout_pattern = re.compile(
186
+ r"Successfully migrated (\d+) assets to the retirement queue on 2025-03-24\."
187
+ )
188
+ blocks = notion_utils.get_all_blocks_recursively(notion, log_page_id)
189
+ match_found = False
190
+ for blk in blocks:
191
+ if blk.get("type") == "callout":
192
+ text = notion_utils.get_block_plain_text(blk)
193
+ m = callout_pattern.search(text)
194
+ if m:
195
+ migrated_num = int(m.group(1))
196
+ if migrated_num == len(expected_serials):
197
+ match_found = True
198
+ else:
199
+ print(
200
+ f"Error: Callout reports {migrated_num} assets, but {len(retirement_pages)} retirement pages found.",
201
+ file=sys.stderr,
202
+ )
203
+ return False
204
+ break
205
+ if not match_found:
206
+ print(
207
+ "Error: Required callout block not found in migration log page.",
208
+ file=sys.stderr,
209
+ )
210
+ return False
211
+
212
+ print("Success: All verification criteria satisfied.")
213
+ return True
214
+
215
+
216
+ def main():
217
+ notion = notion_utils.get_notion_client()
218
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
219
+ if verify(notion, main_id):
220
+ sys.exit(0)
221
+ else:
222
+ sys.exit(1)
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
tasks/notion/standard/it_trouble_shooting_hub/security_audit_ticket/description.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Please help me create a comprehensive security audit ticket based on the data already stored in the **IT Trouble Shooting Hub** page.
2
+
3
+ Your automation should:
4
+
5
+ 1. In the **IT Inventory** database, find every item whose **Expiration date** is **before 2023-07-15**.
6
+ 2. In the **IT FAQs** database, look up any FAQ entries that have the **"Security"** tag.
7
+ 3. **Create a new page** inside the **IT Requests** database with **exact title**:
8
+
9
+ `Quarterly Security Audit - Expired Assets Review`
10
+ 4. Set its **Priority** property to **High**.
11
+ 5. Set its **Due** property to **2023-06-22**.
12
+ 6. In the page body, add a bullet-list block that enumerates **each expired inventory item**. **Each bullet item must follow this exact text format (including the dashes):**
13
+
14
+ `<Serial> - <Tag> - <Recommendation>`
15
+
16
+ β€’ `<Serial>` is the item’s Serial value.
17
+ β€’ `<Tag>` is the first tag assigned to the inventory item (e.g., "Laptop").
18
+ β€’ `<Recommendation>` is a brief action you suggest based on the security FAQ entry (any text is acceptable).
19
+
20
+ Example (do **not** copy):
21
+ `ABC123 - Laptop - Renew warranty and enable disk encryption`
tasks/notion/standard/it_trouble_shooting_hub/security_audit_ticket/meta.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "security_audit_ticket",
3
+ "task_name": "Security Audit Ticket",
4
+ "category_id": "it_trouble_shooting_hub",
5
+ "category_name": "IT Trouble Shooting Hub",
6
+ "description": "Create a comprehensive security audit ticket based on expired inventory items and security FAQ entries.",
7
+ "author": "Zijian Wu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "conditional filtering",
12
+ "database manipulation",
13
+ "data aggregation",
14
+ "report generation"
15
+ ],
16
+ "mcp": [
17
+ "notion"
18
+ ],
19
+ "meta_data": {
20
+ "stateType": "url",
21
+ "stateContent": null,
22
+ "stateUrl": "https://painted-tennis-ebc.notion.site/It-Trouble-Shooting-Hub-23e81626b6d78020aba7eb65ae1cc2d5",
23
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/it-trouble-shooting-hub"
24
+ }
25
+ }
tasks/notion/standard/it_trouble_shooting_hub/security_audit_ticket/verify.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+ import re
5
+
6
+
7
+ def _get_title_text(page_properties: dict) -> str:
8
+ """Extract the plain text of the first title property from a page."""
9
+ for prop in page_properties.values():
10
+ if prop.get("type") == "title":
11
+ title_rich = prop.get("title", [])
12
+ if title_rich:
13
+ return title_rich[0].get("plain_text")
14
+ return ""
15
+
16
+
17
+ def verify(notion: Client, main_id: str | None = None) -> bool:
18
+ """Verify that the automation created the expected security audit ticket."""
19
+
20
+ # ----------------------------------------------------------------------------------
21
+ # Locate the root page (IT Trouble Shooting Hub) either via main_id or by title.
22
+ # ----------------------------------------------------------------------------------
23
+ root_page_id = None
24
+ if main_id:
25
+ found_id, object_type = notion_utils.find_page_or_database_by_id(
26
+ notion, main_id
27
+ )
28
+ if found_id and object_type == "page":
29
+ root_page_id = found_id
30
+
31
+ if not root_page_id:
32
+ root_page_id = notion_utils.find_page(notion, "IT Trouble Shooting Hub")
33
+ if not root_page_id:
34
+ print(
35
+ "Error: Could not locate the 'IT Trouble Shooting Hub' page.",
36
+ file=sys.stderr,
37
+ )
38
+ return False
39
+
40
+ # ----------------------------------------------------------------------------------
41
+ # Find the IT Requests database under the root page.
42
+ # ----------------------------------------------------------------------------------
43
+ requests_db_id = notion_utils.find_database_in_block(
44
+ notion, root_page_id, "IT Requests"
45
+ )
46
+ if not requests_db_id:
47
+ print(
48
+ "Error: 'IT Requests' database not found in the workspace.", file=sys.stderr
49
+ )
50
+ return False
51
+
52
+ # ----------------------------------------------------------------------------------
53
+ # Search for the expected ticket inside the IT Requests database.
54
+ # ----------------------------------------------------------------------------------
55
+ expected_title = "Quarterly Security Audit - Expired Assets Review"
56
+ results = notion.databases.query(database_id=requests_db_id).get("results", [])
57
+
58
+ target_page = None
59
+ for page in results:
60
+ title_text = _get_title_text(page.get("properties", {}))
61
+ if title_text == expected_title:
62
+ target_page = page
63
+ break
64
+
65
+ if not target_page:
66
+ print(
67
+ f"Failure: Ticket with title '{expected_title}' was not found in 'IT Requests' database.",
68
+ file=sys.stderr,
69
+ )
70
+ return False
71
+
72
+ props = target_page.get("properties", {})
73
+
74
+ # ----------------------------------------------------------------------------------
75
+ # Validate Priority property.
76
+ # ----------------------------------------------------------------------------------
77
+ priority_value = props.get("Priority", {}).get("select", {}).get("name")
78
+ if priority_value != "High":
79
+ print(
80
+ f"Failure: Expected Priority 'High', found '{priority_value}'.",
81
+ file=sys.stderr,
82
+ )
83
+ return False
84
+
85
+ # ----------------------------------------------------------------------------------
86
+ # Validate Due date property.
87
+ # ----------------------------------------------------------------------------------
88
+ due_date_start = props.get("Due", {}).get("date", {}).get("start")
89
+ expected_due_iso = "2023-06-22"
90
+ if not due_date_start or not due_date_start.startswith(expected_due_iso):
91
+ print(
92
+ f"Failure: Expected Due date '{expected_due_iso}', found '{due_date_start}'.",
93
+ file=sys.stderr,
94
+ )
95
+ return False
96
+
97
+ # ----------------------------------------------------------------------------------
98
+ # Validate the bulleted list contains the correct expired items in required format.
99
+ # ----------------------------------------------------------------------------------
100
+ page_id = target_page["id"]
101
+ blocks = notion.blocks.children.list(block_id=page_id).get("results", [])
102
+ bullet_texts = [
103
+ notion_utils.get_block_plain_text(b)
104
+ for b in blocks
105
+ if b.get("type") == "bulleted_list_item"
106
+ ]
107
+
108
+ expected_items = {
109
+ "192371-8910/54": "Computer Accessory",
110
+ "32x11PIP": "Computer Accessory",
111
+ "76x87PCY": "Laptop",
112
+ "36x10PIQ": "Computer Accessory",
113
+ "65XYQ/GB": "License",
114
+ }
115
+
116
+ if len(bullet_texts) != len(expected_items):
117
+ print(
118
+ f"Failure: Expected {len(expected_items)} bullet items, found {len(bullet_texts)}.",
119
+ file=sys.stderr,
120
+ )
121
+ return False
122
+
123
+ bullet_pattern = re.compile(r"^\s*(.*?)\s+-\s+(.*?)\s+-\s+(.+?)\s*$")
124
+ matched = set()
125
+ for text in bullet_texts:
126
+ m = bullet_pattern.match(text)
127
+ if not m:
128
+ print(
129
+ f"Failure: Bullet item '{text}' does not follow '<Serial> - <Tag> - <Recommendation>' format.",
130
+ file=sys.stderr,
131
+ )
132
+ return False
133
+ serial, tag, advice = m.group(1).strip(), m.group(2).strip(), m.group(3).strip()
134
+ if serial not in expected_items:
135
+ print(
136
+ f"Failure: Unexpected Serial '{serial}' found in bullet list.",
137
+ file=sys.stderr,
138
+ )
139
+ return False
140
+ if expected_items[serial] != tag:
141
+ print(
142
+ f"Failure: Serial '{serial}' expected tag '{expected_items[serial]}', found '{tag}'.",
143
+ file=sys.stderr,
144
+ )
145
+ return False
146
+ if not advice:
147
+ print(
148
+ f"Failure: Bullet item for Serial '{serial}' is missing a recommendation/advice.",
149
+ file=sys.stderr,
150
+ )
151
+ return False
152
+ matched.add(serial)
153
+
154
+ if len(matched) != len(expected_items):
155
+ missing = set(expected_items.keys()) - matched
156
+ print(
157
+ f"Failure: Missing bullet items for serials: {', '.join(missing)}.",
158
+ file=sys.stderr,
159
+ )
160
+ return False
161
+
162
+ print("Success: All verification criteria satisfied.")
163
+ return True
164
+
165
+
166
+ def main():
167
+ notion = notion_utils.get_notion_client()
168
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
169
+ if verify(notion, main_id):
170
+ sys.exit(0)
171
+ else:
172
+ sys.exit(1)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
tasks/notion/standard/it_trouble_shooting_hub/verification_expired_update/description.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **Task Overview**
2
+
3
+ My IT knowledge base contains pages whose verification status has expired:
4
+
5
+ **Task Requirements**
6
+ 1. Locate the database named **"IT Homepage"** inside the main page **"It Trouble Shooting Hub"**.
7
+ 2. Within that database, find every page (except for **"It Inventory"**) where the **Verification** property state contains `expired`.
8
+ 3. For **each** expired page:
9
+ β€’ Insert a **callout block** at the very top (as the first child block) whose rich-text content is:
10
+ `VERIFICATION EXPIRED - This page needs review and re-verification`
11
+ β€’ Set the callout’s icon to ⚠️.
12
+ β€’ Set the callout’s colour to `red_background`.
13
+ 4. Create a new entry in the **"IT Requests"** database with:
14
+ β€’ Title (property **Task name**) **exactly** `Batch Verification Update Required`.
15
+ β€’ **Priority** set to `High`.
16
+ β€’ **Status** set to `In progress`.
17
+ β€’ In the page body add a **bulleted list** where each bullet is a **mention** of the page processed in step 3 (i.e., use the Notion mention object linking to that page).
tasks/notion/standard/it_trouble_shooting_hub/verification_expired_update/meta.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "verification_expired_update",
3
+ "task_name": "Verification Expired Update",
4
+ "category_id": "it_trouble_shooting_hub",
5
+ "category_name": "IT Trouble Shooting Hub",
6
+ "description": "Update pages with expired verification status by adding warning callouts and creating a batch update request.",
7
+ "author": "Zijian Wu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "conditional filtering",
12
+ "visual formatting",
13
+ "database manipulation",
14
+ "cross-reference linking",
15
+ "status tracking"
16
+ ],
17
+ "mcp": [
18
+ "notion"
19
+ ],
20
+ "meta_data": {
21
+ "stateType": "url",
22
+ "stateContent": null,
23
+ "stateUrl": "https://painted-tennis-ebc.notion.site/It-Trouble-Shooting-Hub-23e81626b6d78020aba7eb65ae1cc2d5",
24
+ "stateOriginalUrl": "https://www.notion.so/marketplace/templates/it-trouble-shooting-hub"
25
+ }
26
+ }
tasks/notion/standard/it_trouble_shooting_hub/verification_expired_update/verify.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from notion_client import Client
3
+ from tasks.utils import notion_utils
4
+
5
+ CALL_OUT_TEXT = "VERIFICATION EXPIRED - This page needs review and re-verification"
6
+ CALL_OUT_ICON = "⚠️"
7
+ CALL_OUT_COLOR = "red_background"
8
+ IT_HOMEPAGE_DB_TITLE = "IT Homepage"
9
+ IT_REQUESTS_DB_TITLE = "IT Requests"
10
+ REQUEST_TITLE = "Batch Verification Update Required"
11
+ PRIORITY_HIGH = "High"
12
+ STATUS_IN_PROGRESS = "In progress"
13
+
14
+
15
+ def _get_main_page_id(notion: Client, main_id: str | None) -> str | None:
16
+ """Resolve the main page id starting from CLI arg or by title search."""
17
+ if main_id:
18
+ found_id, obj_type = notion_utils.find_page_or_database_by_id(notion, main_id)
19
+ if found_id and obj_type == "page":
20
+ return found_id
21
+ # Fallback to title search (case-insensitive)
22
+ return notion_utils.find_page(notion, "It Trouble Shooting Hub")
23
+
24
+
25
+ def _fetch_database_id(
26
+ notion: Client, parent_page_id: str, db_title: str
27
+ ) -> str | None:
28
+ """Locate a child database by title inside a given page."""
29
+ return notion_utils.find_database_in_block(notion, parent_page_id, db_title)
30
+
31
+
32
+ def _expired_pages(notion: Client, db_id: str) -> list[dict]:
33
+ """Return list of page objects with Verification.state == 'expired'."""
34
+ # Query all pages (API max 100 per call). If many pages expected, iterate.
35
+ results = notion.databases.query(database_id=db_id).get("results", [])
36
+ expired = []
37
+ for page in results:
38
+ verification_prop = page.get("properties", {}).get("Verification", {})
39
+ state = verification_prop.get("verification", {}).get("state")
40
+ # Skip the IT Inventory database entry
41
+ title_prop = page.get("properties", {}).get("Page", {}).get("title", [])
42
+ title_text = title_prop[0].get("plain_text") if title_prop else ""
43
+ if title_text.strip().lower() == "it inventory":
44
+ continue
45
+
46
+ if state and "expired" in state.lower():
47
+ expired.append(page)
48
+ return expired
49
+
50
+
51
+ def _check_callout_present(notion: Client, page_id: str) -> bool:
52
+ """Verify the specified callout is the first child block of the page."""
53
+ children = notion.blocks.children.list(block_id=page_id, page_size=1).get(
54
+ "results", []
55
+ )
56
+ if not children:
57
+ return False
58
+ first_block = children[0]
59
+ if first_block.get("type") != "callout":
60
+ return False
61
+ data = first_block.get("callout", {})
62
+ # Check color
63
+ if data.get("color") != CALL_OUT_COLOR:
64
+ return False
65
+
66
+ # Check icon
67
+ icon = data.get("icon", {})
68
+ if icon.get("type") != "emoji" or icon.get("emoji") != CALL_OUT_ICON:
69
+ return False
70
+
71
+ # Check text content (callout rich text plain text)
72
+ plain_text = notion_utils.get_block_plain_text(first_block)
73
+ return CALL_OUT_TEXT in plain_text
74
+
75
+
76
+ def _find_request_page(notion: Client, db_id: str) -> dict | None:
77
+ """Find the IT Request page with the expected title."""
78
+ # Use a simple search inside database
79
+ res = notion.databases.query(
80
+ database_id=db_id,
81
+ filter={"property": "Task name", "title": {"equals": REQUEST_TITLE}},
82
+ ).get("results", [])
83
+ return res[0] if res else None
84
+
85
+
86
+ def _check_request_properties(page: dict) -> bool:
87
+ props = page.get("properties", {})
88
+ priority = props.get("Priority", {}).get("select", {}).get("name")
89
+ status = (
90
+ props.get("Status", {}).get("status", {}).get("name")
91
+ if props.get("Status", {}).get("status")
92
+ else props.get("Status", {}).get("select", {}).get("name")
93
+ )
94
+ return priority == PRIORITY_HIGH and status == STATUS_IN_PROGRESS
95
+
96
+
97
+ def _request_page_contains_mentions(
98
+ notion: Client, request_page_id: str, expected_page_ids: list[str]
99
+ ) -> bool:
100
+ children = notion.blocks.children.list(block_id=request_page_id, page_size=100).get(
101
+ "results", []
102
+ )
103
+ bullet_blocks = [b for b in children if b.get("type") == "bulleted_list_item"]
104
+ mentioned_ids: set[str] = set()
105
+ for block in bullet_blocks:
106
+ rich_text = block.get("bulleted_list_item", {}).get("rich_text", [])
107
+ for rt in rich_text:
108
+ if rt.get("type") == "mention":
109
+ mention = rt.get("mention", {})
110
+ if mention.get("type") == "page":
111
+ mentioned_ids.add(mention.get("page", {}).get("id"))
112
+ if len(mentioned_ids) < len(expected_page_ids):
113
+ return False
114
+ return all(pid in mentioned_ids for pid in expected_page_ids)
115
+
116
+
117
+ def verify(notion: Client, main_id: str | None = None) -> bool:
118
+ main_page_id = _get_main_page_id(notion, main_id)
119
+ if not main_page_id:
120
+ print(
121
+ "Error: Could not locate the main page 'It Trouble Shooting Hub'.",
122
+ file=sys.stderr,
123
+ )
124
+ return False
125
+
126
+ # Locate required databases
127
+ it_home_db_id = _fetch_database_id(notion, main_page_id, IT_HOMEPAGE_DB_TITLE)
128
+ it_req_db_id = _fetch_database_id(notion, main_page_id, IT_REQUESTS_DB_TITLE)
129
+ if not all([it_home_db_id, it_req_db_id]):
130
+ print(
131
+ "Error: Required databases not found under the main page.", file=sys.stderr
132
+ )
133
+ return False
134
+
135
+ # Identify expired pages
136
+ expired_pages = _expired_pages(notion, it_home_db_id)
137
+ if not expired_pages:
138
+ print(
139
+ "Failure: No expired pages found; expected at least one for this task.",
140
+ file=sys.stderr,
141
+ )
142
+ return False
143
+
144
+ # Verify callout on each expired page
145
+ for pg in expired_pages:
146
+ pid = pg["id"]
147
+ if not _check_callout_present(notion, pid):
148
+ print(
149
+ f"Failure: Callout missing or incorrect on page {pid}.", file=sys.stderr
150
+ )
151
+ return False
152
+
153
+ # Verify IT Request entry
154
+ request_page = _find_request_page(notion, it_req_db_id)
155
+ if not request_page:
156
+ print(
157
+ "Failure: IT Request 'Batch Verification Update Required' not found.",
158
+ file=sys.stderr,
159
+ )
160
+ return False
161
+ if not _check_request_properties(request_page):
162
+ print("Failure: Priority or Status incorrect on IT Request.", file=sys.stderr)
163
+ return False
164
+
165
+ # Verify bullet list in IT Request body
166
+ expired_titles = []
167
+ for p in expired_pages:
168
+ title_prop = p.get("properties", {}).get("Page", {}).get("title", [])
169
+ title_text = title_prop[0].get("plain_text") if title_prop else None
170
+ if title_text:
171
+ expired_titles.append(title_text)
172
+ expected_page_ids = [p["id"] for p in expired_pages]
173
+ if not _request_page_contains_mentions(
174
+ notion, request_page["id"], expected_page_ids
175
+ ):
176
+ print(
177
+ "Failure: IT Request body does not contain mentions for all affected pages.",
178
+ file=sys.stderr,
179
+ )
180
+ return False
181
+
182
+ print("Success: All verification checks passed.")
183
+ return True
184
+
185
+
186
+ def main():
187
+ notion = notion_utils.get_notion_client()
188
+ main_id = sys.argv[1] if len(sys.argv) > 1 else None
189
+ if verify(notion, main_id):
190
+ sys.exit(0)
191
+ sys.exit(1)
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
tasks/notion/standard/japan_travel_planner/daily_itinerary_overview/description.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Create a comprehensive daily itinerary overview page to help organize my Japan travel plans. I need you to create a new page called 'Daily Itinerary Overview' as a child of the main Japan Travel Planner page.
2
+
3
+ **Task Requirements:**
4
+ 1. Create a new page titled 'Daily Itinerary Overview' as a child page of the main Japan Travel Planner page
5
+ 2. Query the Travel Itinerary database to retrieve all activities
6
+ 3. Structure the page with the following specific format:
7
+ - Add a heading_1 block with text "πŸ“… Daily Itinerary Overview"
8
+ - Add a heading_2 block with text "πŸ“Š Trip Summary"
9
+ - Under Trip Summary, add a paragraph listing the total number of visited activities
10
+ - Create heading_2 blocks for "πŸŒ… Day 1", "πŸŒ† Day 2", and "πŸŒƒ Day 3"
11
+ - Under each day heading, list the activities scheduled for that day in to do list
12
+ - Each activity (use To-do list) should show: Activity Name - City (if available), for example, "Osaka Castle - Osaka". Check it if it's visited.
13
+ 4. The summary paragraph must contain the exact text "Total activities visited (from Day 1 to Day 3): [NUMBER]" where [NUMBER] is the actual count.
14
+ 5. Ensure all headings use the exact emoji and text format specified above
tasks/notion/standard/japan_travel_planner/daily_itinerary_overview/meta.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "task_id": "daily_itinerary_overview",
3
+ "task_name": "Daily Itinerary Overview",
4
+ "category_id": "japan_travel_planner",
5
+ "category_name": "Japan Travel Planner",
6
+ "description": "Create a comprehensive daily itinerary overview page to organize Japan travel plans with structured day-by-day activities.",
7
+ "author": "Xiangyan Liu",
8
+ "created_at": "2025-07-27",
9
+ "difficulty": "L3",
10
+ "tags": [
11
+ "database manipulation",
12
+ "data aggregation",
13
+ "report generation",
14
+ "visual formatting",
15
+ "status tracking"
16
+ ],
17
+ "mcp": [
18
+ "notion"
19
+ ],
20
+ "meta_data": {
21
+ "stateType": "url",
22
+ "stateContent": null,
23
+ "stateUrl": "https://painted-tennis-ebc.notion.site/Japan-Travel-Planner-23181626b6d781c4b6bedb12786b5abe"
24
+ }
25
+ }