luplo.core.ideas¶
CRUD operations for the ideas table.
Ideas are append-only ideation notes attached to a work unit — separate
from items because their lifecycle and intent differ. There is
intentionally no update_idea or delete_idea here: mistakes
are recovered via redact_idea(), which preserves the audit row.
The Python API surface is the policy boundary; the database has no triggers enforcing append-only. Direct SQL is still allowed for admin cleanup.
Functions¶
|
Insert an idea attached to a work unit. |
|
List ideas for a work unit, newest first. |
|
Full-text search over ideas, project-scoped. |
|
Mark an idea as redacted (idempotent — no-op if already redacted). |
|
Fetch a single idea by id (or hex prefix). Includes redacted rows. |
Module Contents¶
- async luplo.core.ideas.add_idea(conn: psycopg.AsyncConnection[Any], *, project_id: str, work_unit_id: str, text: str, created_by: str | None = None, id: str | None = None) luplo.core.models.Idea¶
Insert an idea attached to a work unit.
Accepts a full UUID or 8+ hex prefix for
work_unit_id. The work unit must exist, belong toproject_id, and not bearchivedorabandoned.doneis allowed (retro notes).Status check + INSERT happen in a single SQL statement — no TOCTOU window between “I checked archived” and “I inserted”.
- Raises:
ValidationError – empty text, cross-project WU, or WU in a status that rejects new ideas.
NotFoundError –
work_unit_iddoes not resolve to any row.
- async luplo.core.ideas.list_ideas(conn: psycopg.AsyncConnection[Any], *, work_unit_id: str, project_id: str | None = None, limit: int = 100, include_redacted: bool = False) list[luplo.core.models.Idea]¶
List ideas for a work unit, newest first.
Accepts a full UUID or 8+ hex prefix for
work_unit_id.project_idscopes prefix resolution and is also enforced at the SELECT level so a full UUID from another project returns[]instead of leaking rows. Redacted rows are excluded by default;include_redacted=Trueopts back in (admin / audit flows).
- async luplo.core.ideas.search_ideas(conn: psycopg.AsyncConnection[Any], *, project_id: str, query: str | None = None, tsquery: str | None = None, work_unit_id: str | None = None, author: str | None = None, since: datetime.datetime | None = None, until: datetime.datetime | None = None, include_redacted: bool = False, limit: int = 50) list[luplo.core.models.Idea]¶
Full-text search over ideas, project-scoped.
Two query modes (mutually exclusive):
query— simple dialect, glossary-expanded. Same dialect asluplo.core.search.pipeline.search()for items.tsquery— rawto_tsqueryexpression. Caller owns synonym coverage and validity. Malformed expressions surface aspsycopg.errors.SyntaxErrorfrom Postgres.
Filters:
work_unit_id(narrow scope, accepts UUID or hex prefix),author(actor id),since/until(datetime — caller does string parsing). Bothqueryandtsquerymay beNonefor a filter-only search (e.g. “내가 이번 주 적은 아이디어”). Redacted rows excluded unlessinclude_redacted=True.include_redacted=Truecannot be combined withquery/tsquery— the body is masked at the response layer, but a text predicate on redacted rows would leak via match/no-match (a keyword oracle). Use filter-only mode for audit flows, or fetch raw text via the SaaS-side admin path.
- async luplo.core.ideas.redact_idea(conn: psycopg.AsyncConnection[Any], *, idea_id: str, redacted_by: str, project_id: str | None = None) tuple[luplo.core.models.Idea, bool]¶
Mark an idea as redacted (idempotent — no-op if already redacted).
Append-only invariant preserved: the row remains,
textis not cleared, and downstream readers filter onredacted_at IS NULLby default.project_id(when provided) scopes prefix resolution and is enforced in the UPDATE/SELECT predicate so a full UUID from another project cannot mutate or read this row. Ambiguity errors surface only the matchedidvalues, never the text content.Returns
(idea, newly_redacted):newly_redacted=Trueon first transition to redacted,Falseon idempotent retry. Callers can skip side-effects (audit, notifications) whenFalse.- Raises:
NotFoundError – when
idea_iddoes not resolve to an existing row in this project.
- async luplo.core.ideas.get_idea(conn: psycopg.AsyncConnection[Any], idea_id: str, *, project_id: str | None = None) luplo.core.models.Idea | None¶
Fetch a single idea by id (or hex prefix). Includes redacted rows.
SaaS-side permission checks need to fetch the row before deciding whether redact is allowed — hence this is a separate helper rather than relying on list/search.
project_idscopes prefix resolution and is enforced in the SELECT predicate so a full UUID from another project returnsNoneinstead of leaking the row. Ambiguity errors surface only the matchedidvalues.