Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
d948528
feat(email): preserve dedupe provenance from genuine Date headers
seonghobae Aug 4, 2026
8682434
chore(pr1195): materialize reviewed provenance fixes
seonghobae Aug 4, 2026
e249788
ci: fix source-bound email deduplication
seonghobae Aug 4, 2026
a2124fb
chore(pr1195): remove superseded review-fix workflow
seonghobae Aug 4, 2026
6580d40
chore(pr1195): repair source-dedupe materializer
seonghobae Aug 4, 2026
138157b
fix(ci): trigger PR 1195 materializer repair on review transition
seonghobae Aug 4, 2026
f9efcf7
fix(ci): make PR 1195 materializer repair structural
seonghobae Aug 4, 2026
1c4c781
fix(ci): preserve escaped POP3 assertion in PR 1195 repair
seonghobae Aug 4, 2026
112c2d5
ci: make source-bound dedupe repair deterministic
seonghobae Aug 4, 2026
7a76bb4
ci: replace brittle PR 1195 materializer
seonghobae Aug 4, 2026
cd68cbe
ci: remove superseded PR 1195 repair helper
seonghobae Aug 4, 2026
08a7cb1
ci: finalize PR 1195 source-bound dedupe
seonghobae Aug 4, 2026
3a35907
ci: harden PR 1195 source-identity finalizer
seonghobae Aug 4, 2026
97bf6d4
ci: repair PR 1195 source-identity finalizer
seonghobae Aug 4, 2026
3b7fa3c
ci: remove superseded PR 1195 repair helper
seonghobae Aug 4, 2026
1f72d4d
ci: repair PR 1195 finalizer date cleanup
seonghobae Aug 4, 2026
0cc519f
fix(ci): execute PR 1195 date cleanup helper
seonghobae Aug 4, 2026
8557cad
ci(pr-1195): bootstrap finalizer guard repair
seonghobae Aug 4, 2026
d05a500
ci(pr-1195): activate finalizer bootstrap
seonghobae Aug 4, 2026
0508cc7
fix(ci): trigger PR 1195 cleanup on synchronization
seonghobae Aug 4, 2026
1ab3939
fix(ci): consolidate PR 1195 finalizer repair
seonghobae Aug 4, 2026
4d584bf
fix(ci): make PR 1195 helper workflow valid
seonghobae Aug 4, 2026
e4b0c59
ci: repair empty replacement handling in PR 1195 finalizer
seonghobae Aug 4, 2026
121d5ac
chore(pr-1195): remove temporary finalizer bootstrap
seonghobae Aug 4, 2026
fa48028
chore(pr-1195): remove temporary finalizer hotfix
seonghobae Aug 4, 2026
ca63640
chore(pr-1195): remove temporary date repair workflow
seonghobae Aug 4, 2026
7fd7d48
chore(pr-1195): remove temporary source finalizer
seonghobae Aug 4, 2026
c51131c
test(email): prove source-bound fallback identity
seonghobae Aug 5, 2026
6e69b8e
chore(email): add one-shot source identity repair script
seonghobae Aug 5, 2026
96fd422
test(email): add source-bound fallback regressions
seonghobae Aug 5, 2026
64d5a84
fix(ci): make source-identity repair workflow valid
seonghobae Aug 5, 2026
898201e
docs(email): record source-bound identity contract
seonghobae Aug 5, 2026
2412770
ci(email): repair POP3 source-byte reconstruction
seonghobae Aug 5, 2026
4c6b21a
fix(email): bind fallback dedupe to immutable source
github-actions[bot] Aug 5, 2026
6622f65
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
opencode-agent[bot] Aug 5, 2026
4369f80
test(email): reject unsupported canonical source values
seonghobae Aug 5, 2026
375d6c5
fix(email): validate canonical source value types
seonghobae Aug 5, 2026
24efad3
docs(email): summarize record-linkage evidence model
seonghobae Aug 5, 2026
596aeb6
Merge protected develop into email dedupe provenance
seonghobae Aug 14, 2026
26dfa22
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
opencode-agent[bot] Aug 15, 2026
bee6519
merge(develop): refresh email dedupe provenance
seonghobae Aug 15, 2026
510ec77
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
seonghobae Aug 17, 2026
f6e60ef
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
seonghobae Aug 17, 2026
f98c6fe
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
cursoragent Aug 17, 2026
6aeda8f
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
opencode-agent[bot] Aug 20, 2026
c7aedc6
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 21, 2026
0b74bef
Merge branch 'develop' into claude/contextualwisdomlab-audit-governan…
seonghobae Aug 26, 2026
66273d5
test(email): reject zone-less Date as provenance
seonghobae Sep 17, 2026
37429cc
fix(email): reject zone-less Date as provenance
seonghobae Sep 17, 2026
70bf46e
docs(email): trace zone-less Date provenance repair
seonghobae Sep 17, 2026
449a6cc
chore(email): adopt current dependency prerequisite
seonghobae Sep 17, 2026
9fdb120
test(email): require complete metadata for strong dedupe evidence
seonghobae Sep 17, 2026
d2ae9d6
fix(email): withhold strong dedupe on incomplete metadata
seonghobae Sep 17, 2026
540f6e4
test(email): pin canonical provenance migration contract
seonghobae Sep 17, 2026
5d68f08
test(email): expose import strong-evidence boundary
seonghobae Sep 17, 2026
c39f517
fix(email): gate import strong evidence on complete metadata
seonghobae Sep 17, 2026
fd4cd4e
test(email): expose IMAP strong-evidence boundary
seonghobae Sep 17, 2026
d8a08de
fix(email): gate IMAP strong evidence on complete metadata
seonghobae Sep 17, 2026
809c8ad
docs(email): trace complete-metadata dedupe evidence
seonghobae Sep 17, 2026
81e230a
test(email): assert import review disposition end to end
seonghobae Sep 17, 2026
7ef016b
test(email): isolate import review disposition contract
seonghobae Sep 17, 2026
1fa8243
test(mail): reproduce duplicate sync count inflation
seonghobae Sep 17, 2026
88556ba
test(mail): reproduce POP3 duplicate count inflation
seonghobae Sep 17, 2026
6999f18
fix(mail): count only newly persisted sync messages
seonghobae Sep 17, 2026
e045e03
fix(mail): align POP3 sync count with persistence disposition
seonghobae Sep 17, 2026
e18510d
test(mail): align IMAP sync count contract with persistence result
seonghobae Sep 17, 2026
908a254
test(mail): align POP3 sync count contract with persistence result
seonghobae Sep 17, 2026
517ba20
test(pop3): require newest bounded fetch window
seonghobae Sep 17, 2026
72c64b4
fix(pop3): fetch newest bounded mailbox window
seonghobae Sep 17, 2026
0726c4b
docs(email): trace POP3 bounded fetch decision
seonghobae Sep 17, 2026
64baa1e
test(email): preserve POP3 retrieval on QUIT failure
seonghobae Sep 17, 2026
fa06c56
fix(email): keep POP3 retrieval when QUIT cleanup fails
seonghobae Sep 17, 2026
a74e02b
test(email): require POP3 socket close after QUIT failure
seonghobae Sep 17, 2026
e809575
fix(email): close POP3 transport after QUIT failure
seonghobae Sep 17, 2026
b440e36
docs(email): record POP3 teardown and UIDL progress gap
seonghobae Sep 17, 2026
77d7f23
test(email): define durable POP3 UIDL progress contract
seonghobae Sep 17, 2026
b5344c4
feat(email): model durable POP3 UIDL observations
seonghobae Sep 17, 2026
970c6d3
feat(email): add POP3 UIDL progress migration
seonghobae Sep 17, 2026
5fe993c
feat(email): advance bounded POP3 sync with durable UIDL progress
seonghobae Sep 17, 2026
0ce2227
test(email): align QUIT regression with UIDL retrieval result
seonghobae Sep 17, 2026
3776e54
test(email): align POP3 worker contracts with UIDL progress
seonghobae Sep 17, 2026
c5a9976
test(email): pin POP3 UIDL migration contract
seonghobae Sep 17, 2026
99e2e7e
fix(email): align POP3 UIDL model index with migration
seonghobae Sep 17, 2026
c762b10
test(email): preserve partial POP3 RETR progress
seonghobae Sep 17, 2026
37a390c
fix(email): preserve partial POP3 retrieval progress
seonghobae Sep 17, 2026
3c734b5
docs(email): trace UIDL progress and partial POP3 recovery
seonghobae Sep 17, 2026
d9c8708
test(pop3): reproduce RETR negative-response starvation
seonghobae Sep 17, 2026
38803cc
fix(pop3): continue after per-message RETR rejection
seonghobae Sep 17, 2026
7606387
test(pop3): distinguish RETR rejection from malformed protocol
seonghobae Sep 17, 2026
a5d7d21
fix(pop3): distinguish RETR rejection from protocol failure
seonghobae Sep 17, 2026
fcef178
docs(pop3): trace RETR negative-response recovery
seonghobae Sep 17, 2026
5d40f83
test(pop3): reproduce retry-window starvation across polls
seonghobae Sep 17, 2026
7406cb6
fix(pop3): persist retry disposition without starving fresh UIDLs
seonghobae Sep 17, 2026
630d546
docs(pop3): trace durable retry progress contract
seonghobae Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions backend/alembic/versions/0018_email_date_provenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""add date_provenance to email_records

Revision ID: 0018_email_date_provenance
Revises: 0017_merge_newsdom_carddav_heads
Create Date: 2026-07-30 00:00:00.000000

Records the provenance of each stored email ``date`` so a synthetic
collection-time fallback (missing/invalid RFC822 Date header) is never treated
as original sender metadata when seeding a strong auto-dedupe fingerprint
(naruon#1086). Nullable-free with a ``"unknown"`` server default so existing
rows backfill safely: their date provenance is genuinely unknown, and only
``"parsed"`` rows are eligible to seed a strong fingerprint, so the backfill is
conservative (it can only widen review, never manufacture a duplicate).
"""

from alembic import op
import sqlalchemy as sa

revision = "0018_email_date_provenance"
down_revision = "0017_merge_newsdom_carddav_heads"

_EMAIL_TABLE = "email_records"
_PROVENANCE_COLUMN = "date_provenance"


def upgrade() -> None:
"""Add the ``date_provenance`` column, backfilling existing rows to unknown."""
connection = op.get_bind()
inspector = sa.inspect(connection)
columns = {column["name"] for column in inspector.get_columns(_EMAIL_TABLE)}
if _PROVENANCE_COLUMN not in columns:
op.add_column(
_EMAIL_TABLE,
sa.Column(
_PROVENANCE_COLUMN,
sa.String(),
nullable=False,
server_default="unknown",
),
)


def downgrade() -> None:
"""Drop the ``date_provenance`` column if present."""
connection = op.get_bind()
inspector = sa.inspect(connection)
columns = {column["name"] for column in inspector.get_columns(_EMAIL_TABLE)}
if _PROVENANCE_COLUMN in columns:
op.drop_column(_EMAIL_TABLE, _PROVENANCE_COLUMN)
76 changes: 76 additions & 0 deletions backend/alembic/versions/0019_pop3_observed_uidl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""add durable POP3 UIDL collection progress

Revision ID: 0019_pop3_observed_uidl
Revises: 0018_email_date_provenance
Create Date: 2026-09-17 00:00:00.000000

Persists RFC 1939 UIDL provider identity and bounded retry disposition per
mailbox configuration. Provider state remains collection progress only; Naruon
email Message-ID and source fingerprints remain canonical message/deduplication
evidence.

This revision identifier and parent are branch-local until the canonical
workspace/Alembic owner is integrated; #1195 must rechain this schema after the
then-protected migration head before merge.
"""

from alembic import op
import sqlalchemy as sa

revision = "0019_pop3_observed_uidl"
down_revision = "0018_email_date_provenance"

_TABLE = "pop3_observed_messages"


def upgrade() -> None:
"""Create owner-scoped durable POP3 UIDL collection progress if absent."""
connection = op.get_bind()
inspector = sa.inspect(connection)
if _TABLE in inspector.get_table_names():
return

op.create_table(
_TABLE,
sa.Column("observed_message_id", sa.Integer(), primary_key=True),
sa.Column(
"tenant_config_id",
sa.Integer(),
sa.ForeignKey("tenant_configs.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("provider_uidl", sa.String(length=70), nullable=False),
sa.Column(
"collection_disposition",
sa.String(length=16),
nullable=False,
server_default="observed",
),
sa.Column("retry_after", sa.DateTime(timezone=True), nullable=True),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=True),
sa.UniqueConstraint(
"tenant_config_id",
"provider_uidl",
name="uq_pop3_observed_messages_account_uidl",
),
)
op.create_index(
"ix_pop3_observed_messages_account_observed",
_TABLE,
["tenant_config_id", "observed_at"],
unique=False,
)
op.create_index(
"ix_pop3_observed_messages_account_retry",
_TABLE,
["tenant_config_id", "collection_disposition", "retry_after"],
unique=False,
)


def downgrade() -> None:
"""Drop durable POP3 UIDL collection progress if present."""
connection = op.get_bind()
inspector = sa.inspect(connection)
if _TABLE in inspector.get_table_names():
op.drop_table(_TABLE)
7 changes: 7 additions & 0 deletions backend/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,13 @@ def owner_filters(cls, user_id: str, organization_id: str | None):
in_reply_to: Mapped[str | None] = mapped_column(String, nullable=True)
references: Mapped[str | None] = mapped_column(String, nullable=True)
date: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), index=True)
# Provenance of the stored ``date``: "parsed" (genuine RFC822 Date header),
# "missing"/"invalid" (a synthetic collection-time fallback), or "unknown"
# (rows stored before provenance tracking). Only "parsed" rows may seed a
# strong auto-dedupe fingerprint (naruon#1086).
date_provenance: Mapped[str] = mapped_column(
String, nullable=False, server_default="unknown", default="unknown"
)
body: Mapped[str] = mapped_column(Text)
# IMAP \Seen read state; defaults read so historical/file imports don't nag.
is_read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
Expand Down
58 changes: 58 additions & 0 deletions backend/db/pop3_collection_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import datetime

from sqlalchemy import DateTime, ForeignKey, Index, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column

from db.models import Base


class Pop3ObservedMessage(Base):
"""Durable provider progress for one POP3 UIDL within an account.

POP3 message numbers are session-local positions. ``provider_uidl`` stores
RFC 1939 provider identity so bounded polling can make progress across
reconnects and renumbering. ``collection_disposition`` distinguishes
successfully observed messages from retryable collection failures; neither
state redefines Naruon's Message-ID or source-fingerprint identity.
"""

__tablename__ = "pop3_observed_messages"
__table_args__ = (
UniqueConstraint(
"tenant_config_id",
"provider_uidl",
name="uq_pop3_observed_messages_account_uidl",
),
Index(
"ix_pop3_observed_messages_account_observed",
"tenant_config_id",
"observed_at",
),
Index(
"ix_pop3_observed_messages_account_retry",
"tenant_config_id",
"collection_disposition",
"retry_after",
),
)

observed_message_id: Mapped[int] = mapped_column(primary_key=True)
tenant_config_id: Mapped[int] = mapped_column(
ForeignKey("tenant_configs.id", ondelete="CASCADE"),
nullable=False,
)
provider_uidl: Mapped[str] = mapped_column(String(70), nullable=False)
collection_disposition: Mapped[str] = mapped_column(
String(16),
default="observed",
nullable=False,
)
retry_after: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
observed_at: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.datetime.now(datetime.timezone.utc),
nullable=True,
)
Loading