elspais 0.11.2__py3-none-any.whl → 0.43.5__py3-none-any.whl

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 (147) hide show
  1. elspais/__init__.py +1 -10
  2. elspais/{sponsors/__init__.py → associates.py} +102 -56
  3. elspais/cli.py +366 -69
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +118 -169
  6. elspais/commands/changed.py +12 -23
  7. elspais/commands/config_cmd.py +10 -13
  8. elspais/commands/edit.py +33 -13
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +161 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -115
  13. elspais/commands/init.py +99 -22
  14. elspais/commands/reformat_cmd.py +41 -433
  15. elspais/commands/rules_cmd.py +2 -2
  16. elspais/commands/trace.py +443 -324
  17. elspais/commands/validate.py +193 -411
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -2
  20. elspais/docs/cli/assertions.md +67 -0
  21. elspais/docs/cli/commands.md +304 -0
  22. elspais/docs/cli/config.md +262 -0
  23. elspais/docs/cli/format.md +66 -0
  24. elspais/docs/cli/git.md +45 -0
  25. elspais/docs/cli/health.md +190 -0
  26. elspais/docs/cli/hierarchy.md +60 -0
  27. elspais/docs/cli/ignore.md +72 -0
  28. elspais/docs/cli/mcp.md +245 -0
  29. elspais/docs/cli/quickstart.md +58 -0
  30. elspais/docs/cli/traceability.md +89 -0
  31. elspais/docs/cli/validation.md +96 -0
  32. elspais/graph/GraphNode.py +383 -0
  33. elspais/graph/__init__.py +40 -0
  34. elspais/graph/annotators.py +927 -0
  35. elspais/graph/builder.py +1886 -0
  36. elspais/graph/deserializer.py +248 -0
  37. elspais/graph/factory.py +284 -0
  38. elspais/graph/metrics.py +127 -0
  39. elspais/graph/mutations.py +161 -0
  40. elspais/graph/parsers/__init__.py +156 -0
  41. elspais/graph/parsers/code.py +213 -0
  42. elspais/graph/parsers/comments.py +112 -0
  43. elspais/graph/parsers/config_helpers.py +29 -0
  44. elspais/graph/parsers/heredocs.py +225 -0
  45. elspais/graph/parsers/journey.py +131 -0
  46. elspais/graph/parsers/remainder.py +79 -0
  47. elspais/graph/parsers/requirement.py +347 -0
  48. elspais/graph/parsers/results/__init__.py +6 -0
  49. elspais/graph/parsers/results/junit_xml.py +229 -0
  50. elspais/graph/parsers/results/pytest_json.py +313 -0
  51. elspais/graph/parsers/test.py +305 -0
  52. elspais/graph/relations.py +78 -0
  53. elspais/graph/serialize.py +216 -0
  54. elspais/html/__init__.py +8 -0
  55. elspais/html/generator.py +731 -0
  56. elspais/html/templates/trace_view.html.j2 +2151 -0
  57. elspais/mcp/__init__.py +45 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +1998 -244
  61. elspais/testing/__init__.py +3 -3
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/scanner.py +301 -12
  65. elspais/utilities/__init__.py +1 -0
  66. elspais/utilities/docs_loader.py +115 -0
  67. elspais/utilities/git.py +607 -0
  68. elspais/{core → utilities}/hasher.py +8 -22
  69. elspais/utilities/md_renderer.py +189 -0
  70. elspais/{core → utilities}/patterns.py +56 -51
  71. elspais/utilities/reference_config.py +626 -0
  72. elspais/validation/__init__.py +19 -0
  73. elspais/validation/format.py +264 -0
  74. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  75. elspais-0.43.5.dist-info/RECORD +80 -0
  76. elspais/config/defaults.py +0 -179
  77. elspais/config/loader.py +0 -494
  78. elspais/core/__init__.py +0 -21
  79. elspais/core/git.py +0 -346
  80. elspais/core/models.py +0 -320
  81. elspais/core/parser.py +0 -639
  82. elspais/core/rules.py +0 -509
  83. elspais/mcp/context.py +0 -172
  84. elspais/mcp/serializers.py +0 -112
  85. elspais/reformat/__init__.py +0 -50
  86. elspais/reformat/detector.py +0 -112
  87. elspais/reformat/hierarchy.py +0 -247
  88. elspais/reformat/line_breaks.py +0 -218
  89. elspais/reformat/prompts.py +0 -133
  90. elspais/reformat/transformer.py +0 -266
  91. elspais/trace_view/__init__.py +0 -55
  92. elspais/trace_view/coverage.py +0 -183
  93. elspais/trace_view/generators/__init__.py +0 -12
  94. elspais/trace_view/generators/base.py +0 -334
  95. elspais/trace_view/generators/csv.py +0 -118
  96. elspais/trace_view/generators/markdown.py +0 -170
  97. elspais/trace_view/html/__init__.py +0 -33
  98. elspais/trace_view/html/generator.py +0 -1140
  99. elspais/trace_view/html/templates/base.html +0 -283
  100. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  101. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  102. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  103. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  104. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  105. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  106. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  107. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  108. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  109. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  110. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  111. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  112. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  113. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  114. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  115. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  116. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  117. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  118. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  119. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  120. elspais/trace_view/models.py +0 -378
  121. elspais/trace_view/review/__init__.py +0 -63
  122. elspais/trace_view/review/branches.py +0 -1142
  123. elspais/trace_view/review/models.py +0 -1200
  124. elspais/trace_view/review/position.py +0 -591
  125. elspais/trace_view/review/server.py +0 -1032
  126. elspais/trace_view/review/status.py +0 -455
  127. elspais/trace_view/review/storage.py +0 -1343
  128. elspais/trace_view/scanning.py +0 -213
  129. elspais/trace_view/specs/README.md +0 -84
  130. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  131. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  132. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  133. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  134. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  135. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  136. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  137. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  138. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  139. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  140. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  141. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  142. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  143. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  144. elspais-0.11.2.dist-info/RECORD +0 -101
  145. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  146. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  147. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
@@ -1,1032 +0,0 @@
1
- # Implements: REQ-tv-d00014 (Review API Server)
2
- """
3
- Review API Server for trace_view
4
-
5
- Flask-based API server for the review system that handles:
6
- - Thread creation and comment persistence
7
- - Status change requests and approvals
8
- - Review flag management
9
- - Package management
10
- - Git sync operations
11
-
12
- IMPLEMENTS REQUIREMENTS:
13
- REQ-tv-d00014: Review API Server
14
- """
15
-
16
- from pathlib import Path
17
- from typing import Optional
18
-
19
- from flask import Flask, jsonify, request, send_from_directory
20
- from flask_cors import CORS
21
-
22
- from .branches import (
23
- commit_and_push_reviews,
24
- fetch_package_branches,
25
- get_current_package_context,
26
- # REQ-d00098: Git audit trail
27
- get_git_context,
28
- has_reviews_changes,
29
- )
30
- from .models import (
31
- Approval,
32
- ReviewFlag,
33
- ReviewPackage,
34
- StatusRequest,
35
- Thread,
36
- )
37
- from .status import change_req_status, get_req_status
38
- from .storage import (
39
- add_approval,
40
- add_comment_to_thread,
41
- add_req_to_package,
42
- add_thread,
43
- # REQ-d00097: Archive operations
44
- archive_package,
45
- check_auto_archive,
46
- create_package,
47
- create_status_request,
48
- get_archived_package,
49
- list_archived_packages,
50
- load_archived_threads,
51
- load_packages,
52
- load_review_flag,
53
- load_status_requests,
54
- normalize_req_id,
55
- remove_req_from_package,
56
- resolve_thread,
57
- save_packages,
58
- save_review_flag,
59
- unresolve_thread,
60
- update_package,
61
- )
62
-
63
-
64
- def create_app(
65
- repo_root: Path,
66
- static_dir: Optional[Path] = None,
67
- auto_sync: bool = True,
68
- register_static_routes: bool = True,
69
- ) -> Flask:
70
- """
71
- Create Flask app with review API endpoints.
72
-
73
- REQ-tv-d00014-A: The API server SHALL be implemented as a Flask
74
- application with a `create_app(repo_root, static_dir)` factory function.
75
-
76
- Args:
77
- repo_root: Repository root path for .reviews/ storage
78
- static_dir: Optional directory to serve static files from
79
- auto_sync: Whether to auto-commit and push after write operations
80
- register_static_routes: Whether to register default static file routes
81
- (set to False if caller will define custom static routes)
82
-
83
- Returns:
84
- Flask application
85
- """
86
- app = Flask(__name__)
87
- # REQ-tv-d00014-F: Enable CORS for cross-origin requests
88
- CORS(app)
89
-
90
- # Store configuration in app config
91
- app.config["REPO_ROOT"] = repo_root
92
- app.config["STATIC_DIR"] = static_dir or repo_root
93
- app.config["AUTO_SYNC"] = auto_sync
94
-
95
- def trigger_auto_sync(message: str, user: str = "system") -> Optional[dict]:
96
- """
97
- Trigger auto-sync if enabled.
98
-
99
- REQ-tv-d00014-H: All write endpoints SHALL optionally trigger
100
- auto-sync based on configuration.
101
-
102
- Args:
103
- message: Commit message describing the change
104
- user: Username for commit attribution
105
-
106
- Returns:
107
- dict with sync result, or None if auto-sync disabled
108
- """
109
- if not app.config.get("AUTO_SYNC"):
110
- return None
111
-
112
- repo = app.config["REPO_ROOT"]
113
- success, msg = commit_and_push_reviews(repo, message, user)
114
- return {"success": success, "message": msg}
115
-
116
- # ==========================================================================
117
- # Static File Serving
118
- # REQ-tv-d00014-G: Serve static files from the configured static directory
119
- # ==========================================================================
120
-
121
- if register_static_routes:
122
-
123
- @app.route("/")
124
- def index():
125
- """Serve index from static directory"""
126
- return send_from_directory(app.config["STATIC_DIR"], "index.html")
127
-
128
- @app.route("/<path:path>")
129
- def serve_static(path):
130
- """Serve static files from configured static directory"""
131
- return send_from_directory(app.config["STATIC_DIR"], path)
132
-
133
- # ==========================================================================
134
- # Health Check
135
- # REQ-tv-d00014-J: Provide /api/health endpoint for health checks
136
- # ==========================================================================
137
-
138
- @app.route("/api/health", methods=["GET"])
139
- def health_check():
140
- """Health check endpoint"""
141
- return jsonify(
142
- {
143
- "status": "ok",
144
- "repo_root": str(app.config["REPO_ROOT"]),
145
- "reviews_dir": str(app.config["REPO_ROOT"] / ".reviews"),
146
- }
147
- )
148
-
149
- # ==========================================================================
150
- # File Content API (for external repo files)
151
- # ==========================================================================
152
-
153
- @app.route("/api/files", methods=["GET"])
154
- def get_file_content():
155
- """
156
- Fetch file content for viewing in the browser.
157
-
158
- Query params:
159
- path: Absolute path to the file
160
-
161
- Returns file content as text. Only allows reading files in spec/
162
- directories for security.
163
- """
164
- file_path = request.args.get("path")
165
- if not file_path:
166
- return jsonify({"error": "Missing path parameter"}), 400
167
-
168
- # Security: only allow reading spec files
169
- path = Path(file_path)
170
- if not path.is_absolute():
171
- return jsonify({"error": "Path must be absolute"}), 400
172
-
173
- # Check that path contains /spec/ for security
174
- if "/spec/" not in str(path):
175
- return jsonify({"error": "Only spec files can be read"}), 403
176
-
177
- if not path.exists():
178
- return jsonify({"error": f"File not found: {path}"}), 404
179
-
180
- if not path.is_file():
181
- return jsonify({"error": "Path is not a file"}), 400
182
-
183
- try:
184
- content = path.read_text(encoding="utf-8")
185
- return content, 200, {"Content-Type": "text/plain; charset=utf-8"}
186
- except Exception as e:
187
- return jsonify({"error": f"Failed to read file: {e}"}), 500
188
-
189
- # ==========================================================================
190
- # Thread API
191
- # REQ-tv-d00014-B: Thread endpoints for create, comment, resolve, unresolve
192
- # ==========================================================================
193
-
194
- @app.route("/api/reviews/reqs/<req_id>/threads", methods=["POST"])
195
- def create_thread_endpoint(req_id):
196
- """
197
- Create a new comment thread.
198
-
199
- REQ-tv-d00014-B: POST create thread endpoint.
200
- """
201
- repo = app.config["REPO_ROOT"]
202
- normalized_id = normalize_req_id(req_id)
203
- data = request.get_json(silent=True)
204
-
205
- if not data:
206
- return jsonify({"error": "No data provided"}), 400
207
-
208
- try:
209
- thread = Thread.from_dict(data)
210
- add_thread(repo, normalized_id, thread)
211
-
212
- # REQ-tv-d00014-H: Auto-sync after write operation
213
- user = thread.createdBy or "system"
214
- sync_result = trigger_auto_sync(f"New thread on REQ-{normalized_id}", user)
215
-
216
- response = {"success": True, "thread": thread.to_dict()}
217
- if sync_result:
218
- response["sync"] = sync_result
219
-
220
- return jsonify(response), 201
221
- except Exception as e:
222
- return jsonify({"error": str(e)}), 400
223
-
224
- @app.route("/api/reviews/reqs/<req_id>/threads/<thread_id>/comments", methods=["POST"])
225
- def add_comment_endpoint(req_id, thread_id):
226
- """
227
- Add a comment to an existing thread.
228
-
229
- REQ-tv-d00014-B: POST add comment endpoint.
230
- """
231
- repo = app.config["REPO_ROOT"]
232
- normalized_id = normalize_req_id(req_id)
233
- data = request.get_json(silent=True)
234
-
235
- if not data:
236
- return jsonify({"error": "No data provided"}), 400
237
-
238
- try:
239
- author = data.get("author")
240
- body = data.get("body")
241
-
242
- if not author:
243
- return jsonify({"error": "Comment author is required"}), 400
244
- if not body:
245
- return jsonify({"error": "Comment body is required"}), 400
246
-
247
- comment = add_comment_to_thread(repo, normalized_id, thread_id, author, body)
248
-
249
- # REQ-tv-d00014-H: Auto-sync after write operation
250
- sync_result = trigger_auto_sync(f"Comment on REQ-{normalized_id}", author)
251
-
252
- response = {"success": True, "comment": comment.to_dict()}
253
- if sync_result:
254
- response["sync"] = sync_result
255
-
256
- return jsonify(response), 201
257
- except Exception as e:
258
- return jsonify({"error": str(e)}), 400
259
-
260
- @app.route("/api/reviews/reqs/<req_id>/threads/<thread_id>/resolve", methods=["POST"])
261
- def resolve_thread_endpoint(req_id, thread_id):
262
- """
263
- Resolve a thread.
264
-
265
- REQ-tv-d00014-B: POST resolve endpoint.
266
- REQ-d00097-D: Resolving all threads in a package SHALL trigger auto-archive.
267
- """
268
- repo = app.config["REPO_ROOT"]
269
- normalized_id = normalize_req_id(req_id)
270
- data = request.get_json(silent=True) or {}
271
- user = data.get("user", "anonymous")
272
- package_id = data.get("packageId") # Optional: for auto-archive check
273
-
274
- try:
275
- resolve_thread(repo, normalized_id, thread_id, user)
276
-
277
- # REQ-tv-d00014-H: Auto-sync after write operation
278
- sync_result = trigger_auto_sync(f"Resolved thread on REQ-{normalized_id}", user)
279
-
280
- response = {"success": True}
281
- if sync_result:
282
- response["sync"] = sync_result
283
-
284
- # REQ-d00097-D: Check for auto-archive if packageId provided
285
- if package_id:
286
- was_archived = check_auto_archive(repo, package_id, user)
287
- if was_archived:
288
- response["packageArchived"] = True
289
- response["archiveReason"] = "resolved"
290
-
291
- return jsonify(response), 200
292
- except Exception as e:
293
- return jsonify({"error": str(e)}), 400
294
-
295
- @app.route("/api/reviews/reqs/<req_id>/threads/<thread_id>/unresolve", methods=["POST"])
296
- def unresolve_thread_endpoint(req_id, thread_id):
297
- """
298
- Unresolve a thread.
299
-
300
- REQ-tv-d00014-B: POST unresolve endpoint.
301
- """
302
- repo = app.config["REPO_ROOT"]
303
- normalized_id = normalize_req_id(req_id)
304
- data = request.get_json(silent=True) or {}
305
- user = data.get("user", "anonymous")
306
-
307
- try:
308
- unresolve_thread(repo, normalized_id, thread_id)
309
-
310
- # REQ-tv-d00014-H: Auto-sync after write operation
311
- sync_result = trigger_auto_sync(f"Unresolved thread on REQ-{normalized_id}", user)
312
-
313
- response = {"success": True}
314
- if sync_result:
315
- response["sync"] = sync_result
316
-
317
- return jsonify(response), 200
318
- except Exception as e:
319
- return jsonify({"error": str(e)}), 400
320
-
321
- # ==========================================================================
322
- # Review Flag API
323
- # ==========================================================================
324
-
325
- @app.route("/api/reviews/reqs/<req_id>/flag", methods=["GET"])
326
- def get_flag(req_id):
327
- """Get review flag for a requirement"""
328
- repo = app.config["REPO_ROOT"]
329
- normalized_id = normalize_req_id(req_id)
330
- flag = load_review_flag(repo, normalized_id)
331
- return jsonify(flag.to_dict())
332
-
333
- @app.route("/api/reviews/reqs/<req_id>/flag", methods=["POST"])
334
- def set_flag(req_id):
335
- """Set review flag for a requirement"""
336
- repo = app.config["REPO_ROOT"]
337
- normalized_id = normalize_req_id(req_id)
338
- data = request.get_json(silent=True)
339
-
340
- if not data:
341
- return jsonify({"error": "No data provided"}), 400
342
-
343
- try:
344
- flag = ReviewFlag.from_dict(data)
345
- save_review_flag(repo, normalized_id, flag)
346
-
347
- # REQ-tv-d00014-H: Auto-sync after write operation
348
- user = flag.flaggedBy or "system"
349
- sync_result = trigger_auto_sync(f"Flagged REQ-{normalized_id} for review", user)
350
-
351
- response = {"success": True, "flag": flag.to_dict()}
352
- if sync_result:
353
- response["sync"] = sync_result
354
-
355
- return jsonify(response), 200
356
- except Exception as e:
357
- return jsonify({"error": str(e)}), 400
358
-
359
- @app.route("/api/reviews/reqs/<req_id>/flag", methods=["DELETE"])
360
- def clear_flag(req_id):
361
- """Clear review flag for a requirement"""
362
- repo = app.config["REPO_ROOT"]
363
- normalized_id = normalize_req_id(req_id)
364
- data = request.get_json(silent=True) or {}
365
- user = data.get("user", "anonymous")
366
-
367
- flag = ReviewFlag.cleared()
368
- save_review_flag(repo, normalized_id, flag)
369
-
370
- # REQ-tv-d00014-H: Auto-sync after write operation
371
- sync_result = trigger_auto_sync(f"Cleared flag on REQ-{normalized_id}", user)
372
-
373
- response = {"success": True}
374
- if sync_result:
375
- response["sync"] = sync_result
376
-
377
- return jsonify(response), 200
378
-
379
- # ==========================================================================
380
- # Status Request API
381
- # REQ-tv-d00014-C: Status endpoints for GET/POST requests and approvals
382
- # ==========================================================================
383
-
384
- @app.route("/api/reviews/reqs/<req_id>/status", methods=["GET"])
385
- def get_status(req_id):
386
- """
387
- Get the current status of a requirement from the spec file.
388
-
389
- REQ-tv-d00014-C: GET status endpoint.
390
- """
391
- repo = app.config["REPO_ROOT"]
392
- normalized_id = normalize_req_id(req_id)
393
-
394
- status = get_req_status(repo, normalized_id)
395
- if status is None:
396
- return jsonify({"error": f"REQ-{normalized_id} not found"}), 404
397
-
398
- return jsonify({"reqId": normalized_id, "status": status})
399
-
400
- @app.route("/api/reviews/reqs/<req_id>/status", methods=["POST"])
401
- def set_status(req_id):
402
- """
403
- Change the status of a requirement in its spec file.
404
-
405
- REQ-tv-d00014-C: POST change status endpoint.
406
- """
407
- repo = app.config["REPO_ROOT"]
408
- normalized_id = normalize_req_id(req_id)
409
- data = request.get_json(silent=True)
410
-
411
- if not data:
412
- return jsonify({"error": "No data provided"}), 400
413
-
414
- new_status = data.get("newStatus")
415
- if not new_status:
416
- return jsonify({"error": "newStatus is required"}), 400
417
-
418
- user = data.get("user", "api")
419
-
420
- success, message = change_req_status(repo, normalized_id, new_status, user)
421
-
422
- if success:
423
- # REQ-tv-d00014-H: Auto-sync after write operation
424
- sync_result = trigger_auto_sync(
425
- f"Changed REQ-{normalized_id} status to {new_status}", user
426
- )
427
-
428
- response = {"success": True, "message": message}
429
- if sync_result:
430
- response["sync"] = sync_result
431
-
432
- return jsonify(response), 200
433
- else:
434
- return jsonify({"success": False, "error": message}), 400
435
-
436
- @app.route("/api/reviews/reqs/<req_id>/requests", methods=["GET"])
437
- def get_status_requests(req_id):
438
- """
439
- Get status change requests for a requirement.
440
-
441
- REQ-tv-d00014-C: GET requests endpoint.
442
- """
443
- repo = app.config["REPO_ROOT"]
444
- normalized_id = normalize_req_id(req_id)
445
- status_file = load_status_requests(repo, normalized_id)
446
- return jsonify([r.to_dict() for r in status_file.requests])
447
-
448
- @app.route("/api/reviews/reqs/<req_id>/requests", methods=["POST"])
449
- def create_status_request_endpoint(req_id):
450
- """
451
- Create a status change request.
452
-
453
- REQ-tv-d00014-C: POST requests endpoint.
454
- """
455
- repo = app.config["REPO_ROOT"]
456
- normalized_id = normalize_req_id(req_id)
457
- data = request.get_json(silent=True)
458
-
459
- if not data:
460
- return jsonify({"error": "No data provided"}), 400
461
-
462
- try:
463
- status_request = StatusRequest.from_dict(data)
464
- create_status_request(repo, normalized_id, status_request)
465
-
466
- # REQ-tv-d00014-H: Auto-sync after write operation
467
- user = status_request.requestedBy or "system"
468
- sync_result = trigger_auto_sync(
469
- f"Status change request for REQ-{normalized_id}: "
470
- f"{status_request.fromStatus} -> {status_request.toStatus}",
471
- user,
472
- )
473
-
474
- response = {"success": True, "request": status_request.to_dict()}
475
- if sync_result:
476
- response["sync"] = sync_result
477
-
478
- return jsonify(response), 201
479
- except Exception as e:
480
- return jsonify({"error": str(e)}), 400
481
-
482
- @app.route("/api/reviews/reqs/<req_id>/requests/<request_id>/approvals", methods=["POST"])
483
- def add_approval_endpoint(req_id, request_id):
484
- """
485
- Add an approval to a status change request.
486
-
487
- REQ-tv-d00014-C: POST approvals endpoint.
488
- """
489
- repo = app.config["REPO_ROOT"]
490
- normalized_id = normalize_req_id(req_id)
491
- data = request.get_json(silent=True)
492
-
493
- if not data:
494
- return jsonify({"error": "No data provided"}), 400
495
-
496
- try:
497
- approval = Approval.from_dict(data)
498
- add_approval(
499
- repo, normalized_id, request_id, approval.user, approval.decision, approval.comment
500
- )
501
-
502
- # REQ-tv-d00014-H: Auto-sync after write operation
503
- user = approval.user or "system"
504
- sync_result = trigger_auto_sync(
505
- f"Approval on REQ-{normalized_id} status request: {approval.decision}", user
506
- )
507
-
508
- response = {"success": True, "approval": approval.to_dict()}
509
- if sync_result:
510
- response["sync"] = sync_result
511
-
512
- return jsonify(response), 201
513
- except Exception as e:
514
- return jsonify({"error": str(e)}), 400
515
-
516
- # ==========================================================================
517
- # Review Packages API
518
- # REQ-tv-d00014-D: Package endpoints for CRUD and membership
519
- # ==========================================================================
520
-
521
- @app.route("/api/reviews/packages", methods=["GET"])
522
- def get_packages():
523
- """
524
- Get all review packages.
525
-
526
- REQ-tv-d00014-D: GET packages endpoint.
527
- """
528
- repo = app.config["REPO_ROOT"]
529
- pf = load_packages(repo)
530
- return jsonify(
531
- {"packages": [p.to_dict() for p in pf.packages], "activePackageId": pf.activePackageId}
532
- )
533
-
534
- @app.route("/api/reviews/packages", methods=["POST"])
535
- def create_package_endpoint():
536
- """
537
- Create a new review package.
538
-
539
- REQ-tv-d00014-D: POST packages endpoint.
540
- REQ-d00098-A: Package SHALL record branchName when created.
541
- REQ-d00098-B: Package SHALL record creationCommitHash when created.
542
- """
543
- repo = app.config["REPO_ROOT"]
544
- data = request.get_json(silent=True)
545
-
546
- if not data:
547
- return jsonify({"error": "No data provided"}), 400
548
-
549
- name = data.get("name")
550
- if not name:
551
- return jsonify({"error": "name is required"}), 400
552
-
553
- description = data.get("description", "")
554
- user = data.get("user", "api")
555
-
556
- pkg = ReviewPackage.create(name, description, user)
557
-
558
- # REQ-d00098: Add git context for audit trail
559
- git_context = get_git_context(repo)
560
- pkg.branchName = git_context.get("branchName")
561
- pkg.creationCommitHash = git_context.get("commitHash")
562
- pkg.lastReviewedCommitHash = git_context.get("commitHash")
563
-
564
- create_package(repo, pkg)
565
-
566
- # REQ-tv-d00014-H: Auto-sync after write operation
567
- sync_result = trigger_auto_sync(f"Created package: {name}", user)
568
-
569
- response = {"success": True, "package": pkg.to_dict()}
570
- if sync_result:
571
- response["sync"] = sync_result
572
-
573
- return jsonify(response), 201
574
-
575
- @app.route("/api/reviews/packages/<package_id>", methods=["GET"])
576
- def get_package_endpoint(package_id):
577
- """
578
- Get a specific package.
579
-
580
- REQ-tv-d00014-D: GET package by ID endpoint.
581
- """
582
- repo = app.config["REPO_ROOT"]
583
- pf = load_packages(repo)
584
- pkg = pf.get_by_id(package_id)
585
-
586
- if not pkg:
587
- return jsonify({"error": "Package not found"}), 404
588
-
589
- return jsonify(pkg.to_dict())
590
-
591
- @app.route("/api/reviews/packages/<package_id>", methods=["PUT"])
592
- def update_package_endpoint(package_id):
593
- """
594
- Update a package.
595
-
596
- REQ-tv-d00014-D: PUT package endpoint.
597
- """
598
- repo = app.config["REPO_ROOT"]
599
- data = request.get_json(silent=True)
600
-
601
- if not data:
602
- return jsonify({"error": "No data provided"}), 400
603
-
604
- user = data.get("user", "api")
605
-
606
- # Load existing package
607
- pf = load_packages(repo)
608
- pkg = pf.get_by_id(package_id)
609
-
610
- if not pkg:
611
- return jsonify({"error": "Package not found"}), 404
612
-
613
- # Update fields
614
- if "name" in data:
615
- pkg.name = data["name"]
616
- if "description" in data:
617
- pkg.description = data["description"]
618
-
619
- success = update_package(repo, pkg)
620
-
621
- if success:
622
- # REQ-tv-d00014-H: Auto-sync after write operation
623
- sync_result = trigger_auto_sync(f"Updated package: {pkg.name}", user)
624
-
625
- response = {"success": True, "package": pkg.to_dict()}
626
- if sync_result:
627
- response["sync"] = sync_result
628
-
629
- return jsonify(response)
630
- else:
631
- return jsonify({"error": "Package not found"}), 404
632
-
633
- @app.route("/api/reviews/packages/<package_id>", methods=["DELETE"])
634
- def delete_package_endpoint(package_id):
635
- """
636
- Delete a package (archives it instead of destroying).
637
-
638
- REQ-tv-d00014-D: DELETE package endpoint.
639
- REQ-d00097-E: Deleting a package SHALL move it to archive rather than destroying.
640
- """
641
- from .models import ARCHIVE_REASON_DELETED
642
-
643
- repo = app.config["REPO_ROOT"]
644
- data = request.get_json(silent=True) or {}
645
- user = data.get("user", "api")
646
-
647
- # Get package name before archiving
648
- pf = load_packages(repo)
649
- pkg = pf.get_by_id(package_id)
650
-
651
- if not pkg:
652
- return jsonify({"error": "Package not found"}), 404
653
-
654
- # Don't allow deleting default package
655
- if pkg.isDefault:
656
- return jsonify({"error": "Cannot delete default package"}), 400
657
-
658
- pkg_name = pkg.name
659
-
660
- # REQ-d00097-E: Archive instead of delete
661
- try:
662
- success = archive_package(repo, package_id, ARCHIVE_REASON_DELETED, user)
663
-
664
- if success:
665
- # REQ-tv-d00014-H: Auto-sync after write operation
666
- sync_result = trigger_auto_sync(f"Archived (deleted) package: {pkg_name}", user)
667
-
668
- response = {"success": True, "archived": True}
669
- if sync_result:
670
- response["sync"] = sync_result
671
-
672
- return jsonify(response)
673
- else:
674
- return jsonify({"error": "Failed to archive package"}), 400
675
- except ValueError as e:
676
- return jsonify({"error": str(e)}), 400
677
-
678
- @app.route("/api/reviews/packages/<package_id>/reqs/<req_id>", methods=["POST"])
679
- def add_req_to_package_endpoint(package_id, req_id):
680
- """
681
- Add a REQ to a package.
682
-
683
- REQ-tv-d00014-D: POST membership endpoint.
684
- """
685
- repo = app.config["REPO_ROOT"]
686
- data = request.get_json(silent=True) or {}
687
- user = data.get("user", "api")
688
-
689
- normalized_id = normalize_req_id(req_id)
690
- success = add_req_to_package(repo, package_id, normalized_id)
691
-
692
- if success:
693
- # REQ-tv-d00014-H: Auto-sync after write operation
694
- sync_result = trigger_auto_sync(f"Added REQ-{normalized_id} to package", user)
695
-
696
- response = {"success": True}
697
- if sync_result:
698
- response["sync"] = sync_result
699
-
700
- return jsonify(response)
701
- else:
702
- return jsonify({"error": "Package not found"}), 404
703
-
704
- @app.route("/api/reviews/packages/<package_id>/reqs/<req_id>", methods=["DELETE"])
705
- def remove_req_from_package_endpoint(package_id, req_id):
706
- """
707
- Remove a REQ from a package.
708
-
709
- REQ-tv-d00014-D: DELETE membership endpoint.
710
- """
711
- repo = app.config["REPO_ROOT"]
712
- data = request.get_json(silent=True) or {}
713
- user = data.get("user", "api")
714
-
715
- normalized_id = normalize_req_id(req_id)
716
- success = remove_req_from_package(repo, package_id, normalized_id)
717
-
718
- if success:
719
- # REQ-tv-d00014-H: Auto-sync after write operation
720
- sync_result = trigger_auto_sync(f"Removed REQ-{normalized_id} from package", user)
721
-
722
- response = {"success": True}
723
- if sync_result:
724
- response["sync"] = sync_result
725
-
726
- return jsonify(response)
727
- else:
728
- return jsonify({"error": "Package not found"}), 404
729
-
730
- @app.route("/api/reviews/packages/active", methods=["GET"])
731
- def get_active_package_endpoint():
732
- """
733
- Get the currently active package.
734
-
735
- REQ-tv-d00014-D: GET active endpoint.
736
- """
737
- repo = app.config["REPO_ROOT"]
738
- pf = load_packages(repo)
739
- pkg = pf.get_active()
740
-
741
- if pkg:
742
- return jsonify(pkg.to_dict())
743
- else:
744
- return jsonify(None)
745
-
746
- @app.route("/api/reviews/packages/active", methods=["PUT"])
747
- def set_active_package_endpoint():
748
- """
749
- Set the active package.
750
-
751
- REQ-tv-d00014-D: PUT active endpoint.
752
- """
753
- repo = app.config["REPO_ROOT"]
754
- data = request.get_json(silent=True) or {}
755
- user = data.get("user", "api")
756
-
757
- package_id = data.get("packageId")
758
-
759
- # Load packages
760
- pf = load_packages(repo)
761
-
762
- # Validate package exists if setting active
763
- if package_id and not pf.get_by_id(package_id):
764
- return jsonify({"error": "Package not found"}), 404
765
-
766
- # Set active package
767
- pf.activePackageId = package_id
768
- save_packages(repo, pf)
769
-
770
- # REQ-tv-d00014-H: Auto-sync after write operation
771
- msg = f"Set active package: {package_id}" if package_id else "Cleared active package"
772
- sync_result = trigger_auto_sync(msg, user)
773
-
774
- response = {"success": True, "activePackageId": package_id}
775
- if sync_result:
776
- response["sync"] = sync_result
777
-
778
- return jsonify(response)
779
-
780
- # ==========================================================================
781
- # Archive API
782
- # REQ-d00097: Review Package Archival
783
- # REQ-d00099: Review Archive Viewer
784
- # ==========================================================================
785
-
786
- @app.route("/api/reviews/packages/<package_id>/archive", methods=["POST"])
787
- def archive_package_endpoint(package_id):
788
- """
789
- Manually archive a package.
790
-
791
- REQ-d00097-D: Archive SHALL be triggered by manual action (reason: "manual").
792
- """
793
- from .models import ARCHIVE_REASON_MANUAL
794
-
795
- repo = app.config["REPO_ROOT"]
796
- data = request.get_json(silent=True) or {}
797
- user = data.get("user", "api")
798
-
799
- # Get package name for response
800
- pf = load_packages(repo)
801
- pkg = pf.get_by_id(package_id)
802
-
803
- if not pkg:
804
- return jsonify({"error": "Package not found"}), 404
805
-
806
- # Don't allow archiving default package
807
- if pkg.isDefault:
808
- return jsonify({"error": "Cannot archive default package"}), 400
809
-
810
- pkg_name = pkg.name
811
-
812
- try:
813
- success = archive_package(repo, package_id, ARCHIVE_REASON_MANUAL, user)
814
-
815
- if success:
816
- # REQ-tv-d00014-H: Auto-sync after write operation
817
- sync_result = trigger_auto_sync(f"Archived package: {pkg_name}", user)
818
-
819
- response = {"success": True, "archived": True, "packageId": package_id}
820
- if sync_result:
821
- response["sync"] = sync_result
822
-
823
- return jsonify(response)
824
- else:
825
- return jsonify({"error": "Failed to archive package"}), 400
826
- except ValueError as e:
827
- return jsonify({"error": str(e)}), 400
828
-
829
- @app.route("/api/reviews/archive", methods=["GET"])
830
- def list_archived_packages_endpoint():
831
- """
832
- List all archived packages.
833
-
834
- REQ-d00099-A: The UI SHALL display a list of archived packages.
835
- """
836
- repo = app.config["REPO_ROOT"]
837
- packages = list_archived_packages(repo)
838
- return jsonify({"packages": [p.to_dict() for p in packages]})
839
-
840
- @app.route("/api/reviews/archive/<package_id>", methods=["GET"])
841
- def get_archived_package_endpoint(package_id):
842
- """
843
- Get a specific archived package.
844
-
845
- REQ-d00099-B: Archived packages SHALL open in read-only mode.
846
- """
847
- repo = app.config["REPO_ROOT"]
848
- pkg = get_archived_package(repo, package_id)
849
-
850
- if not pkg:
851
- return jsonify({"error": "Archived package not found"}), 404
852
-
853
- return jsonify(pkg.to_dict())
854
-
855
- @app.route("/api/reviews/archive/<package_id>/reqs/<req_id>/threads", methods=["GET"])
856
- def get_archived_threads_endpoint(package_id, req_id):
857
- """
858
- Get threads for a requirement from an archived package.
859
-
860
- REQ-d00099-B: Archived packages SHALL open in read-only mode.
861
- REQ-d00097-F: Archived data SHALL be read-only.
862
- """
863
- repo = app.config["REPO_ROOT"]
864
- normalized_id = normalize_req_id(req_id)
865
-
866
- threads_file = load_archived_threads(repo, package_id, normalized_id)
867
-
868
- if not threads_file:
869
- return jsonify({"error": "Threads not found in archived package"}), 404
870
-
871
- return jsonify(threads_file.to_dict())
872
-
873
- # ==========================================================================
874
- # Git Sync API
875
- # REQ-tv-d00014-E: Sync endpoints for status, push, fetch, fetch-all-package
876
- # ==========================================================================
877
-
878
- @app.route("/api/reviews/sync/status", methods=["GET"])
879
- def get_sync_status_endpoint():
880
- """
881
- Get the current sync status.
882
-
883
- REQ-tv-d00014-E: GET status endpoint.
884
- """
885
- repo = app.config["REPO_ROOT"]
886
-
887
- # Get basic status info
888
- has_changes = has_reviews_changes(repo)
889
- context = get_current_package_context(repo)
890
-
891
- status = {
892
- "has_changes": has_changes,
893
- "package_id": context[0] if context else None,
894
- "user": context[1] if context else None,
895
- "auto_sync_enabled": app.config.get("AUTO_SYNC", True),
896
- }
897
-
898
- return jsonify(status)
899
-
900
- @app.route("/api/reviews/sync/push", methods=["POST"])
901
- def sync_push():
902
- """
903
- Manually trigger a sync (commit and push).
904
-
905
- REQ-tv-d00014-E: POST push endpoint.
906
- """
907
- repo = app.config["REPO_ROOT"]
908
- data = request.get_json(silent=True) or {}
909
- user = data.get("user", "manual")
910
- message = data.get("message", "Manual sync")
911
-
912
- success, msg = commit_and_push_reviews(repo, message, user)
913
- return jsonify({"success": success, "message": msg})
914
-
915
- @app.route("/api/reviews/sync/fetch", methods=["POST"])
916
- def sync_fetch():
917
- """
918
- Fetch latest review data from remote.
919
-
920
- REQ-tv-d00014-E: POST fetch endpoint.
921
- """
922
- repo = app.config["REPO_ROOT"]
923
-
924
- # Get current package context
925
- context = get_current_package_context(repo)
926
- if context[0]:
927
- branches = fetch_package_branches(repo, context[0])
928
- return jsonify(
929
- {"success": True, "package_id": context[0], "branches_fetched": branches}
930
- )
931
- else:
932
- return jsonify(
933
- {"success": True, "message": "Not on a review branch", "branches_fetched": []}
934
- )
935
-
936
- @app.route("/api/reviews/sync/fetch-all-package", methods=["POST"])
937
- def sync_fetch_all_package():
938
- """
939
- Fetch and merge review data from all users' branches for the current package.
940
-
941
- REQ-tv-d00014-E: POST fetch-all-package endpoint.
942
- """
943
- repo = app.config["REPO_ROOT"]
944
-
945
- # Get current package context from branch name
946
- context = get_current_package_context(repo)
947
- if not context[0]:
948
- # Not on a review branch - return empty data
949
- return jsonify(
950
- {"threads": {}, "flags": {}, "contributors": [], "error": "Not on a review branch"}
951
- )
952
-
953
- package_id = context[0]
954
-
955
- # Fetch remote branches (if remote exists)
956
- branches = fetch_package_branches(repo, package_id)
957
-
958
- # Return information about fetched branches
959
- return jsonify({"success": True, "package_id": package_id, "branches": branches})
960
-
961
- return app
962
-
963
-
964
- def main():
965
- """Run the review server"""
966
- import argparse
967
-
968
- parser = argparse.ArgumentParser(description="Review API Server")
969
- parser.add_argument("--port", type=int, default=8080, help="Port to run on")
970
- parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
971
- parser.add_argument("--repo", type=Path, default=Path.cwd(), help="Repository root")
972
- parser.add_argument("--static", type=Path, default=None, help="Static files directory")
973
- parser.add_argument("--debug", action="store_true", help="Enable debug mode")
974
- parser.add_argument(
975
- "--no-auto-sync",
976
- action="store_true",
977
- help="Disable automatic git commit/push after changes",
978
- )
979
-
980
- args = parser.parse_args()
981
-
982
- auto_sync = not args.no_auto_sync
983
- static_dir = args.static or args.repo.resolve()
984
- app = create_app(args.repo.resolve(), static_dir=static_dir, auto_sync=auto_sync)
985
-
986
- sync_status = "ENABLED" if auto_sync else "DISABLED"
987
- print(
988
- f"""
989
- ======================================
990
- Review API Server
991
- ======================================
992
-
993
- Repository: {args.repo.resolve()}
994
- Server: http://{args.host}:{args.port}
995
- Auto-Sync: {sync_status}
996
-
997
- API Endpoints:
998
- GET /api/health - Health check
999
- GET /api/reviews/reqs/<id>/flag - Get review flag
1000
- POST /api/reviews/reqs/<id>/flag - Set review flag
1001
- POST /api/reviews/reqs/<id>/threads - Create thread
1002
- POST /api/reviews/reqs/<id>/threads/<tid>/comments - Add comment
1003
- POST /api/reviews/reqs/<id>/threads/<tid>/resolve - Resolve thread
1004
- POST /api/reviews/reqs/<id>/threads/<tid>/unresolve - Unresolve thread
1005
- GET /api/reviews/reqs/<id>/status - Get REQ status
1006
- POST /api/reviews/reqs/<id>/status - Change REQ status
1007
- GET /api/reviews/reqs/<id>/requests - Get status requests
1008
- POST /api/reviews/reqs/<id>/requests - Create status request
1009
- POST /api/reviews/reqs/<id>/requests/<rid>/approvals - Add approval
1010
- GET /api/reviews/packages - List packages
1011
- POST /api/reviews/packages - Create package
1012
- GET /api/reviews/packages/<id> - Get package
1013
- PUT /api/reviews/packages/<id> - Update package
1014
- DELETE /api/reviews/packages/<id> - Delete package
1015
- POST /api/reviews/packages/<id>/reqs/<req_id> - Add REQ to package
1016
- DELETE /api/reviews/packages/<id>/reqs/<req_id> - Remove REQ from package
1017
- GET /api/reviews/packages/active - Get active package
1018
- PUT /api/reviews/packages/active - Set active package
1019
- GET /api/reviews/sync/status - Get sync status
1020
- POST /api/reviews/sync/push - Manual sync
1021
- POST /api/reviews/sync/fetch - Fetch from remote
1022
- POST /api/reviews/sync/fetch-all-package - Fetch all package branches
1023
-
1024
- Press Ctrl+C to stop
1025
- """
1026
- )
1027
-
1028
- app.run(host=args.host, port=args.port, debug=args.debug)
1029
-
1030
-
1031
- if __name__ == "__main__":
1032
- main()