Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2e83a8f
Implement world seed and hub backend
perlaimad Aug 5, 2026
4cf0a6e
Harden world hub and bible validation
perlaimad Aug 5, 2026
ccb21de
Add structured contribution draft workflow
perlaimad Aug 5, 2026
0f7b120
Implement world seed and hub backend
perlaimad Aug 5, 2026
1231993
Harden world hub and bible validation
perlaimad Aug 5, 2026
8d3518a
Add structured contribution draft workflow
perlaimad Aug 5, 2026
a0ab185
feat(worlds): connect frontend to world seed backend
KareemN1936 Aug 5, 2026
d44a50a
feat(ui): port responsive auth and navigation polish
KareemN1936 Aug 7, 2026
0a9f340
fix(api): align world routes with session verification
KareemN1936 Aug 7, 2026
1e4bdf7
feat(ui): add lore entity CRUD frontend
KareemN1936 Aug 7, 2026
dde4fe2
feat(api): add lore entity CRUD routes
KareemN1936 Aug 8, 2026
350b9ac
feat(lore): add relationship graph linking
KareemN1936 Aug 9, 2026
c6bc1a9
fix(ui): tighten lore detail layout
KareemN1936 Aug 9, 2026
95f4330
fix(lore): address relationship review blockers
KareemN1936 Aug 9, 2026
ce8c1b9
fix(lore): address relationship review follow-ups
KareemN1936 Aug 10, 2026
6d4281b
fix(lore): address final relationship review blockers
KareemN1936 Aug 10, 2026
9679e33
fix(lore): resolve final graph review followups
KareemN1936 Aug 10, 2026
446de62
fix(lore): close relationship governance review blockers
KareemN1936 Aug 10, 2026
78db984
fix(lore): close relationship review blockers
KareemN1936 Aug 10, 2026
3cfb817
fix(lore): restore hub authoring relationship paths
KareemN1936 Aug 10, 2026
88ecd25
style(lore): format relationship follow-up
KareemN1936 Aug 10, 2026
6262751
fix(lore): align auth provider and list projection
KareemN1936 Aug 11, 2026
a8b2383
test(api): fake active refresh sessions in route tests
KareemN1936 Aug 11, 2026
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ Auth challenge, verification, and refresh endpoints use Fastify rate limiting ba

Google auth routes are not registered as a default login path. With `GOOGLE_AUTH_ENABLED=false`, `/auth/google/*` returns a not-enabled response. Enabling Google currently exposes the guarded boundary only; Google OAuth account linking and automatic Hive provisioning remain deferred until secure non-custodial account creation and key delivery are specified.

Implemented world endpoints:

- `POST /worlds`: requires authentication, creates a mutable off-chain World Seed and initial `WorldBibleVersion` in one transaction, and assigns the authenticated user an active `FOUNDER` membership. The route never accepts a client-supplied founder or role and does not publish to Hive.
- `GET /worlds`: browses discoverable worlds with pagination and basic title/description, genre, and tone filters.
- `GET /worlds/:worldId`: returns one world with its founder, seed, and current latest World Bible version.
- `GET /worlds/:worldId/hub`: returns the World Hub aggregate for the frontend, including world seed, current bible preview, simple canon/proposal stats, and latest canon lore entry summaries.
- `PATCH /worlds/:worldId`: requires `EDIT_INITIAL_CANON` world permission and updates mutable off-chain world title/description, seed, and latest initial bible content only. It does not create proposals, canonize content, or publish to Hive.

Implemented contribution endpoints:

- `POST /worlds/:worldId/contributions`: requires authentication and `CREATE_LORE_DRAFT` world permission, creates an author-owned structured contribution draft for lore or story content, and optionally links it to a lore entry in the same world.
- `GET /worlds/:worldId/contributions`: requires authentication and lists only the authenticated writer's contributions in the world with page/pageSize pagination and optional status/kind filters.
- `GET /worlds/:worldId/contributions/:contributionId`: returns an authenticated writer's own contribution draft or submitted contribution without exposing other writers' private drafts.
- `PATCH /worlds/:worldId/contributions/:contributionId`: requires `EDIT_OWN_DRAFT`, updates only mutable draft fields, and rejects submitted contributions.
- `DELETE /worlds/:worldId/contributions/:contributionId`: requires `EDIT_OWN_DRAFT`, deletes only draft contributions, and never deletes submitted proposals.
- `POST /worlds/:worldId/contributions/:contributionId/submit`: requires `SUBMIT_PROPOSAL`, atomically locks the draft, creates exactly one submitted proposal with an immutable `proposedContent` snapshot, and does not publish to Hive.

---

# Reputation & Rewards
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE "WorldSeed" (
"id" TEXT NOT NULL,
"worldId" TEXT NOT NULL,
"premise" TEXT NOT NULL,
"genre" TEXT NOT NULL,
"tone" TEXT NOT NULL,
"mainConflict" TEXT NOT NULL,
"startingLocation" TEXT,
"firstCharacters" JSONB,
"firstFactions" JSONB,
"firstHistoricalEvent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "WorldSeed_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "WorldSeed_worldId_key" ON "WorldSeed"("worldId");
CREATE INDEX "WorldSeed_genre_idx" ON "WorldSeed"("genre");
CREATE INDEX "WorldSeed_tone_idx" ON "WorldSeed"("tone");

ALTER TABLE "WorldSeed" ADD CONSTRAINT "WorldSeed_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- CreateEnum
CREATE TYPE "ContributionKind" AS ENUM ('LORE', 'STORY');

-- CreateEnum
CREATE TYPE "ContributionStatus" AS ENUM ('DRAFT', 'SUBMITTED');

-- AlterEnum
ALTER TYPE "ProposalType" ADD VALUE 'ADD_STORY';

-- AlterEnum
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_UPDATED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_DELETED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_SUBMITTED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'PROPOSAL_CREATED';

-- AlterTable
ALTER TABLE "Proposal" ADD COLUMN "contributionKind" "ContributionKind";

-- CreateTable
CREATE TABLE "ContributionDraft" (
"id" TEXT NOT NULL,
"worldId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"targetLoreEntryId" TEXT,
"kind" "ContributionKind" NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT,
"content" JSONB NOT NULL,
"status" "ContributionStatus" NOT NULL DEFAULT 'DRAFT',
"proposalId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"submittedAt" TIMESTAMP(3),

CONSTRAINT "ContributionDraft_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "ContributionDraft_proposalId_key" ON "ContributionDraft"("proposalId");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_idx" ON "ContributionDraft"("worldId");

-- CreateIndex
CREATE INDEX "ContributionDraft_authorId_idx" ON "ContributionDraft"("authorId");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_status_idx" ON "ContributionDraft"("worldId", "status");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_authorId_status_idx" ON "ContributionDraft"("worldId", "authorId", "status");

-- CreateIndex
CREATE INDEX "ContributionDraft_targetLoreEntryId_idx" ON "ContributionDraft"("targetLoreEntryId");

-- CreateIndex
CREATE INDEX "Proposal_worldId_contributionKind_idx" ON "Proposal"("worldId", "contributionKind");

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_targetLoreEntryId_fkey" FOREIGN KEY ("targetLoreEntryId") REFERENCES "LoreEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "Proposal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TYPE "LoreType" ADD VALUE IF NOT EXISTS 'QUEST';
ALTER TYPE "LoreType" ADD VALUE IF NOT EXISTS 'STORY';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_ENTRY_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_ENTRY_UPDATED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_ENTRY_DELETED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_RELATIONSHIP_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_RELATIONSHIP_DELETED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
UPDATE "LoreEntry"
SET "loreType" = 'QUEST'
WHERE "loreType" = 'OTHER'
AND "status" <> 'PUBLISHED_CANON'
AND "content"->>'entityType' = 'QUEST';

UPDATE "LoreEntry"
SET "loreType" = 'STORY'
WHERE "loreType" = 'OTHER'
AND "status" <> 'PUBLISHED_CANON'
AND "content"->>'entityType' = 'STORY_CONTRIBUTION';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Support deterministic lore list and relation projections.
CREATE INDEX "LoreEntry_worldId_updatedAt_id_idx" ON "LoreEntry"("worldId", "updatedAt", "id");
CREATE INDEX "LoreRelationship_sourceId_updatedAt_id_idx" ON "LoreRelationship"("sourceId", "updatedAt", "id");
CREATE INDEX "LoreRelationship_targetId_updatedAt_id_idx" ON "LoreRelationship"("targetId", "updatedAt", "id");
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
WITH repaired AS (
UPDATE "LoreEntry"
SET "loreType" = CASE
WHEN "content"->>'entityType' = 'QUEST' THEN 'QUEST'::"LoreType"
WHEN "content"->>'entityType' = 'STORY_CONTRIBUTION' THEN 'STORY'::"LoreType"
ELSE "loreType"
END
WHERE "status" = 'PUBLISHED_CANON'
AND "loreType" = 'OTHER'
AND "content"->>'entityType' IN ('QUEST', 'STORY_CONTRIBUTION')
RETURNING
"id",
"worldId",
"loreType",
"content"->>'entityType' AS "entityType"
)
INSERT INTO "WorldAuditLog" (
"id",
"worldId",
"actorId",
"action",
"targetType",
"targetId",
"metadata",
"createdAt"
)
SELECT
gen_random_uuid()::text,
repaired."worldId",
NULL,
'LORE_ENTRY_UPDATED',
'LORE_ENTRY',
repaired."id",
jsonb_build_object(
'reason', 'Corrected a published canon lore type that still used OTHER after the QUEST/STORY type split.',
'migration', '20260810190000_repair_published_canon_quest_story_lore_types',
'loreType', repaired."loreType",
'entityType', repaired."entityType"
),
CURRENT_TIMESTAMP
FROM repaired
WHERE NOT EXISTS (
SELECT 1
FROM "WorldAuditLog"
WHERE "WorldAuditLog"."targetType" = 'LORE_ENTRY'
AND "WorldAuditLog"."targetId" = repaired."id"
AND "WorldAuditLog"."action" = 'LORE_ENTRY_UPDATED'
AND "WorldAuditLog"."metadata"->>'migration' =
'20260810190000_repair_published_canon_quest_story_lore_types'
);
Loading
Loading