Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(at): add computer.aesthetic.paper lexicon + backfill 22 papers

- New lexicon schema for papers.aesthetic.computer papers
- Published to ATProto network as 7th custom lexicon
- Backfill script creates records under @jeffrey's PDS account
- All 22 papers synced with title, slug, pdfUrl, languages, revisions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+264
+57
at/lexicons/computer/aesthetic/paper.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "computer.aesthetic.paper", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "An academic paper published on papers.aesthetic.computer", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["title", "slug", "pdfUrl", "when"], 12 + "properties": { 13 + "title": { 14 + "type": "string", 15 + "description": "Paper title", 16 + "maxLength": 300 17 + }, 18 + "slug": { 19 + "type": "string", 20 + "description": "Site PDF name (e.g., 'aesthetic-computer-26-arxiv')", 21 + "maxLength": 128 22 + }, 23 + "pdfUrl": { 24 + "type": "string", 25 + "format": "uri", 26 + "description": "URL to the PDF on papers.aesthetic.computer", 27 + "maxLength": 512 28 + }, 29 + "languages": { 30 + "type": "array", 31 + "maxLength": 10, 32 + "items": { 33 + "type": "string", 34 + "maxLength": 5 35 + }, 36 + "description": "Available language codes (e.g., en, da, es, zh)" 37 + }, 38 + "revisions": { 39 + "type": "integer", 40 + "description": "Revision count", 41 + "minimum": 0 42 + }, 43 + "when": { 44 + "type": "string", 45 + "format": "datetime", 46 + "description": "Publication date (ISO 8601)" 47 + }, 48 + "ref": { 49 + "type": "string", 50 + "description": "Paper directory key for bidirectional reference", 51 + "maxLength": 64 52 + } 53 + } 54 + } 55 + } 56 + } 57 + }
+207
at/scripts/atproto/backfill-papers-to-atproto.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + /** 4 + * Backfill Papers to ATProto 5 + * 6 + * Creates computer.aesthetic.paper records on the PDS for each paper 7 + * in the PAPER_MAP. Papers are published under @jeffrey's PDS account. 8 + * 9 + * Usage: 10 + * node backfill-papers-to-atproto.mjs [options] 11 + * 12 + * Options: 13 + * --dry-run Show what would be created without making changes 14 + * --limit N Only process N papers 15 + */ 16 + 17 + import { AtpAgent } from "@atproto/api"; 18 + import { readFileSync, existsSync } from "fs"; 19 + import { join, dirname } from "path"; 20 + import { fileURLToPath } from "url"; 21 + import { connect } from "../../../system/backend/database.mjs"; 22 + import { config } from "dotenv"; 23 + 24 + config({ path: "../../../system/.env" }); 25 + 26 + const __dirname = dirname(fileURLToPath(import.meta.url)); 27 + const PAPERS_DIR = join(__dirname, "../../../system/public/papers.aesthetic.computer"); 28 + const METADATA_PATH = join(__dirname, "../../../papers/metadata.json"); 29 + 30 + const PDS_URL = process.env.PDS_URL || "https://at.aesthetic.computer"; 31 + const PAPERS_BASE_URL = "https://papers.aesthetic.computer"; 32 + const COLLECTION = "computer.aesthetic.paper"; 33 + 34 + const args = process.argv.slice(2); 35 + const dryRun = args.includes("--dry-run"); 36 + const limitIndex = args.indexOf("--limit"); 37 + const limit = limitIndex !== -1 ? parseInt(args[limitIndex + 1]) : null; 38 + 39 + // Same PAPER_MAP from papers/cli.mjs 40 + const PAPER_MAP = { 41 + "arxiv-ac": { siteName: "aesthetic-computer-26-arxiv", title: "Aesthetic Computer '26" }, 42 + "arxiv-api": { siteName: "piece-api-26-arxiv", title: "From setup() to boot()" }, 43 + "arxiv-archaeology": { siteName: "repo-archaeology-26-arxiv", title: "Repository Archaeology" }, 44 + "arxiv-dead-ends": { siteName: "dead-ends-26-arxiv", title: "Vestigial Features" }, 45 + "arxiv-diversity": { siteName: "citation-diversity-audit-26", title: "Citation Diversity Audit" }, 46 + "arxiv-goodiepal": { siteName: "radical-computer-art-26-arxiv", title: "Radical Computer Art" }, 47 + "arxiv-kidlisp": { siteName: "kidlisp-26-arxiv", title: "KidLisp '26" }, 48 + "arxiv-kidlisp-reference": { siteName: "kidlisp-reference-26-arxiv", title: "KidLisp Language Reference" }, 49 + "arxiv-network-audit": { siteName: "network-audit-26-arxiv", title: "Network Audit" }, 50 + "arxiv-notepat": { siteName: "notepat-26-arxiv", title: "notepat.com" }, 51 + "arxiv-os": { siteName: "ac-native-os-26-arxiv", title: "AC Native OS" }, 52 + "arxiv-pieces": { siteName: "pieces-not-programs-26-arxiv", title: "Pieces Not Programs" }, 53 + "arxiv-sustainability": { siteName: "who-pays-for-creative-tools-26-arxiv", title: "Who Pays for Creative Tools?" }, 54 + "arxiv-whistlegraph": { siteName: "whistlegraph-26-arxiv", title: "Whistlegraph" }, 55 + "arxiv-plork": { siteName: "plorking-the-planet-26-arxiv", title: "PLOrk'ing the Planet" }, 56 + "arxiv-folk-songs": { siteName: "folk-songs-26-arxiv", title: "Playable Folk Songs" }, 57 + "arxiv-complex": { siteName: "sucking-on-the-complex-26-arxiv", title: "Sucking on the Complex" }, 58 + "arxiv-kidlisp-cards": { siteName: "kidlisp-cards-26-arxiv", title: "KidLisp Cards" }, 59 + "arxiv-score-analysis": { siteName: "reading-the-score-26-arxiv", title: "Reading the Score" }, 60 + "arxiv-calarts": { siteName: "calarts-callouts-papers-26-arxiv", title: "CalArts, Callouts, and Papers" }, 61 + "arxiv-open-schools": { siteName: "open-schools-26-arxiv", title: "Get Closed Source Out of Schools" }, 62 + "arxiv-futures": { siteName: "five-years-from-now-26-arxiv", title: "Five Years from Now" }, 63 + }; 64 + 65 + const LANGS = ["en", "da", "es", "zh"]; 66 + 67 + async function backfillPapers() { 68 + console.log("📄 Backfill Papers to ATProto\n"); 69 + console.log(`Mode: ${dryRun ? "🔍 DRY RUN" : "✍️ LIVE"}`); 70 + console.log(`PDS: ${PDS_URL}`); 71 + console.log(); 72 + 73 + // Load metadata 74 + const metadata = JSON.parse(readFileSync(METADATA_PATH, "utf8")); 75 + 76 + // Get jeffrey's PDS credentials 77 + const database = await connect(); 78 + const users = database.db.collection("users"); 79 + const handles = database.db.collection("@handles"); 80 + 81 + // Find jeffrey's auth0 ID 82 + const jeffreyHandle = await handles.findOne({ handle: "jeffrey" }); 83 + if (!jeffreyHandle) { 84 + console.error("❌ Could not find @jeffrey handle"); 85 + await database.disconnect(); 86 + process.exit(1); 87 + } 88 + 89 + const jeffreyUser = await users.findOne({ _id: jeffreyHandle._id }); 90 + if (!jeffreyUser?.atproto?.did || !jeffreyUser?.atproto?.password) { 91 + console.error("❌ @jeffrey has no ATProto credentials"); 92 + await database.disconnect(); 93 + process.exit(1); 94 + } 95 + 96 + const did = jeffreyUser.atproto.did; 97 + console.log(`Publishing as: ${did} (@jeffrey)\n`); 98 + 99 + // Login to PDS 100 + const agent = new AtpAgent({ service: PDS_URL }); 101 + await agent.login({ identifier: did, password: jeffreyUser.atproto.password }); 102 + 103 + // Check existing records 104 + let existingRkeys = new Set(); 105 + try { 106 + let cursor; 107 + do { 108 + const res = await agent.com.atproto.repo.listRecords({ 109 + repo: did, 110 + collection: COLLECTION, 111 + limit: 100, 112 + cursor, 113 + }); 114 + for (const r of res.data.records || []) { 115 + existingRkeys.add(r.value.ref || r.value.slug); 116 + } 117 + cursor = res.data.cursor; 118 + } while (cursor); 119 + } catch { 120 + // Collection may not exist yet 121 + } 122 + 123 + console.log(`Existing paper records on PDS: ${existingRkeys.size}\n`); 124 + 125 + const entries = Object.entries(PAPER_MAP); 126 + const toProcess = limit ? entries.slice(0, limit) : entries; 127 + 128 + let synced = 0, skipped = 0, failed = 0; 129 + 130 + for (let i = 0; i < toProcess.length; i++) { 131 + const [dir, paper] = toProcess[i]; 132 + const meta = metadata[dir]; 133 + const pdfPath = join(PAPERS_DIR, `${paper.siteName}.pdf`); 134 + const pdfExists = existsSync(pdfPath); 135 + 136 + // Detect available languages 137 + const langs = LANGS.filter((l) => { 138 + if (l === "en") return pdfExists; 139 + return existsSync(join(PAPERS_DIR, `${paper.siteName}-${l}.pdf`)); 140 + }); 141 + 142 + const pdfUrl = `${PAPERS_BASE_URL}/${paper.siteName}.pdf`; 143 + 144 + // Skip if already on PDS 145 + if (existingRkeys.has(dir)) { 146 + console.log(` [${i + 1}/${toProcess.length}] ⏭️ ${paper.title} (already synced)`); 147 + skipped++; 148 + continue; 149 + } 150 + 151 + if (!pdfExists) { 152 + console.log(` [${i + 1}/${toProcess.length}] ⚠️ ${paper.title} (no PDF found)`); 153 + skipped++; 154 + continue; 155 + } 156 + 157 + console.log( 158 + ` [${i + 1}/${toProcess.length}] ${dryRun ? "Would sync" : "Syncing"}: ${paper.title} [${langs.join(",")}]`, 159 + ); 160 + 161 + if (dryRun) { 162 + synced++; 163 + continue; 164 + } 165 + 166 + try { 167 + const record = { 168 + $type: COLLECTION, 169 + title: paper.title, 170 + slug: paper.siteName, 171 + pdfUrl, 172 + languages: langs, 173 + revisions: meta?.revisions || 1, 174 + when: meta?.created 175 + ? new Date(meta.created + "T00:00:00Z").toISOString() 176 + : new Date().toISOString(), 177 + ref: dir, 178 + }; 179 + 180 + const res = await agent.com.atproto.repo.createRecord({ 181 + repo: did, 182 + collection: COLLECTION, 183 + record, 184 + }); 185 + 186 + const rkey = (res.uri || res.data?.uri).split("/").pop(); 187 + console.log(` ✅ → ${rkey}`); 188 + synced++; 189 + } catch (error) { 190 + console.log(` ❌ ${error.message}`); 191 + failed++; 192 + } 193 + } 194 + 195 + console.log("\n" + "═".repeat(50)); 196 + console.log(`✅ Synced: ${synced}`); 197 + console.log(`⏭️ Skipped: ${skipped}`); 198 + console.log(`❌ Failed: ${failed}`); 199 + console.log(`📊 Total: ${toProcess.length}\n`); 200 + 201 + await database.disconnect(); 202 + } 203 + 204 + backfillPapers().catch((err) => { 205 + console.error("Fatal:", err); 206 + process.exit(1); 207 + });