Skip to content

Commit db07930

Browse files
feat: hotspot → top symbols drill-down, writable decision linkage (#191)
Hotspot table now expands each row into the importance-ranked top symbols in that file (server gains a `file_path` filter on /api/symbols); clicking a symbol opens the existing SymbolDrawer. Decision detail page replaces the read-only affected-files block with a ModuleLinkEditor — module-path autocomplete pulls from /modules/health. PATCH /decisions/{id} now accepts optional `affected_modules` / `affected_files` alongside `status` so the editor can persist without forcing a status change.
1 parent 44223d8 commit db07930

18 files changed

Lines changed: 827 additions & 34 deletions

File tree

packages/core/src/repowise/core/persistence/crud.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,30 @@ async def list_decisions(
10541054
return list(result.scalars().all())
10551055

10561056

1057+
async def update_decision_metadata(
1058+
session: AsyncSession,
1059+
decision_id: str,
1060+
*,
1061+
affected_modules: list[str] | None = None,
1062+
affected_files: list[str] | None = None,
1063+
) -> DecisionRecord | None:
1064+
"""Patch the module/file linkage on a decision record.
1065+
1066+
Each argument left as ``None`` is preserved. Pass an empty list to clear.
1067+
Returns the updated record, or ``None`` if the id was not found.
1068+
"""
1069+
rec = await session.get(DecisionRecord, decision_id)
1070+
if rec is None:
1071+
return None
1072+
if affected_modules is not None:
1073+
rec.affected_modules_json = json.dumps(affected_modules)
1074+
if affected_files is not None:
1075+
rec.affected_files_json = json.dumps(affected_files)
1076+
rec.updated_at = _now_utc()
1077+
await session.flush()
1078+
return rec
1079+
1080+
10571081
async def update_decision_status(
10581082
session: AsyncSession,
10591083
decision_id: str,

packages/server/src/repowise/server/routers/decisions.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,43 @@ async def patch_decision(
124124
body: DecisionStatusUpdate,
125125
session: AsyncSession = Depends(get_db_session), # noqa: B008
126126
) -> DecisionRecordResponse:
127-
"""Update the status of a decision record (confirm, deprecate, supersede)."""
128-
try:
129-
rec = await crud.update_decision_status(
127+
"""Update a decision record.
128+
129+
Accepts status transitions (confirm / deprecate / supersede) and / or
130+
governance edits (``affected_modules``, ``affected_files``). Any field
131+
left as ``None`` in the body is preserved.
132+
"""
133+
rec = await crud.get_decision(session, decision_id)
134+
if rec is None or rec.repository_id != repo_id:
135+
raise HTTPException(status_code=404, detail="Decision not found")
136+
137+
if body.status is not None:
138+
try:
139+
rec = await crud.update_decision_status(
140+
session,
141+
decision_id,
142+
body.status,
143+
superseded_by=body.superseded_by,
144+
)
145+
except ValueError as exc:
146+
raise HTTPException(status_code=400, detail=str(exc)) from exc
147+
if rec is None:
148+
raise HTTPException(status_code=404, detail="Decision not found")
149+
elif body.superseded_by is not None:
150+
raise HTTPException(
151+
status_code=400,
152+
detail="superseded_by requires status='superseded'",
153+
)
154+
155+
if body.affected_modules is not None or body.affected_files is not None:
156+
rec = await crud.update_decision_metadata(
130157
session,
131158
decision_id,
132-
body.status,
133-
superseded_by=body.superseded_by,
159+
affected_modules=body.affected_modules,
160+
affected_files=body.affected_files,
134161
)
135-
except ValueError as exc:
136-
raise HTTPException(status_code=400, detail=str(exc)) from exc
137-
if rec is None:
138-
raise HTTPException(status_code=404, detail="Decision not found")
139-
if rec.repository_id != repo_id:
140-
raise HTTPException(status_code=404, detail="Decision not found")
162+
if rec is None:
163+
raise HTTPException(status_code=404, detail="Decision not found")
164+
165+
assert rec is not None
141166
return DecisionRecordResponse.from_orm(rec)

packages/server/src/repowise/server/routers/symbols.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ async def search_symbols(
9696
kind: str | None = Query(None, description="Filter by symbol kind"),
9797
language: str | None = Query(None, description="Filter by language"),
9898
visibility: str | None = Query(None, description="Filter by visibility"),
99+
file_path: str | None = Query(
100+
None, description="Filter by exact source file path (for hotspot drill-down)"
101+
),
99102
in_hot_files: bool = Query(False, description="Only symbols whose file is a hotspot"),
100103
in_entry_points: bool = Query(False, description="Only symbols in entry-point files"),
101104
sort: SortKey = Query("importance", description="Sort key"),
@@ -120,6 +123,8 @@ async def search_symbols(
120123
base = base.where(WikiSymbol.language == language)
121124
if visibility:
122125
base = base.where(WikiSymbol.visibility == visibility)
126+
if file_path:
127+
base = base.where(WikiSymbol.file_path == file_path)
123128

124129
# Optional file-level filters require resolving file paths up front.
125130
if in_hot_files or in_entry_points:

packages/server/src/repowise/server/schemas.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,8 +941,17 @@ class DecisionCreate(BaseModel):
941941

942942

943943
class DecisionStatusUpdate(BaseModel):
944-
status: str
944+
"""PATCH body for /decisions/{id}.
945+
946+
All fields are optional — clients can update status alone (the historical
947+
contract), the linked modules / files alone (governance editor), or both
948+
in a single request. Fields left at ``None`` are preserved.
949+
"""
950+
951+
status: str | None = None
945952
superseded_by: str | None = None
953+
affected_modules: list[str] | None = None
954+
affected_files: list[str] | None = None
946955

947956

948957
# ---------------------------------------------------------------------------

packages/types/src/decisions.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,15 @@ export interface DecisionCreateInput {
5656
tags?: string[];
5757
}
5858

59+
/**
60+
* PATCH body for /api/repos/{id}/decisions/{decision_id}. All fields are
61+
* optional — clients can update just the status, just the linkage, or both.
62+
*/
5963
export interface DecisionStatusUpdate {
60-
status: DecisionStatus;
64+
status?: DecisionStatus;
6165
superseded_by?: string;
66+
affected_modules?: string[];
67+
affected_files?: string[];
6268
}
6369

6470
export interface DecisionHealth {

packages/ui/src/decisions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./decision-health-widget.js";
22
export * from "./decisions-table.js";
3+
export * from "./module-link-editor.js";

0 commit comments

Comments
 (0)