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