project-agent 0.2.1__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. {project_agent-0.2.1 → project_agent-0.2.4}/PKG-INFO +1 -1
  2. {project_agent-0.2.1 → project_agent-0.2.4}/pyproject.toml +1 -1
  3. project_agent-0.2.4/src/project_agent/application/context/add_project_member.py +50 -0
  4. project_agent-0.2.4/src/project_agent/application/context/get_project_context.py +128 -0
  5. project_agent-0.2.4/src/project_agent/application/context/project_summary.py +160 -0
  6. project_agent-0.2.4/src/project_agent/application/context/set_project_owner.py +59 -0
  7. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc/render.py +99 -17
  8. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc/sync.py +9 -2
  9. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc/writeback.py +5 -2
  10. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc_hub/manage.py +4 -2
  11. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc_hub/read.py +1 -0
  12. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/create_version.py +2 -2
  13. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/finalize_version_closure.py +9 -0
  14. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/propose_change.py +6 -0
  15. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/read.py +18 -83
  16. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/signoff_proposal.py +2 -0
  17. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/update_version.py +3 -0
  18. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/report/build.py +173 -76
  19. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/scheduled/run.py +252 -36
  20. project_agent-0.2.4/src/project_agent/application/shared/project_participants.py +123 -0
  21. project_agent-0.2.4/src/project_agent/application/shared/user_refs.py +105 -0
  22. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/doc/models.py +6 -3
  23. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/mind/models.py +7 -13
  24. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/monitoring/models.py +26 -17
  25. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/project_context/models.py +63 -0
  26. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/report/models.py +21 -4
  27. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/scheduled/models.py +10 -1
  28. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/shared/documents.py +2 -1
  29. project_agent-0.2.4/src/project_agent/domain/shared/project_participants.py +14 -0
  30. project_agent-0.2.4/src/project_agent/domain/shared/user_refs.py +31 -0
  31. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/clock/workday_clock.py +15 -0
  32. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/db/repositories.py +62 -8
  33. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/db/schema.py +93 -28
  34. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/db/tables.py +5 -2
  35. project_agent-0.2.4/src/project_agent/infrastructure/feature_flags.py +18 -0
  36. project_agent-0.2.4/src/project_agent/interface/cli/commands/_monitoring_support.py +6 -0
  37. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/context.py +362 -8
  38. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/doc.py +55 -5
  39. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/doc_hub.py +16 -8
  40. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/mind.py +73 -1
  41. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/monitoring.py +3 -4
  42. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/report.py +4 -3
  43. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/scheduled.py +15 -2
  44. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/task_event_notify.py +1 -1
  45. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/helptext.py +2 -2
  46. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent.egg-info/PKG-INFO +1 -1
  47. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent.egg-info/SOURCES.txt +14 -0
  48. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_bootstrap_checkpoint.py +3 -1
  49. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_cli_help.py +39 -2
  50. project_agent-0.2.4/tests/test_context_add_project_member.py +217 -0
  51. project_agent-0.2.4/tests/test_context_get_project.py +241 -0
  52. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_get_project_pending_states.py +4 -19
  53. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_list_projects.py +75 -11
  54. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_list_tasks.py +9 -9
  55. project_agent-0.2.4/tests/test_context_project_binding.py +154 -0
  56. project_agent-0.2.4/tests/test_context_project_summary.py +837 -0
  57. project_agent-0.2.4/tests/test_context_set_project_owner.py +174 -0
  58. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_db_migration.py +67 -0
  59. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_db_schema.py +13 -7
  60. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_db_session.py +31 -0
  61. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_doc_hub.py +5 -1
  62. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_doc_render_planned_versions.py +3 -0
  63. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_finalize_version_closure.py +77 -0
  64. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_mind_governance.py +59 -0
  65. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_mind_proposal_extensions.py +9 -4
  66. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_mind_read.py +185 -6
  67. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_monitoring_run.py +85 -0
  68. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_project_group_boundary_guard.py +110 -1
  69. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_project_group_routing_contracts.py +48 -25
  70. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_report_build.py +179 -14
  71. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_runtime_bundles.py +18 -0
  72. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_runtime_output_hygiene_contracts.py +1 -16
  73. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_scheduled_run.py +498 -56
  74. project_agent-0.2.1/src/project_agent/application/context/get_project_context.py +0 -59
  75. {project_agent-0.2.1 → project_agent-0.2.4}/README.md +0 -0
  76. {project_agent-0.2.1 → project_agent-0.2.4}/setup.cfg +0 -0
  77. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/__init__.py +0 -0
  78. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/__init__.py +0 -0
  79. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/bootstrap/__init__.py +0 -0
  80. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/bootstrap/checkpoint.py +0 -0
  81. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/bootstrap/init.py +0 -0
  82. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/bootstrap/loop_state.py +0 -0
  83. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/__init__.py +0 -0
  84. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/binding_normalization.py +0 -0
  85. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/get_project_owner.py +0 -0
  86. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/get_task.py +0 -0
  87. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/get_user_context.py +0 -0
  88. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/list_projects.py +0 -0
  89. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/list_tasks.py +0 -0
  90. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/resolve_binding.py +0 -0
  91. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/resolve_group.py +0 -0
  92. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/resolve_user.py +0 -0
  93. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/search_user.py +0 -0
  94. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/context/upsert_user_identity.py +0 -0
  95. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc/__init__.py +0 -0
  96. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/doc_hub/__init__.py +0 -0
  97. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/loop/__init__.py +0 -0
  98. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/loop/enqueue.py +0 -0
  99. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/loop/run.py +0 -0
  100. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/__init__.py +0 -0
  101. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/delete_task.py +0 -0
  102. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/milestones.py +0 -0
  103. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/planned_versions.py +0 -0
  104. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/set_tracking.py +0 -0
  105. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/snooze_task_reminder.py +0 -0
  106. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/switch_version.py +0 -0
  107. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/mind/write_task_update.py +0 -0
  108. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/monitoring/__init__.py +0 -0
  109. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/monitoring/evaluate_followups.py +0 -0
  110. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/monitoring/plan.py +0 -0
  111. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/monitoring/run.py +0 -0
  112. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/report/__init__.py +0 -0
  113. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/scheduled/__init__.py +0 -0
  114. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/version_closure/__init__.py +0 -0
  115. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/application/version_closure/evaluate.py +0 -0
  116. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/__init__.py +0 -0
  117. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/bootstrap/__init__.py +0 -0
  118. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/bootstrap/models.py +0 -0
  119. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/doc/__init__.py +0 -0
  120. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/doc_hub/__init__.py +0 -0
  121. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/doc_hub/models.py +0 -0
  122. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/loop/__init__.py +0 -0
  123. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/loop/models.py +0 -0
  124. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/mind/__init__.py +0 -0
  125. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/monitoring/__init__.py +0 -0
  126. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/monitoring/evaluator.py +0 -0
  127. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/project_context/__init__.py +0 -0
  128. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/report/__init__.py +0 -0
  129. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/scheduled/__init__.py +0 -0
  130. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/shared/__init__.py +0 -0
  131. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/shared/models.py +0 -0
  132. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/shared/project_mind_derived.py +0 -0
  133. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/shared/project_mind_readiness.py +0 -0
  134. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/shared/statuses.py +0 -0
  135. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/user/__init__.py +0 -0
  136. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/user/models.py +0 -0
  137. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/version_closure/__init__.py +0 -0
  138. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/version_closure/evaluator.py +0 -0
  139. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/domain/version_closure/models.py +0 -0
  140. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/__init__.py +0 -0
  141. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/clock/__init__.py +0 -0
  142. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/db/__init__.py +0 -0
  143. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/db/migration.py +0 -0
  144. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/infrastructure/db/session.py +0 -0
  145. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/__init__.py +0 -0
  146. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/__init__.py +0 -0
  147. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/app.py +0 -0
  148. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/__init__.py +0 -0
  149. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/bootstrap.py +0 -0
  150. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/db.py +0 -0
  151. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/loop.py +0 -0
  152. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/project_boundary.py +0 -0
  153. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/upgrade.py +0 -0
  154. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent/interface/cli/commands/version_closure.py +0 -0
  155. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent.egg-info/dependency_links.txt +0 -0
  156. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent.egg-info/entry_points.txt +0 -0
  157. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent.egg-info/requires.txt +0 -0
  158. {project_agent-0.2.1 → project_agent-0.2.4}/src/project_agent.egg-info/top_level.txt +0 -0
  159. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_cli_version.py +0 -0
  160. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_get_project_owner.py +0 -0
  161. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_resolve_binding.py +0 -0
  162. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_search_user.py +0 -0
  163. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_context_upsert_user_identity.py +0 -0
  164. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_loop_jobs.py +0 -0
  165. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_mind_proposal_signoff.py +0 -0
  166. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_monitoring_plan.py +0 -0
  167. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_overview_sync_hints.py +0 -0
  168. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_pending_state_idempotency.py +0 -0
  169. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_release_versioning.py +0 -0
  170. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_report_persistence.py +0 -0
  171. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_version_closure_trigger.py +0 -0
  172. {project_agent-0.2.1 → project_agent-0.2.4}/tests/test_version_status_auto_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: project-agent
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
4
  Summary: Project Agent Python CLI core
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer<1,>=0.12
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "project-agent"
7
- version = "0.2.1"
7
+ version = "0.2.4"
8
8
  description = "Project Agent Python CLI core"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -0,0 +1,50 @@
1
+ from typing import Any, Protocol
2
+
3
+ from project_agent.domain.project_context.models import AddProjectMemberResult, ProjectParticipantSummary
4
+
5
+
6
+ class ProjectStore(Protocol):
7
+ def find_by_id(self, project_id: str) -> Any: ...
8
+
9
+
10
+ class UserStore(Protocol):
11
+ def find_by_id(self, user_id: str) -> Any: ...
12
+
13
+
14
+ class ParticipantWriteStore(Protocol):
15
+ def upsert_participant(self, payload: dict) -> dict: ...
16
+
17
+
18
+ def add_project_member(
19
+ *,
20
+ project_id: str,
21
+ user_id: str,
22
+ project_store: ProjectStore,
23
+ user_store: UserStore,
24
+ participant_store: ParticipantWriteStore,
25
+ ) -> AddProjectMemberResult:
26
+ if not project_store.find_by_id(project_id):
27
+ raise ValueError(f"project not found: {project_id}")
28
+ if not user_store.find_by_id(user_id):
29
+ raise ValueError(f"user not found: {user_id}")
30
+
31
+ participant = participant_store.upsert_participant(
32
+ {
33
+ "project_id": project_id,
34
+ "user_id": user_id,
35
+ "role": "member",
36
+ "status": "active",
37
+ }
38
+ )
39
+ return AddProjectMemberResult(
40
+ status="ok",
41
+ participant=ProjectParticipantSummary(
42
+ participant_id=participant["participant_id"],
43
+ project_id=participant["project_id"],
44
+ user_id=participant["user_id"],
45
+ role=participant["role"],
46
+ status=participant["status"],
47
+ created_at=participant.get("created_at"),
48
+ updated_at=participant.get("updated_at"),
49
+ ),
50
+ )
@@ -0,0 +1,128 @@
1
+ from typing import Any, Protocol
2
+
3
+ from project_agent.application.shared.project_participants import build_project_participant_summaries
4
+ from project_agent.application.shared.user_refs import build_structured_user_ref
5
+ from project_agent.domain.project_context.models import ProjectContext
6
+ from project_agent.domain.shared.project_mind_derived import build_project_intro, collect_participants
7
+
8
+
9
+ class ProjectStore(Protocol):
10
+ def find_by_id(self, project_id: str) -> Any: ...
11
+
12
+
13
+ class BindingStore(Protocol):
14
+ def list_by_project_id(self, project_id: str) -> list[Any]: ...
15
+
16
+
17
+ class PendingStateStore(Protocol):
18
+ def list_open_by_project_id(self, project_id: str) -> list[Any]: ...
19
+
20
+
21
+ class DocumentStore(Protocol):
22
+ def list_by_project_id(self, project_id: str) -> list[Any]: ...
23
+
24
+
25
+ class ParticipantStore(Protocol):
26
+ def list_by_project_id(self, project_id: str) -> list[Any]: ...
27
+
28
+
29
+ class VersionStore(Protocol):
30
+ def list_by_project_id(self, project_id: str) -> list[Any]: ...
31
+
32
+
33
+ class TaskStore(Protocol):
34
+ def list_by_project_id(self, project_id: str, task_id: str | None = None) -> list[Any]: ...
35
+
36
+
37
+ class UserStore(Protocol):
38
+ def find_by_id(self, user_id: str) -> Any: ...
39
+
40
+
41
+ class UserIdentityStore(Protocol):
42
+ def list_by_user_id(self, user_id: str) -> list[Any]: ...
43
+
44
+
45
+ def get_project_context(
46
+ *,
47
+ project_id: str,
48
+ channel: str | None = None,
49
+ project_store: ProjectStore,
50
+ binding_store: BindingStore | None = None,
51
+ pending_state_store: PendingStateStore | None = None,
52
+ document_store: DocumentStore | None = None,
53
+ participant_store: ParticipantStore | None = None,
54
+ version_store: VersionStore | None = None,
55
+ task_store: TaskStore | None = None,
56
+ user_store: UserStore | None = None,
57
+ identity_store: UserIdentityStore | None = None,
58
+ ) -> ProjectContext | None:
59
+ project = project_store.find_by_id(project_id)
60
+
61
+ if not project:
62
+ return None
63
+
64
+ bindings = []
65
+ if binding_store is not None:
66
+ bindings = binding_store.list_by_project_id(project_id)
67
+
68
+ pending_states = []
69
+ if pending_state_store is not None:
70
+ pending_states = pending_state_store.list_open_by_project_id(project_id)
71
+
72
+ documents = []
73
+ if document_store is not None:
74
+ documents = document_store.list_by_project_id(project_id)
75
+
76
+ participant_rows = []
77
+ if participant_store is not None:
78
+ participant_rows = participant_store.list_by_project_id(project_id)
79
+
80
+ versions = []
81
+ if version_store is not None:
82
+ versions = version_store.list_by_project_id(project_id)
83
+
84
+ tasks = []
85
+ if task_store is not None:
86
+ tasks = task_store.list_by_project_id(project_id)
87
+
88
+ owner_user = build_structured_user_ref(
89
+ user_id=project.get("owner_user_id"),
90
+ user_store=user_store,
91
+ user_identity_store=identity_store,
92
+ identity_cache={},
93
+ user_cache={},
94
+ )
95
+ user_cache: dict[str, dict | None] = {}
96
+ identity_cache: dict[str, list[dict]] = {}
97
+ participant_summaries = build_project_participant_summaries(
98
+ participant_user_ids=collect_participants(
99
+ versions=versions,
100
+ tasks=tasks,
101
+ participants=participant_rows,
102
+ ),
103
+ participant_rows=participant_rows,
104
+ owner_user_id=project.get("owner_user_id"),
105
+ user_store=user_store,
106
+ user_identity_store=identity_store,
107
+ channel=channel,
108
+ user_cache=user_cache,
109
+ identity_cache=identity_cache,
110
+ )
111
+
112
+ return ProjectContext(
113
+ project_id=project_id,
114
+ owner_user=owner_user,
115
+ primary_goal=project.get("primary_goal"),
116
+ project_intro=build_project_intro(project=project, versions=versions, tasks=tasks),
117
+ lifecycle_status=project["lifecycle_status"],
118
+ mind_stage=project["mind_stage"],
119
+ current_revision_id=project.get("current_revision_id"),
120
+ current_version_id=project.get("current_version_id"),
121
+ tracking_mode=project.get("tracking_mode") or "active",
122
+ tracking_paused_reason=project.get("tracking_paused_reason"),
123
+ tracking_paused_at=project.get("tracking_paused_at"),
124
+ bindings=bindings,
125
+ participants=participant_summaries,
126
+ pending_states=pending_states,
127
+ documents=documents,
128
+ )
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, date, datetime
4
+ from typing import Any, Protocol
5
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
6
+
7
+ from project_agent.domain.project_context.models import (
8
+ PersonalTaskSummary,
9
+ ProjectSummaryResult,
10
+ TeamTaskSummary,
11
+ )
12
+ from project_agent.domain.shared.statuses import (
13
+ is_done_status,
14
+ is_terminal_task_status,
15
+ normalize_task_status,
16
+ )
17
+
18
+
19
+ class ProjectStore(Protocol):
20
+ def find_by_id(self, project_id: str) -> Any: ...
21
+
22
+
23
+ class TaskStore(Protocol):
24
+ def list_by_project_id(self, project_id: str, task_id: str | None = None) -> list[Any]: ...
25
+
26
+
27
+ def build_project_summary(
28
+ *,
29
+ project_id: str,
30
+ user_id: str,
31
+ timezone: str | None,
32
+ project_store: ProjectStore,
33
+ task_store: TaskStore,
34
+ ) -> ProjectSummaryResult | None:
35
+ project = project_store.find_by_id(project_id)
36
+ if not project:
37
+ return None
38
+
39
+ tz = resolve_summary_timezone(timezone or project.get("timezone") or "UTC")
40
+ today = datetime.now(tz).date()
41
+ tasks = task_store.list_by_project_id(project_id)
42
+
43
+ return ProjectSummaryResult(
44
+ project_id=project["project_id"],
45
+ project_name=project["project_name"],
46
+ team_task=_build_team_task_summary(tasks, today=today, tz=tz),
47
+ personal=_build_personal_task_summary(tasks, user_id=user_id, today=today, tz=tz),
48
+ updated_at=_utc_millis(project.get("last_updated_at")),
49
+ )
50
+
51
+
52
+ def resolve_summary_timezone(value: str) -> ZoneInfo:
53
+ try:
54
+ return ZoneInfo(value)
55
+ except ZoneInfoNotFoundError as exc:
56
+ raise ValueError(f"unknown timezone: {value}") from exc
57
+
58
+
59
+ def _build_team_task_summary(
60
+ tasks: list[Any],
61
+ *,
62
+ today: date,
63
+ tz: ZoneInfo,
64
+ ) -> TeamTaskSummary:
65
+ new_assigned_today_tasks = [
66
+ task for task in tasks if _is_today(_task_value(task, "created_at"), today, tz)
67
+ ]
68
+ new_assigned_today = len(new_assigned_today_tasks)
69
+ if new_assigned_today_tasks and all(
70
+ is_done_status(_task_value(task, "task_status")) for task in new_assigned_today_tasks
71
+ ):
72
+ new_assigned_today = 0
73
+
74
+ return TeamTaskSummary(
75
+ status="HAS_TASK" if tasks else "NO_TASK",
76
+ new_assigned_today=new_assigned_today,
77
+ completed_today=sum(
78
+ 1
79
+ for task in tasks
80
+ if is_done_status(_task_value(task, "task_status"))
81
+ and _is_today(_task_value(task, "updated_at") or _task_value(task, "last_updated_at"), today, tz)
82
+ ),
83
+ in_progress=sum(
84
+ 1
85
+ for task in tasks
86
+ if normalize_task_status(_task_value(task, "task_status")) == "in_progress"
87
+ ),
88
+ )
89
+
90
+
91
+ def _build_personal_task_summary(
92
+ tasks: list[Any],
93
+ *,
94
+ user_id: str,
95
+ today: date,
96
+ tz: ZoneInfo,
97
+ ) -> PersonalTaskSummary:
98
+ personal_tasks = [task for task in tasks if _task_value(task, "owner_user_id") == user_id]
99
+ return PersonalTaskSummary(
100
+ new_tasks_today=sum(
101
+ 1 for task in personal_tasks if _is_today(_task_value(task, "created_at"), today, tz)
102
+ ),
103
+ due_tasks_today=sum(
104
+ 1
105
+ for task in personal_tasks
106
+ if _task_due_date(_task_value(task, "end_date"), tz=tz) == today
107
+ and not is_terminal_task_status(_task_value(task, "task_status"))
108
+ ),
109
+ )
110
+
111
+
112
+ def _task_value(task: Any, key: str) -> Any:
113
+ if isinstance(task, dict):
114
+ return task.get(key)
115
+ return getattr(task, key, None)
116
+
117
+
118
+ def _is_today(value: Any, today: date, tz: ZoneInfo) -> bool:
119
+ parsed = _parse_datetime(value, tz=tz)
120
+ return parsed is not None and parsed.date() == today
121
+
122
+
123
+ def _task_due_date(value: Any, *, tz: ZoneInfo) -> date | None:
124
+ if value is None:
125
+ return None
126
+ if isinstance(value, date) and not isinstance(value, datetime):
127
+ return value
128
+ token = str(value).strip()
129
+ if not token:
130
+ return None
131
+ try:
132
+ return date.fromisoformat(token[:10])
133
+ except ValueError:
134
+ parsed = _parse_datetime(token, tz=tz)
135
+ return parsed.date() if parsed is not None else None
136
+
137
+
138
+ def _parse_datetime(value: Any, *, tz: ZoneInfo) -> datetime | None:
139
+ if value is None:
140
+ return None
141
+ if isinstance(value, datetime):
142
+ parsed = value
143
+ else:
144
+ token = str(value).strip()
145
+ if not token:
146
+ return None
147
+ try:
148
+ parsed = datetime.fromisoformat(token.replace("Z", "+00:00"))
149
+ except ValueError:
150
+ return None
151
+ if parsed.tzinfo is None:
152
+ parsed = parsed.replace(tzinfo=UTC)
153
+ return parsed.astimezone(tz)
154
+
155
+
156
+ def _utc_millis(value: Any) -> int | None:
157
+ parsed = _parse_datetime(value, tz=ZoneInfo("UTC"))
158
+ if parsed is None:
159
+ return None
160
+ return int(parsed.timestamp() * 1000)
@@ -0,0 +1,59 @@
1
+ from typing import Any, Protocol
2
+
3
+ from project_agent.domain.project_context.models import SetProjectOwnerResult
4
+
5
+
6
+ class ProjectStore(Protocol):
7
+ def find_by_id(self, project_id: str) -> Any: ...
8
+
9
+ def update_owner_user_id(self, *, project_id: str, owner_user_id: str) -> dict: ...
10
+
11
+
12
+ class UserStore(Protocol):
13
+ def find_by_id(self, user_id: str) -> Any: ...
14
+
15
+
16
+ class ParticipantWriteStore(Protocol):
17
+ def upsert_participant(self, payload: dict) -> dict: ...
18
+
19
+
20
+ def set_project_owner(
21
+ *,
22
+ project_id: str,
23
+ user_id: str,
24
+ project_store: ProjectStore,
25
+ user_store: UserStore,
26
+ participant_store: ParticipantWriteStore,
27
+ ) -> SetProjectOwnerResult:
28
+ project = project_store.find_by_id(project_id)
29
+ if not project:
30
+ raise ValueError(f"project not found: {project_id}")
31
+ if not user_store.find_by_id(user_id):
32
+ raise ValueError(f"user not found: {user_id}")
33
+
34
+ previous_owner_user_id = project.get("owner_user_id")
35
+ project_store.update_owner_user_id(project_id=project_id, owner_user_id=user_id)
36
+ participant_store.upsert_participant(
37
+ {
38
+ "project_id": project_id,
39
+ "user_id": user_id,
40
+ "role": "owner",
41
+ "status": "active",
42
+ }
43
+ )
44
+ if previous_owner_user_id and previous_owner_user_id != user_id:
45
+ participant_store.upsert_participant(
46
+ {
47
+ "project_id": project_id,
48
+ "user_id": previous_owner_user_id,
49
+ "role": "member",
50
+ "status": "active",
51
+ }
52
+ )
53
+
54
+ return SetProjectOwnerResult(
55
+ status="ok",
56
+ project_id=project_id,
57
+ owner_user_id=user_id,
58
+ previous_owner_user_id=previous_owner_user_id,
59
+ )
@@ -34,6 +34,10 @@ class ParticipantStore(Protocol):
34
34
  def list_by_project_id(self, project_id: str) -> list[Any]: ...
35
35
 
36
36
 
37
+ class UserStore(Protocol):
38
+ def find_by_id(self, user_id: str) -> Any: ...
39
+
40
+
37
41
  class DocumentStore(Protocol):
38
42
  def list_by_project_id(self, project_id: str) -> list[Any]: ...
39
43
 
@@ -74,9 +78,12 @@ def render_project_document(
74
78
  document_store: DocumentStore | None = None,
75
79
  doc_hub_entry_store: DocHubEntryStore | None = None,
76
80
  channel: str | None = None,
81
+ provider: str | None = None,
82
+ user_store: UserStore | None = None,
77
83
  user_identity_store: Any | None = None,
78
84
  ) -> DocRenderResult | None:
79
85
  del template_ref
86
+ provider = provider or channel
80
87
 
81
88
  project = project_store.find_by_id(project_id)
82
89
  if not project:
@@ -97,7 +104,9 @@ def render_project_document(
97
104
  tasks=tasks,
98
105
  version_id=version_id,
99
106
  )
100
- _enrich_sections_with_channel_identities(sections, channel, user_identity_store)
107
+ _enrich_sections_with_user_display_values(sections, user_store)
108
+ if provider == "feishu":
109
+ _enrich_sections_with_channel_identities(sections, "feishu", user_identity_store)
101
110
  return _with_render_hash(DocRenderResult(
102
111
  project_id=project_id,
103
112
  doc_kind=doc_kind,
@@ -190,7 +199,9 @@ def render_project_document(
190
199
  ),
191
200
  ]
192
201
  title = project["project_name"]
193
- _enrich_sections_with_channel_identities(sections, channel, user_identity_store)
202
+ _enrich_sections_with_user_display_values(sections, user_store)
203
+ if provider == "feishu":
204
+ _enrich_sections_with_channel_identities(sections, "feishu", user_identity_store)
194
205
  return _with_render_hash(DocRenderResult(
195
206
  project_id=project_id,
196
207
  doc_kind=doc_kind,
@@ -204,7 +215,7 @@ def render_project_document(
204
215
  project_id=project_id,
205
216
  doc_hub_entry_store=doc_hub_entry_store,
206
217
  )
207
- sections = _build_docs_index_sections(entries=entries, relations=relations, channel=channel)
218
+ sections = _build_docs_index_sections(entries=entries, relations=relations, provider=provider)
208
219
  title = f"{project['project_name']} Docs Index"
209
220
  return _with_render_hash(DocRenderResult(
210
221
  project_id=project_id,
@@ -222,11 +233,11 @@ def render_project_document(
222
233
  planned_versions = _planned_versions(versions=versions, current_version_id=current_version_id)
223
234
  past_versions = _past_versions(versions=versions)
224
235
  documents = document_store.list_by_project_id(project_id) if document_store is not None else []
225
- snapshot_links = _version_snapshot_links(documents=documents, channel=channel)
236
+ snapshot_links = _version_snapshot_links(documents=documents, provider=provider)
226
237
  docs_index_url = _single_document_link(
227
238
  documents=documents,
228
239
  doc_kind="docs_index",
229
- channel=channel,
240
+ provider=provider,
230
241
  )
231
242
  sections = _build_overview_sections(
232
243
  project=project,
@@ -241,7 +252,9 @@ def render_project_document(
241
252
  docs_index_url=docs_index_url,
242
253
  )
243
254
  title = f"{project['project_name']} Overview"
244
- _enrich_sections_with_channel_identities(sections, channel, user_identity_store)
255
+ _enrich_sections_with_user_display_values(sections, user_store)
256
+ if provider == "feishu":
257
+ _enrich_sections_with_channel_identities(sections, "feishu", user_identity_store)
245
258
  return _with_render_hash(DocRenderResult(
246
259
  project_id=project_id,
247
260
  doc_kind=doc_kind,
@@ -265,18 +278,38 @@ def _load_doc_hub_index_inputs(
265
278
 
266
279
 
267
280
  def _with_render_hash(result: DocRenderResult) -> DocRenderResult:
268
- payload = result.model_dump(mode="json", by_alias=True, exclude={"render_hash", "status"})
281
+ payload = _strip_render_hash_ignored_hints(
282
+ result.model_dump(mode="json", by_alias=True, exclude={"render_hash", "status"})
283
+ )
269
284
  encoded = json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
270
285
  return result.model_copy(update={"render_hash": hashlib.sha256(encoded.encode("utf-8")).hexdigest()})
271
286
 
272
287
 
288
+ def _strip_render_hash_ignored_hints(value: Any) -> Any:
289
+ if isinstance(value, dict):
290
+ return {
291
+ key: _strip_render_hash_ignored_hints(item)
292
+ for key, item in value.items()
293
+ if key
294
+ not in {
295
+ "displayValue",
296
+ "userDisplayNames",
297
+ "task_owner_display_value",
298
+ "planned_version_owner_display_value",
299
+ }
300
+ }
301
+ if isinstance(value, list):
302
+ return [_strip_render_hash_ignored_hints(item) for item in value]
303
+ return value
304
+
305
+
273
306
  def _build_docs_index_sections(
274
307
  *,
275
308
  entries: list[Any],
276
309
  relations: list[Any],
277
- channel: str | None,
310
+ provider: str | None,
278
311
  ) -> list[DocSection]:
279
- index_payload = _build_docs_index_payload(entries=entries, relations=relations, channel=channel)
312
+ index_payload = _build_docs_index_payload(entries=entries, relations=relations, provider=provider)
280
313
  sections = [
281
314
  DocSection(
282
315
  heading="Registered Documents",
@@ -297,13 +330,13 @@ def _build_docs_index_payload(
297
330
  *,
298
331
  entries: list[Any],
299
332
  relations: list[Any],
300
- channel: str | None,
333
+ provider: str | None,
301
334
  ) -> dict[str, list[dict]]:
302
335
  rows = []
303
336
  filtered_entry_ids: set[str] = set()
304
337
  entry_titles_by_id: dict[str, str] = {}
305
338
  for entry in entries:
306
- if channel is not None and entry.get("provider") != channel:
339
+ if provider is not None and entry.get("provider") != provider:
307
340
  continue
308
341
  entry_id = entry.get("entry_id")
309
342
  filtered_entry_ids.add(entry_id)
@@ -341,6 +374,45 @@ def _build_docs_index_payload(
341
374
  return {"documents": rows, "relations": relation_rows}
342
375
 
343
376
 
377
+ def _enrich_sections_with_user_display_values(
378
+ sections: list[DocSection],
379
+ user_store: UserStore | None,
380
+ ) -> None:
381
+ if user_store is None:
382
+ return
383
+
384
+ user_ids: set[str] = set()
385
+ _collect_user_ids(sections, user_ids)
386
+ if not user_ids:
387
+ return
388
+
389
+ display_names: dict[str, str] = {}
390
+ for uid in user_ids:
391
+ user = user_store.find_by_id(uid)
392
+ display_names[uid] = (user or {}).get("display_name") or uid
393
+
394
+ for section in sections:
395
+ for field in section.payload.get("fields", []):
396
+ value = field.get("value", "")
397
+ if not isinstance(value, str):
398
+ continue
399
+ uids = [token for token in value.split(", ") if token.startswith("usr_")]
400
+ if not uids:
401
+ continue
402
+ names = [display_names.get(uid, uid) for uid in uids]
403
+ field["displayValue"] = ", ".join(names)
404
+ if len(names) > 1:
405
+ field["userDisplayNames"] = names
406
+ for item in section.payload.get("tasks", []):
407
+ owner = item.get("task_owner", "")
408
+ if isinstance(owner, str) and owner.startswith("usr_"):
409
+ item["task_owner_display_value"] = display_names.get(owner, owner)
410
+ for item in section.payload.get("versions", []):
411
+ owner = item.get("planned_version_owner", "")
412
+ if isinstance(owner, str) and owner.startswith("usr_"):
413
+ item["planned_version_owner_display_value"] = display_names.get(owner, owner)
414
+
415
+
344
416
  def _enrich_sections_with_channel_identities(
345
417
  sections: list[DocSection],
346
418
  channel: str | None,
@@ -535,6 +607,11 @@ def _build_overview_sections(
535
607
  "label": "Goal",
536
608
  "value": (current_version or {}).get("version_goal") or "TBD",
537
609
  },
610
+ {
611
+ "name": "version_summary",
612
+ "label": "Version Summary",
613
+ "value": (current_version or {}).get("version_summary") or "TBD",
614
+ },
538
615
  {
539
616
  "name": "version_owner",
540
617
  "label": "Owner",
@@ -693,6 +770,7 @@ def _build_planned_versions_payload(*, planned_versions: list[dict], current_ver
693
770
  "planned_version_name": "TBD",
694
771
  "planned_version_milestone": "TBD",
695
772
  "planned_version_goal": "TBD",
773
+ "planned_version_summary": "TBD",
696
774
  "planned_version_owner": "TBD",
697
775
  "planned_version_target_start_date": "TBD",
698
776
  "planned_version_target_start_date_note": "(待补充)",
@@ -710,6 +788,7 @@ def _build_planned_versions_payload(*, planned_versions: list[dict], current_ver
710
788
  "planned_version_name": version.get("version_name") or "TBD",
711
789
  "planned_version_milestone": _display_milestone(version),
712
790
  "planned_version_goal": version.get("version_goal") or "TBD",
791
+ "planned_version_summary": version.get("version_summary") or "TBD",
713
792
  "planned_version_owner": version.get("owner_user_id") or "TBD",
714
793
  "planned_version_target_start_date": version.get("start_date") or "TBD",
715
794
  "planned_version_target_start_date_note": _planned_start_note(
@@ -765,6 +844,7 @@ def _build_version_snapshot(
765
844
  "fields": [
766
845
  {"name": "version_name", "label": "Version Name", "value": version.get("version_name") or "TBD"},
767
846
  {"name": "version_goal", "label": "Goal", "value": version.get("version_goal") or "TBD"},
847
+ {"name": "version_summary", "label": "Version Summary", "value": version.get("version_summary") or "TBD"},
768
848
  {"name": "version_owner", "label": "Owner", "value": version.get("owner_user_id") or "TBD"},
769
849
  {"name": "start_date", "label": "Start Date", "value": version.get("start_date") or "TBD"},
770
850
  {
@@ -875,18 +955,19 @@ def _build_past_version_snapshots_payload(past_versions: list[dict], *, snapshot
875
955
  }
876
956
 
877
957
 
878
- def _version_snapshot_links(*, documents: list[dict], channel: str | None) -> dict[str, str]:
958
+ def _version_snapshot_links(*, documents: list[dict], provider: str | None) -> dict[str, str]:
879
959
  links: dict[str, str] = {}
880
960
  for document in sorted(
881
961
  documents,
882
962
  key=lambda item: (
883
- item.get("channel") or "",
963
+ item.get("provider") or item.get("channel") or "",
884
964
  item.get("document_id") or "",
885
965
  ),
886
966
  ):
887
967
  if document.get("doc_kind") != "version_snapshot":
888
968
  continue
889
- if channel is not None and document.get("channel") != channel:
969
+ document_provider = document.get("provider") or document.get("channel")
970
+ if provider is not None and document_provider != provider:
890
971
  continue
891
972
  version_id = document.get("version_id")
892
973
  if not version_id or version_id in links:
@@ -897,17 +978,18 @@ def _version_snapshot_links(*, documents: list[dict], channel: str | None) -> di
897
978
  return links
898
979
 
899
980
 
900
- def _single_document_link(*, documents: list[dict], doc_kind: str, channel: str | None) -> str:
981
+ def _single_document_link(*, documents: list[dict], doc_kind: str, provider: str | None) -> str:
901
982
  for document in sorted(
902
983
  documents,
903
984
  key=lambda item: (
904
- item.get("channel") or "",
985
+ item.get("provider") or item.get("channel") or "",
905
986
  item.get("document_id") or "",
906
987
  ),
907
988
  ):
908
989
  if document.get("doc_kind") != doc_kind:
909
990
  continue
910
- if channel is not None and document.get("channel") != channel:
991
+ document_provider = document.get("provider") or document.get("channel")
992
+ if provider is not None and document_provider != provider:
911
993
  continue
912
994
  url = document.get("url")
913
995
  if url and document.get("last_verified_render_hash"):