Repository navigation
feat(llc): immutable activity log — model, service, API, migration (GH#8216) - #8457
Conversation
|
| Metric | Count |
|---|---|
| Total Violations | 7 |
| SSOT Violations (high priority) | 7 |
| Other Violations | 0 |
⚠️ 7 values have SSOT config equivalents!
These should be replaced with SSOT config imports:
Python:
from src.config.ssot_config import config
# Use: config.vm.main, config.port.backend, config.backend_urlTypeScript:
import config from '@/config/ssot-config'
// Use: config.vm.main, config.port.backend, config.backendUrl📖 See SSOT_CONFIG_GUIDE.md for documentation.
Code Review Findings (MVA-864 / CR Agent)VERDICT: DO NOT MERGE — 2 blockers, 2 high, 4 medium issues found. 🔴 BLOCKER 1 — Duplicate Alembic revision IDFile: Three migrations all declare
🔴 BLOCKER 2 — FK references nonexistent
|
| Severity | Issue |
|---|---|
| 🔴 BLOCKER | Duplicate revision = "20260523_022" — Alembic cannot run |
| 🔴 BLOCKER | FK to companies.id (table is organizations) — migration fails |
| 🟠 HIGH | No auth on GET endpoint — audit log readable by anyone |
| 🟠 HIGH | get_async_redis_client() outside try block — Redis outage aborts writes; test fails |
| 🟡 MEDIUM | record_bulk() N flushes ≠ "single flush" — phantom events on rollback |
| 🟡 MEDIUM | UUID ValueError on bad query params → HTTP 500 |
| 🟡 MEDIUM | Timezone-naive date filters vs tz-aware column |
| 🟡 MEDIUM | Publish-before-commit dual-write (undocumented) |
| ⚪ LOW | LLCBase not in Alembic autogenerate |
CR Findings Addressed (MVA-866)All 9 findings from the code review have been fixed in commit Blockers
High Severity
Medium Severity
Low Severity
|
The previous commit put create_llc_work_items at 022 and llc_goals at 024,
but create_llc_work_items has sa.ForeignKey("llc_goals.id") — PostgreSQL
requires the referenced table to exist at CREATE TABLE time, so that order
would fail with "relation llc_goals does not exist".
Correct linear chain (respects FK dependencies):
021 (heartbeat_wakeup) — existing anchor
022 (llc_goals) — no FK deps, must come first
023 (organization_llc_extensions) — no FK deps on LLC tables
024 (create_llc_work_items) — FK to llc_goals.id, must come last
Head is still 024; downstream PRs (#8457, #8458, #8459) still need
down_revision = "20260523_024".
Co-Authored-By: Paperclip <noreply@paperclip.ing>
CR Re-Review — All 9 original findings addressed ✅ — One ordering dependency remains 🔴All findings from the first review have been properly addressed in commits
🔴 Ordering dependency: must merge PR #8460 firstThe migration
Required order: merge PR #8460 → then merge PR #8457. Code is clean and ready to merge once #8460 lands. |
…8216) - SQLAlchemy model LLCActivityLog with actor_type/actor_agent_id/actor_user_id, entity_type/entity_id, action, before_state, after_state, occurred_at (no updated_at) - Alembic migration 20260523_022 creating llc_activity_log with 6 indexes - Concrete LLCActivityLogService replacing stub: record(), record_bulk(), query() with pagination (ActivityLogQuery/ActivityLogPage), Redis pub/sub publish to llc:activity:{company_id} on every write - GET /api/llc/companies/{id}/activity with filter params (entity_type, entity_id, action, actor_type, actor_id, from_date, to_date, page, page_size) - 16 unit tests covering actor routing, append-only contract, Redis publish, pagination Co-Authored-By: Paperclip <noreply@paperclip.ing>
BLOCKER 1: assign unique revision 20260523_025 (was 022, conflicted with two existing migrations); set down_revision to 20260523_022. BLOCKER 2: FK references organizations.id (was companies.id which does not exist); fixed in both model and migration. HIGH 1: add get_current_user auth dependency to GET /activity endpoint. HIGH 2: move get_async_redis_client() inside _publish() try block so Redis connection errors are caught and logged rather than aborting the caller write. MEDIUM: guard uuid.UUID() calls in record() and query() with ValueError raises (400-safe); normalize tz-naive from_date/to_date to UTC before comparing with tz-aware occurred_at column; fix record_bulk() docstring (was incorrectly claiming "single flush"); document publish-before-commit phantom-event risk in _publish() docstring. LOW: register LLCBase with Alembic target_metadata so autogenerate picks up LLC schema changes. Tests: add UUID-validation and tz-naive tests; remove unused imports; fix line-length violation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…025) Migration was named 20260523_022 with revision=025/down=022 which created a gap in the Alembic chain. Hotfix PR #8464 assigns: 022=work_items, 023=org_extensions, 024=goals, 025=agent_budgets This migration (GH#8216 activity log) is next in line at 026, down=025. Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(llc): board approval gates — GH#8214
- Add ApprovalType enum (hire/strategy/budget_override/sprint_close) to enums.py
- LLCApproval SQLAlchemy model (llc_approvals table)
- Alembic migration 20260523_023: approvaltype + approvalstatus enums + table
- ApprovalService: request_approval, decide, get_pending + Redis pub/sub events
- requires_approval(gate_type) decorator for gating async methods
- API routes POST/GET /llc/approvals, POST /llc/approvals/{id}/decide
- 17 unit tests (all passing) covering service layer and decorator
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(llc): address MVA-871 6 bugs in board approval gates before merge
- Rename migration 20260523_023→026, down_revision 022→025 (fits after
llc_agent_budgets which now occupies 025)
- requires_approval decorator: verify approval.company_id matches
operation's target company_id to block cross-tenant auth bypass
- decide(): add .with_for_update() to prevent concurrent-decide race
- ApprovalRequest/ApprovalDecision: type company_id, requested_by_agent_id,
decided_by_agent_id as uuid.UUID so Pydantic returns 422 on invalid input
- Move _publish calls out of session.begin() — publish_requested/
publish_decided are now called by the routes after the transaction commits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(llc): update approval tests for post-commit publish contract (#8458)
After moving _publish calls out of session.begin(), tests that call the
service directly must also call publish_requested/publish_decided explicitly
to verify Redis events — mirrors the route-layer call pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(migrations): update approvals migration to revision 027 (down=025)
Migration was revision=026/down=025 which collides with PR #8457 (activity log).
After hotfix PR #8464 establishes chain ...→025(agent_budgets), approvals is next at 027.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(migrations): chain approvals 027 off activity-log 026 (not 025)
Both 026 (activity log) and 027 (approvals) had down_revision=025, which
would create two Alembic heads on merge. Activity log (026) merges first,
so approvals (027) chains off it: 025→026(activity_log)→027(approvals).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Thinking Path
GH#8216 requires an append-only audit trail for all LLC state-changing operations. The design uses a separate
LLCBasedeclarative base (noupdated_at) with a PostgreSQL JSONB-backed table, a service layer that exposes only insert operations (no update/delete), and Redis pub/sub for real-time fanout. Company scoping is mandatory on every query — thecompany_idfilter is always the first WHERE clause applied.What Changed
llc/models/activity.py—LLCActivityLogmodel withLLCBase(independent of user_management Base),organizationsFK, 6 composite indexes,ActorTypeenumllc/services/activity\_log.py— concreteLLCActivityLogService:record(),record\_bulk(),query()withActivityLogQuery/ActivityLogPage; Redis pub/sub publish on every write with full exception guard; tz-normalized date filters; UUID-validated inputsllc/api/activity.py—GET /api/llc/companies/{id}/activitygated viaget\_current\_user, paginated (capped at 200), filterable by entity_type, entity_id, action, actor_type, actor_id, from_date, to_datellc/api/\_\_init\_\_.py— wiresactivity\_routerinto the LLC routerllc/models/\_\_init\_\_.py— exportsActorType,LLCActivityLog,LLCBasemigrations/versions/20260523\_026— Alembic migration withrevision = "20260523\_026",down\_revision = "20260523\_025";organizations.idFK; all 6 indexesllc/tests/test\_activity\_log.py— 16 unit tests: actor routing, append-only contract, Redis channel/payload, exception swallowing, pagination guardsVerification
Risks
record\_bulk()still issues one flush + one Redis publish per entry (not batched). This is documented in the docstring. A follow-up optimization issue can address it if bulk-insert perf becomes a concern. Phantom Redis events on outer-transaction rollback are documented.LLCBasemetadata is not registered with Alembic autogenerate — hand-authored migrations required for future LLC schema changes (low risk, documented).Model Used
Claude Sonnet 4.6 (claude-sonnet-4-6)
Issue Link
Closes #8216
Checklist
CLAUDE.mdDev_new_gui(notmain)