# `MishkaBlog.Dev.Seeds`
[🔗](https://github.com/mishka-group/mishka_blog/blob/v0.0.1/lib/mishka_blog/dev/seeds.ex#L5)

Dev and stress seeds for MishkaBlog resources.

"Archived" is seeded exactly one way: `Ash.destroy!/2` through a resource's `:destroy` action, in
the very last pass of a run. That writes the `archived_at` soft-delete timestamp AND force-sets
`status: :archived` on `BlogPost` and `Collection`, the two resources whose status enum carries
that value.

Nothing else ever writes `status: :archived`. The status pool in `MishkaBlog.Dev.Generator` leaves
it out, and the seeds no longer mark live rows with the label: only `archived_at` decides which tab
a row appears in, so a labelled-but-live post reads to an admin as an archived post sitting in All
Posts. Seeded data therefore satisfies **status `:archived` if and only if `archived_at` is set**,
which `MishkaBlog.Dev.SeedsArchivalTest` pins.

`Comment`, `Tag`, `Blogger`, `Team` and `AutoPosting` have no `:archived` status at all: for them
the soft delete is the only archival concept, and their destroy actions leave the status untouched
— an archived `:spam` comment keeps its `:spam` label.

Receives foundation data from `MishkaCmsCore.Dev.Seeds.dev_seed/0`.

# `archive_pass`

Executes the plan, children before parents, and reads one row back through `:get_archived` to prove
the rows really moved to the Archived tab.

`Ash.destroy!/2` on these resources is a soft delete: `AshArchival` rewrites the `:destroy` action to
set `archived_at` instead of removing the row. `:permanent_destroy` is the one that really deletes
and is never used here. On `BlogPost` and `Collection` the destroy also force-sets `status:
:archived`; on `Comment`, `Tag`, `Blogger` and `AutoPosting` the status is left alone, so an archived
`:spam` comment keeps its moderation label.

A team's `MishkaBlog.TeamMember` rows are left in place: that join resource has no archival, so
destroying them would be a real delete and the membership could not come back with an unarchive.

Nothing may write to a record after this pass — the `prevent_archived_*_updates` triggers raise on
any update of an archived row.

# `archive_plan`

Chooses the rows that will be soft-deleted, without touching the database.

The slice is picked from a hash of `{site_id, record_id, :archive}` so the archived set is the same
on every run, and it stays a genuine minority: a soft delete is a full Ash destroy that writes a
PaperTrail version, emits an event and broadcasts over PubSub.

Two rules shape the selection. Language masters are only archived together with their whole group,
since a master with live translations is a state the app declares invalid. And post comments are
archived alongside their post, because no resource declares `archive_related` — an orphan comment
would render as "On: Unknown".

# `backdate`

Spreads creation timestamps over the last 18 months.

`inserted_at` is a `create_timestamp` and appears in no `accept` list, so `Ash.Seed.update!/3` is the
only Ash-native way to set it. That call goes straight to the data layer: no action, no validation,
no notifier and no PaperTrail version, which is why a backdated row's `inserted_at` will not match
the timestamp of its own first version. It also applies update defaults, so `updated_at` has to be
passed explicitly or it silently resets to now.

This pass must run before `archive_pass/2`: the `prevent_archived_*_updates` triggers raise on any
update of a row that already has an `archived_at`.

# `dev_seed`

```elixir
@spec dev_seed(String.t(), [struct()], (String.t() -&gt; any())) ::
  map() | :already_seeded
```

Creates realistic blog seed data for a single site: multilingual posts, threaded comments, views,
engagement rows and, as the final pass, a soft-deleted minority that populates the admin's
Archived tab.

Per-site volumes are derived from a hash of `site_id`, so a run is reproducible while the three
sites differ from one another. `users` should include both master and tenant users; users belonging
to another site are filtered out before bloggers and comment authors are picked.

RUN AGAIN ON A SITE THAT HAS A BLOG, this adds no generated row and answers `:already_seeded`.
Every row it generates has no name to recognise it by afterwards, so what is either there or not
is the blog itself, and a site that has one keeps exactly the posts, comments and ratings it has
rather than being given a second set beside them. The locked and unlisted examples are recognised
by their slugs, so a re-run writes only the ones the site is missing.

# `seed_engagement`

Fills the engagement tables the admin aggregates over — ratings, bookmarks, comment votes, blogger
and team followers — so the star, bookmark and follower columns are not hard-zero.

`MishkaBlog.TeamPermission` is not seeded: `dev_seed/3` receives only sites and users, and a team
permission needs a `MishkaCmsCore.Accounts.Permission` record, which the core seed does not hand to
the extension seeds.

Archived posts and comments are skipped, so the aggregates the admin renders describe live rows.

# `site_users`

Keeps only the users that may legitimately author content on this site: master users (`site_id` nil)
and tenant users of this very site.

`MishkaCmsCore.Dev.Seeds` hands every site the same "master" list, and that list contains
`none@example.com`, which is created as a tenant user of the localhost site. Without this filter a
blogger or comment author on one site can be another site's user.

# `stress_seed`

Stress seed: the same building blocks as `dev_seed/3` — multilingual posts, views, engagement and
the archival pass — at large volumes, so stress data exercises the code paths the admin renders.
The `:posts` option is the total post count across all five languages.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
