Problem / Goal
There is no factual universe in PortOS, and none ships:
- The universe record (
server/services/universeBuilder/sanitize.js:683-790) has no factual/fiction axis. Canon trunks are characters/places/objects; category kinds are the same three plus other.
createUniverse mints randomUUID() and accepts no caller id (server/services/universeBuilder/crud.js:244-247).
data.reference/ contains no universes/ directory; bootstrapServices reads data.reference only for providers.json (server/services/bootstrap.js:264). Universes are PG rows (server/lib/db/schema/universes.js:19), so setup-data.js is not the seeding mechanism — a migration is.
- Repo-wide there is no
factual / non-fiction / isFiction concept on a scrap, an ingredient, or a universe.
Goal: factual/autobiographical capture has a home from first boot, and the universe carries the flag that tells the extractor (landed in #7609) how to read the text.
Approach
- Add
factual: boolean to the universe record in sanitizeTemplate (server/services/universeBuilder/sanitize.js:683-790), persisted only when true so every existing record keeps its on-disk shape and wire checksum.
- Bump
universes 11 → 12 in server/lib/schemaVersions.js:82, with the rationale inline: a ≤v11 peer strips factual, then LWW's the stripped record back and silently reclassifies the user's Reality universe as fiction. Update the three assertions in server/lib/schemaVersions.test.js (:27, :64, :71).
- Seed migration
scripts/migrations/NNN-reality-universe-seed.js: a universe with the deterministic id universe-reality (matches UNIVERSE_ID_RE = /^[A-Za-z0-9-]{8,80}$/, sanitize.js:51), name: 'Reality', factual: true, ephemeral: false, and empty logline / premise / styleNotes. Shipping invented copy for the user's own life is wrong, and an LLM call at migration time is barred by the AI Provider Usage Policy in AGENTS.md.
- Deterministic id so a user's federated machines converge on ONE Reality by LWW instead of accumulating one per install.
- Gate on absence INCLUDING tombstones.
insertUniverseWithId already exists (server/services/universeBuilder/crud.js:319) and takes a caller-supplied id — but it deliberately overwrites a tombstone (wasResurrection at :326-334), which is right for share-bucket re-import and wrong here. So the migration reads await store().loadOne('universe-reality') first and no-ops when any row exists, deleted true or false, only calling insertUniverseWithId when there is none. A user who deleted Reality must not have it resurrected on the next upgrade. Do not add a resurrect: false option — the check belongs in the migration, not in a shared importer path.
- Surface
factual in the Universe Builder settings panel as a "This is a real-world universe" toggle, and badge it in the universe list.
Acceptance criteria
Notes
This is independently shippable, but it is what makes 7615's Reality default mean anything — land them close together.
Also fold in here
Phase 1 (#7609, PR #7614) marks factual rows with factual / real-person
tags. normalizeTags treats those like any user tag — canonicalized by key,
user-editable, user-deletable, federated with the row — so today the only
machine-readable marker that a catalog row describes a real person is a string
the user can rename away. Once this phase lands a real factual field, demote
those two tags to a display affordance derived from it rather than the source of
truth. Do NOT write a migration that strips existing ones: they are
indistinguishable from tags the user typed.
Problem / Goal
There is no factual universe in PortOS, and none ships:
server/services/universeBuilder/sanitize.js:683-790) has no factual/fiction axis. Canon trunks arecharacters/places/objects; category kinds are the same three plusother.createUniversemintsrandomUUID()and accepts no caller id (server/services/universeBuilder/crud.js:244-247).data.reference/contains nouniverses/directory;bootstrapServicesreadsdata.referenceonly forproviders.json(server/services/bootstrap.js:264). Universes are PG rows (server/lib/db/schema/universes.js:19), sosetup-data.jsis not the seeding mechanism — a migration is.factual/non-fiction/isFictionconcept on a scrap, an ingredient, or a universe.Goal: factual/autobiographical capture has a home from first boot, and the universe carries the flag that tells the extractor (landed in #7609) how to read the text.
Approach
factual: booleanto the universe record insanitizeTemplate(server/services/universeBuilder/sanitize.js:683-790), persisted only whentrueso every existing record keeps its on-disk shape and wire checksum.universes11 → 12 inserver/lib/schemaVersions.js:82, with the rationale inline: a ≤v11 peer stripsfactual, then LWW's the stripped record back and silently reclassifies the user's Reality universe as fiction. Update the three assertions inserver/lib/schemaVersions.test.js(:27,:64,:71).scripts/migrations/NNN-reality-universe-seed.js: a universe with the deterministic iduniverse-reality(matchesUNIVERSE_ID_RE = /^[A-Za-z0-9-]{8,80}$/,sanitize.js:51),name: 'Reality',factual: true,ephemeral: false, and empty logline / premise / styleNotes. Shipping invented copy for the user's own life is wrong, and an LLM call at migration time is barred by the AI Provider Usage Policy inAGENTS.md.insertUniverseWithIdalready exists (server/services/universeBuilder/crud.js:319) and takes a caller-supplied id — but it deliberately overwrites a tombstone (wasResurrectionat:326-334), which is right for share-bucket re-import and wrong here. So the migration readsawait store().loadOne('universe-reality')first and no-ops when any row exists,deletedtrue or false, only callinginsertUniverseWithIdwhen there is none. A user who deleted Reality must not have it resurrected on the next upgrade. Do not add aresurrect: falseoption — the check belongs in the migration, not in a shared importer path.factualin the Universe Builder settings panel as a "This is a real-world universe" toggle, and badge it in the universe list.Acceptance criteria
Reality, iduniverse-reality,factual: true. Re-running the migration is a no-op.PORTOS_SCHEMA_VERSIONS.universesis 12;server/lib/schemaVersions.test.jsandserver/lib/db.ddlParity.test.jspass, and the existing version-gate coverage guards still hold.factualis absent from the persisted record when false, so an existing universe's wire checksum is unchanged — a test pins this.client/src/AGENTS.md.docs/STORAGE.mdand the catalog docs describe the shipped Reality universe.Notes
This is independently shippable, but it is what makes 7615's Reality default mean anything — land them close together.
Also fold in here
Phase 1 (#7609, PR #7614) marks factual rows with
factual/real-persontags.
normalizeTagstreats those like any user tag — canonicalized by key,user-editable, user-deletable, federated with the row — so today the only
machine-readable marker that a catalog row describes a real person is a string
the user can rename away. Once this phase lands a real
factualfield, demotethose two tags to a display affordance derived from it rather than the source of
truth. Do NOT write a migration that strips existing ones: they are
indistinguishable from tags the user typed.