open source is social v-it.org
0
fork

Configure Feed

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

cap lexicon: add title, description, ref fields

- Add title (512b/64g), description (3000b/300g), ref (128b/64g)
to org.v-it.cap lexicon schema as required fields
- Wire --title, --description, --ref as required options in vit ship
- Validate ref format: three lowercase alpha words with dashes
- Update vit skim text output to display new fields with graceful
degradation for older caps missing them
- Update vit firehose to prefer title over text in event summary
- Add ship validation tests (missing args, ref format)
- Update VOCAB.md cap definition and ship description

+104 -10
+8 -1
docs/VOCAB.md
··· 33 33 - evidence 34 34 - artifacts 35 35 36 + caps also include structured fields: 37 + - `title` (short capability title) 38 + - `description` (longer capability summary) 39 + - `ref` (three lowercase words separated by dashes) 40 + 36 41 **kinds** 37 42 (examples) 38 43 - `feat` ··· 183 188 publish (posts) a new cap to your feed. 184 189 185 190 ```bash 186 - vit ship 191 + vit ship "<text>" --title "<title>" --description "<description>" --ref "<one-two-three>" 187 192 ``` 188 193 189 194 ship creates: 190 195 - a new cap 191 196 - or a recap (quote post) if remixed from another cap 197 + 198 + required flags for ship are `--title`, `--description`, and `--ref`. 192 199 193 200 ship is the outward publishing and sharing act. 194 201
+19 -1
lexicons/org/v-it/cap.json
··· 8 8 "key": "tid", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["text", "createdAt"], 11 + "required": ["text", "createdAt", "title", "description", "ref"], 12 12 "properties": { 13 + "title": { 14 + "type": "string", 15 + "maxLength": 512, 16 + "maxGraphemes": 64, 17 + "description": "Short title for the capability" 18 + }, 19 + "description": { 20 + "type": "string", 21 + "maxLength": 3000, 22 + "maxGraphemes": 300, 23 + "description": "Longer description of the capability" 24 + }, 25 + "ref": { 26 + "type": "string", 27 + "maxLength": 128, 28 + "maxGraphemes": 64, 29 + "description": "Three lowercase words separated by dashes, e.g. fast-cache-invalidation" 30 + }, 13 31 "text": { 14 32 "type": "string", 15 33 "maxLength": 3000,
+1 -1
src/cmd/firehose.js
··· 35 35 return `[${time}] ${operation} ${collection} from ${didShort} rkey=${rkey}`; 36 36 } 37 37 38 - const message = event.commit?.record?.text; 38 + const message = event.commit?.record?.title || event.commit?.record?.text; 39 39 if (typeof message === 'string') { 40 40 return `[${time}] ${operation} ${collection} from ${didShort} rkey=${rkey} — "${message}"`; 41 41 }
+11 -5
src/cmd/ship.js
··· 14 14 .description('Publish a cap to your feed') 15 15 .option('-v, --verbose', 'Show step-by-step details') 16 16 .option('--did <did>', 'DID to use (reads saved DID from config if not provided)') 17 + .requiredOption('--title <title>', 'Short title for the cap') 18 + .requiredOption('--description <description>', 'Description of the cap') 19 + .requiredOption('--ref <ref>', 'Three lowercase words with dashes (e.g. fast-cache-invalidation)') 17 20 .action(async (text, opts) => { 18 21 try { 19 22 const { verbose } = opts; ··· 24 27 return; 25 28 } 26 29 if (verbose) console.log(`[verbose] DID: ${did}`); 30 + 31 + const REF_PATTERN = /^[a-z]+-[a-z]+-[a-z]+$/; 32 + if (!REF_PATTERN.test(opts.ref)) { 33 + console.error('error: --ref must be exactly three lowercase words separated by dashes (e.g. fast-cache-invalidation)'); 34 + process.exitCode = 1; 35 + return; 36 + } 27 37 const now = new Date().toISOString(); 28 38 29 39 const { agent, session } = await restoreAgent(did); 30 40 if (verbose) console.log(`[verbose] Session restored, PDS: ${session.serverMetadata?.issuer}`); 31 41 32 - const record = { 33 - $type: CAP_COLLECTION, 34 - text, 35 - createdAt: now, 36 - }; 42 + const record = { $type: CAP_COLLECTION, text, title: opts.title, description: opts.description, ref: opts.ref, createdAt: now }; 37 43 const projectConfig = readProjectConfig(); 38 44 if (projectConfig.beacon) record.beacon = projectConfig.beacon; 39 45 if (verbose && projectConfig.beacon) console.log(`[verbose] Beacon: ${projectConfig.beacon}`);
+16 -2
src/cmd/skim.js
··· 101 101 console.log('no caps found for this beacon.'); 102 102 } 103 103 for (const rec of capped) { 104 - // extract author DID from URI: at://did:plc:xxx/org.v-it.cap/tid 105 104 const author = rec.uri.split('/')[2]; 106 105 const short = author.length > 20 ? author.slice(0, 20) + '…' : author; 107 106 const time = rec.value.createdAt || 'unknown'; 107 + const title = rec.value.title || ''; 108 + const description = rec.value.description || ''; 109 + const ref = rec.value.ref || ''; 108 110 const text = rec.value.text || ''; 109 111 console.log(`[${short}] ${time}`); 110 - console.log(` ${text}`); 112 + if (title || ref) { 113 + const parts = [title, ref ? `(${ref})` : ''].filter(Boolean).join(' '); 114 + console.log(` ${parts}`); 115 + } 116 + if (description) { 117 + console.log(` ${description}`); 118 + } 119 + if ((title || ref || description) && text) { 120 + console.log(' ---'); 121 + } 122 + if (text) { 123 + console.log(` ${text}`); 124 + } 111 125 console.log(); 112 126 } 113 127 }
+49
test/ship.test.js
··· 1 + // SPDX-License-Identifier: AGPL-3.0-only 2 + // Copyright (c) 2026 sol pbc 3 + 4 + import { describe, test, expect } from 'bun:test'; 5 + import { run } from './helpers.js'; 6 + 7 + describe('vit ship', () => { 8 + test('fails when --title is missing', () => { 9 + const r = run('ship "hello" --description "desc" --ref "one-two-three"'); 10 + expect(r.exitCode).not.toBe(0); 11 + expect(r.stderr).toMatch(/--title/i); 12 + }); 13 + 14 + test('fails when --description is missing', () => { 15 + const r = run('ship "hello" --title "Hi" --ref "one-two-three"'); 16 + expect(r.exitCode).not.toBe(0); 17 + expect(r.stderr).toMatch(/--description/i); 18 + }); 19 + 20 + test('fails when --ref is missing', () => { 21 + const r = run('ship "hello" --title "Hi" --description "desc"'); 22 + expect(r.exitCode).not.toBe(0); 23 + expect(r.stderr).toMatch(/--ref/i); 24 + }); 25 + 26 + test('rejects ref with uppercase letters', () => { 27 + const r = run('ship "hello" --title "Hi" --description "desc" --ref "Bad-Ref-Here"'); 28 + expect(r.exitCode).not.toBe(0); 29 + expect(r.stderr).toMatch(/three lowercase words/i); 30 + }); 31 + 32 + test('rejects ref with wrong segment count', () => { 33 + const r = run('ship "hello" --title "Hi" --description "desc" --ref "only-two"'); 34 + expect(r.exitCode).not.toBe(0); 35 + expect(r.stderr).toMatch(/three lowercase words/i); 36 + }); 37 + 38 + test('rejects ref with digits', () => { 39 + const r = run('ship "hello" --title "Hi" --description "desc" --ref "has-num-3bers"'); 40 + expect(r.exitCode).not.toBe(0); 41 + expect(r.stderr).toMatch(/three lowercase words/i); 42 + }); 43 + 44 + test('accepts valid ref format (fails at auth, not validation)', () => { 45 + const r = run('ship "hello" --title "Hi" --description "desc" --ref "one-two-three" --did "did:plc:abc"'); 46 + expect(r.exitCode).not.toBe(0); 47 + expect(r.stderr).not.toMatch(/three lowercase words/i); 48 + }); 49 + });