Adds `content_rating TEXT NOT NULL DEFAULT 'safe'` to modules + bundles.
Defaults all pre-015 rows to 'safe'. Repository create/update accept a
new content_rating kwarg with validation against {'safe', 'nsfw'}.
Importer _insert_module / _insert_bundle pass entity.content_rating
through; _update_module / _update_bundle use COALESCE(?, content_rating)
so an install path that omits the key preserves the existing flag.
Why: user wants two NSFW behaviors:
1. Edit page toggle to mark a module/bundle as NSFW locally.
2. Community posts with content_rating='nsfw' auto-stamp NSFW on
the local row at install time.
This is the engine half of that work -- the schema, the data
plumbing, and validation. The SPA half (IdentityCard toggle + pill
render on ModuleListView + install.ts plumbing) lands next.
Tests cover: migration head bump to 15, column existence on both
tables, safe default backfill, accepts nsfw, rejects invalid, update
toggle round-trip, update without content_rating preserves prior.
Implements spec §4 Flow 4: bulk-at-boot is NOT a parallel forward-chain
implementation; it's eager iteration of lazy_migrate_row over every
stale row. Same original-first logic, same re-projection, zero
divergence. Eliminates the transient-wrong window where bulk could
write fabricated defaults over recoverable data in original_payload_json.
run_bulk_lazy returns the failure count; caller surfaces a dashboard
banner when > 0. STALE_PREDICATE catches both 'sv < current' AND
'tolerantly-installed row whose original now outranks the projection'.
Migration 014 invokes run_bulk_lazy at the end of up() — no-op pass
at v1, but validates the wiring is correct. Future bump migrations
(015 = v2, etc) will reuse the same helper.
Adds the four payload-versioning columns to modules + bundles per
the community-web spec sections 4-5:
- schema_version (NOT NULL DEFAULT 1)
- original_payload_json (NULL - only set during tolerant install)
- tolerant_drift_status ('none' | 'pending' | 'broken')
- schema_migrated_at (separate from updated_at - structural migration
is not a user edit)
Backfill is implicit via the DEFAULT on the NOT NULL columns.
Library rows installed from the community embed now carry the source
post slug + installed version_number in the engine sqlite. The SPA
uses both surfaces: a "community" pill next to the row name (so users
can tell at a glance which entries were authored locally vs pulled
from the embed), and a warn-coloured "v{N}" pill when the post has
shipped a newer version since install.
Engine:
- Migration 013 adds community_post_slug + community_version_number
to modules + bundles. Both nullable; locally-authored rows stay
NULL forever. Implemented as a .py migration with PRAGMA
table_info guards so rerunning against an already-migrated DB
(test fixtures rewind migrations to exercise older paths) is a
clean no-op rather than "duplicate column name".
- _insert_module / _insert_bundle (importer.py) persist the fields
when the entity dict carries them.
- _row_to_module / _row_to_bundle (repositories.py) surface them on
every read path. Try/except guard for pre-013 DB shape so a
connection opened mid-rollout does not crash.
Manager:
- installEnvelope grows optional `origin: { post_slug, version_number }`
in InstallOptions. When set, stamps every module + bundle in the
selection before the commit roundtrip.
- Host bridge in main.ts threads origin from `install(envelope, opts)`
through to installEnvelope. ABI stays at 1 (additive).
- New communityUpdateStore walks the library snapshot, batch-fetches
each unique community post (deduped, 8-wide concurrency), surfaces
rows whose installed version is behind the post's
latest_version_number. Network failures clear the store silently
so an offline session does not render misleading "v2 available"
pills.
- AppLayout kicks one check after the initial library fetch
resolves. No background polling.
- ModuleListView renders two distinct pills per row: a static
"community" indicator on origin-stamped rows, and a separate
warn-coloured "v{N}" pill when the update store has an entry.
Migration tests updated for the new schema head + bundle/module
column sets.
Introduces a persisted prompt-template library: migration 012 creates the
templates table (name/description/category_id FK/tags/is_favorite/
template_string), TemplateRepository provides CRUD with partial-update
sentinel semantics, and wp_api/templates.py exposes list/get/create/update/
delete/favorite with FK violations mapped to 400. Bumps the migration-head
version assertions to 12.
Migration 006 added the snapshot_fingerprint column but only populated
it on subsequent writes — every pre-006 row stayed NULL. The collision
detector correctly handles NULL as `exists-unknown` (amber EXISTING
badge), but a freshly exported-then-reimported untouched row still
showed EXISTING when content was byte-identical. Backfilling on first
open turns those into silent-skip (no badge), removing the visual noise.
Pure read+UPDATE — no schema change, no version bump on rows.
Adds engine/importer.py exposing commit_import / get_undo_entry /
undo_import. The committer applies a classified adds/replaces/renames
payload inside a single sqlite transaction, dispatching by `kind` to
the modules table (5 module types), bundles, or module_categories.
Module writes recompute snapshot_fingerprint; bundle writes recompute
payload_hash; categories use case-insensitive name-based merge
(matching the legacy import_bundle handler).
Every successful commit persists an import_undo row with enough
metadata (imported_records, replaced_snapshots, rename_map) to
reverse the commit. undo_import restores replaced rows bit-for-bit
(preserving original version + payload_hash + timestamps) and
deletes inserted rows, then evicts the undo entry.
Migration 007 creates the import_undo metadata table. Adds
17 importer tests covering adds across all 7 buckets, replace
snapshotting, rename with old→new mapping, collision rollback,
and undo restore/delete paths.