Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

add sosoft/ proposal for Casey Reas' Social Software Cycle 2

PDF proposal for DESMA 596/199 cohort membership with real
MongoDB network stats, plain-English flow charts, and Puppeteer
PDF generation pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+600 -27
+16 -9
fedac/scripts/make-alpine-kiosk.sh
··· 540 540 echo -e " ${GREEN}Initramfs regenerated${NC}" 541 541 fi 542 542 543 - # ── 3i. Inject custom /findroot into initramfs ── 543 + # ── 3i. Replace /init in initramfs with our findroot script ── 544 544 # Alpine's nlplug-findfs can't resolve PARTUUID/LABEL for raw SquashFS 545 - # partitions. We extract the initramfs, add a /findroot script, create a 546 - # /sysroot mount point, and repack as a single gzip cpio archive. 545 + # partitions. We extract the initramfs, rename the original /init to 546 + # /init.alpine as backup, and replace /init with our findroot script 547 + # that scans devices and mounts the squashfs root. 547 548 # (Appending a separate cpio archive after gzip doesn't work reliably 548 549 # on all kernels/hardware.) 549 550 INITRD_FILE="$ROOTFS_DIR/boot/initramfs-lts" ··· 558 559 # Create /sysroot mount point (Alpine uses /newroot, we need /sysroot) 559 560 mkdir -p "$INJECT_DIR/root/sysroot" 560 561 561 - # Write findroot script 562 - cat > "$INJECT_DIR/root/findroot" << 'FINDROOTEOF' 562 + # Rename original Alpine init as backup 563 + if [ -f "$INJECT_DIR/root/init" ]; then 564 + mv "$INJECT_DIR/root/init" "$INJECT_DIR/root/init.alpine" 565 + echo -e " Renamed original /init → /init.alpine" 566 + fi 567 + 568 + # Replace /init with our findroot script 569 + cat > "$INJECT_DIR/root/init" << 'FINDROOTEOF' 563 570 #!/bin/sh 564 571 mount -t proc proc /proc 2>/dev/null 565 572 mount -t sysfs sysfs /sys 2>/dev/null ··· 583 590 echo "findroot: could not find squashfs root partition" 584 591 exec /bin/sh 585 592 FINDROOTEOF 586 - chmod +x "$INJECT_DIR/root/findroot" 593 + chmod +x "$INJECT_DIR/root/init" 587 594 588 595 # Repack as a single gzip cpio archive 589 - echo -e " Repacking initramfs with findroot..." 596 + echo -e " Repacking initramfs with findroot as /init..." 590 597 (cd "$INJECT_DIR/root" && find . | cpio -o -H newc --quiet | gzip -1 > "$INITRD_FILE") 591 598 rm -rf "$INJECT_DIR" 592 - echo -e " ${GREEN}Injected /findroot into initramfs${NC}" 599 + echo -e " ${GREEN}Replaced /init in initramfs with findroot${NC}" 593 600 fi 594 601 595 602 # ══════════════════════════════════════════ ··· 768 775 terminal_input console 769 776 terminal_output gfxterm 770 777 menuentry "FedOS Alpine" { 771 - linux /boot/vmlinuz rdinit=/findroot rootfstype=squashfs ro quiet loglevel=0 mitigations=off 778 + linux /boot/vmlinuz rdinit=/init console=tty0 ro quiet loglevel=0 mitigations=off 772 779 initrd /boot/initramfs 773 780 } 774 781 GRUBEOF
+54 -10
oven/os-builder.mjs
··· 14 14 import { execSync } from "child_process"; 15 15 import { createHash, randomUUID } from "crypto"; 16 16 import { createBundle, createJSPieceBundle } from "./bundler.mjs"; 17 - import { S3Client, PutObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3"; 17 + import { S3Client, PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } from "@aws-sdk/client-s3"; 18 18 import { MongoClient } from "mongodb"; 19 19 20 20 // ─── MongoDB (OS build history) ───────────────────────────────────── ··· 120 120 } 121 121 122 122 // Upload a built ISO to CDN for fast repeat downloads. 123 - async function uploadToCDN(imagePath, key, target) { 123 + async function uploadToCDN(imagePath, key, target, flavor) { 124 124 const client = getSpacesClient(); 125 125 if (!client) return null; 126 126 await client.send( ··· 129 129 Key: key, 130 130 Body: fsSync.createReadStream(imagePath), 131 131 ContentType: "application/octet-stream", 132 - ContentDisposition: `attachment; filename="${target}-os.iso"`, 132 + ContentDisposition: `attachment; filename="${target}-${flavor}-os.iso"`, 133 133 ACL: "public-read", 134 134 CacheControl: "public, max-age=86400", // 24h — rebuild invalidates via new hash 135 135 }), 136 136 ); 137 137 return buildCDNUrl(key); 138 + } 139 + 140 + // Purge all cached OS build images from CDN, optionally filtered by flavor. 141 + export async function purgeOSBuildCache(flavor) { 142 + const client = getSpacesClient(); 143 + if (!client) return { deleted: 0, error: "S3 not configured" }; 144 + 145 + let deleted = 0; 146 + let continuationToken; 147 + 148 + try { 149 + do { 150 + const listCmd = new ListObjectsV2Command({ 151 + Bucket: SPACES_BUCKET, 152 + Prefix: `${SPACES_PREFIX}/`, 153 + ContinuationToken: continuationToken, 154 + }); 155 + const listing = await client.send(listCmd); 156 + continuationToken = listing.IsTruncated ? listing.NextContinuationToken : undefined; 157 + 158 + if (!listing.Contents?.length) break; 159 + 160 + // Filter by flavor suffix if specified (e.g. "-alpine.img" or "-fedora.img"). 161 + const toDelete = flavor 162 + ? listing.Contents.filter((o) => o.Key.endsWith(`-${flavor}.img`)) 163 + : listing.Contents; 164 + 165 + if (!toDelete.length) continue; 166 + 167 + await client.send( 168 + new DeleteObjectsCommand({ 169 + Bucket: SPACES_BUCKET, 170 + Delete: { Objects: toDelete.map((o) => ({ Key: o.Key })) }, 171 + }), 172 + ); 173 + deleted += toDelete.length; 174 + } while (continuationToken); 175 + } catch (err) { 176 + console.error("[os] CDN purge error:", err.message); 177 + return { deleted, error: err.message }; 178 + } 179 + 180 + console.log(`[os] Purged ${deleted} cached OS build(s) from CDN${flavor ? ` (${flavor})` : ""}`); 181 + return { deleted }; 138 182 } 139 183 140 184 function hashContent(content) { ··· 858 902 859 903 // ─── Main Build Flow ──────────────────────────────────────────────── 860 904 861 - export async function streamOSImage(res, target, isJSPiece, density, onProgress, flavor = "alpine") { 905 + export async function streamOSImage(res, target, isJSPiece, density, onProgress, flavor = "alpine", { nocache = false } = {}) { 862 906 if (activeBuildCount >= MAX_CONCURRENT_BUILDS) { 863 907 throw new Error( 864 908 `Server busy: ${activeBuildCount}/${MAX_CONCURRENT_BUILDS} OS builds in progress. Try again shortly.`, ··· 906 950 907 951 // 3. Check CDN cache — skip the entire build if an identical ISO exists. 908 952 const cacheStart = Date.now(); 909 - onProgress?.({ stage: "cache-check", message: "Checking CDN cache...", step: 3, totalSteps: 9 }); 910 - const cdnHit = await checkCDNCache(cdnKey); 953 + onProgress?.({ stage: "cache-check", message: nocache ? "Skipping cache (nocache)..." : "Checking CDN cache...", step: 3, totalSteps: 9 }); 954 + const cdnHit = nocache ? false : await checkCDNCache(cdnKey); 911 955 timing.cacheCheck = Date.now() - cacheStart; 912 956 913 957 if (cdnHit) { ··· 944 988 if (res) { 945 989 res.redirect(302, cdnUrl); 946 990 } 947 - return { buildId, elapsed, timings: timing, cdnUrl, cached: true, filename: `${target}-os.iso` }; 991 + return { buildId, elapsed, timings: timing, cdnUrl, cached: true, filename: `${target}-${flavor}-os.iso` }; 948 992 } 949 993 950 994 // 4. Copy base image to temp. ··· 1003 1047 1004 1048 // 8. Upload to CDN (concurrent with streaming to client). 1005 1049 onProgress?.({ stage: "upload", message: "Uploading ISO to CDN for caching...", step: 8, totalSteps: 9 }); 1006 - const uploadPromise = uploadToCDN(tempImagePath, cdnKey, target) 1050 + const uploadPromise = uploadToCDN(tempImagePath, cdnKey, target, flavor) 1007 1051 .then((url) => { 1008 1052 if (url) { 1009 1053 console.log(`[os] CDN upload complete: ${url}`); ··· 1022 1066 if (res) { 1023 1067 const streamStart = Date.now(); 1024 1068 const fileStat = await fs.stat(tempImagePath); 1025 - const filename = `${target}-os.iso`; 1069 + const filename = `${target}-${flavor}-os.iso`; 1026 1070 res.set({ 1027 1071 "Content-Type": "application/octet-stream", 1028 1072 "Content-Disposition": `attachment; filename="${filename}"`, ··· 1081 1125 timings: timing, 1082 1126 cdnUrl, 1083 1127 flavor, 1084 - filename: `${target}-os.iso`, 1128 + filename: `${target}-${flavor}-os.iso`, 1085 1129 }; 1086 1130 } catch (err) { 1087 1131 const elapsed = Date.now() - startTime;
+22 -8
oven/server.mjs
··· 13 13 import { grabHandler, grabGetHandler, grabIPFSHandler, grabPiece, getCachedOrGenerate, getActiveGrabs, getRecentGrabs, getLatestKeepThumbnail, getLatestIPFSUpload, getAllLatestIPFSUploads, setNotifyCallback, setLogCallback, cleanupStaleGrabs, clearAllActiveGrabs, getQueueStatus, getCurrentProgress, getAllProgress, getConcurrencyStatus, IPFS_GATEWAY, generateKidlispOGImage, getOGImageCacheStatus, getFrozenPieces, clearFrozenPiece, getLatestOGImageUrl, regenerateOGImagesBackground, generateKidlispBackdrop, getLatestBackdropUrl, APP_SCREENSHOT_PRESETS, generateNotepatOGImage, getLatestNotepatOGUrl } from './grabber.mjs'; 14 14 import archiver from 'archiver'; 15 15 import { createBundle, createJSPieceBundle, createM4DBundle, generateDeviceHTML, prewarmCache, getCacheStatus, setSkipMinification } from './bundler.mjs'; 16 - import { streamOSImage, getOSBuildStatus, invalidateManifest } from './os-builder.mjs'; 16 + import { streamOSImage, getOSBuildStatus, invalidateManifest, purgeOSBuildCache } from './os-builder.mjs'; 17 17 import { startOSBaseBuild, getOSBaseBuild, getOSBaseBuildsSummary, cancelOSBaseBuild } from './os-base-build.mjs'; 18 18 19 19 const app = express(); ··· 2428 2428 const format = req.query.format || 'download'; 2429 2429 const density = parseInt(req.query.density) || 8; 2430 2430 const flavor = (req.query.flavor || 'alpine').toLowerCase(); 2431 + const nocache = req.query.nocache === '1' || req.query.nocache === 'true'; 2431 2432 2432 2433 if (!['alpine', 'fedora'].includes(flavor)) { 2433 2434 return res.status(400).json({ error: "Invalid flavor. Use 'alpine' or 'fedora'." }); ··· 2442 2443 }); 2443 2444 } 2444 2445 2445 - addServerLog('info', '💿', `OS ISO build started: ${target} (${flavor})`); 2446 + addServerLog('info', '💿', `OS ISO build started: ${target} (${flavor})${nocache ? ' [nocache]' : ''}`); 2446 2447 2447 2448 // SSE streaming progress mode (for UI) 2448 2449 if (format === 'stream') { ··· 2460 2461 }; 2461 2462 2462 2463 try { 2463 - const result = await streamOSImage(null, target, isJSPiece, density, (p) => sendEvent('progress', p), flavor); 2464 + const result = await streamOSImage(null, target, isJSPiece, density, (p) => sendEvent('progress', p), flavor, { nocache }); 2464 2465 const downloadParam = isJSPiece ? `piece=${encodeURIComponent(target)}` : `code=${encodeURIComponent(target)}`; 2465 2466 // Prefer CDN URL for fast download; fall back to oven direct. 2466 2467 const downloadUrl = result.cdnUrl || `/os?${downloadParam}&density=${density}&flavor=${flavor}`; ··· 2484 2485 try { 2485 2486 const result = await streamOSImage(res, target, isJSPiece, density, (p) => { 2486 2487 console.log(`[os] ${p.stage}: ${p.message}`); 2487 - }, flavor); 2488 + }, flavor, { nocache }); 2488 2489 addServerLog('success', '💿', `OS ISO build complete: ${target}/${flavor} (${Math.round(result.elapsed / 1000)}s)`); 2489 2490 } catch (err) { 2490 2491 console.error('[os] Build failed:', err); ··· 2604 2605 { imageSizeGB, publish, flavor }, 2605 2606 { 2606 2607 onStart: (j) => addServerLog('info', '💿', `OS base build started: ${j.id} (${flavor}, ${imageSizeGB}GiB)`), 2607 - onUploadComplete: (j) => { 2608 + onUploadComplete: async (j) => { 2608 2609 addServerLog('success', '☁️', `OS base upload complete: ${j.upload.imageKey}`); 2609 2610 invalidateManifest(flavor); 2610 2611 addServerLog('info', '💿', `OS manifest cache invalidated (${flavor}) after base upload`); 2612 + // Purge all cached per-piece builds for this flavor — the new base image 2613 + // changes the image layout, so old cached builds are stale. 2614 + const purgeResult = await purgeOSBuildCache(flavor); 2615 + addServerLog('info', '🗑️', `Purged ${purgeResult.deleted} cached ${flavor} build(s) from CDN`); 2611 2616 }, 2612 2617 onSuccess: (j) => addServerLog('success', '💿', `OS base build complete: ${j.id} (${flavor})`), 2613 2618 onError: (j) => addServerLog('error', '❌', `OS base build failed: ${j.id} (${j.error})`), ··· 2631 2636 return res.json(result); 2632 2637 }); 2633 2638 2634 - app.post('/os-invalidate', (req, res) => { 2635 - invalidateManifest(); 2636 - addServerLog('info', '💿', 'OS base image manifest cache invalidated'); 2639 + app.post('/os-invalidate', async (req, res) => { 2640 + const purge = req.body?.purge === true; 2641 + const flavor = req.body?.flavor; 2642 + invalidateManifest(flavor); 2643 + addServerLog('info', '💿', `OS base image manifest cache invalidated${flavor ? ` (${flavor})` : ''}`); 2644 + 2645 + if (purge) { 2646 + const purgeResult = await purgeOSBuildCache(flavor); 2647 + addServerLog('info', '🗑️', `Purged ${purgeResult.deleted} cached build(s) from CDN${flavor ? ` (${flavor})` : ''}`); 2648 + return res.json({ ok: true, message: 'Manifest + CDN build cache purged.', purged: purgeResult.deleted }); 2649 + } 2650 + 2637 2651 res.json({ ok: true, message: 'Manifest cache invalidated — next build will re-fetch.' }); 2638 2652 }); 2639 2653
+30
sosoft/generate-pdf.mjs
··· 1 + #!/usr/bin/env node 2 + 3 + // Generate the SO SOFT proposal PDF from proposal.html using Puppeteer. 4 + // Usage: node sosoft/generate-pdf.mjs 5 + 6 + import puppeteer from "puppeteer"; 7 + import { fileURLToPath } from "url"; 8 + import path from "path"; 9 + 10 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 11 + const htmlPath = path.join(__dirname, "proposal.html"); 12 + const outputPath = path.join(__dirname, "proposal.pdf"); 13 + 14 + const browser = await puppeteer.launch({ 15 + headless: true, 16 + args: ["--no-sandbox", "--disable-setuid-sandbox"], 17 + }); 18 + 19 + const page = await browser.newPage(); 20 + await page.goto(`file://${htmlPath}`, { waitUntil: "networkidle0" }); 21 + 22 + await page.pdf({ 23 + path: outputPath, 24 + format: "Letter", 25 + printBackground: true, 26 + margin: { top: "0.9in", bottom: "0.9in", left: "1in", right: "1in" }, 27 + }); 28 + 29 + await browser.close(); 30 + console.log(`PDF written to ${outputPath}`);
+311
sosoft/proposal.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <style> 6 + @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,600;1,400&display=swap'); 7 + 8 + * { margin: 0; padding: 0; box-sizing: border-box; } 9 + 10 + @page { 11 + size: letter; 12 + margin: 1in 1.2in; 13 + } 14 + 15 + body { 16 + font-family: 'IBM Plex Sans', sans-serif; 17 + font-size: 11pt; 18 + line-height: 1.6; 19 + color: #111; 20 + } 21 + 22 + .header { 23 + margin-bottom: 2.5em; 24 + } 25 + 26 + .header h1 { 27 + font-family: 'IBM Plex Mono', monospace; 28 + font-size: 22pt; 29 + font-weight: 600; 30 + letter-spacing: -0.02em; 31 + line-height: 1.2; 32 + margin-bottom: 0.15em; 33 + } 34 + 35 + .header .subtitle { 36 + font-size: 12pt; 37 + font-weight: 300; 38 + color: #555; 39 + margin-bottom: 1.2em; 40 + } 41 + 42 + .header .meta { 43 + font-size: 9.5pt; 44 + color: #777; 45 + line-height: 1.7; 46 + } 47 + 48 + h2 { 49 + font-family: 'IBM Plex Mono', monospace; 50 + font-size: 11pt; 51 + font-weight: 600; 52 + margin-top: 2em; 53 + margin-bottom: 0.5em; 54 + text-transform: uppercase; 55 + letter-spacing: 0.06em; 56 + } 57 + 58 + p { 59 + margin-bottom: 0.75em; 60 + } 61 + 62 + ul { 63 + margin: 0.4em 0 0.9em 1.2em; 64 + } 65 + 66 + li { 67 + margin-bottom: 0.2em; 68 + } 69 + 70 + code { 71 + font-family: 'IBM Plex Mono', monospace; 72 + font-size: 9.5pt; 73 + background: #f0f0f0; 74 + padding: 0.1em 0.3em; 75 + border-radius: 2px; 76 + } 77 + 78 + .links { 79 + font-size: 9.5pt; 80 + color: #666; 81 + margin-top: 1.5em; 82 + line-height: 1.8; 83 + } 84 + 85 + .links a { 86 + color: #333; 87 + text-decoration: none; 88 + font-family: 'IBM Plex Mono', monospace; 89 + font-size: 9pt; 90 + } 91 + 92 + hr { 93 + border: none; 94 + border-top: 1px solid #ddd; 95 + margin: 1.5em 0; 96 + } 97 + 98 + .diagram-section { 99 + page-break-before: always; 100 + padding-top: 0.5em; 101 + } 102 + 103 + .diagram-section h2 { 104 + margin-top: 0; 105 + } 106 + 107 + .stat-grid { 108 + display: grid; 109 + grid-template-columns: 1fr 1fr 1fr; 110 + gap: 0.6em; 111 + margin: 0.8em 0; 112 + } 113 + 114 + .stat-box { 115 + background: #f7f7f7; 116 + border: 1px solid #e0e0e0; 117 + padding: 0.5em 0.7em; 118 + text-align: center; 119 + } 120 + 121 + .stat-box .num { 122 + font-family: 'IBM Plex Mono', monospace; 123 + font-size: 18pt; 124 + font-weight: 600; 125 + color: #111; 126 + line-height: 1.2; 127 + } 128 + 129 + .stat-box .label { 130 + font-size: 8pt; 131 + text-transform: uppercase; 132 + letter-spacing: 0.08em; 133 + color: #888; 134 + margin-top: 0.15em; 135 + } 136 + 137 + .doc-sample { 138 + background: #fafafa; 139 + border: 1px solid #e0e0e0; 140 + padding: 0.5em 0.7em; 141 + margin: 0.5em 0; 142 + font-family: 'IBM Plex Mono', monospace; 143 + font-size: 8pt; 144 + line-height: 1.5; 145 + color: #444; 146 + white-space: pre-wrap; 147 + } 148 + 149 + .doc-label { 150 + font-family: 'IBM Plex Mono', monospace; 151 + font-size: 8pt; 152 + color: #999; 153 + text-transform: uppercase; 154 + letter-spacing: 0.08em; 155 + margin-top: 0.7em; 156 + margin-bottom: 0.15em; 157 + } 158 + 159 + .doc-label:first-child { margin-top: 0; } 160 + 161 + .flow { 162 + font-family: 'IBM Plex Mono', monospace; 163 + font-size: 9pt; 164 + line-height: 2; 165 + margin: 0.6em 0; 166 + text-align: center; 167 + color: #333; 168 + } 169 + 170 + .flow .arrow { color: #999; } 171 + .flow .node { font-weight: 600; } 172 + .flow .note { color: #888; font-size: 8pt; font-weight: normal; } 173 + </style> 174 + </head> 175 + <body> 176 + 177 + <div class="header"> 178 + <h1>Aesthetic Computer as Social Instrument</h1> 179 + <div class="subtitle">Score for Social Software &mdash; Cycle 2 Proposal</div> 180 + <div class="meta"> 181 + Jeffrey Scudder &ensp;/&ensp; @jeffrey<br> 182 + DESMA 596/199 &ensp;&middot;&ensp; March 2026 183 + </div> 184 + </div> 185 + 186 + <h2>The Score</h2> 187 + 188 + <p> 189 + I want to use this cycle to develop the social choreography of Aesthetic Computer (AC) &mdash; the open-source creative computing platform I've been building since 2021. AC is a mobile-first runtime and social network where users write, publish, and share small interactive programs called <em>pieces</em>. It already has the technical infrastructure for social software: real-time chat, @handles with custom colors, ephemeral status updates (moods), a pixel painting gallery, a built-in Lisp dialect (KidLisp) for generative art, multiplayer WebSocket sessions, and user profiles that track creative output. 190 + </p> 191 + 192 + <p> 193 + What I haven't had the chance to develop is the <em>social composition</em> &mdash; deliberate structures for how people use this system together. The platform has 351 built-in pieces and ~2,800 registered handles, but the social dynamics are still largely emergent. I'd like to design and test specific social scores: structured rituals like a daily painting circle, weekly piece exchanges where members fork and remix each other's published programs, collaborative KidLisp jams, and constraint-based chat performances. I want to see what happens when a committed group performs these scores together over 10 weeks. 194 + </p> 195 + 196 + <p> 197 + Concretely, I'd build new AC pieces during the cycle &mdash; a cohort dashboard, multiplayer collaborative tools, and score templates that define social rules as playable software. I'd also write a "Write a Score" guide that documents how to compose social structures on the platform, parallel to AC's existing "Write a Piece" programming guide. 198 + </p> 199 + 200 + <h2>Audience</h2> 201 + 202 + <p> 203 + The immediate audience is this cohort. The scores I want to test need real people showing up regularly &mdash; you can't evaluate social software alone. Longer term, anything built during the cycle would be live on the public platform for AC's existing community, and the documentation would be reusable for other educators running creative computing workshops. 204 + </p> 205 + 206 + <h2>Why This Cycle</h2> 207 + 208 + <p> 209 + AC's <code>SCORE.md</code> already uses the metaphor of a musical score to organize the project &mdash; the platform's interface is designed to function like an instrument where users discover memorizable paths, build literacy through play, and eventually improvise. Casey's framing of "scores for social software" maps directly onto this. I've been building the instrument; this cycle is a chance to compose for it with peers who are thinking about similar questions from different angles. 210 + </p> 211 + 212 + <p> 213 + I also want the critical feedback. I've been deep in the technical architecture for years and need outside eyes on the social design &mdash; what feels inviting, what's confusing, what rituals actually sustain participation. A flat cohort of practitioners working on their own social software projects is the right context for that kind of dialogue. 214 + </p> 215 + 216 + <h2>Practice</h2> 217 + 218 + <p> 219 + I'm an artist, educator, and software developer. I build software as a medium for art and education. Before AC, I created No Paint (2020), a pixel art tool whose community of non-technical users taught me how people learn computing through social participation in software they love. AC extends that into a full platform: anyone can write, publish, and share interactive programs at a URL. The codebase is open source and has been in continuous development for 4+ years. 220 + </p> 221 + 222 + <p> 223 + I teach creative computing and have used AC as infrastructure in courses and workshops. 224 + </p> 225 + 226 + <div class="links"> 227 + <a href="https://aesthetic.computer">aesthetic.computer</a> &ensp;&middot;&ensp; 228 + <a href="https://github.com/whistlegraph/aesthetic-computer">github</a> &ensp;&middot;&ensp; 229 + <a href="https://nopaint.art">nopaint.art</a> &ensp;&middot;&ensp; 230 + <a href="https://news.ycombinator.com/item?id=41526754">notepat on hn</a> 231 + </div> 232 + 233 + <!-- NETWORK REPORT CARD --> 234 + <div class="diagram-section"> 235 + 236 + <h2>Appendix: The Network</h2> 237 + 238 + <p style="font-size: 9.5pt; color: #666; margin-bottom: 0.8em;"> 239 + Live data from AC's MongoDB cluster as of March 2, 2026. This is the material a score composes over. 240 + </p> 241 + 242 + <div class="stat-grid"> 243 + <div class="stat-box"><div class="num">2,798</div><div class="label">@handles</div></div> 244 + <div class="stat-box"><div class="num">18,016</div><div class="label">chat messages</div></div> 245 + <div class="stat-box"><div class="num">4,392</div><div class="label">paintings</div></div> 246 + <div class="stat-box"><div class="num">2,900</div><div class="label">moods</div></div> 247 + <div class="stat-box"><div class="num">16,174</div><div class="label">kidlisp programs</div></div> 248 + <div class="stat-box"><div class="num">265</div><div class="label">published pieces</div></div> 249 + <div class="stat-box"><div class="num">333</div><div class="label">clocks</div></div> 250 + <div class="stat-box"><div class="num">102</div><div class="label">tapes</div></div> 251 + <div class="stat-box"><div class="num">93,122</div><div class="label">boot events</div></div> 252 + </div> 253 + 254 + <p style="font-size: 9pt; color: #666; margin: 0.3em 0 0;"> 255 + <strong style="color:#444;">Who makes things:</strong> 256 + 1,067 users have painted &middot; 257 + 997 have posted moods &middot; 258 + 59 have written KidLisp &middot; 259 + 19 have published pieces 260 + </p> 261 + 262 + <div class="doc-label">The instrument loop</div> 263 + <div class="flow"> 264 + <span class="node">prompt</span> 265 + <span class="arrow">&ensp;&rarr;&ensp;</span> 266 + type a piece name 267 + <span class="arrow">&ensp;&rarr;&ensp;</span> 268 + <span class="node">enter</span> 269 + <span class="arrow">&ensp;&rarr;&ensp;</span> 270 + play the piece 271 + <span class="arrow">&ensp;&rarr;&ensp;</span> 272 + <span class="node">esc</span> 273 + <span class="arrow">&ensp;&rarr;&ensp;</span> 274 + back to prompt 275 + </div> 276 + 277 + <div class="doc-label">How user data flows</div> 278 + <div class="flow"> 279 + <span class="node">@handle</span> 280 + <span class="arrow">&ensp;&rarr;&ensp;</span> 281 + paint / chat / mood / kid / publish 282 + <span class="arrow">&ensp;&rarr;&ensp;</span> 283 + <span class="node">MongoDB</span> 284 + <span class="arrow">&ensp;&rarr;&ensp;</span> 285 + profile scorecard 286 + <span class="arrow">&ensp;&rarr;&ensp;</span> 287 + <span class="node">public URL</span> 288 + <br> 289 + <span class="note">moods also mirror to Bluesky via ATProto &ensp;&middot;&ensp; paintings stored on DigitalOcean Spaces CDN</span> 290 + </div> 291 + 292 + <div class="doc-label">Sample documents from MongoDB (live)</div> 293 + 294 + <div class="doc-sample"><strong>mood</strong> { mood: "studying astronomy", when: "2026-03-02T12:32:49Z", atproto: { rkey: "3mg3b6jcj4k2x" } }</div> 295 + 296 + <div class="doc-sample"><strong>painting</strong> { code: "gfl", slug: "2026.03.02.14.14.39", when: "2026-03-02T13:14:48Z", atproto: { rkey: "3mg3djpo6ck2x" } }</div> 297 + 298 + <div class="doc-sample"><strong>kidlisp</strong> { code: "27z", source: "fade:red-blue-black-blue-red\nscroll (* 100 amp)", hits: 1, when: "2026-03-01" }</div> 299 + 300 + <div class="doc-sample"><strong>chat</strong> { text: "y'all gonna piss me off", when: "2026-03-02T05:26:25Z", font: "font_1" }</div> 301 + 302 + <div class="doc-sample"><strong>piece</strong> { code: "zod", slug: "zod", name: "3d-cube", extension: ".mjs", hits: 1, when: "2026-02-12" }</div> 303 + 304 + <p style="font-size: 9pt; color: #666; margin-top: 1em;"> 305 + Every document ties back to an authenticated user. Every user has a profile that counts their contributions. The question for this cycle: what social structures &mdash; what <em>scores</em> &mdash; can make these numbers move together instead of in isolation? 306 + </p> 307 + 308 + </div> 309 + 310 + </body> 311 + </html>
sosoft/proposal.pdf

This is a binary file and will not be displayed.

+167
sosoft/report.md
··· 1 + # SO SOFT — Social Software Cycle Proposal 2 + 3 + ## Proposal for DESMA 596/199: Score for Social Software 4 + ### Jeffrey Scudder / @jeffrey / Aesthetic Computer 5 + 6 + --- 7 + 8 + ## 1. The Score 9 + 10 + **Title:** *Aesthetic Computer as Social Instrument* 11 + 12 + **Core idea:** Develop Aesthetic Computer's existing social infrastructure into a 13 + legible, playable *score* — a set of rules, rituals, and interfaces that a small 14 + community performs together over the 10-week cycle. 15 + 16 + AC already has the bones of social software built into its runtime: 17 + 18 + - **@handles** — unique identity with per-character color customization 19 + - **chat** — real-time group messaging with hearts/reactions 20 + - **moods** — ephemeral status updates (mirrored to Bluesky via ATProto) 21 + - **paintings** — collaborative pixel art gallery tied to user profiles 22 + - **KidLisp** — a minimal Lisp dialect anyone can use to publish generative art 23 + - **pieces** — URL-addressable programs anyone can write and publish at `@handle/piece-name` 24 + - **multiplayer sessions** — WebSocket rooms that any piece can use for real-time collaboration 25 + - **profiles** — live scorecards showing each user's creative output across all media types 26 + 27 + What's missing is not technology — it's *choreography*. The platform has 351 28 + built-in pieces, ~2800 registered handles, and 16k+ chat messages, but the 29 + social dynamics are still largely emergent and undirected. The cycle offers a 30 + chance to compose deliberate social scores *on top of* the existing runtime. 31 + 32 + ### Proposed Scores to Develop 33 + 34 + **Score A: "Daily Painting Circle"** 35 + A structured daily practice where cycle members each create and publish one 36 + painting per day using AC's pixel tools (`new`, `rect`, `line`, `smear`, `fill`, 37 + `shape`). Paintings are published via `done` and visible on each member's 38 + profile. The group reviews the day's paintings together in weekly meetings. 39 + This is a simple, repeatable ritual that teaches the platform's creative tools 40 + through committed practice. 41 + 42 + **Score B: "Piece Exchange"** 43 + Each member writes and publishes one AC piece (a small interactive program) per 44 + week, shared at their `@handle/piece-name`. The group plays each other's pieces 45 + and discusses them. Members can fork each other's work via `source @handle/piece` 46 + and publish remixes. This teaches the programming API through peer learning and 47 + creates a growing library of social artifacts. 48 + 49 + **Score C: "KidLisp Jam"** 50 + Weekly sessions where members collectively write KidLisp programs — AC's 51 + built-in Lisp dialect for generative art. KidLisp has 118 built-in functions 52 + and programs can be stored and shared instantly. The constraint of a minimal 53 + language (no external libraries, immediate visual output) creates a level 54 + playing field between experienced programmers and beginners. 55 + 56 + **Score D: "Chat as Performance"** 57 + Using AC's real-time chat as a performance medium. Structured chat sessions with 58 + rules — e.g., "only respond with piece names," "conversation through shared 59 + paintings," "one word per message." The chat system supports custom fonts, 60 + handle colors, and hearts, making it already suited to expressive constraint-based 61 + communication. 62 + 63 + ### What I Would Build During the Cycle 64 + 65 + 1. **A `sosoft` piece** — a dedicated AC piece (`aesthetic.computer/sosoft`) that 66 + serves as the cohort's home base: a dashboard showing all members' recent 67 + activity, paintings, published pieces, and moods in one view. 68 + 69 + 2. **Score templates** — simple markdown + code templates that define the rules 70 + for each social score, publishable as AC pieces themselves. 71 + 72 + 3. **Multiplayer scoring pieces** — new pieces that use AC's session server for 73 + real-time collaborative drawing, KidLisp editing, or structured turn-taking. 74 + 75 + 4. **Documentation** — a "Write a Score" guide parallel to AC's existing 76 + "Write a Piece" guide, teaching others to compose their own social software 77 + scores on the platform. 78 + 79 + --- 80 + 81 + ## 2. Intended Users / Audience / Community 82 + 83 + **Primary:** The cycle's own members (MFA and BA students in DESMA 596/199). 84 + The scores are designed to be performed by a small group (5–12 people) who 85 + commit to regular participation. 86 + 87 + **Secondary:** AC's existing community (~2800 registered handles). Anything 88 + built during the cycle would be immediately live on the public platform. 89 + Existing AC users could discover and join the scores organically. 90 + 91 + **Tertiary:** Creative computing educators and students elsewhere. The "Write a 92 + Score" documentation would be a reusable framework for anyone running a creative 93 + computing workshop or class using AC as infrastructure. 94 + 95 + --- 96 + 97 + ## 3. Why This Cycle 98 + 99 + Aesthetic Computer has been in development since 2021, growing from No Paint 100 + (2020, discussed on Hacker News) into a full runtime with 351 pieces, a Lisp 101 + dialect, multiplayer, chat, and a handle system. The technical infrastructure 102 + is mature. What it needs now is *social composition* — deliberate experiments 103 + in how people use this system together. 104 + 105 + Casey's framing of "scores for social software" maps directly onto how I already 106 + think about AC. The SCORE.md file in the repository literally uses the metaphor 107 + of a musical score to organize the project. AC's interface is designed to 108 + function like a musical instrument — users discover memorizable paths, build 109 + literacy through play, and eventually improvise and compose. 110 + 111 + This cycle offers: 112 + 113 + - **A committed cohort** to test social features that need real human 114 + participation to evaluate (you can't A/B test a conversation) 115 + - **Critical feedback** from Casey and peers on the social design, not just the 116 + technical architecture 117 + - **A structured timeframe** (10 weeks) that creates urgency and rhythm — 118 + exactly what a score needs to be performed 119 + - **Cross-pollination** with other participants' social software projects, 120 + creating a richer discourse than working in isolation 121 + 122 + I'm also interested in this cycle as a way to develop AC's role as 123 + *educational infrastructure* — something I can bring to my own teaching practice 124 + and share with other educators. 125 + 126 + --- 127 + 128 + ## 4. Practice Description 129 + 130 + I'm Jeffrey Scudder (@jeffrey), an artist, educator, and software developer. 131 + I direct Aesthetic Computer (https://aesthetic.computer), an open-source 132 + creative computing platform and social network. 133 + 134 + My practice centers on building software as a medium for art and education. 135 + Before AC, I created No Paint (nopaint.art, 2020), a pixel art tool that was 136 + discussed on Hacker News and used by a community of non-technical artists who 137 + learned computing through contributing to the software they loved. 138 + 139 + AC extends this into a full platform: a mobile-first runtime where anyone can 140 + write, publish, and share interactive programs. It includes KidLisp (a Lisp 141 + dialect for generative art), real-time multiplayer, a chat system, and a social 142 + handle system. The codebase is open source with ~78 API endpoints and has been 143 + in continuous development for 4+ years. 144 + 145 + I teach creative computing and have used AC as infrastructure in courses and 146 + workshops. My interest in "social software" comes directly from watching 147 + non-technical users learn computation through social participation in software 148 + communities — first in No Paint, now in AC. 149 + 150 + **Links:** 151 + - Aesthetic Computer: https://aesthetic.computer 152 + - GitHub: https://github.com/whistlegraph/aesthetic-computer 153 + - No Paint: https://nopaint.art 154 + - Notepat on HN: https://news.ycombinator.com/item?id=41526754 155 + 156 + --- 157 + 158 + ## Technical Stack for PDF Generation 159 + 160 + - **Content source:** This report.md 161 + - **PDF generation:** Puppeteer (already in project dependencies) rendering a 162 + styled HTML template to PDF 163 + - **Script:** `sosoft/generate-pdf.mjs` — converts the HTML to a print-quality 164 + PDF with proper typography 165 + - **Output:** `sosoft/proposal.pdf` 166 + 167 + To generate: `node sosoft/generate-pdf.mjs`