pi-simocracy 0.6.2 → 0.7.0
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 +1 -1
- package/docs/SIM_AUTHORED_PROPOSALS.md +47 -12
- package/package.json +1 -1
- package/src/index.ts +131 -2
- package/src/simocracy.ts +28 -0
- package/src/writes.ts +81 -0
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ pi install npm:pi-simocracy
|
|
|
39
39
|
| `simocracy_chat` | Send one message to a sim and get a quoted reply, **without** changing the active session persona. Needs `OPENROUTER_API_KEY`. |
|
|
40
40
|
| `simocracy_lookup_record` | Fetch a sim / proposal / gathering / decision / comment by AT-URI or fuzzy name. Returns the record + comment subtree, with sim-authored comments flagged inline (🐾) so you can tell which opinions are human and which are sim. Use this before `simocracy_post_comment` to find the right `subjectUri`. |
|
|
41
41
|
| `simocracy_post_comment` | Post a comment on a record **as the loaded sim**. Writes the comment plus an `org.simocracy.history` sidecar that attributes it to the sim. Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_COMMENTS.md`](docs/SIM_AUTHORED_COMMENTS.md) for the design. |
|
|
42
|
-
| `simocracy_post_proposal` | Submit a new funding proposal (`org.hypercerts.claim.activity`) **as the loaded sim**. Writes the proposal
|
|
42
|
+
| `simocracy_post_proposal` | Submit a new funding proposal (`org.hypercerts.claim.activity`) **as the loaded sim**. Writes three records to the user's PDS: the proposal itself, an `org.simocracy.proposalContext` sidecar binding it to a parent gathering or FtC SF floor (required for visibility on `/proposals`), and an `org.simocracy.history` sidecar with `type: "proposal"`. You must pass exactly one of `gatheringUri` (an AT-URI to an `org.simocracy.gathering` — use `simocracy_lookup_record` to resolve a name) or `ftcSfFloor` (1–14). Optional itemized `budgetItems`, `workScope` tags, `contributors`, and an https `imageUri` (the default Simocracy banner is used otherwise — image upload from disk is intentionally not supported). Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_PROPOSALS.md`](docs/SIM_AUTHORED_PROPOSALS.md) for the design. |
|
|
43
43
|
| `simocracy_update_sim` | Rewrite the loaded sim's constitution (`shortDescription` + `description`) and/or speaking `style` and persist to your PDS. Requires `/sim login` + sim ownership. |
|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -12,7 +12,7 @@ subject collection.
|
|
|
12
12
|
## TL;DR
|
|
13
13
|
|
|
14
14
|
When pi submits a proposal on behalf of a loaded sim, it writes
|
|
15
|
-
**
|
|
15
|
+
**three** records to the user's PDS:
|
|
16
16
|
|
|
17
17
|
1. **`org.hypercerts.claim.activity`** — the proposal itself, in the
|
|
18
18
|
exact shape simocracy.org's `ProposalFormDialog` already writes
|
|
@@ -20,16 +20,27 @@ When pi submits a proposal on behalf of a loaded sim, it writes
|
|
|
20
20
|
`workScope` / `contributors` / `image`, `createdAt`). Old readers
|
|
21
21
|
(Hyperindexer, the existing webapp) see this as a regular
|
|
22
22
|
user-authored proposal — graceful degradation.
|
|
23
|
-
2. **`org.simocracy.
|
|
23
|
+
2. **`org.simocracy.proposalContext`** — sidecar binding the proposal
|
|
24
|
+
to its parent gathering (StrongRef to `org.simocracy.gathering`)
|
|
25
|
+
or to a Frontier Tower SF floor (`floorNumber` integer). Required
|
|
26
|
+
for visibility: post-Phase-5, simocracy.org's `/proposals` feed
|
|
27
|
+
sources its seed set from `org.simocracy.proposalContext` records,
|
|
28
|
+
not from a hashtag scan, so a proposal without this sidecar is
|
|
29
|
+
invisible. Lives in the proposer's PDS so the resolver's tier-1
|
|
30
|
+
(proposer) > tier-2 (facilitator backfill) precedence works.
|
|
31
|
+
3. **`org.simocracy.history`** — sidecar record with `type:
|
|
24
32
|
"proposal"`, `subjectUri` pointing at the proposal we just wrote,
|
|
25
33
|
`simUris[]` / `simNames[]` declaring which sim spoke. New `type`
|
|
26
34
|
value, but the lexicon's `type` field is free-form string and the
|
|
27
35
|
indexer already accepts new event types as they appear.
|
|
28
36
|
|
|
29
|
-
Renderers that understand the join (simocracy.org,
|
|
30
|
-
change
|
|
37
|
+
Renderers that understand the history join (simocracy.org, since
|
|
38
|
+
the planned change landed) display a sim badge on the proposal card;
|
|
31
39
|
renderers that don't keep showing it as a regular user proposal.
|
|
32
|
-
**Zero hypercerts lexicon changes
|
|
40
|
+
**Zero hypercerts lexicon changes.** One new Simocracy lexicon was
|
|
41
|
+
added for proposalContext (see `simocracy-v2/lexicons/org/simocracy/proposalContext.json`)
|
|
42
|
+
so every Simocracy-bound proposal carries a typed back-pointer to
|
|
43
|
+
its parent.
|
|
33
44
|
|
|
34
45
|
---
|
|
35
46
|
|
|
@@ -67,6 +78,17 @@ Use it as-is.
|
|
|
67
78
|
Implemented by `simocracy_post_proposal` in `src/index.ts`:
|
|
68
79
|
|
|
69
80
|
```ts
|
|
81
|
+
// Tool input — exactly one of gatheringUri / ftcSfFloor must be set.
|
|
82
|
+
// gatheringUri is fetched ahead of any writes so a typo or unreachable
|
|
83
|
+
// PDS surfaces *before* an orphaned proposal lands in the user's repo.
|
|
84
|
+
const gatheringRef =
|
|
85
|
+
gatheringUri
|
|
86
|
+
? await getRecordRefFromPds(parsed.did, "org.simocracy.gathering", parsed.rkey)
|
|
87
|
+
: undefined;
|
|
88
|
+
const resolvedContext = gatheringRef
|
|
89
|
+
? { kind: "gathering" as const, uri: gatheringRef.uri, cid: gatheringRef.cid }
|
|
90
|
+
: { kind: "ftc-sf" as const, floorNumber: ftcSfFloor };
|
|
91
|
+
|
|
70
92
|
// 1. The proposal — same shape as ProposalFormDialog writes today.
|
|
71
93
|
const proposal = await createProposal({
|
|
72
94
|
agent, did,
|
|
@@ -78,7 +100,17 @@ const proposal = await createProposal({
|
|
|
78
100
|
image: { $type: "org.hypercerts.defs#uri", uri: imageUri ?? DEFAULT_BANNER },
|
|
79
101
|
});
|
|
80
102
|
|
|
81
|
-
// 2. The
|
|
103
|
+
// 2. The parent-context sidecar — required for visibility on /proposals.
|
|
104
|
+
// A failure here doesn't roll back the proposal but is surfaced as
|
|
105
|
+
// a `contextSidecarWarning` so the LLM can retry just the sidecar.
|
|
106
|
+
await createProposalContext({
|
|
107
|
+
agent, did,
|
|
108
|
+
proposalUri: proposal.uri,
|
|
109
|
+
proposalCid: proposal.cid,
|
|
110
|
+
context: resolvedContext,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 3. The sim-attribution sidecar — required, since attribution is the whole
|
|
82
114
|
// point of this tool.
|
|
83
115
|
await createProposalHistory({
|
|
84
116
|
agent, did,
|
|
@@ -190,9 +222,12 @@ chat / hearing / sprocess events — no new indexer queries needed.
|
|
|
190
222
|
## Status
|
|
191
223
|
|
|
192
224
|
- ✅ Implemented in pi-simocracy `simocracy_post_proposal` (this repo)
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
The proposal + history
|
|
197
|
-
|
|
198
|
-
|
|
225
|
+
- ✅ Renderer changes shipped in simocracy-v2
|
|
226
|
+
- ✅ `org.simocracy.proposalContext` lexicon shipped (1 new Simocracy lexicon, 0 hypercerts changes)
|
|
227
|
+
|
|
228
|
+
The proposal + proposalContext + history triple is being written
|
|
229
|
+
today. Every pi-authored proposal carries both the sim badge
|
|
230
|
+
(history sidecar) and the parent-gathering binding
|
|
231
|
+
(proposalContext sidecar), so it surfaces correctly on
|
|
232
|
+
`simocracy.org/proposals` and under its parent gathering page — no
|
|
233
|
+
migration, no backfill needed for new submissions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-simocracy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Pi extension: load a Simocracy sim into your chat — see its pixel-art sprite render inline in the terminal and roleplay with it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "David Dao <david@gainforest.earth> (https://github.com/daviddao)",
|
package/src/index.ts
CHANGED
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
fetchSkillMd,
|
|
99
99
|
resolveHandle,
|
|
100
100
|
parseAtUri,
|
|
101
|
+
getRecordRefFromPds,
|
|
101
102
|
type AgentsRecord,
|
|
102
103
|
type SimMatch,
|
|
103
104
|
type StyleRecord,
|
|
@@ -129,6 +130,7 @@ import {
|
|
|
129
130
|
createComment,
|
|
130
131
|
createCommentHistory,
|
|
131
132
|
createProposal,
|
|
133
|
+
createProposalContext,
|
|
132
134
|
createProposalHistory,
|
|
133
135
|
createStyle,
|
|
134
136
|
findRkeyForSim,
|
|
@@ -137,6 +139,7 @@ import {
|
|
|
137
139
|
NotSimOwnerError,
|
|
138
140
|
updateAgents,
|
|
139
141
|
updateStyle,
|
|
142
|
+
type ProposalContextTarget,
|
|
140
143
|
} from "./writes.ts";
|
|
141
144
|
|
|
142
145
|
// ---------------------------------------------------------------------------
|
|
@@ -787,6 +790,25 @@ const PostProposalToolParams = Type.Object({
|
|
|
787
790
|
"https URL for the cover image. Defaults to the Simocracy banner if omitted. Image upload from disk is not supported — pass a URL or leave blank.",
|
|
788
791
|
}),
|
|
789
792
|
),
|
|
793
|
+
// Parent-context binding — effectively required for the proposal to
|
|
794
|
+
// be discoverable on simocracy.org (post-Phase-5 the read paths only
|
|
795
|
+
// surface proposals that have a sidecar). Exactly one of the two must
|
|
796
|
+
// be passed; the execute handler enforces that.
|
|
797
|
+
gatheringUri: Type.Optional(
|
|
798
|
+
Type.String({
|
|
799
|
+
description:
|
|
800
|
+
"AT-URI of the parent gathering this proposal belongs to (an `org.simocracy.gathering` record). Pass exactly one of `gatheringUri` or `ftcSfFloor`. Get the URI by calling `simocracy_lookup_record` first — the LLM should resolve a fuzzy gathering name to a real AT-URI before submitting. Without a parent context, the proposal will be invisible on simocracy.org's `/proposals` feed.",
|
|
801
|
+
pattern: "^at://[^/]+/org\\.simocracy\\.gathering/[^/]+$",
|
|
802
|
+
}),
|
|
803
|
+
),
|
|
804
|
+
ftcSfFloor: Type.Optional(
|
|
805
|
+
Type.Integer({
|
|
806
|
+
description:
|
|
807
|
+
"Frontier Tower SF floor number this proposal belongs to (1–14, with the static configuration in simocracy-v2's `lib/ftc-sf-data.ts`). Pass exactly one of `gatheringUri` or `ftcSfFloor`. Use this for proposals submitted to the Frontier Tower SF agentic funding experiment instead of a regular gathering.",
|
|
808
|
+
minimum: 1,
|
|
809
|
+
maximum: 14,
|
|
810
|
+
}),
|
|
811
|
+
),
|
|
790
812
|
});
|
|
791
813
|
|
|
792
814
|
const LookupRecordToolParams = Type.Object({
|
|
@@ -1492,11 +1514,21 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1492
1514
|
name: "simocracy_post_proposal",
|
|
1493
1515
|
label: "Submit a Simocracy proposal as the loaded sim",
|
|
1494
1516
|
description:
|
|
1495
|
-
"Submit a new funding proposal to Simocracy on behalf of the currently loaded sim. The sim should write the title + shortDescription + description in their own voice (their persona is already in your system prompt). Writes
|
|
1517
|
+
"Submit a new funding proposal to Simocracy on behalf of the currently loaded sim. The sim should write the title + shortDescription + description in their own voice (their persona is already in your system prompt). Writes THREE records to the user's PDS: (1) the proposal itself, (2) an `org.simocracy.proposalContext` sidecar binding the proposal to its parent gathering / FtC SF floor (required for the proposal to appear in simocracy.org's `/proposals` feed), and (3) an `org.simocracy.history` sidecar attributing the draft to the loaded sim. Use this when the user asks the sim to draft, propose, or submit a proposal — e.g. \"Mr Meow, propose a cat sanctuary\" or \"draft a proposal for solar panels\". You MUST pass exactly one of `gatheringUri` (to bind the proposal to a gathering) or `ftcSfFloor` (to bind to a Frontier Tower SF floor); call `simocracy_lookup_record` first to resolve a gathering name to its AT-URI. Pass `budgetItems` if a budget request was discussed; pass `workScope` for tag-style categorization; pass `contributors` for credited humans. Image is optional and URL-only (the default Simocracy banner is used otherwise). Requires /sim login + a loaded sim the user owns.",
|
|
1496
1518
|
parameters: PostProposalToolParams,
|
|
1497
1519
|
async execute(
|
|
1498
1520
|
_id,
|
|
1499
|
-
{
|
|
1521
|
+
{
|
|
1522
|
+
title,
|
|
1523
|
+
shortDescription,
|
|
1524
|
+
description,
|
|
1525
|
+
workScope,
|
|
1526
|
+
contributors,
|
|
1527
|
+
budgetItems,
|
|
1528
|
+
imageUri,
|
|
1529
|
+
gatheringUri,
|
|
1530
|
+
ftcSfFloor,
|
|
1531
|
+
},
|
|
1500
1532
|
) {
|
|
1501
1533
|
if (!loadedSim) {
|
|
1502
1534
|
throw new Error(
|
|
@@ -1520,6 +1552,64 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1520
1552
|
throw new Error(`ATProto auth failed: ${(err as Error).message}`);
|
|
1521
1553
|
}
|
|
1522
1554
|
|
|
1555
|
+
// Parent-context validation — exactly one of gatheringUri / ftcSfFloor.
|
|
1556
|
+
// Without one, the proposal is invisible on /proposals, so we require
|
|
1557
|
+
// it up-front rather than letting the LLM ship orphaned proposals.
|
|
1558
|
+
const hasGathering = gatheringUri !== undefined;
|
|
1559
|
+
const hasFloor = ftcSfFloor !== undefined;
|
|
1560
|
+
if (hasGathering && hasFloor) {
|
|
1561
|
+
throw new Error(
|
|
1562
|
+
"Pass exactly one of `gatheringUri` (for a gathering) or `ftcSfFloor` (for FtC SF), not both.",
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
if (!hasGathering && !hasFloor) {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
"A parent context is required: pass `gatheringUri` (an AT-URI to an `org.simocracy.gathering` — use `simocracy_lookup_record` to resolve a name) or `ftcSfFloor` (a Frontier Tower SF floor number 1–14). Without one, the proposal will be invisible on simocracy.org's /proposals feed.",
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Resolve gathering URI → StrongRef now (before any writes) so a
|
|
1572
|
+
// typo or unreachable PDS surfaces *before* we put a half-orphaned
|
|
1573
|
+
// proposal record into the user's repo.
|
|
1574
|
+
let resolvedContext: ProposalContextTarget;
|
|
1575
|
+
if (hasGathering) {
|
|
1576
|
+
let gatheringDid: string;
|
|
1577
|
+
let gatheringRkey: string;
|
|
1578
|
+
try {
|
|
1579
|
+
const parsed = parseAtUri(gatheringUri!);
|
|
1580
|
+
if (parsed.collection !== "org.simocracy.gathering") {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
`gatheringUri must point at an org.simocracy.gathering record (got ${parsed.collection}).`,
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
gatheringDid = parsed.did;
|
|
1586
|
+
gatheringRkey = parsed.rkey;
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
`gatheringUri is not a valid AT-URI: ${(err as Error).message}`,
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
let gatheringRef: { uri: string; cid: string };
|
|
1593
|
+
try {
|
|
1594
|
+
gatheringRef = await getRecordRefFromPds(
|
|
1595
|
+
gatheringDid,
|
|
1596
|
+
"org.simocracy.gathering",
|
|
1597
|
+
gatheringRkey,
|
|
1598
|
+
);
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
throw new Error(
|
|
1601
|
+
`Couldn't fetch gathering record from owner's PDS — verify the URI is correct and the gathering still exists. ${(err as Error).message}`,
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
resolvedContext = {
|
|
1605
|
+
kind: "gathering",
|
|
1606
|
+
uri: gatheringRef.uri,
|
|
1607
|
+
cid: gatheringRef.cid,
|
|
1608
|
+
};
|
|
1609
|
+
} else {
|
|
1610
|
+
resolvedContext = { kind: "ftc-sf", floorNumber: ftcSfFloor! };
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1523
1613
|
// Resolve the cover image — either an LLM-supplied https URL or
|
|
1524
1614
|
// the simocracy.org default banner. Reject non-https schemes
|
|
1525
1615
|
// (data:, javascript:, file://) defensively even though the only
|
|
@@ -1573,6 +1663,26 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1573
1663
|
throw new Error(`Proposal write failed: ${(err as Error).message}`);
|
|
1574
1664
|
}
|
|
1575
1665
|
|
|
1666
|
+
// Parent-context sidecar — written FIRST after the proposal so a
|
|
1667
|
+
// visibility failure is loud and immediate. If this fails the
|
|
1668
|
+
// proposal is already on the user's PDS but won't surface on
|
|
1669
|
+
// /proposals; we surface a warning so the LLM can retry just this
|
|
1670
|
+
// sidecar instead of re-submitting the whole proposal.
|
|
1671
|
+
let contextSidecarUri: string | undefined;
|
|
1672
|
+
let contextSidecarWarning: string | undefined;
|
|
1673
|
+
try {
|
|
1674
|
+
const ctxWrite = await createProposalContext({
|
|
1675
|
+
agent: pdsAgent,
|
|
1676
|
+
did: auth.did,
|
|
1677
|
+
proposalUri: proposal.uri,
|
|
1678
|
+
proposalCid: proposal.cid,
|
|
1679
|
+
context: resolvedContext,
|
|
1680
|
+
});
|
|
1681
|
+
contextSidecarUri = ctxWrite.uri;
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
contextSidecarWarning = `Parent-context sidecar failed: ${(err as Error).message}`;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1576
1686
|
let sidecarUri: string | undefined;
|
|
1577
1687
|
let sidecarWarning: string | undefined;
|
|
1578
1688
|
try {
|
|
@@ -1593,12 +1703,25 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1593
1703
|
sidecarWarning = `Sim-attribution sidecar failed: ${(err as Error).message}`;
|
|
1594
1704
|
}
|
|
1595
1705
|
|
|
1706
|
+
const parentLabel =
|
|
1707
|
+
resolvedContext.kind === "gathering"
|
|
1708
|
+
? `gathering ${resolvedContext.uri}`
|
|
1709
|
+
: `FtC SF floor ${resolvedContext.floorNumber}`;
|
|
1596
1710
|
const lines = [
|
|
1597
1711
|
`Submitted proposal as ${loadedSim.name}${loadedSim.handle ? ` (@${loadedSim.handle})` : ""}:`,
|
|
1598
1712
|
` title: ${title}`,
|
|
1599
1713
|
` proposal URI: ${proposal.uri}`,
|
|
1714
|
+
` parent: ${parentLabel}`,
|
|
1600
1715
|
` image: ${imageRef.uri}`,
|
|
1601
1716
|
];
|
|
1717
|
+
if (contextSidecarUri) {
|
|
1718
|
+
lines.push(` context: ${contextSidecarUri} (org.simocracy.proposalContext sidecar)`);
|
|
1719
|
+
} else if (contextSidecarWarning) {
|
|
1720
|
+
lines.push(` WARNING: ${contextSidecarWarning}`);
|
|
1721
|
+
lines.push(
|
|
1722
|
+
` The proposal is posted but will be invisible on /proposals until a proposalContext sidecar is written.`,
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1602
1725
|
if (sidecarUri) {
|
|
1603
1726
|
lines.push(` attribution: ${sidecarUri} (org.simocracy.history sidecar)`);
|
|
1604
1727
|
} else if (sidecarWarning) {
|
|
@@ -1621,6 +1744,12 @@ export default async function simocracy(pi: ExtensionAPI) {
|
|
|
1621
1744
|
budgetItemCount: budgetItems?.length ?? 0,
|
|
1622
1745
|
simUri: loadedSim.uri,
|
|
1623
1746
|
simName: loadedSim.name,
|
|
1747
|
+
parent:
|
|
1748
|
+
resolvedContext.kind === "gathering"
|
|
1749
|
+
? { kind: "gathering", uri: resolvedContext.uri, cid: resolvedContext.cid }
|
|
1750
|
+
: { kind: "ftc-sf", floorNumber: resolvedContext.floorNumber },
|
|
1751
|
+
contextSidecarUri,
|
|
1752
|
+
contextSidecarWarning,
|
|
1624
1753
|
sidecarUri,
|
|
1625
1754
|
sidecarWarning,
|
|
1626
1755
|
},
|
package/src/simocracy.ts
CHANGED
|
@@ -252,6 +252,34 @@ export async function getRecordFromPds<T>(did: string, collection: string, rkey:
|
|
|
252
252
|
return json.value as T;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Read a record's StrongRef ({uri, cid}) directly from the owner's PDS.
|
|
257
|
+
*
|
|
258
|
+
* Same wire call as `getRecordFromPds` but returns the metadata needed to
|
|
259
|
+
* reference the record from another record (e.g. an `org.simocracy.proposalContext`
|
|
260
|
+
* sidecar's `subject` or `context.gathering` field). The body is intentionally
|
|
261
|
+
* dropped — callers that need it should use `getRecordFromPds` instead.
|
|
262
|
+
*/
|
|
263
|
+
export async function getRecordRefFromPds(
|
|
264
|
+
did: string,
|
|
265
|
+
collection: string,
|
|
266
|
+
rkey: string,
|
|
267
|
+
): Promise<{ uri: string; cid: string }> {
|
|
268
|
+
const pds = await resolvePds(did);
|
|
269
|
+
const url =
|
|
270
|
+
`${pds.replace(/\/+$/, "")}/xrpc/com.atproto.repo.getRecord` +
|
|
271
|
+
`?repo=${encodeURIComponent(did)}` +
|
|
272
|
+
`&collection=${encodeURIComponent(collection)}` +
|
|
273
|
+
`&rkey=${encodeURIComponent(rkey)}`;
|
|
274
|
+
const res = await fetch(url);
|
|
275
|
+
if (!res.ok) throw new Error(`PDS getRecord failed: ${res.status}`);
|
|
276
|
+
const json = (await res.json()) as { uri?: string; cid?: string };
|
|
277
|
+
if (!json.uri || !json.cid) {
|
|
278
|
+
throw new Error(`PDS getRecord returned no uri/cid: ${url}`);
|
|
279
|
+
}
|
|
280
|
+
return { uri: json.uri, cid: json.cid };
|
|
281
|
+
}
|
|
282
|
+
|
|
255
283
|
/** List records by paging com.atproto.repo.listRecords on a PDS. */
|
|
256
284
|
export async function listRecordsFromPds<T>(did: string, collection: string): Promise<Array<{ uri: string; cid: string; value: T }>> {
|
|
257
285
|
const pds = await resolvePds(did);
|
package/src/writes.ts
CHANGED
|
@@ -130,6 +130,7 @@ const COLLECTION_STYLE = "org.simocracy.style";
|
|
|
130
130
|
const COLLECTION_COMMENT = "org.impactindexer.review.comment";
|
|
131
131
|
const COLLECTION_HISTORY = "org.simocracy.history";
|
|
132
132
|
const COLLECTION_PROPOSAL = "org.hypercerts.claim.activity";
|
|
133
|
+
const COLLECTION_PROPOSAL_CONTEXT = "org.simocracy.proposalContext";
|
|
133
134
|
|
|
134
135
|
/**
|
|
135
136
|
* Defense-in-depth: every write helper below verifies the target
|
|
@@ -455,6 +456,86 @@ export async function createProposal(opts: {
|
|
|
455
456
|
};
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Discriminated parent target for an `org.simocracy.proposalContext`
|
|
461
|
+
* sidecar. Mirrors the lexicon's `context` union (#gatheringContext
|
|
462
|
+
* vs #ftcSfContext); see `simocracy-v2/lexicons/org/simocracy/proposalContext.json`.
|
|
463
|
+
*/
|
|
464
|
+
export type ProposalContextTarget =
|
|
465
|
+
| { kind: "gathering"; uri: string; cid: string }
|
|
466
|
+
| { kind: "ftc-sf"; floorNumber: number };
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* POST `org.simocracy.proposalContext` (parent-context sidecar).
|
|
470
|
+
*
|
|
471
|
+
* Binds a proposal to its parent container — either an
|
|
472
|
+
* `org.simocracy.gathering` record (via StrongRef) or a static FtC SF
|
|
473
|
+
* floor number. Without this sidecar, simocracy.org's read paths
|
|
474
|
+
* (post-Phase-5) won't surface the proposal on `/proposals` or under
|
|
475
|
+
* the gathering / floor it belongs to. The sidecar is therefore
|
|
476
|
+
* effectively required for any proposal submitted via this tool.
|
|
477
|
+
*
|
|
478
|
+
* Same trust model as `org.simocracy.history` — lives in the
|
|
479
|
+
* proposer's own PDS so the resolver's tier-1 (proposer-PDS) >
|
|
480
|
+
* tier-2 (facilitator-PDS) precedence rule lets a backfill record
|
|
481
|
+
* be silently superseded if the proposer ever re-saves.
|
|
482
|
+
*
|
|
483
|
+
* No sim-ownership precondition: a proposal isn't sim-owned, and
|
|
484
|
+
* the sidecar references the proposal + parent gathering, not the
|
|
485
|
+
* sim. The OAuth precondition ($DID matches signed-in DID and the
|
|
486
|
+
* agent is authenticated) is enforced upstream by
|
|
487
|
+
* `assertCanWriteToSim` at the tool entry point.
|
|
488
|
+
*/
|
|
489
|
+
export async function createProposalContext(opts: {
|
|
490
|
+
agent: Agent;
|
|
491
|
+
did: string;
|
|
492
|
+
proposalUri: string;
|
|
493
|
+
proposalCid: string;
|
|
494
|
+
context: ProposalContextTarget;
|
|
495
|
+
}): Promise<{ uri: string; cid: string; rkey: string }> {
|
|
496
|
+
let context: Record<string, unknown>;
|
|
497
|
+
if (opts.context.kind === "gathering") {
|
|
498
|
+
if (!opts.context.uri || !opts.context.cid) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"Gathering parent context requires both uri and cid (a full StrongRef).",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
context = {
|
|
504
|
+
$type: `${COLLECTION_PROPOSAL_CONTEXT}#gatheringContext`,
|
|
505
|
+
gathering: { uri: opts.context.uri, cid: opts.context.cid },
|
|
506
|
+
};
|
|
507
|
+
} else {
|
|
508
|
+
if (
|
|
509
|
+
!Number.isInteger(opts.context.floorNumber) ||
|
|
510
|
+
opts.context.floorNumber < 1
|
|
511
|
+
) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`FtC SF floor number must be a positive integer (got ${opts.context.floorNumber}).`,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
context = {
|
|
517
|
+
$type: `${COLLECTION_PROPOSAL_CONTEXT}#ftcSfContext`,
|
|
518
|
+
floorNumber: opts.context.floorNumber,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const record = {
|
|
522
|
+
$type: COLLECTION_PROPOSAL_CONTEXT,
|
|
523
|
+
subject: { uri: opts.proposalUri, cid: opts.proposalCid },
|
|
524
|
+
context,
|
|
525
|
+
createdAt: new Date().toISOString(),
|
|
526
|
+
};
|
|
527
|
+
const res = await opts.agent.com.atproto.repo.createRecord({
|
|
528
|
+
repo: opts.did,
|
|
529
|
+
collection: COLLECTION_PROPOSAL_CONTEXT,
|
|
530
|
+
record,
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
uri: res.data.uri,
|
|
534
|
+
cid: res.data.cid,
|
|
535
|
+
rkey: res.data.uri.split("/").pop() ?? "",
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
458
539
|
/**
|
|
459
540
|
* Sim-attribution sidecar for a proposal.
|
|
460
541
|
*
|