Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/server/src/repowise/server/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

from ._datetime import UTCDateTime
from .architecture import (
ArchEdgeResponse,
ArchitectureViewResponse,
Expand Down Expand Up @@ -471,6 +472,7 @@
"SymbolNodeSummary",
"SymbolResponse",
"TransitiveEntry",
"UTCDateTime",
"UnclusteredFiles",
"UpdateMcpToolsRequest",
"VersionResponse",
Expand Down
26 changes: 26 additions & 0 deletions packages/server/src/repowise/server/schemas/_datetime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Shared datetime types for the REST API."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Annotated

from pydantic import PlainSerializer


def _serialize_utc(value: datetime) -> str:
"""Serialize an instant as an explicit UTC ISO-8601 value.

SQLite drops ``tzinfo`` when round-tripping SQLAlchemy datetime columns,
while PostgreSQL preserves it. The persistence layer stores these values as
UTC, so a naive value read from SQLite is UTC as well. Stamping it here
keeps every REST client from interpreting it as local browser time.
"""
value = value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
return value.isoformat().replace("+00:00", "Z")


UTCDateTime = Annotated[
datetime,
PlainSerializer(_serialize_utc, return_type=str, when_used="json"),
]
8 changes: 4 additions & 4 deletions packages/server/src/repowise/server/schemas/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from __future__ import annotations

import json
from datetime import datetime
from typing import Any, Literal

from pydantic import BaseModel, Field

from repowise.server.chat_artifacts import normalize_message_artifacts
from repowise.server.schemas._datetime import UTCDateTime


class ChatPageContext(BaseModel):
Expand Down Expand Up @@ -68,8 +68,8 @@ class ConversationResponse(BaseModel):
title: str
message_count: int = 0
pinned: bool = False
created_at: datetime
updated_at: datetime
created_at: UTCDateTime
updated_at: UTCDateTime

@classmethod
def from_orm(cls, obj: object, message_count: int = 0) -> ConversationResponse:
Expand Down Expand Up @@ -103,7 +103,7 @@ class ChatMessageResponse(BaseModel):
conversation_id: str
role: str
content: dict
created_at: datetime
created_at: UTCDateTime

@classmethod
def from_orm(cls, obj: object) -> ChatMessageResponse:
Expand Down
8 changes: 4 additions & 4 deletions packages/server/src/repowise/server/schemas/code_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from __future__ import annotations

import json
from datetime import datetime

from pydantic import BaseModel

from repowise.core.analysis.dead_code.risk_factors import (
effective_safe_to_delete,
path_risk_factors,
)
from repowise.server.schemas._datetime import UTCDateTime


class DeadCodeFindingResponse(BaseModel):
Expand Down Expand Up @@ -40,7 +40,7 @@ class DeadCodeFindingResponse(BaseModel):
# confidence ladder. Deliberately not ``age_days``: that is measured from
# the *first* commit, so it answers "how old is this file", not "how long
# has this been dead", and the two disagree on 75% of findings.
last_commit_at: datetime | None
last_commit_at: UTCDateTime | None
# Commits to the file in the last 90 days. Top rung of the confidence
# ladder (0 commits is what earns the high tiers), so surfacing it is what
# makes a low confidence score legible: the file is still being worked on.
Expand Down Expand Up @@ -98,7 +98,7 @@ class SecurityFindingResponse(BaseModel):
kind: str
severity: str
snippet: str | None
detected_at: datetime
detected_at: UTCDateTime
# Where in the file. Checked against the live tree before serving, so a
# line that drifted is either corrected or withdrawn — see
# ``services/security_lines.py``. ``None`` means the snippet is gone from
Expand All @@ -110,7 +110,7 @@ class SecurityFindingResponse(BaseModel):
# Present when the finding was sourced from git history (full-history
# scan). ``None`` for working-tree findings produced during indexing.
commit_sha: str | None
commit_at: datetime | None
commit_at: UTCDateTime | None
found_in_history: bool


Expand Down
12 changes: 7 additions & 5 deletions packages/server/src/repowise/server/schemas/decisions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from __future__ import annotations

import json
from datetime import datetime

from pydantic import BaseModel, ConfigDict, Field

from repowise.core.analysis.decisions.policy import DISCOVERY_BOUNDS
from repowise.core.analysis.decisions.scope import derive_decision_scope
from repowise.server.schemas._datetime import UTCDateTime


class EvidencePreview(BaseModel):
Expand Down Expand Up @@ -46,9 +46,9 @@ class DecisionRecordResponse(BaseModel):
# the linkage fields, so old records get it too.
scope: str | None = None
superseded_by: str | None
last_code_change: datetime | None
created_at: datetime
updated_at: datetime
last_code_change: UTCDateTime | None
created_at: UTCDateTime
updated_at: UTCDateTime
# List-row evidence preview: the top-ranked evidence row's verbatim quote
# plus how many evidence rows back the record. Populated by the list
# endpoint only (None on detail/graph responses, which have the full
Expand Down Expand Up @@ -280,7 +280,9 @@ class DecisionLineageResponse(BaseModel):
#: Sourced from the policy registry so the wire bounds cannot drift from the
#: ones the resolver enforces.
_DISCOVERY_DEFAULTS = {key: bounds[2] for key, bounds in DISCOVERY_BOUNDS.items()}
_DISCOVERY_RANGE = {key: {"ge": bounds[0], "le": bounds[1]} for key, bounds in DISCOVERY_BOUNDS.items()}
_DISCOVERY_RANGE = {
key: {"ge": bounds[0], "le": bounds[1]} for key, bounds in DISCOVERY_BOUNDS.items()
}


class DecisionSourceState(BaseModel):
Expand Down
14 changes: 7 additions & 7 deletions packages/server/src/repowise/server/schemas/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from __future__ import annotations

import json
from datetime import datetime
from typing import Any

from pydantic import BaseModel

from repowise.core.co_change import parse_partners
from repowise.server.schemas._datetime import UTCDateTime
from repowise.server.schemas.risk_semantics import RiskAuthority


Expand All @@ -17,8 +17,8 @@ class GitMetadataResponse(BaseModel):
commit_count_total: int
commit_count_90d: int
commit_count_30d: int
first_commit_at: datetime | None
last_commit_at: datetime | None
first_commit_at: UTCDateTime | None
last_commit_at: UTCDateTime | None
primary_owner_name: str | None
primary_owner_email: str | None
primary_owner_commit_pct: float | None
Expand Down Expand Up @@ -50,7 +50,7 @@ class GitMetadataResponse(BaseModel):
# the same at two weeks and two years. Empty/None on a pre-rollup index.
fix_symbol_counts: dict = {}
bug_magnet: bool = False
last_fix_at: datetime | None = None
last_fix_at: UTCDateTime | None = None
temporal_hotspot_score: float | None = None
commit_count_capped: bool = False
# Rename lineage: the file's path before its most recent move, if any.
Expand Down Expand Up @@ -136,7 +136,7 @@ class HotspotResponse(BaseModel):
merge_commit_count_90d: int = 0
commit_count_capped: bool = False
age_days: int = 0
last_commit_at: datetime | None = None
last_commit_at: UTCDateTime | None = None
# Change-complexity + defect-history signals.
change_entropy: float = 0.0
change_entropy_pct: float = 0.0
Expand All @@ -145,7 +145,7 @@ class HotspotResponse(BaseModel):
# age describes "fixed 4x last month" and "fixed 4x two years ago"
# identically. Consumers drop the flag when the timestamp is missing.
bug_magnet: bool = False
last_fix_at: datetime | None = None
last_fix_at: UTCDateTime | None = None
original_path: str | None = None


Expand Down Expand Up @@ -179,7 +179,7 @@ class CommitResponse(BaseModel):
short_sha: str
author_name: str
author_email: str
committed_at: datetime | None
committed_at: UTCDateTime | None
subject: str
lines_added: int
lines_deleted: int
Expand Down
12 changes: 6 additions & 6 deletions packages/server/src/repowise/server/schemas/ownership.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from __future__ import annotations

from datetime import datetime

from pydantic import BaseModel

from repowise.server.schemas._datetime import UTCDateTime


class KnowledgeMapOwner(BaseModel):
email: str
Expand Down Expand Up @@ -44,7 +44,7 @@ class OwnerListEntry(BaseModel):
dead_code_files_owned: int
dead_code_lines_owned: int
commit_count_90d: int # sum of per-file 90d commits attributed to this person
last_commit_at: datetime | None
last_commit_at: UTCDateTime | None
bus_factor_risk_files: int # files they own where bus_factor <= 1


Expand All @@ -61,7 +61,7 @@ class OwnerFileEntry(BaseModel):
churn_percentile: float # 0-100
bus_factor: int
is_hotspot: bool
last_commit_at: datetime | None
last_commit_at: UTCDateTime | None
primary_owner_commit_pct: float | None


Expand Down Expand Up @@ -100,8 +100,8 @@ class OwnerProfileResponse(BaseModel):
dead_code_files_owned: int
dead_code_lines_owned: int
commit_count_90d: int
last_commit_at: datetime | None
first_commit_at: datetime | None
last_commit_at: UTCDateTime | None
first_commit_at: UTCDateTime | None
bus_factor_risk_files: int

# 90d activity proxies (approximated from file-level totals weighted by
Expand Down
17 changes: 9 additions & 8 deletions packages/server/src/repowise/server/schemas/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from __future__ import annotations

import json
from datetime import datetime

from pydantic import BaseModel

from repowise.server.schemas._datetime import UTCDateTime


def _layer_stamp(obj: object, metadata: dict | None) -> tuple[str | None, str | None]:
"""Which layer this page belongs to, read off its metadata blob.
Expand Down Expand Up @@ -148,8 +149,8 @@ class PageSummaryResponse(BaseModel):
display_order: int = 0
section_number: str | None = None
structural_key: str | None = None
created_at: datetime
updated_at: datetime
created_at: UTCDateTime
updated_at: UTCDateTime

@classmethod
def from_orm(cls, obj: object) -> PageSummaryResponse:
Expand Down Expand Up @@ -185,7 +186,7 @@ class PageVersionResponse(BaseModel):
input_tokens: int
output_tokens: int
confidence: float
archived_at: datetime
archived_at: UTCDateTime

@classmethod
def from_orm(cls, obj: object) -> PageVersionResponse:
Expand Down Expand Up @@ -218,10 +219,10 @@ class JobResponse(BaseModel):
current_level: int
error_message: str | None
config: dict
created_at: datetime
updated_at: datetime
started_at: datetime | None
finished_at: datetime | None
created_at: UTCDateTime
updated_at: UTCDateTime
started_at: UTCDateTime | None
finished_at: UTCDateTime | None
# Short-lived token for the SSE progress stream (an EventSource can't send
# the bearer header). Only minted while the job is live; ``None`` once it
# reaches a terminal state, since there's nothing left to stream. Any client
Expand Down
10 changes: 5 additions & 5 deletions packages/server/src/repowise/server/schemas/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path

from pydantic import BaseModel, field_validator

from repowise.core.docs_mode import DocsMode
from repowise.server.schemas._datetime import UTCDateTime


class RepoCreate(BaseModel):
Expand Down Expand Up @@ -50,8 +50,8 @@ class RepoResponse(BaseModel):
default_branch: str
head_commit: str | None
settings: dict
created_at: datetime
updated_at: datetime
created_at: UTCDateTime
updated_at: UTCDateTime
# Workspace context — populated when the server is running in
# workspace mode. ``status`` indicates whether the repo has been
# indexed yet; the web UI uses it to render "needs index" CTA cards
Expand Down Expand Up @@ -121,7 +121,7 @@ class RepoSummaryRow(BaseModel):
id: str
name: str
local_path: str
updated_at: datetime | None = None
updated_at: UTCDateTime | None = None
#: "indexed" | "needs_index" | "missing_dir" — same vocabulary as
#: ``RepoResponse.workspace_status``, which the sidebar already renders.
status: str = "indexed"
Expand All @@ -147,7 +147,7 @@ class RepoSummaryRow(BaseModel):
#: distinct from a score of 0, which would mean "analysed, and terrible".
average_health: float | None = None
hotspot_health: float | None = None
health_taken_at: datetime | None = None
health_taken_at: UTCDateTime | None = None

#: Index-vs-checkout freshness. ``index_behind`` is ``None`` when the
#: comparison could not run (no git checkout on disk, unreadable HEAD)
Expand Down
Loading
Loading