fix(explorer): /api/decisions returns 422 — coerce decision timestamp to str - #937
fix(explorer): /api/decisions returns 422 — coerce decision timestamp to str#937logan-jl-cc wants to merge 2 commits into
Conversation
…i/decisions ContextGraph stores decision timestamps as POSIX floats (e.g. 1786513069.69), but DecisionResponse.timestamp is typed Optional[str]. Pydantic strict validation rejects the float and the whole /api/decisions endpoint returns HTTP 422 "Invalid input", which breaks the Decisions workspace in the Knowledge Explorer entirely (no decision can be listed). Coerce the value to str (preserving None) in _node_to_decision so the response validates. Verified: /api/decisions now returns 200 and the 3 sample decisions render in the Decisions workspace.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
PR Summary by QodoFix /api/decisions 422 by coercing decision timestamp to string
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
Code Review by Qodo
1. Inconsistent timestamp in metadata
|
| timestamp=None if properties.get("timestamp") is None else str(properties.get("timestamp")), | ||
| metadata=properties, |
There was a problem hiding this comment.
1. Inconsistent timestamp in metadata 🐞 Bug ≡ Correctness
_node_to_decision() now stringifies the top-level timestamp but still returns the raw node properties as metadata, so responses can contain timestamp as str while metadata['timestamp'] remains a float. This creates inconsistent API output and can break consumers that read/compare the timestamp from metadata instead of the top-level field.
Agent Prompt
### Issue description
`_node_to_decision()` coerces `DecisionResponse.timestamp` to `str`, but `metadata` is set to the original `properties` dict. When the stored value is a float (as produced by `ContextGraph.record_decision()`), the response contains two different timestamp types (`timestamp: str` vs `metadata.timestamp: float`).
### Issue Context
- This PR intentionally coerces the boundary to satisfy `DecisionResponse.timestamp: Optional[str]`.
- The endpoint also returns `metadata=properties`, which includes the same `timestamp` key.
### Fix Focus Areas
- semantica/explorer/routes/decisions.py[17-28]
### Suggested fix
1. Read the timestamp once into a local variable.
2. Build a new `metadata` dict (copy) and normalize or remove the `timestamp` key so it matches the top-level field.
Example:
```py
properties = node.get("properties", {})
ts_value = properties.get("timestamp")
ts = None if ts_value is None else str(ts_value)
metadata = dict(properties)
metadata["timestamp"] = ts # or: metadata.pop("timestamp", None)
return DecisionResponse(
...,
timestamp=ts,
metadata=metadata,
)
```
Optionally add a small unit test for `_node_to_decision()` with `properties['timestamp']` as `float` to lock in the behavior.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
@logan-jl-cc can handle the qodo reviews before we review it. |
Regression tests for the 422 fix in _node_to_decision. Covers the cases that produced HTTP 422 (float / int timestamps from ContextGraph) and the ones that must keep working (None, already-string, missing key). Verified the suite catches the regression: with the fix reverted, the float / int / nan / inf cases fail with the same ValidationError that caused the 422; with the fix applied all 6 pass.
Problem
GET /api/decisionsreturns HTTP 422 (Unprocessable Entity) for any graph that contains real decisions, which makes the Decisions workspace in the Knowledge Explorer fail to load entirely — no decision can ever be listed.Root cause
ContextGraph.record_decision()stores the decision timestamp as a POSIX float (e.g.1786513069.69):But the explorer response model types it as
Optional[str]:_node_to_decision()insemantica/explorer/routes/decisions.pypasses the raw float through:Pydantic (strict mode) rejects the
float → strcoercion and raises a validation error, so the endpoint returns 422:This affects every endpoint that builds a
DecisionResponse(/api/decisions,/api/decisions/{id},/api/decisions/{id}/precedents).Fix
Coerce the timestamp to
strat the boundary (preservingNone), so the value matches the declared schema regardless of how the source store serializes it:Minimal, one-line, no schema change.
Verification
Before —
GET /api/decisionson a graph with 3 recorded decisions:After — same request:
The Decisions workspace then renders all 3 decisions, and clicking one opens its causal chain (
Causal Chain · 7 steps) as expected.Notes
DecisionResponse.timestamptyping (keptOptional[str]); the explorer frontend already consumes it as a string. Coercing at the adapter is the lowest-risk fix.Optional[Union[str, float]], but that pushes serialization ambiguity onto every consumer; coercing at the single boundary is cleaner.Checklist
ContextGraphwith recorded decisions_node_to_decisionwith a float timestamp (happy to add if maintainers want)