Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/scale/WORKSPACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ repowise serve

In workspace mode, the web UI adds:

- **Workspace Dashboard** (`/workspace`), aggregate stats across all repos, repo cards with file/symbol/coverage counts, and cross-repo intelligence summary
- **Workspace Dashboard** (`/workspace`), aggregate stats across all repos, repo rows with code health, file, hotspot and doc freshness counts, and cross-repo intelligence summary. Doc freshness is fresh pages over all pages.
- **System Map** (`/workspace/system-map`), the [Live System Map](#live-system-map): a code-derived diagram of services and their typed relationships
- **Contracts View** (`/workspace/contracts`), all detected API contracts with provider/consumer matching, filterable by type and repo
- **Co-Changes View** (`/workspace/co-changes`), cross-repo file pairs ranked by co-change strength
Expand All @@ -355,7 +355,7 @@ The sidebar shows all workspace repos under **Repositories**. Click any repo to

The System Map renders the [system graph](#system-graph) as an always-current diagram. It is the visual counterpart to the REST endpoint, the same nodes and edges, laid out and explorable, never a hand-drawn picture.

- **Service nodes**, coloured by category (service, frontend, worker, library, external), with a health ring rolled up from the owning repo and small flags for orphan or isolated services.
- **Service nodes**, coloured by category (service, frontend, worker, library, external), with the owning repo's code health score ("No health score" when health has not scored that repo) and small flags for orphan or isolated services.
- **Typed edges** distinguished by `kind` (colour + glyph) and by `match_type` (solid for exact/manual, dashed for candidate, dotted for inferred co-change). Behavioral co-change edges read differently from structural contract/dependency edges.
- **Filters** to toggle each edge kind on or off, and a **service / repo** switch that collapses a monorepo's services into one node per repository.
- **Drill-down**: click a service to inspect its providers/consumers and connected services; click an edge to see its match type, confidence, weight, and the underlying contract evidence, with a jump to the Contracts view.
Expand Down
8 changes: 6 additions & 2 deletions packages/api-client/src/types/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export interface WorkspaceRepoEntry {
file_count: number;
symbol_count: number;
page_count: number;
// Pages whose freshness status is fresh, out of `page_count`.
fresh_page_count: number;
// Mean wiki page confidence, 0-100. Not a share of files documented.
doc_coverage_pct: number;
hotspot_count: number;
// Canonical 0-100 health score. Optional so a frontend ahead of its server
Expand Down Expand Up @@ -202,9 +205,10 @@ export interface WorkspaceGraphNode {
repo_id: string;
name: string;
file_count: number;
/** Mean wiki page confidence, 0-100. */
coverage_pct: number;
health_score: number;
health_score_source: "canonical" | "derived";
/** Code health, 0-100; null when health has not scored the repo. */
health_score: number | null;
top_language: string;
}

Expand Down
58 changes: 15 additions & 43 deletions packages/server/src/repowise/server/routers/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,47 +91,23 @@ def _query_top_language(db_path: Path) -> str:
return "unknown"


def _compute_health_score(file_count: int, doc_coverage_pct: float, hotspot_count: int) -> int:
"""Derive a 0-100 health score from available repo metrics."""
if file_count == 0:
return 0
coverage_component = doc_coverage_pct * 0.6
hotspot_ratio = min(hotspot_count / max(file_count, 1), 1.0)
hotspot_component = (1.0 - hotspot_ratio) * 100 * 0.4
return max(0, min(100, round(coverage_component + hotspot_component)))


def _resolve_graph_health_score(stats: dict) -> tuple[float, str]:
canonical = stats.get("health_score")
if canonical is not None:
return canonical, "canonical"
return (
float(
_compute_health_score(
stats.get("file_count", 0),
stats.get("doc_coverage_pct", 0.0),
stats.get("hotspot_count", 0),
)
),
"derived",
)


def _query_repo_stats(db_path: Path) -> dict:
"""Query basic stats from a repo's wiki.db using raw sqlite3.

Returns a dict with repo_id, file_count, symbol_count, page_count,
doc_coverage_pct, hotspot_count, health_score, status, docs_enabled,
docs_skip_reason, and (when the row is read) indexed_at. All values default to sensible neutrals on error.
fresh_page_count, doc_coverage_pct, hotspot_count, health_score, status,
docs_enabled, docs_skip_reason, and (when the row is read) indexed_at.
All values default to sensible neutrals on error.

``health_score`` is None when the repo carries no health metrics; callers
decide whether to fall back to a derived score or to render nothing.
``health_score`` is None when the repo carries no health metrics, and
stays None: no other figure here measures code health.
"""
result: dict = {
"repo_id": None,
"file_count": 0,
"symbol_count": 0,
"page_count": 0,
"fresh_page_count": 0,
"doc_coverage_pct": 0.0,
"hotspot_count": 0,
"health_score": None,
Expand Down Expand Up @@ -174,13 +150,14 @@ def _query_repo_stats(db_path: Path) -> dict:
row = c.execute("SELECT COUNT(*) FROM wiki_symbols").fetchone()
result["symbol_count"] = int(row[0]) if row else 0

# page count
row = c.execute("SELECT COUNT(*) FROM wiki_pages").fetchone()
result["page_count"] = row[0] if row else 0

# doc coverage (avg confidence * 100)
row = c.execute("SELECT AVG(confidence) FROM wiki_pages").fetchone()
result["doc_coverage_pct"] = round(float(row[0] or 0.0) * 100, 1)
# Pages, fresh pages, and mean page confidence * 100. The last is named
# doc_coverage_pct on the wire; it is not a share of files documented.
pages, fresh, confidence = c.execute(
"SELECT COUNT(*), SUM(freshness_status = 'fresh'), AVG(confidence) FROM wiki_pages"
).fetchone()
result["page_count"] = pages
result["fresh_page_count"] = int(fresh or 0)
result["doc_coverage_pct"] = round(float(confidence or 0.0) * 100, 1)

# hotspot count — use the canonical is_hotspot flag, matching the rest
# of the codebase (module_health, extract-demo-data). The earlier
Expand Down Expand Up @@ -460,10 +437,6 @@ async def get_workspace_graph(
db_path = repo_path / ".repowise" / "wiki.db"
stats = _query_repo_stats(db_path)
top_language = _query_top_language(db_path)
health_score, health_score_source = _resolve_graph_health_score(stats)
else:
health_score = 0.0
health_score_source = "derived"
rid = stats.get("repo_id") or r.alias
repo_id_map[r.alias] = rid
nodes.append(
Expand All @@ -472,8 +445,7 @@ async def get_workspace_graph(
name=r.alias,
file_count=stats.get("file_count", 0),
coverage_pct=stats.get("doc_coverage_pct", 0.0),
health_score=health_score,
health_score_source=health_score_source,
health_score=stats.get("health_score"),
top_language=top_language,
)
)
Expand Down
8 changes: 6 additions & 2 deletions packages/server/src/repowise/server/schemas/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class WorkspaceRepoEntry(BaseModel):
file_count: int = 0
symbol_count: int = 0
page_count: int = 0
# Pages whose freshness status is fresh, out of ``page_count``.
fresh_page_count: int = 0
# Mean wiki page confidence, 0-100. Not a share of files documented.
doc_coverage_pct: float = 0.0
hotspot_count: int = 0
# Canonical 0-100 health score, or None when the repo carries no health
Expand Down Expand Up @@ -212,9 +215,10 @@ class WorkspaceGraphNode(BaseModel):
repo_id: str
name: str
file_count: int = 0
#: Mean wiki page confidence, 0-100, like the entry's ``doc_coverage_pct``.
coverage_pct: float = 0.0
health_score: float = 0.0
health_score_source: str = "derived"
#: The repo's code health, 0-100; None when health has not scored it.
health_score: float | None = None
top_language: str = "unknown"


Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/generated/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4671,8 +4671,7 @@ export interface WorkspaceGraphNode {
name: string;
file_count?: number;
coverage_pct?: number;
health_score?: number;
health_score_source?: string;
health_score?: number | null;
top_language?: string;
}

Expand Down Expand Up @@ -4761,6 +4760,7 @@ export interface WorkspaceRepoEntry {
file_count?: number;
symbol_count?: number;
page_count?: number;
fresh_page_count?: number;
doc_coverage_pct?: number;
hotspot_count?: number;
health_score?: number | null;
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/COMPONENT_CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ role (`provider` / `consumer`).

### `workspace/repo-card` — `RepoCard`

Tile linking to `/repos/{id}/overview` with file count, doc-coverage,
Tile linking to `/repos/{id}/overview` with file count, doc confidence,
hotspot count, and a primary indicator.

| Prop | Type | Required |
Expand Down
16 changes: 14 additions & 2 deletions packages/ui/__tests__/overview/overview-body.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { OverviewSummaryResponse } from "@repowise-dev/types/overview";
import { AttentionAreas } from "../../src/overview/attention-areas.js";
import { HealthLede } from "../../src/overview/health-lede.js";
import { OverviewBody } from "../../src/overview/overview-body.js";
import { buildReads, inventoryRows } from "../../src/overview/overview-model.js";
import { buildExplore, buildReads, inventoryRows } from "../../src/overview/overview-model.js";

function summary(): OverviewSummaryResponse {
return {
Expand Down Expand Up @@ -225,7 +225,8 @@ describe("inventoryRows", () => {
it("counts each area on the same basis as its card and page", () => {
const rows = inventoryRows({
...summary(),
health: { ...summary().health, finding_counts: { inventory: 321 } },
// The queue inventory is lower; the row prints the open_findings headline.
health: { ...summary().health, open_findings: 321, finding_counts: { inventory: 200 } },
attention_summary: {
total: 99,
by_source: {},
Expand Down Expand Up @@ -256,6 +257,17 @@ describe("inventoryRows", () => {
});
});

describe("buildExplore", () => {
it("names the open_findings headline on the code health door, not the queue inventory", () => {
const s = summary();
const entries = buildExplore(
{ ...s, health: { ...s.health, open_findings: 13563, finding_counts: { inventory: 11371 } } } as unknown as OverviewSummaryResponse,
{ base: "/r" },
);
expect(entries.find((e) => e.key === "code-health")?.description).toContain("13,563 open findings");
});
});

describe("AttentionAreas", () => {
it("shows the worst item's severity once, as a dot and a word, with no hidden copy", () => {
render(
Expand Down
56 changes: 56 additions & 0 deletions packages/ui/__tests__/workspace/repo-card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { ReactFlowProvider } from "@xyflow/react";
import type { NodeProps } from "@xyflow/react";

import { RepoCard } from "../../src/workspace/repo-card.js";
import { WorkspaceGraphNode } from "../../src/workspace/workspace-graph-node.js";

describe("RepoCard", () => {
it("names the figure as doc confidence, floored", () => {
render(
<RepoCard
repoId="r1"
alias="api"
name="api"
path="./api"
isPrimary={false}
stats={{ file_count: 12, doc_coverage_pct: 99.8 }}
gitSummary={{ hotspot_count: 3 }}
/>,
);
expect(screen.getByText("99% doc confidence")).toBeInTheDocument();
expect(screen.queryByText(/% docs$/)).toBeNull();
});
});

function renderNode(healthScore: number | null) {
const data = {
repoId: "r1",
name: "api",
fileCount: 12,
coveragePct: 99.8,
healthScore,
topLanguage: "python",
};
return render(
<ReactFlowProvider>
<WorkspaceGraphNode {...({ data } as unknown as NodeProps)} />
</ReactFlowProvider>,
);
}

describe("WorkspaceGraphNode", () => {
it("labels mean page confidence as doc confidence, not coverage", () => {
renderNode(72);
expect(screen.getByText("99% doc conf.")).toBeInTheDocument();
expect(screen.queryByText(/% cov$/)).toBeNull();
expect(screen.getByLabelText("Health score: 72")).toBeInTheDocument();
});

it("shows no score for a repo health has not scored", () => {
renderNode(null);
expect(screen.getByText("no score")).toBeInTheDocument();
expect(screen.queryByLabelText(/Health score/)).toBeNull();
});
});
8 changes: 6 additions & 2 deletions packages/ui/src/overview/overview-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export function buildExplore(
key: "code-health",
label: "Code health",
href: `${r.base}/code-health`,
description: `Per-file scores, ${formatNumber(health.finding_counts?.inventory ?? health.open_findings)} open findings, coverage and refactoring targets`,
description: `Per-file scores, ${formatNumber(health.open_findings)} open findings, coverage and refactoring targets`,
},
{
key: "knowledge-graph",
Expand Down Expand Up @@ -456,11 +456,15 @@ export type InventoryRow = OverviewAttentionAreaRow & { unit?: string };
* matches in shipped source for security, open opportunities for
* refactoring, decisions to review, and sole-owned files for ownership. Rows
* whose basis the payload does not carry keep the server's count.
*
* Open findings is `open_findings`, the total get_overview and `repowise
* health` print. The queue's `finding_counts.inventory` leaves zero-impact
* findings out, so it is not the number those surfaces call open findings.
*/
export function inventoryRows(summary: OverviewSummaryResponse): InventoryRow[] {
const areas = summary.attention_summary?.areas ?? [];
const explore = summary.explore;
const findings = summary.health.finding_counts?.inventory;
const findings = summary.health.open_findings;
return areas.map((area): InventoryRow => {
switch (area.key) {
case "health":
Expand Down
8 changes: 6 additions & 2 deletions packages/ui/src/workspace/repo-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,12 @@ export function RepoCard({
</div>
<div className="flex items-center gap-1.5">
<BarChart3 className="h-3.5 w-3.5 text-[var(--color-text-tertiary)]" />
<span className="text-xs text-[var(--color-text-secondary)]">
{stats ? `${Math.round(stats.doc_coverage_pct)}% docs` : "—"}
{/* `doc_coverage_pct` is mean page confidence, not a share of files documented. */}
<span
className="text-xs text-[var(--color-text-secondary)]"
title="Average confidence of the generated doc pages"
>
{stats ? `${Math.floor(stats.doc_coverage_pct)}% doc confidence` : "—"}
</span>
</div>
<div className="flex items-center gap-1.5">
Expand Down
4 changes: 1 addition & 3 deletions packages/ui/src/workspace/system-map/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import type { SystemEdge, SystemNode } from "@repowise-dev/types";

/** Repo-level health, joined client-side onto service nodes by repo alias. */
export interface RepoHealth {
/** 0-100 health score. */
/** 0-100 code health score. */
score: number;
/** Whether the score is canonical (computed) or derived (estimated). */
source: "canonical" | "derived";
}

/** React Flow node payload — the `SystemNode` plus its joined health, if any. */
Expand Down
Loading
Loading