Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
12 changes: 6 additions & 6 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

48 changes: 42 additions & 6 deletions backend/src/tripl/services/demo/builders/branches.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
"""Feature-branch journey builder.

Seeds a minimal but API-visible collaboration story: a base ``PlanRevision``
snapshot, one working ``PlanBranch`` (a feature branch — the kind enum is
main/working, so a working branch with a feature-style name), and a top-level
``PlanBranchComment``. Reachable via ``/branches``, ``/branches/{id}/comments``,
and ``/revisions``.
Seeds an API-visible collaboration story: a base ``PlanRevision`` snapshot, one
working ``PlanBranch`` (a feature branch — the kind enum is main/working, so a
working branch with a feature-style name), a top-level ``PlanBranchComment``,
and — new in recipe 4 — a REAL pending change: the main plan is deep-copied
onto the branch and exactly one branch-side edit is applied, so
``/branches/{id}/diff`` shows one modified event and the merge preview is
non-empty. Reachable via ``/branches``, ``/branches/{id}/comments``,
``/branches/{id}/diff``, and ``/revisions``.

The ORM writes mirror ``plan_branch_service.create_branch`` inline because that
service commits internally, which would break the seeder's single end-of-function
commit. No plan deep-copy is done — the branch is intentionally minimal.
commit; the plan copy itself reuses the service's non-committing
``deep_copy_plan_to_branch`` helper so the two paths cannot drift.
"""

from __future__ import annotations

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from tripl.models.event import Event
from tripl.models.plan_branch import BranchKind, BranchStatus, PlanBranch
from tripl.models.plan_branch_comment import PlanBranchComment
from tripl.models.plan_revision import PlanRevision
from tripl.services.demo.scenario import DemoContext
from tripl.services.plan_branch_service import deep_copy_plan_to_branch
from tripl.services.plan_revision_service import build_plan_snapshot

_BRANCH_NAME = "feature/checkout-funnel"
Expand All @@ -28,6 +35,15 @@
"copy and moves the Buy CTA above the fold — see the linked Figma spec."
)

# The single branch-side edit (the pending change). Keyed by event name so the
# diff reports exactly one changed event; the copy matches the branch's story —
# the paywall CTA copy moves from "Buy now" to "Start free trial".
CHANGED_EVENT_NAME = "Buy Button Click"
CHANGED_EVENT_DESCRIPTION = (
"User taps the primary CTA on the paywall. Checkout-funnel redesign: CTA "
"copy changes from 'Buy now' to 'Start free trial', moved above the fold."
)


async def build_branches(session: AsyncSession, ctx: DemoContext) -> None:
# Merge base: a snapshot of the (now fully-seeded) main plan.
Expand All @@ -53,6 +69,26 @@ async def build_branches(session: AsyncSession, ctx: DemoContext) -> None:
session.add(branch)
await session.flush()

# Isolated branch copy of the whole plan, then ONE modification on the copy,
# so the branch diff is exactly one changed event and nothing else.
await deep_copy_plan_to_branch(
session,
project_id=ctx.project_id,
source_branch_id=ctx.branch_id,
target_branch_id=branch.id,
)
await session.flush()
branch_event = (
await session.execute(
select(Event).where(
Event.project_id == ctx.project_id,
Event.branch_id == branch.id,
Event.name == CHANGED_EVENT_NAME,
)
)
).scalar_one()
branch_event.description = CHANGED_EVENT_DESCRIPTION

session.add(
PlanBranchComment(
branch_id=branch.id,
Expand Down
48 changes: 46 additions & 2 deletions backend/src/tripl/services/demo/builders/variables.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
"""Variables builder: the templating layer.

Seeds the project variables, their *observed* value contexts (what the scan
pipeline would discover), and one *authored* per-event override — a
pipeline would discover), one *authored* per-event override — a
``VariableEventValueOverride`` documenting the allowed values for a variable in
one event's context. Both the observed contexts and the authored override are
one event's context — and one OPEN ``VariableValueDrift``: a value observed
outside that documented list, so the variables drift UI (``list_value_drifts``,
the open-drift counts, the event drift badge) has a real row to show. It does
NOT feed the firing alert rule's replay: replay candidates come from
``MetricAnomaly`` plus the schema/distribution drift loaders, and the live
worker path filters on ``scan_config_id`` (``None`` here). All rows are
reachable through the variables API.
"""

from __future__ import annotations

from datetime import timedelta

from sqlalchemy.ext.asyncio import AsyncSession

from tripl.models.schema_drift import SCHEMA_DRIFT_STATUS_OPEN
from tripl.models.variable import Variable
from tripl.models.variable_event_value_override import VariableEventValueOverride
from tripl.models.variable_value import VariableValue, VariableValueKind
from tripl.models.variable_value_drift import VariableValueDrift
from tripl.services.demo.scenario import DemoContext

# (name, source_name, variable_type, description)
Expand All @@ -24,10 +33,19 @@
)


# The one value observed OUTSIDE the documented override list below — the
# seeded drift row. Kept module-level so tests can assert against the contract.
DRIFT_OBSERVED_VALUES = ["prod_weekly"]
# Detected recently (well inside the 30-day read-time retention window) and
# derived from ctx.now, so two seeds with the same clock produce identical rows.
_DRIFT_DETECTED_AGO = timedelta(hours=6)


async def build_variables(session: AsyncSession, ctx: DemoContext) -> None:
await _build_variables(session, ctx)
await _build_observed_values(session, ctx)
await _build_authored_override(session, ctx)
await _build_value_drift(session, ctx)


async def _build_variables(session: AsyncSession, ctx: DemoContext) -> None:
Expand Down Expand Up @@ -103,3 +121,29 @@ async def _build_authored_override(session: AsyncSession, ctx: DemoContext) -> N
)
)
await session.flush()


async def _build_value_drift(session: AsyncSession, ctx: DemoContext) -> None:
"""One OPEN value drift: ``prod_weekly`` seen on Trial Started's product_id.

Mirrors what the scan upsert would write when it observes a value outside
the documented override list above. ``scan_config_id`` stays NULL — the
warehouse builder (and its ScanConfig) runs after this one, and the column
is nullable by design (``SET NULL`` on scan deletion). That NULL also means
the scan worker's drift-candidate loader (which filters on its own
``scan_config_id``) never picks this row up, and rule replay builds its
candidates from ``MetricAnomaly`` + schema/distribution drifts only — so
the row surfaces exclusively in the variables drift UI, never in a firing.
"""
session.add(
VariableValueDrift(
project_id=ctx.project_id,
variable_id=ctx.variable_ids["product_id"],
event_id=ctx.event_ids["Trial Started"],
scan_config_id=None,
observed_values=list(DRIFT_OBSERVED_VALUES),
status=SCHEMA_DRIFT_STATUS_OPEN,
detected_at=ctx.now - _DRIFT_DETECTED_AGO,
)
)
await session.flush()
13 changes: 10 additions & 3 deletions backend/src/tripl/services/demo/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
# and a feature-branch journey), so demos seeded under recipe "1" are semantically
# older. Reset re-runs the current recipe, so re-seeding yields equivalent
# semantics with no duplicate rows (every id is minted fresh per run).
DEMO_RECIPE_VERSION = "3"
# Bumped to "4" for tripl-odrj.3: the feature branch now carries a REAL pending
# change (the main plan is deep-copied onto it and one event description is
# edited, so the branch diff / merge preview are non-empty) and one open
# variable-value drift is seeded on product_id (Trial Started).
DEMO_RECIPE_VERSION = "4"

# Default scenario seed. Fixed so the seeded metrics/anomalies/drift are
# reproducible for a given clock; overridable per DemoContext for tests.
Expand Down Expand Up @@ -105,7 +109,8 @@ def build_default_scenario() -> DemoScenario:
# 1. Plan schema: event types, fields, meta fields, events, tags,
# field/meta values, one relation, one owner.
plan.build_plan,
# 2. Variables + observed values + one authored per-event override.
# 2. Variables + observed values + one authored per-event override
# + one open variable-value drift outside that override's list.
variables.build_variables,
# 3. Activity: event change history + a figma spec (no stored bytes).
activity.build_activity,
Expand All @@ -126,7 +131,9 @@ def build_default_scenario() -> DemoScenario:
# catalog so it can reference the seeded metrics/anomalies. Emits
# zero network side effects.
alerts.build_alerts,
# 9. A small feature-branch journey (revision + branch + comment).
# 9. Feature-branch journey: revision + branch + comment, plus a
# deep copy of the main plan onto the branch with exactly one
# branch-side edit (a real pending change for diff/merge preview).
branches.build_branches,
# 10. Search reindex (last).
search.build_search,
Expand Down
8 changes: 6 additions & 2 deletions backend/src/tripl/services/plan_branch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ async def get_branch(
return await _to_detail(session, branch)


async def _deep_copy_plan(
async def deep_copy_plan_to_branch(
session: AsyncSession,
*,
project_id: uuid.UUID,
Expand All @@ -268,6 +268,10 @@ async def _deep_copy_plan(
because no ORM relationship orders that insert (see below).
Child tables (field_definitions, event_field_values, event_meta_values,
event_tags) inherit their branch from the parent and carry no branch_id.

Never commits or flushes beyond the photo-comment ordering flush, so it can
run inside a caller-owned transaction — ``create_branch`` below and the demo
seeder's branches builder both reuse it.
"""
event_types = (
(
Expand Down Expand Up @@ -621,7 +625,7 @@ async def create_branch(
session.add(branch)
await session.flush()

await _deep_copy_plan(
await deep_copy_plan_to_branch(
session,
project_id=project.id,
source_branch_id=main_branch_id,
Expand Down
Loading
Loading