Provide access to all scope files

This commit is contained in:
2026-05-02 17:01:24 +02:00
parent 14fce4f4d6
commit 11f0230775
3 changed files with 99 additions and 15 deletions

View File

@@ -22,7 +22,7 @@ start: ## Start the API server in the background on port 8002
for i in $$(seq 1 50); do \ for i in $$(seq 1 50); do \
if [ -f $(PIDFILE) ]; then PID=$$(cat $(PIDFILE)); fi; \ if [ -f $(PIDFILE) ]; then PID=$$(cat $(PIDFILE)); fi; \
if [ -n "$$PID" ] && ! kill -0 $$PID 2>/dev/null; then break; fi; \ if [ -n "$$PID" ] && ! kill -0 $$PID 2>/dev/null; then break; fi; \
if curl -fsS $(HEALTH_URL) >/dev/null 2>&1; then \ if [ -n "$$PID" ] && curl -fsS $(HEALTH_URL) >/dev/null 2>&1; then \
echo "Started (PID $$PID) — http://127.0.0.1:$(PORT)/ui"; \ echo "Started (PID $$PID) — http://127.0.0.1:$(PORT)/ui"; \
exit 0; \ exit 0; \
fi; \ fi; \

View File

@@ -17,12 +17,23 @@ router = APIRouter(include_in_schema=False)
APP_NAME = "Repository Scoping" APP_NAME = "Repository Scoping"
def page(title: str, body: str, selected_repository: str | None = None) -> HTMLResponse: def page(
repository_context = ( title: str,
f'<span class="header-context">{escape(selected_repository)}</span>' body: str,
if selected_repository selected_repository: str | None = None,
else "" selected_repository_id: int | None = None,
) ) -> HTMLResponse:
if selected_repository and selected_repository_id is not None:
repository_context = (
f'<a class="header-context" href="/ui/repos/{selected_repository_id}">'
f"{escape(selected_repository)}</a>"
)
elif selected_repository:
repository_context = (
f'<span class="header-context">{escape(selected_repository)}</span>'
)
else:
repository_context = ""
return HTMLResponse( return HTMLResponse(
f""" f"""
<!doctype html> <!doctype html>
@@ -197,7 +208,6 @@ def page(title: str, body: str, selected_repository: str | None = None) -> HTMLR
<nav class="actions"> <nav class="actions">
<a href="/ui/search">Search</a> <a href="/ui/search">Search</a>
<a href="/ui/discovery">Discovery</a> <a href="/ui/discovery">Discovery</a>
<a href="/ui/scope">SCOPE</a>
<a href="/docs">API Docs</a> <a href="/docs">API Docs</a>
</nav> </nav>
</header> </header>
@@ -307,6 +317,43 @@ def scope_document() -> HTMLResponse:
return page("SCOPE.md", body) return page("SCOPE.md", body)
@router.get("/ui/repos/{repository_id}/scope")
def repository_scope_document(
repository_id: int,
service: RegistryService = Depends(get_service),
) -> HTMLResponse:
repository = service.get_repository(repository_id)
checkout = service.ingestion.cached_checkout(repository.url)
if checkout is None:
rendered = (
'<p class="muted">No cached checkout exists for this repository yet. '
"Run analysis first, or use a local repository path.</p>"
)
else:
scope_path = checkout.source_path / "SCOPE.md"
if scope_path.exists():
content = scope_path.read_text(encoding="utf-8")
rendered = f'<pre class="scope-document">{escape(content)}</pre>'
else:
rendered = (
'<p class="muted">No SCOPE.md file was found in this '
"repository checkout.</p>"
)
body = f"""
<h1>SCOPE.md</h1>
<section class="panel stack">
<p class="muted">Canonical scope summary for the {escape(repository.name)} repository.</p>
{rendered}
</section>
"""
return page(
"SCOPE.md",
body,
selected_repository=repository.name,
selected_repository_id=repository.id,
)
@router.get("/ui/discovery") @router.get("/ui/discovery")
def discovery_page(service: RegistryService = Depends(get_service)) -> HTMLResponse: def discovery_page(service: RegistryService = Depends(get_service)) -> HTMLResponse:
repositories = service.list_repositories() repositories = service.list_repositories()
@@ -701,6 +748,7 @@ def repository_detail(
<div class="actions"> <div class="actions">
<h1 style="margin-right:auto">{escape(repository.name)}</h1> <h1 style="margin-right:auto">{escape(repository.name)}</h1>
<a class="button secondary" href="/ui/repos/{repository_id}/export">Export</a> <a class="button secondary" href="/ui/repos/{repository_id}/export">Export</a>
<a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a>
<a class="button secondary" href="/ui">Back</a> <a class="button secondary" href="/ui">Back</a>
</div> </div>
<p class="muted">{escape(repository.description or '')}</p> <p class="muted">{escape(repository.description or '')}</p>
@@ -840,7 +888,12 @@ def repository_detail(
</form> </form>
</section> </section>
""" """
return page(repository.name, body, selected_repository=repository.name) return page(
repository.name,
body,
selected_repository=repository.name,
selected_repository_id=repository.id,
)
@router.get("/ui/repos/{repository_id}/export") @router.get("/ui/repos/{repository_id}/export")
@@ -1296,6 +1349,7 @@ def analysis_run_detail(
f"{repository.name} Run {analysis_run_id}", f"{repository.name} Run {analysis_run_id}",
body, body,
selected_repository=repository.name, selected_repository=repository.name,
selected_repository_id=repository.id,
) )
@@ -1462,7 +1516,12 @@ def repository_element_listing(
</table> </table>
</section> </section>
""" """
return page(title, body, selected_repository=repository.name) return page(
title,
body,
selected_repository=repository.name,
selected_repository_id=repository.id,
)
@router.get("/ui/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}") @router.get("/ui/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}")
@@ -1519,6 +1578,7 @@ def analysis_run_diff_detail(
f"{diff.repository.name} Change Review", f"{diff.repository.name} Change Review",
body, body,
selected_repository=diff.repository.name, selected_repository=diff.repository.name,
selected_repository_id=diff.repository.id,
) )

View File

@@ -1209,6 +1209,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
"# UI Repo\nReports service status through API and CLI entry points.\n", "# UI Repo\nReports service status through API and CLI entry points.\n",
encoding="utf-8", encoding="utf-8",
) )
(source / "SCOPE.md").write_text(
"# SCOPE\n\n## One-liner\n\nUI Repo owns the status reporting scope.\n",
encoding="utf-8",
)
(source / "requirements.txt").write_text("fastapi\n", encoding="utf-8") (source / "requirements.txt").write_text("fastapi\n", encoding="utf-8")
(source / "app.py").write_text( (source / "app.py").write_text(
"from fastapi import FastAPI\n" "from fastapi import FastAPI\n"
@@ -1243,6 +1247,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "Explore after registration" in index_response.text assert "Explore after registration" in index_response.text
assert "Use LLM assistance if configured" in index_response.text assert "Use LLM assistance if configured" in index_response.text
assert "Trusted auto-populate after analysis" in index_response.text assert "Trusted auto-populate after analysis" in index_response.text
assert '<a href="/ui/scope">SCOPE</a>' not in index_response.text
create_response = client.post( create_response = client.post(
"/ui/repos", "/ui/repos",
@@ -1268,6 +1273,19 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "Use LLM assistance if configured" in detail_response.text assert "Use LLM assistance if configured" in detail_response.text
assert "Trusted auto-populate after analysis" in detail_response.text assert "Trusted auto-populate after analysis" in detail_response.text
assert "Repository Metadata" in detail_response.text assert "Repository Metadata" in detail_response.text
assert (
f'<a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a>'
in detail_response.text
)
repo_scope_response = client.get(f"/ui/repos/{repository_id}/scope")
assert repo_scope_response.status_code == 200
assert (
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo</a>'
in repo_scope_response.text
)
assert "Canonical scope summary for the UI Repo repository." in repo_scope_response.text
assert "UI Repo owns the status reporting scope." in repo_scope_response.text
edit_repository_response = client.post( edit_repository_response = client.post(
f"{repository_path}/edit", f"{repository_path}/edit",
@@ -1305,7 +1323,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "1 abilities" in run_detail.text assert "1 abilities" in run_detail.text
assert "2 capabilities" in run_detail.text assert "2 capabilities" in run_detail.text
assert "2 features" in run_detail.text assert "2 features" in run_detail.text
assert "7 facts" in run_detail.text assert "8 facts" in run_detail.text
assert "Content Chunks" in run_detail.text assert "Content Chunks" in run_detail.text
assert "README.md:1-2" in run_detail.text assert "README.md:1-2" in run_detail.text
assert "ID " in run_detail.text assert "ID " in run_detail.text
@@ -1359,7 +1377,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
approved_detail = client.get(approve_response.headers["location"]) approved_detail = client.get(approve_response.headers["location"])
assert approved_detail.status_code == 200 assert approved_detail.status_code == 200
assert '<span class="header-context">UI Repo Edited</span>' in approved_detail.text assert (
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo Edited</a>'
in approved_detail.text
)
assert "Approved Characteristics" in approved_detail.text assert "Approved Characteristics" in approved_detail.text
assert "Approved Characteristic Tree" in approved_detail.text assert "Approved Characteristic Tree" in approved_detail.text
assert '<strong><a href="/ui/repos/1">UI Repo</a></strong>' in approved_detail.text assert '<strong><a href="/ui/repos/1">UI Repo</a></strong>' in approved_detail.text
@@ -1374,7 +1395,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "1 candidate abilities" in approved_detail.text assert "1 candidate abilities" in approved_detail.text
assert "2 candidate capabilities" in approved_detail.text assert "2 candidate capabilities" in approved_detail.text
assert "2 candidate features" in approved_detail.text assert "2 candidate features" in approved_detail.text
assert "7 candidate facts" in approved_detail.text assert "8 candidate facts" in approved_detail.text
assert "Use Approved Registry" in approved_detail.text assert "Use Approved Registry" in approved_detail.text
assert "Search Profile" in approved_detail.text assert "Search Profile" in approved_detail.text
assert "Discovery" in approved_detail.text assert "Discovery" in approved_detail.text
@@ -1387,7 +1408,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
params={"scope": "all", "type": "scopes", "entry_filter": "approved"}, params={"scope": "all", "type": "scopes", "entry_filter": "approved"},
) )
assert scope_listing.status_code == 200 assert scope_listing.status_code == 200
assert '<span class="header-context">UI Repo Edited</span>' in scope_listing.text assert (
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo Edited</a>'
in scope_listing.text
)
assert f'<a href="/ui/repos/{repository_id}">UI Repo</a>' in scope_listing.text assert f'<a href="/ui/repos/{repository_id}">UI Repo</a>' in scope_listing.text
assert ( assert (
f"/ui/repos/{repository_id}/elements?scope=all&amp;entry_filter=approved&amp;type=abilities" f"/ui/repos/{repository_id}/elements?scope=all&amp;entry_filter=approved&amp;type=abilities"
@@ -1521,7 +1545,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
}, },
) )
assert filtered_fact_listing.status_code == 200 assert filtered_fact_listing.status_code == 200
assert "1 of 7 shown" in filtered_fact_listing.text assert "1 of 8 shown" in filtered_fact_listing.text
assert "FastAPI" in filtered_fact_listing.text assert "FastAPI" in filtered_fact_listing.text
assert "python route decorator" not in filtered_fact_listing.text assert "python route decorator" not in filtered_fact_listing.text