# Automation Engine: Internal Sub-structure

Resolves `../architecture/06-open-questions.md` #6: the Automation Engine stays **one module, one schema (`automation`), one tier (INTEL)** at the domain-architecture level — nothing here creates a new module or crosses `../architecture/04-domain-modules.md`'s boundaries. What changes is how its ~20 behaviors (`../architecture/01-product-vision.md`'s automation catalog) are organized *inside* that one module, so it doesn't become the dumping ground the open question warned about.

## Three internal concerns, one schema

| Concern | Responsibility | Table (from `02-module-schemas.md`) |
|---|---|---|
| **Detection** | Scans for a condition worth noticing — a job crossing a low-stock threshold, a duplicate product, an abandoned cart, a missing description | Writes to `automation.detection_result` |
| **Recommendation generation** | Turns detections (and other signals) into a specific, ranked, actionable suggestion | Writes to `automation.recommendation` |
| **Trigger execution** | Carries out the *default* action for recommendations that are safe to auto-run (per `../architecture/02-principles-and-tradeoffs.md`'s rule: automation owns detection and default action, merchant retains final say on anything touching money/customer content) | Creates rows in `marketing.campaign` or `notifications.notification` via those modules' public interfaces — never writes to another module's schema directly |

## Code layout

```
modules/automation/
├── public.ts                    — the one interface other modules call/subscribe to
└── internal/
    ├── detectors/                — one small file per detection rule
    │   ├── low_stock.ts
    │   ├── overstock.ts
    │   ├── slow_mover.ts
    │   ├── trending_product.ts
    │   ├── duplicate_product.ts
    │   ├── missing_info.ts
    │   ├── broken_category.ts
    │   └── abandoned_cart.ts
    ├── recommenders/              — one small file per recommendation type
    │   ├── reorder_suggestion.ts
    │   ├── frequently_bought_together.ts
    │   ├── bundle_suggestion.ts
    │   ├── cross_sell.ts
    │   ├── seasonal_promo.ts
    │   └── discount_suggestion.ts
    └── triggers/                  — one small file per auto-actionable flow
        ├── abandoned_cart_recovery.ts
        ├── back_in_stock_notify.ts
        ├── repeat_purchase_reminder.ts
        └── reengagement_campaign.ts
```

Each file is a `system.job` handler (per `03-event-and-job-system.md`), registered against either a schedule (cron-style, e.g. `low_stock.ts` runs nightly) or a subscribed event (e.g., `duplicate_product.ts` runs on `product.created`/`product.updated`). Growth in the automation catalog means adding a new small file and a registry entry — never growing an existing handler's scope. This is the concrete guardrail against the scope-creep risk the open question flagged.

## Ranking: resolving the suggestion-density cap

`../ux/04-open-questions.md` #3 asked for a cap and prioritization rule so Home doesn't turn into a wall of suggestions. Every row in `automation.recommendation` carries a `rank_score`, computed by the generating recommender from two inputs:

- **Estimated impact** — a rough dollar-value or business-significance estimate (e.g., a low-stock alert on a top-20-revenue product scores higher than one on a rarely-ordered product; an abandoned cart worth $200 scores higher than one worth $15).
- **Confidence** — how certain the detection is (a duplicate-product match at 95% text similarity scores higher confidence than one at 60%).

`rank_score = impact_weight * confidence`. Home's query (`getTopRecommendations(limit=5)`, exposed via `automation.public`) always returns at most 5, ordered by `rank_score` descending, recomputed whenever underlying detections change rather than on a fixed schedule — so the list reflects current reality, not a stale nightly snapshot. Products and Marketing pull their own contextually-scoped slices (e.g., Products asks for recommendations where `subject_type='product' AND subject_id=<this product>`) rather than the global top-5, so a merchant reviewing one product isn't limited by Home's cap.

## Lifecycle

`pending -> accepted | dismissed`. A `dismissed` recommendation of the same type/subject doesn't regenerate identically on the next detection run (the detector checks for an existing non-`pending`-terminal recommendation for the same subject before creating a new one) — this is what `../ux/03-ux-patterns-and-conventions.md` rule 1 means by "the system should learn that context, not nag." An `accepted` recommendation that has a corresponding trigger (e.g., accepting a "launch abandoned cart recovery" suggestion) calls that trigger's execution path; recommendations with no auto-actionable trigger (e.g., "five products missing descriptions") simply deep-link the merchant into the relevant section, per `../ux/03-ux-patterns-and-conventions.md` rule 5.
