5 Commits

Author SHA1 Message Date
rayhpeng
597a11372e fix(feedback): remove order-dependent import and stale layout pointers
model.py imported its exceptions from the package __init__, which itself
imports model -- a cycle that only resolved because __init__ happens to
import exceptions first. Import them from the exceptions submodule
directly, matching service.py, so reordering __init__ can never break
collection.

Also update the two module docstrings that still pointed readers at the
deleted app/infra/ layout and the old deps.py wiring: adapters live in
app/adapters/{context}/ and wiring happens in the composition root,
app/composition.py::build_domain_services.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 11:12:31 +08:00
rayhpeng
793169529c refactor(feedback): align the slice with the hexagonal spec
- command-ify the write use cases (RateRun / RetractRunRating; queries
  keep plain parameters, commands stay dumb data)
- split the domain errors into exceptions.py, a peer of model.py
  (PEP 8 Error suffixes, AWS-style module name)
- unify the aggregate->row mapping as _apply(row, feedback) so one
  explicit field list serves both the insert and the update path
- drop the unused feedback.message_id column (migration 0011): feedback
  is bound to a run, nothing ever wrote or read the field
- pin remove_for_run's equality semantics for user_id=None in the
  contract suite and fix the port docstring that contradicted both
  implementations
2026-07-29 18:28:54 +08:00
rayhpeng
551865abcf fix(feedback): address review on dialog state, errors, and test seams
Four findings from @willem-bd on #4401:

- FeedbackDialog stays mounted across messages, so selected tags and the
  comment survived an ESC/click-outside dismiss and pre-filled the next
  thumbs-down. Reset on every close path via a wrapped onOpenChange.
- Neither the dialog's handleSubmit nor handleDialogSubmit caught a failed
  enrichment PUT, so a rejection went unhandled and the user got no signal.
  Catch in the dialog (where the rejection lands), toast, and keep the input
  for a retry.
- rate_run awaited the RunLookup port before Feedback.create validated the
  rating, contradicting its own "before any I/O happens" docstring: an
  invalid rating on an unknown run surfaced as RunNotFoundError. Validate
  first, restoring the legacy router's 400-before-404 ordering. The service
  test now uses an unknown run id so it actually pins that order.
- InMemoryFeedbackRepository moved out of test_feedback.py into
  tests/feedback_fakes.py (with FakeRunLookup) so two test modules share it
  without one importing the other; conftest states the tests-dir sys.path
  dependency explicitly, which also makes it work under
  --import-mode=importlib.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:58:45 +08:00
rayhpeng
02117a4d77 style(feedback): give domain errors docstrings instead of ... bodies
The single-line `class X: ...` exception stubs tripped
github-code-quality's "Statement has no effect" rule: a bare `...` is a
discarded expression statement. Replace each with a one-line docstring,
matching the empty-error convention used elsewhere (GoalWriteConflict,
ConflictError) and silencing the false positive. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:54:08 +08:00
rayhpeng
44cb9fd375 feat(harness): add feedback domain slice (model, ports, service)
Hexagonal inner ring for the feedback bounded context: frozen aggregate
with construct-time invariants (rating, reason-tag slugs), technology-
neutral ports (typing.Protocol), and the application service with
explicit user_id. Guarded by an AST purity test that keeps domain/ free
of infrastructure imports.
2026-07-23 15:30:33 +08:00