From 545e3218cb3f42678fdfb93e3a688dc9b2829d7a Mon Sep 17 00:00:00 2001 From: Lichen Dev Agent Date: Sat, 22 Aug 2026 22:39:21 +0000 Subject: [PATCH] Open wiki mode M1: data model (lexicons + schema) - wiki.lichen.wiki: add optional openContributions flag - new wiki.lichen.contribution record (pending submission envelope) - new wiki.lichen.contributionApproval record (owner/admin decision) - wikis.contributions_open column + pending_contributions table + index - WikiRow type + test fixtures updated --- docs/open-wiki-plan.md | 6 +- lexicons/wiki.lichen.contribution.json | 55 +++++++++++++++++++ .../wiki.lichen.contributionApproval.json | 37 +++++++++++++ lexicons/wiki.lichen.wiki.json | 4 ++ src/server/db/schema.ts | 25 +++++++++ src/server/db/types.ts | 1 + tests/lib/orchestrators/membership.test.ts | 1 + tests/lib/orchestrators/note.test.ts | 1 + 8 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 lexicons/wiki.lichen.contribution.json create mode 100644 lexicons/wiki.lichen.contributionApproval.json diff --git a/docs/open-wiki-plan.md b/docs/open-wiki-plan.md index 9c8198e..349663d 100644 --- a/docs/open-wiki-plan.md +++ b/docs/open-wiki-plan.md @@ -42,10 +42,10 @@ From there each submission can be **approved** (goes live for everyone) and opti ### 3.2 New lexicon — `wiki.lichen.contribution` (pending submission) Authored by the **submitter on their PDS** (key: TID). Represents a proposed change. ``` -wikiRef (at-uri) — target wiki +WikiRef (at-uri) — target wiki kind "create-note" | "edit-note" -noteRef (at-uri) — for edit-note: the existing note being edited; - create-note: the new note the submitter created +noteRef (at-uri) — required: the note on the submitter's PDS holding the + proposed change (new note for create-note; existing note for edit-note) title string content string message? string — optional change note diff --git a/lexicons/wiki.lichen.contribution.json b/lexicons/wiki.lichen.contribution.json new file mode 100644 index 0000000..a971b6e --- /dev/null +++ b/lexicons/wiki.lichen.contribution.json @@ -0,0 +1,55 @@ +{ + "lexicon": 1, + "id": "wiki.lichen.contribution", + "defs": { + "main": { + "type": "record", + "description": "A proposed change to an open wiki, submitted by a non-contributor and held pending until an owner/admin approves it. The submitter writes the underlying note (and revision) to their own PDS at submit time; this record identifies and describes that proposed change.", + "key": "tid", + "record": { + "type": "object", + "required": ["wikiRef", "kind", "noteRef", "title", "content", "createdAt"], + "properties": { + "wikiRef": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the target (public, open) wiki." + }, + "kind": { + "type": "string", + "maxLength": 32, + "knownValues": ["create-note", "edit-note"], + "description": "Whether this proposes a brand-new note or an edit to an existing note." + }, + "noteRef": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the note on the submitter's PDS holding the proposed change. For kind=create-note this is the newly created note; for kind=edit-note this is the existing note being edited." + }, + "title": { + "type": "string", + "maxGraphemes": 256, + "maxLength": 2560, + "description": "Proposed title for the note." + }, + "content": { + "type": "string", + "maxLength": 1000000, + "description": "Proposed body content for the note." + }, + "message": { + "type": "string", + "maxGraphemes": 1024, + "maxLength": 10240, + "description": "Optional note describing this proposed change." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the contribution was submitted." + } + } + } + } + } +} \ No newline at end of file diff --git a/lexicons/wiki.lichen.contributionApproval.json b/lexicons/wiki.lichen.contributionApproval.json new file mode 100644 index 0000000..92dc603 --- /dev/null +++ b/lexicons/wiki.lichen.contributionApproval.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "wiki.lichen.contributionApproval", + "defs": { + "main": { + "type": "record", + "description": "An owner/admin decision on a pending open contribution. Making approval a record on the moderator's PDS lets other lichen appviews honor the decision, not just this instance.", + "key": "tid", + "record": { + "type": "object", + "required": ["contributionRef", "decision", "createdAt"], + "properties": { + "contributionRef": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the wiki.lichen.contribution record being decided." + }, + "decision": { + "type": "string", + "maxLength": 32, + "knownValues": ["approve", "reject"], + "description": "Whether to approve or reject the contribution." + }, + "promote": { + "type": "boolean", + "description": "When true and decision is 'approve', also grant the submitter the contributor role." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the decision was made." + } + } + } + } + } +} \ No newline at end of file diff --git a/lexicons/wiki.lichen.wiki.json b/lexicons/wiki.lichen.wiki.json index 6c45407..7f20ae0 100644 --- a/lexicons/wiki.lichen.wiki.json +++ b/lexicons/wiki.lichen.wiki.json @@ -22,6 +22,10 @@ "knownValues": ["public", "private"], "description": "Access level." }, + "openContributions": { + "type": "boolean", + "description": "When true (and visibility is 'public'), non-contributors may submit open contributions that require owner/admin approval before they go live. Absent/false means direct edits require a contributor (or admin) role." + }, "createdAt": { "type": "string", "format": "datetime", diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 1aab995..24db53e 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -27,6 +27,7 @@ export function initSchema(db: Database): void { addColumnIfMissing(db, "wikis", "sidebar", "TEXT"); addColumnIfMissing(db, "wikis", "home_slug", "TEXT"); + addColumnIfMissing(db, "wikis", "contributions_open", "INTEGER NOT NULL DEFAULT 0"); db.run(` CREATE TABLE IF NOT EXISTS notes ( @@ -190,6 +191,27 @@ export function initSchema(db: Database): void { ) `); + // Open-contribution review queue. Mirrors wiki.lichen.contribution records; + // holds a proposed change pending until an owner/admin approves or rejects it. + db.run(` + CREATE TABLE IF NOT EXISTS pending_contributions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + contribution_at_uri TEXT NOT NULL UNIQUE, + wiki_at_uri TEXT NOT NULL REFERENCES wikis(at_uri) ON DELETE CASCADE, + wiki_slug TEXT NOT NULL, + note_at_uri TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('create-note', 'edit-note')), + title TEXT NOT NULL, + content TEXT NOT NULL, + message TEXT, + submitter_did TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')), + approved_at TEXT, + approved_by_did TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + `); + // Opaque app sessions. The prod session cookie (`sid`) holds one of these // random ids — never the user's DID, which is public and thus forgeable. Maps // to a DID + expiry; rows are pruned on read and at startup. (Dev mode has no @@ -225,6 +247,9 @@ export function initSchema(db: Database): void { db.run( "CREATE INDEX IF NOT EXISTS idx_app_sessions_expires ON app_sessions(expires_at)", ); + db.run( + "CREATE INDEX IF NOT EXISTS idx_pending_contributions_wiki ON pending_contributions(wiki_at_uri, status)", + ); } function addColumnIfMissing( diff --git a/src/server/db/types.ts b/src/server/db/types.ts index 30ea294..89dcaaf 100644 --- a/src/server/db/types.ts +++ b/src/server/db/types.ts @@ -13,6 +13,7 @@ export interface WikiRow { theme: WikiThemeName; sidebar: string | null; home_slug: string | null; + contributions_open: 0 | 1; at_uri: string; created_at: string; updated_at: string; diff --git a/tests/lib/orchestrators/membership.test.ts b/tests/lib/orchestrators/membership.test.ts index 52bce58..8f48313 100644 --- a/tests/lib/orchestrators/membership.test.ts +++ b/tests/lib/orchestrators/membership.test.ts @@ -57,6 +57,7 @@ const wiki: WikiRow = { theme: "light", sidebar: null, home_slug: null, + contributions_open: 0, at_uri: `at://${OWNER_DID}/wiki.lichen.wiki/${WIKI_SLUG}`, created_at: "2026-01-01T00:00:00.000Z", updated_at: "2026-01-01T00:00:00.000Z", diff --git a/tests/lib/orchestrators/note.test.ts b/tests/lib/orchestrators/note.test.ts index fed91ac..c7935c7 100644 --- a/tests/lib/orchestrators/note.test.ts +++ b/tests/lib/orchestrators/note.test.ts @@ -56,6 +56,7 @@ function makeCtx( theme: "light", sidebar: null, home_slug: null, + contributions_open: 0, at_uri: `at://${WIKI_DID}/wiki.lichen.wiki/${WIKI_SLUG}`, created_at: "2026-01-01T00:00:00.000Z", updated_at: "2026-01-01T00:00:00.000Z", -- 2.51.2