pi-simocracy 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -201
- package/docs/SIM_AUTHORED_COMMENTS.md +197 -0
- package/docs/SIM_AUTHORED_PROPOSALS.md +198 -0
- package/package.json +2 -1
- package/src/index.ts +882 -11
- package/src/lookup.ts +537 -0
- package/src/writes.ts +237 -0
package/src/writes.ts
CHANGED
|
@@ -127,6 +127,9 @@ export async function getAuthenticatedAgent(): Promise<{ agent: Agent; did: stri
|
|
|
127
127
|
|
|
128
128
|
const COLLECTION_AGENTS = "org.simocracy.agents";
|
|
129
129
|
const COLLECTION_STYLE = "org.simocracy.style";
|
|
130
|
+
const COLLECTION_COMMENT = "org.impactindexer.review.comment";
|
|
131
|
+
const COLLECTION_HISTORY = "org.simocracy.history";
|
|
132
|
+
const COLLECTION_PROPOSAL = "org.hypercerts.claim.activity";
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
135
|
* Defense-in-depth: every write helper below verifies the target
|
|
@@ -276,6 +279,240 @@ export async function updateStyle(opts: {
|
|
|
276
279
|
return { uri: res.data.uri, cid: res.data.cid };
|
|
277
280
|
}
|
|
278
281
|
|
|
282
|
+
/**
|
|
283
|
+
* POST `org.impactindexer.review.comment`.
|
|
284
|
+
*
|
|
285
|
+
* Matches the wire shape simocracy.org's `useRecordComments.postComment`
|
|
286
|
+
* already writes today (`subject = { uri, type: 'record' }`, no CID), so
|
|
287
|
+
* comments authored from pi render identically in the webapp and thread
|
|
288
|
+
* correctly under the same parent. The `subject.uri` is the parent
|
|
289
|
+
* record — proposal, gathering, sim, decision, or another comment for
|
|
290
|
+
* a nested reply.
|
|
291
|
+
*
|
|
292
|
+
* No sim-attribution lives in this record. Sim attribution is a
|
|
293
|
+
* sidecar `org.simocracy.history` written by `createCommentHistory`
|
|
294
|
+
* below — see `docs/SIM_AUTHORED_COMMENTS.md` for the full design.
|
|
295
|
+
*/
|
|
296
|
+
export async function createComment(opts: {
|
|
297
|
+
agent: Agent;
|
|
298
|
+
did: string;
|
|
299
|
+
subjectUri: string;
|
|
300
|
+
text: string;
|
|
301
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
302
|
+
const trimmed = opts.text.trim();
|
|
303
|
+
if (!trimmed) {
|
|
304
|
+
throw new Error("Cannot post an empty comment.");
|
|
305
|
+
}
|
|
306
|
+
const record = {
|
|
307
|
+
$type: COLLECTION_COMMENT,
|
|
308
|
+
subject: { uri: opts.subjectUri, type: "record" },
|
|
309
|
+
text: trimmed.slice(0, 5000),
|
|
310
|
+
createdAt: new Date().toISOString(),
|
|
311
|
+
};
|
|
312
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
313
|
+
repo: opts.did,
|
|
314
|
+
collection: COLLECTION_COMMENT,
|
|
315
|
+
record,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
uri: res.data.uri,
|
|
319
|
+
cid: res.data.cid,
|
|
320
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Sim-attribution sidecar for a comment.
|
|
326
|
+
*
|
|
327
|
+
* The `org.impactindexer.review.comment` lexicon has no field for
|
|
328
|
+
* "this comment is the voice of sim X" — and we don't want to extend
|
|
329
|
+
* an impactindexer-owned lexicon for a Simocracy-specific concept.
|
|
330
|
+
* Instead we use Simocracy's existing `org.simocracy.history`
|
|
331
|
+
* lexicon as a join table:
|
|
332
|
+
*
|
|
333
|
+
* commentUri ←— history.subjectUri
|
|
334
|
+
* loaded sim ←— history.simUris[0] / simNames[0]
|
|
335
|
+
*
|
|
336
|
+
* Renderers that understand this pattern (simocracy.org) join the two
|
|
337
|
+
* sets at display time and show the comment with a sim badge;
|
|
338
|
+
* renderers that don't (Bluesky AppView, third-party clients) fall
|
|
339
|
+
* back to displaying the comment as a regular user comment — graceful
|
|
340
|
+
* degradation, zero lexicon changes anywhere.
|
|
341
|
+
*
|
|
342
|
+
* Writes to the *user's* own PDS (the comment author), not a shared
|
|
343
|
+
* facilitator repo, because the attribution is an event the user
|
|
344
|
+
* triggered and naturally belongs in their history.
|
|
345
|
+
*/
|
|
346
|
+
export async function createCommentHistory(opts: {
|
|
347
|
+
agent: Agent;
|
|
348
|
+
did: string;
|
|
349
|
+
commentUri: string;
|
|
350
|
+
simUri: string;
|
|
351
|
+
simName: string;
|
|
352
|
+
text: string;
|
|
353
|
+
/** Title of the parent record (proposal / gathering / sim) — best-effort. */
|
|
354
|
+
proposalTitle?: string;
|
|
355
|
+
/** Collection of the parent record — best-effort. */
|
|
356
|
+
parentCollection?: string;
|
|
357
|
+
/** Human-readable name of the parent — best-effort, denormalized for the timeline. */
|
|
358
|
+
parentName?: string;
|
|
359
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
360
|
+
// Defense-in-depth: the sim must live in the same repo we're writing to.
|
|
361
|
+
// (If it doesn't, the indexer ingests a history record claiming attribution
|
|
362
|
+
// for a sim the actor doesn't own — confusing rather than dangerous, but
|
|
363
|
+
// worth catching here.)
|
|
364
|
+
assertRepoOwnsSimUri(opts.did, opts.simUri);
|
|
365
|
+
const record: Record<string, unknown> = {
|
|
366
|
+
$type: COLLECTION_HISTORY,
|
|
367
|
+
type: "comment",
|
|
368
|
+
actorDid: opts.did,
|
|
369
|
+
simNames: [opts.simName].slice(0, 10),
|
|
370
|
+
simUris: [opts.simUri].slice(0, 10),
|
|
371
|
+
subjectUri: opts.commentUri,
|
|
372
|
+
subjectCollection: COLLECTION_COMMENT,
|
|
373
|
+
content: opts.text.slice(0, 5000),
|
|
374
|
+
createdAt: new Date().toISOString(),
|
|
375
|
+
};
|
|
376
|
+
if (opts.proposalTitle) {
|
|
377
|
+
record.proposalTitle = opts.proposalTitle.slice(0, 500);
|
|
378
|
+
}
|
|
379
|
+
if (opts.parentName) {
|
|
380
|
+
record.subjectName = opts.parentName.slice(0, 500);
|
|
381
|
+
}
|
|
382
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
383
|
+
repo: opts.did,
|
|
384
|
+
collection: COLLECTION_HISTORY,
|
|
385
|
+
record,
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
uri: res.data.uri,
|
|
389
|
+
cid: res.data.cid,
|
|
390
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* POST `org.hypercerts.claim.activity` (a funding proposal).
|
|
396
|
+
*
|
|
397
|
+
* Matches the wire shape simocracy.org's `ProposalFormDialog` writes
|
|
398
|
+
* today (`title`, `shortDescription`, optional `description` /
|
|
399
|
+
* `workScope` / `contributors` / `image`, `createdAt`), so proposals
|
|
400
|
+
* authored from pi render identically in the webapp.
|
|
401
|
+
*
|
|
402
|
+
* No sim-attribution lives in this record. Sim attribution is a
|
|
403
|
+
* sidecar `org.simocracy.history` written by `createProposalHistory`
|
|
404
|
+
* below — same pattern as comments, see
|
|
405
|
+
* `docs/SIM_AUTHORED_PROPOSALS.md` for the design rationale.
|
|
406
|
+
*
|
|
407
|
+
* The proposal itself is the *user's*, not the sim's, so this writer
|
|
408
|
+
* does NOT call `assertRepoOwnsSimUri` — the only precondition is
|
|
409
|
+
* that the user is signed in (enforced at the tool entry point via
|
|
410
|
+
* `assertCanWriteToSim`, which also requires a loaded sim because
|
|
411
|
+
* attribution requires one).
|
|
412
|
+
*/
|
|
413
|
+
export async function createProposal(opts: {
|
|
414
|
+
agent: Agent;
|
|
415
|
+
did: string;
|
|
416
|
+
title: string;
|
|
417
|
+
shortDescription: string;
|
|
418
|
+
description?: string;
|
|
419
|
+
workScope?: string;
|
|
420
|
+
contributors?: Array<{ contributorIdentity: string }>;
|
|
421
|
+
image?: { $type: "org.hypercerts.defs#uri"; uri: string };
|
|
422
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
423
|
+
const title = opts.title.trim();
|
|
424
|
+
if (!title) throw new Error("Proposal title is required.");
|
|
425
|
+
const shortDescription = opts.shortDescription.trim();
|
|
426
|
+
if (!shortDescription)
|
|
427
|
+
throw new Error("Proposal shortDescription is required.");
|
|
428
|
+
const record: Record<string, unknown> = {
|
|
429
|
+
$type: COLLECTION_PROPOSAL,
|
|
430
|
+
title: title.slice(0, 256),
|
|
431
|
+
shortDescription: shortDescription.slice(0, 300),
|
|
432
|
+
createdAt: new Date().toISOString(),
|
|
433
|
+
};
|
|
434
|
+
if (opts.description !== undefined) {
|
|
435
|
+
const body = opts.description.trim();
|
|
436
|
+
if (body) record.description = body;
|
|
437
|
+
}
|
|
438
|
+
if (opts.workScope !== undefined) {
|
|
439
|
+
const ws = opts.workScope.trim();
|
|
440
|
+
if (ws) record.workScope = ws;
|
|
441
|
+
}
|
|
442
|
+
if (opts.contributors && opts.contributors.length > 0) {
|
|
443
|
+
record.contributors = opts.contributors;
|
|
444
|
+
}
|
|
445
|
+
if (opts.image) record.image = opts.image;
|
|
446
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
447
|
+
repo: opts.did,
|
|
448
|
+
collection: COLLECTION_PROPOSAL,
|
|
449
|
+
record,
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
uri: res.data.uri,
|
|
453
|
+
cid: res.data.cid,
|
|
454
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Sim-attribution sidecar for a proposal.
|
|
460
|
+
*
|
|
461
|
+
* Mirrors `createCommentHistory` exactly — same `org.simocracy.history`
|
|
462
|
+
* lexicon, same join key shape, just `type: "proposal"` and
|
|
463
|
+
* `subjectCollection: "org.hypercerts.claim.activity"`. The lexicon's
|
|
464
|
+
* `type` field is free-form string; the webapp doesn't filter
|
|
465
|
+
* histories by `type === "proposal"` today, but adding a new value is
|
|
466
|
+
* fine (history.json already documents that new event types are
|
|
467
|
+
* appended over time).
|
|
468
|
+
*
|
|
469
|
+
* Writes to the *user's* own PDS, not a shared facilitator repo —
|
|
470
|
+
* the attribution is an event the user triggered and naturally
|
|
471
|
+
* belongs in their history.
|
|
472
|
+
*/
|
|
473
|
+
export async function createProposalHistory(opts: {
|
|
474
|
+
agent: Agent;
|
|
475
|
+
did: string;
|
|
476
|
+
proposalUri: string;
|
|
477
|
+
proposalTitle: string;
|
|
478
|
+
simUri: string;
|
|
479
|
+
simName: string;
|
|
480
|
+
/** Plain-text description, denormalized for the timeline (truncated to ~5000 chars). */
|
|
481
|
+
content?: string;
|
|
482
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
483
|
+
// Defense-in-depth: the sim must live in the same repo we're writing to.
|
|
484
|
+
// The proposal record itself isn't sim-owned, but the history sidecar
|
|
485
|
+
// *claims attribution to* a sim — only the sim's owner can make that claim.
|
|
486
|
+
assertRepoOwnsSimUri(opts.did, opts.simUri);
|
|
487
|
+
const title = opts.proposalTitle.trim();
|
|
488
|
+
const record: Record<string, unknown> = {
|
|
489
|
+
$type: COLLECTION_HISTORY,
|
|
490
|
+
type: "proposal",
|
|
491
|
+
actorDid: opts.did,
|
|
492
|
+
simNames: [opts.simName].slice(0, 10),
|
|
493
|
+
simUris: [opts.simUri].slice(0, 10),
|
|
494
|
+
subjectUri: opts.proposalUri,
|
|
495
|
+
subjectCollection: COLLECTION_PROPOSAL,
|
|
496
|
+
subjectName: title.slice(0, 500),
|
|
497
|
+
proposalTitle: title.slice(0, 500),
|
|
498
|
+
createdAt: new Date().toISOString(),
|
|
499
|
+
};
|
|
500
|
+
if (opts.content) {
|
|
501
|
+
const trimmed = opts.content.trim();
|
|
502
|
+
if (trimmed) record.content = trimmed.slice(0, 5000);
|
|
503
|
+
}
|
|
504
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
505
|
+
repo: opts.did,
|
|
506
|
+
collection: COLLECTION_HISTORY,
|
|
507
|
+
record,
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
uri: res.data.uri,
|
|
511
|
+
cid: res.data.cid,
|
|
512
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
279
516
|
/**
|
|
280
517
|
* Best-effort lookup of an existing rkey by listing the collection
|
|
281
518
|
* and finding the record whose `sim.uri` matches. Used by the Apply
|