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.
- elspais/__init__.py +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {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()
|