Skip to content

docs(security): number inbox search invariant 83, not a second 82 - #252

Merged
Ahmustufa merged 1 commit into
mainfrom
docs/renumber-inbox-search-invariant
Sep 24, 2026
Merged

Ahmustufa merged 1 commit into
mainfrom
docs/renumber-inbox-search-invariant

Conversation

@Ahmustufa

Copy link
Copy Markdown
Contributor

#239 (audit log) and #240 (inbox search) both added a `security.md` invariant 82. The audit log keeps 82 because code comments cite it, inbox search becomes 83, and #242 (branching) takes 84.

Docs-only change. No code references the search invariant by number.

🤖 Generated with Claude Code

#239 (audit log) and #240 (inbox search) each added an invariant 82.
The audit log keeps 82. Branching (#242) takes 84, and retention takes
the next free number.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ahmustufa added a commit that referenced this pull request Sep 23, 2026
main now has the audit log at 82 and inbox search at 83 (#252).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ahmustufa
Ahmustufa merged commit 8a00321 into main Sep 24, 2026
9 checks passed
Ahmustufa added a commit that referenced this pull request Sep 24, 2026
main now has the audit log at 82 and inbox search at 83 (#252).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ahmustufa added a commit that referenced this pull request Sep 24, 2026
* feat(sequences): conditional branching for campaign steps

A step can now carry one branch that routes each enrollment after the step
is sent: always / opened / clicked / replied / not_opened / not_replied,
within N days (1-90), optionally narrowed to a reply label, with a yes exit
and a no exit (a null exit ends the path). Steps without a branch keep the
original linear code path unchanged, and a test pins that.

- New table sequence_step_branches, one row per source step. Composite FKs
  keep both exits in the same campaign and tenant. Deleting a target step
  nulls only that exit.
- The route is recomputed from events inside a fixed window measured from
  the step's own send, so a decided answer can't flip. Opens and clicks
  count human events only. Replies look at inbox_messages (the other leg)
  and exclude auto-replies unless the branch names that label.
- Cycles are refused at save time under a per-campaign lock, including for
  step delete and reorder. A runtime backstop ends the path if a loop ever
  gets in.
- Mid-flight edits use the graph as it is when the decision is made. The
  rule is documented in branchroute.go.
- GET /campaigns/{id}/graph, PUT/DELETE /campaigns/{id}/steps/{stepId}/branch.
- Threading replies to the most recently sent email, not the
  highest-numbered step, since a path can go 1 -> 3 -> 2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(api): quote the yes_step_id description so the contract lints

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(sequences): branching review and security fixes

Reply branches (product decision: stop-on-reply stays, so labels win):
- A reply branch on a label that stops the sequence is refused at save
  (400 reply_label_stops_sequence). The API says every default human label
  stops, so a default-label "replied" branch never fires. The replacement
  tests go through the real poll -> classify -> dispatch path.
- A reply nudge only pulls forward a due time that a condition wait set,
  never an out-of-office deferral.

Routing:
- The loop backstop compares against the current step's own send row, not
  enrollment.last_sent_at, which a recover-forward re-stamps.
- Paused and done campaigns neither finish nor park enrollments; the
  status gate now runs before routing.
- opened/clicked/not_opened are refused without tracking or an HTML body
  (400 tracking_required).
- Condition waits no longer count as "deferred" sends.
- Only a real miss is a 404.
- LatestSentForContact is workspace-pinned.
- New test: a routed send still honours suppression.

Migration: the inbox_threads index is its own single-statement
CONCURRENTLY migration (asserted indisvalid on a scratch DB), so the
branching migration no longer blocks the send path.

Docs: Postgres 15+ minimum; mid-flight edit rules corrected; invariant 82
notes that reply evidence is thread-scoped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(sequences): cite the branching invariant as 84 after the rebase

main now has the audit log at 82 and inbox search at 83 (#252).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(db): serialize migrations with a polling lock so CONCURRENTLY builds can't deadlock

golang-migrate's pgx5 driver waits for its advisory lock inside a
running statement, which holds a snapshot. CREATE INDEX CONCURRENTLY
waits for every snapshot, so a second migrator waiting on the lock
deadlocks the build, and the migration is left dirty. This reproduced 3
of 3 times with 6 concurrent migrators on a fresh DB, and it hits both
CI (-p 4) and multi-replica deploys.

Migrate, MigrateDown, MigrateTo and Version now take a separate
session-level lock by polling pg_try_advisory_lock on a dedicated
connection. A waiter is idle between tries and holds no snapshot. The
wait is bounded (15 min, ErrMigrationLockTimeout), and unlock/close run
on a detached context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(sequences): renumber the branching invariant to 86 after the rebase

#253 landed invariants 83-85 (the mail transport seam, AUTH negotiation, EHLO
validation) while this branch was open, so its own 84 collided. Renumbered to
86 and re-pointed the five code comments that cited it.

That last part is the half a textual merge does not catch: main's 84 is now
"IMAP and SMTP authentication negotiate a mechanism", so every comment here
still saying "invariant 84" would have pointed a reader at an unrelated
invariant while reading as correct. This repo has already lost a Critical to a
comment whose attribution outlived its truth.

stepbranch.sql.go is regenerated from its .sql source rather than hand-edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VECELVAKe8Xcp7GH9wGR5t

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant