@ixo/oracle-runtime-workers 0.32.0 → 0.33.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.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ CHAIN_TX_GROUP_MESSAGE_TYPES,
2
3
  CHAIN_TX_MAX_MESSAGES,
3
4
  CHAIN_TX_MESSAGE_TYPES,
4
5
  operationJsonSchema,
@@ -30,6 +31,8 @@ import { toolNotRunMessage } from '../../core/tool-not-run';
30
31
  import {
31
32
  CHAIN_TX_CATALOG,
32
33
  CHAIN_TX_GENERAL_RULES,
34
+ GROUP_ACTION_SCHEMA,
35
+ GROUP_ACTION_TYPES,
33
36
  chainTxMessageInput,
34
37
  hasUnsafeText,
35
38
  } from './chain-tx-catalog';
@@ -100,22 +103,38 @@ const memoSchema = z
100
103
  /**
101
104
  * A catalogue field reduced to its basic type: no descriptions, patterns or
102
105
  * limits (those stay in describe_chain_messages and the handler's check).
106
+ * A union of message types (a new group proposal's `actions`) is given as
107
+ * `typeUnion` gives the messages themselves: fields only for the types in
108
+ * `described`.
103
109
  */
104
- export function fieldsOnly(schema: z.core.$ZodType): z.ZodType {
110
+ export function fieldsOnly(
111
+ schema: z.core.$ZodType,
112
+ described?: ReadonlySet<string>,
113
+ ): z.ZodType {
105
114
  if (schema instanceof z.ZodObject)
106
115
  return z.strictObject(
107
116
  Object.fromEntries(
108
117
  Object.entries(schema.shape).map(([key, field]) => [
109
118
  key,
110
- fieldsOnly(field),
119
+ fieldsOnly(field, described),
111
120
  ]),
112
121
  ),
113
122
  );
114
123
  if (schema instanceof z.ZodOptional)
115
- return fieldsOnly(schema.unwrap()).optional();
124
+ return fieldsOnly(schema.unwrap(), described).optional();
116
125
  if (schema instanceof z.ZodNullable)
117
- return fieldsOnly(schema.unwrap()).nullable();
118
- if (schema instanceof z.ZodArray) return z.array(fieldsOnly(schema.element));
126
+ return fieldsOnly(schema.unwrap(), described).nullable();
127
+ if (schema instanceof z.ZodArray)
128
+ return z.array(fieldsOnly(schema.element, described));
129
+ if (schema instanceof z.ZodDiscriminatedUnion)
130
+ return typeUnion(
131
+ schema.options.map((option) => {
132
+ if (!(option instanceof z.ZodObject))
133
+ throw new Error('A message union holds a non-object branch');
134
+ return [messageTypeOf(option), option] as const;
135
+ }),
136
+ described,
137
+ );
119
138
  if (schema instanceof z.ZodString) return z.string();
120
139
  if (schema instanceof z.ZodNumber) return z.number();
121
140
  if (schema instanceof z.ZodBoolean) return z.boolean();
@@ -125,42 +144,51 @@ export function fieldsOnly(schema: z.core.$ZodType): z.ZodType {
125
144
  );
126
145
  }
127
146
 
147
+ /** The `type` a catalogue message schema is for (its `type` literal). */
148
+ function messageTypeOf(schema: z.ZodObject): string {
149
+ const type = schema.shape.type;
150
+ const [value] = type instanceof z.ZodLiteral ? [...type.values] : [];
151
+ if (typeof value !== 'string')
152
+ throw new Error('A catalogue message schema has no `type` literal');
153
+ return value;
154
+ }
155
+
128
156
  /**
129
157
  * One message type as the model is given it: its field names and basic
130
158
  * types, so a provider that holds a tool call to its schema sends every
131
159
  * field. Loose, so a field the type does not have reaches the handler,
132
160
  * which names it.
133
161
  */
134
- function messageFieldsOnly(schema: z.ZodObject) {
162
+ function messageFieldsOnly(
163
+ schema: z.ZodObject,
164
+ described?: ReadonlySet<string>,
165
+ ) {
135
166
  return z.looseObject(
136
167
  Object.fromEntries(
137
168
  Object.entries(schema.shape).map(([key, field]) => [
138
169
  key,
139
- fieldsOnly(field),
170
+ fieldsOnly(field, described),
140
171
  ]),
141
172
  ),
142
173
  );
143
174
  }
144
175
 
145
176
  /**
146
- * The schema the model is given: per type its fields (messageFieldsOnly)
147
- * when the type was described in this session (`described`; absent: every
148
- * type), and one open branch naming every other type (`type` one of them,
149
- * any other field allowed). A type the model has not read about costs its
150
- * name only; the request handler refuses it until it is described, and
151
- * the next model call is given its fields (`chainTxFieldsMiddleware`).
152
- * Each message is checked by the handler against its full catalogue
153
- * schema.
177
+ * Messages as the model is given them: per type its fields
178
+ * (messageFieldsOnly) when the type was described in this session
179
+ * (`described`; absent: every type), and one open branch naming every
180
+ * other type (`type` one of them, any other field allowed). A union inside
181
+ * a message (a new group proposal's `actions`) takes `nested` instead.
154
182
  */
155
- export function chainTransactionSchemaFor(
156
- catalog: Readonly<Record<string, { schema: z.ZodObject }>>,
183
+ function typeUnion(
184
+ schemas: readonly (readonly [string, z.ZodObject])[],
157
185
  described?: ReadonlySet<string>,
186
+ nested: ReadonlySet<string> | undefined = described,
158
187
  ) {
159
- const entries = Object.entries(catalog);
160
- const withFields = entries
188
+ const withFields = schemas
161
189
  .filter(([type]) => !described || described.has(type))
162
- .map(([, entry]) => messageFieldsOnly(entry.schema));
163
- const [firstUnread, ...unread] = entries
190
+ .map(([, schema]) => messageFieldsOnly(schema, nested));
191
+ const [firstUnread, ...unread] = schemas
164
192
  .filter(([type]) => described && !described.has(type))
165
193
  .map(([type]) => type);
166
194
  const branches =
@@ -172,9 +200,36 @@ export function chainTransactionSchemaFor(
172
200
  ];
173
201
  const [first, ...rest] = branches;
174
202
  if (!first) throw new Error('The chain transaction catalogue is empty');
203
+ return z.discriminatedUnion('type', [first, ...rest]);
204
+ }
205
+
206
+ /**
207
+ * The schema the model is given: the messages as `typeUnion` gives them,
208
+ * a new group proposal's actions the same way. A type the model has not
209
+ * read about costs its name only; the request handler refuses it until it
210
+ * is described, and the next model call is given its fields
211
+ * (`chainTxFieldsMiddleware`). Without a session store (`described`
212
+ * absent) every message type carries its fields, but a new proposal's
213
+ * actions are named only: the 14 action types' fields would repeat their
214
+ * top-level branches (26 228 characters instead of 20 950); the
215
+ * handler names any field it misses. Each message is checked by the
216
+ * handler against its full catalogue schema.
217
+ */
218
+ export function chainTransactionSchemaFor(
219
+ catalog: Readonly<Record<string, { schema: z.ZodObject }>>,
220
+ described?: ReadonlySet<string>,
221
+ ) {
175
222
  return z.strictObject({
176
223
  messages: z
177
- .array(z.discriminatedUnion('type', [first, ...rest]))
224
+ .array(
225
+ typeUnion(
226
+ Object.entries(catalog).map(
227
+ ([type, entry]) => [type, entry.schema] as const,
228
+ ),
229
+ described,
230
+ described ?? new Set<string>(),
231
+ ),
232
+ )
178
233
  .min(1)
179
234
  .max(CHAIN_TX_MAX_MESSAGES),
180
235
  memo: memoSchema,
@@ -649,7 +704,10 @@ async function requestChainTransaction(
649
704
  const unread = [
650
705
  ...new Set(
651
706
  parsed.data.messages
652
- .map(chainTxTypeOf)
707
+ .flatMap((message) => [
708
+ chainTxTypeOf(message),
709
+ ...groupActionTypesOf(message),
710
+ ])
653
711
  .filter(
654
712
  (type): type is ChainTxMessageType =>
655
713
  type !== null && !described.includes(type),
@@ -719,6 +777,19 @@ function strictChainTxMessages(
719
777
  ): { messages: ChainTxMessageInput[] } | { issues: string[] } {
720
778
  const checked: ChainTxMessageInput[] = [];
721
779
  const issues: string[] = [];
780
+ // A group step is a chain of reads in Auth Hub and names a new
781
+ // proposal's number on its receipt: it goes alone on its card (the
782
+ // protocol refuses `messages` otherwise).
783
+ if (
784
+ messages.length > 1 &&
785
+ messages.some((message) => {
786
+ const type = chainTxTypeOf(message);
787
+ return type !== null && GROUP_STEP_TYPES.includes(type);
788
+ })
789
+ )
790
+ issues.push(
791
+ 'messages: a group step (a group vote, carrying out or closing a group proposal, a new group proposal) goes alone on its card; prepare the other messages on another card',
792
+ );
722
793
  messages.forEach((message, index) => {
723
794
  const type = chainTxTypeOf(message);
724
795
  if (!type) {
@@ -739,6 +810,31 @@ function chainTxTypeOf(message: unknown): ChainTxMessageType | null {
739
810
  return CHAIN_TX_MESSAGE_TYPES.find((known) => known === type) ?? null;
740
811
  }
741
812
 
813
+ const GROUP_STEP_TYPES: readonly ChainTxMessageType[] =
814
+ CHAIN_TX_GROUP_MESSAGE_TYPES;
815
+
816
+ /**
817
+ * The action types a new group proposal holds (each a type a group may
818
+ * do): their rules are read like a top-level message's before the card.
819
+ * Anything else is left to the message's own check.
820
+ */
821
+ function groupActionTypesOf(message: unknown): ChainTxMessageType[] {
822
+ if (
823
+ chainTxTypeOf(message) !== 'daodao.proposal.create' ||
824
+ typeof message !== 'object' ||
825
+ message === null ||
826
+ !('actions' in message) ||
827
+ !Array.isArray(message.actions)
828
+ )
829
+ return [];
830
+ return message.actions
831
+ .map(chainTxTypeOf)
832
+ .filter(
833
+ (type): type is ChainTxMessageType =>
834
+ type !== null && GROUP_ACTION_TYPES.some((allowed) => allowed === type),
835
+ );
836
+ }
837
+
742
838
  function chainTxShapeRefusal(
743
839
  issues: readonly string[],
744
840
  ): HubActionRequestResult {
@@ -784,10 +880,11 @@ function issuesOf(
784
880
 
785
881
  /**
786
882
  * The catalogue types this account may use: the Hub capabilities'
787
- * `chainTx.messageTypes`. None when chain transactions cannot be used
883
+ * `chainTx.messageTypes`, which follow the protocol level the Hub speaks
884
+ * with this runtime (level 5: the four 0.6.0 types; 6: 33; 7: all 37, the
885
+ * four group steps included). None when chain transactions cannot be used
788
886
  * here: not a channel turn, Hub actions off, chain transactions off for the
789
- * account, a Hub at protocol level 5 (the 0.6.0 types only, as it lists
790
- * them), or capabilities that cannot be read (the owner object answers its
887
+ * account, or capabilities that cannot be read (the owner object answers its
791
888
  * last good capabilities while a read fails, and does not retry a failed
792
889
  * read for 30 s, so this never holds a turn for long).
793
890
  */
@@ -815,6 +912,19 @@ async function availableChainTxTypes(
815
912
  );
816
913
  }
817
914
 
915
+ /**
916
+ * A new group proposal's action as describe_chain_messages lists it: the
917
+ * types it may be, each with its own fields (described on its own), so one
918
+ * description does not repeat fourteen others.
919
+ */
920
+ const GROUP_ACTION_FIELDS = {
921
+ type: 'object',
922
+ description:
923
+ 'One message of the types listed in `type`, with exactly that type’s fields (describe_chain_messages that type); the group’s own account is its actor.',
924
+ properties: { type: { type: 'string', enum: [...GROUP_ACTION_TYPES] } },
925
+ required: ['type'],
926
+ };
927
+
818
928
  /** Each type's fields as JSON Schema, built once. */
819
929
  const catalogueFields = new Map<ChainTxMessageType, unknown>();
820
930
  function fieldsOf(type: ChainTxMessageType): unknown {
@@ -822,6 +932,14 @@ function fieldsOf(type: ChainTxMessageType): unknown {
822
932
  if (fields === undefined) {
823
933
  const { properties, required } = z.toJSONSchema(
824
934
  CHAIN_TX_CATALOG[type].schema,
935
+ {
936
+ override: ({ zodSchema, jsonSchema }) => {
937
+ if (zodSchema !== GROUP_ACTION_SCHEMA) return;
938
+ for (const key of Object.keys(jsonSchema))
939
+ Reflect.deleteProperty(jsonSchema, key);
940
+ Object.assign(jsonSchema, GROUP_ACTION_FIELDS);
941
+ },
942
+ },
825
943
  );
826
944
  fields = { properties, required };
827
945
  catalogueFields.set(type, fields);
@@ -914,6 +1032,45 @@ export function modelOutcome(
914
1032
  export const CANCELLED_MEANING =
915
1033
  'Withdrawn before anything was sent: nothing happened on chain. The person may have cancelled it, or Auth Hub withdrew it because something it depended on changed after the review; do not say the person cancelled it. If they still want it, offer to prepare a fresh one.';
916
1034
 
1035
+ /**
1036
+ * A carried-out group proposal whose actions failed: the transaction
1037
+ * succeeds (code 0) even then, and the network marks the proposal failed
1038
+ * for good, so the receipt's `groupActions` is the only sign.
1039
+ */
1040
+ export const GROUP_ACTIONS_FAILED_MEANING =
1041
+ 'The transaction went through, but the proposal’s actions failed: nothing in the proposal happened, the network has marked it failed for good and it can’t be carried out again. The network fee was used unless a fee grant covered it. Never say it is done; tell the person plainly.';
1042
+
1043
+ /** A carried-out group proposal whose transaction did not show whether its actions ran. */
1044
+ export const GROUP_ACTIONS_UNKNOWN_MEANING =
1045
+ 'The transaction went through, but Auth Hub could not tell from it whether the proposal’s actions happened. Check the proposal’s status with chain_lookup dao_proposals (executed: they happened; execution_failed: they did not) before saying anything about it; never say it is done until then.';
1046
+
1047
+ /** A new group proposal whose number Auth Hub could not read from the transaction. */
1048
+ export const PROPOSAL_NUMBER_UNKNOWN_MEANING =
1049
+ 'The new proposal was made, but Auth Hub could not read its number from the transaction. Find the new proposal with chain_lookup dao_proposals (the group’s newest) before naming it; never guess its number.';
1050
+
1051
+ /**
1052
+ * What an outcome means beyond its fields, for the model: a withdrawn card,
1053
+ * a carried-out proposal whose actions failed or may have, a new proposal
1054
+ * without its number. Undefined for anything else.
1055
+ */
1056
+ export function outcomeMeaning(
1057
+ outcome: ActionOutcomeV1 | null,
1058
+ ): string | undefined {
1059
+ if (!outcome) return undefined;
1060
+ if (outcome.status === 'cancelled') return CANCELLED_MEANING;
1061
+ const receipt = outcome.receipt;
1062
+ if (receipt?.kind !== 'ixo.chain.tx.v1') return undefined;
1063
+ for (const step of receipt.steps) {
1064
+ if (step.type === 'daodao.proposal.execute') {
1065
+ if (step.groupActions === 'failed') return GROUP_ACTIONS_FAILED_MEANING;
1066
+ if (step.groupActions === 'unknown') return GROUP_ACTIONS_UNKNOWN_MEANING;
1067
+ }
1068
+ if (step.type === 'daodao.proposal.create' && step.proposalId === null)
1069
+ return PROPOSAL_NUMBER_UNKNOWN_MEANING;
1070
+ }
1071
+ return undefined;
1072
+ }
1073
+
917
1074
  async function hubActionStatus(
918
1075
  input: unknown,
919
1076
  ctx: RuntimeContext,
@@ -935,15 +1092,14 @@ async function hubActionStatus(
935
1092
  status: 'unavailable',
936
1093
  reason: 'No Hub action with this id was prepared for this owner.',
937
1094
  };
1095
+ const meaning = outcomeMeaning(read.outcome);
938
1096
  return {
939
1097
  status: 'known',
940
1098
  reviewStatus: read.reviewStatus,
941
1099
  operationStatus: read.operationStatus,
942
1100
  outcome: modelOutcome(read.outcome),
943
1101
  ...(read.summary ? { summary: read.summary } : {}),
944
- ...(read.outcome?.status === 'cancelled'
945
- ? { meaning: CANCELLED_MEANING }
946
- : {}),
1102
+ ...(meaning ? { meaning } : {}),
947
1103
  };
948
1104
  } catch (error) {
949
1105
  // An Auth Hub that must be updated first: plain words, not a tool error.
@@ -1012,6 +1168,7 @@ const manifest: PluginManifest = {
1012
1168
  'The user asks to file or evaluate a claim, or to send coins or impact tokens on chain.',
1013
1169
  'The user asks to stake, unstake or move IXO, claim staking rewards, vote on a network proposal or retire impact tokens.',
1014
1170
  'The user asks to reserve or dispute a claim, create or change a claim collection, give or remove a permission, create or update an entity, or change the links of an identity.',
1171
+ 'The user asks to vote on a proposal of a group (DAO) they belong to, carry out or close a group proposal, or put a new proposal to a group.',
1015
1172
  'The user asks to review an exact private resource or to sign a statement.',
1016
1173
  'The user asks to connect Gmail or Calendar, send a reviewed email or add an event.',
1017
1174
  'The user asks for document access or to save a private note.',
@@ -1043,7 +1200,7 @@ export class HubActionsPlugin extends OraclePlugin {
1043
1200
  /**
1044
1201
  * request_chain_transaction is built per turn: its schema names only the
1045
1202
  * message types this account may use (Auth Hub capabilities, cached by
1046
- * the owner object), so 33 types do not reach a model that may use 4.
1203
+ * the owner object), so 37 types do not reach a model that may use 4.
1047
1204
  */
1048
1205
  override async getRequestTools(ctx: RuntimeContext): Promise<PluginTool[]> {
1049
1206
  const [available, described] = await Promise.all([
@@ -1093,7 +1250,7 @@ export class HubActionsPlugin extends OraclePlugin {
1093
1250
  {
1094
1251
  name: 'hub_action_status',
1095
1252
  description:
1096
- 'Read the verified status of a Hub action already prepared for this owner. Never executes. A `cancelled` outcome was withdrawn before anything was sent, by the person or by Auth Hub (`meaning` says so): never say the person cancelled it.',
1253
+ 'Read the verified status of a Hub action already prepared for this owner. Never executes. A `cancelled` outcome was withdrawn before anything was sent, by the person or by Auth Hub (`meaning` says so): never say the person cancelled it. When `meaning` is present, it says what the outcome means for the person (for example a carried-out group proposal whose actions failed): follow it.',
1097
1254
  schema: statusSchema,
1098
1255
  effect: 'read',
1099
1256
  handler: hubActionStatus,
@@ -56,6 +56,8 @@ const DAODAO_VOTES_FROM: Record<ChainCall['network'], number> = {
56
56
 
57
57
  /** Proposal modules read per DAO (single choice, multiple choice…). */
58
58
  const MODULES_PER_DAO = 3;
59
+ /** DAO DAO's single-choice proposal contract (the only kind cards act on). */
60
+ const SINGLE_CHOICE = 'dao_proposal_single';
59
61
  /** DAOs read per kind of membership when no DAO is named. */
60
62
  const DAOS_PER_MEMBERSHIP = 3;
61
63
 
@@ -85,6 +87,7 @@ const proposalNode = z.looseObject({
85
87
  expiration: z.unknown().optional(),
86
88
  totalPower: opt,
87
89
  votes: z.unknown().optional(),
90
+ allowRevoting: z.boolean().nullish(),
88
91
  daoVotesByProposalModuleAndProposalId: z
89
92
  .object({ nodes: z.array(voteNode) })
90
93
  .optional(),
@@ -92,6 +95,8 @@ const proposalNode = z.looseObject({
92
95
  const moduleNode = z.looseObject({
93
96
  address: str,
94
97
  prefix: opt,
98
+ status: opt,
99
+ moduleType: opt,
95
100
  daoProposalsByProposalModule: connection(proposalNode),
96
101
  });
97
102
  const coreNode = z.looseObject({
@@ -141,12 +146,24 @@ function rowsOf(
141
146
  for (const module of modules.nodes) {
142
147
  const proposals = module.daoProposalsByProposalModule;
143
148
  if (proposals.pageInfo?.hasNextPage) more = true;
149
+ // Cards work only on a single-choice list. On a list the group
150
+ // switched off, votes and closes still work, but a proposal with
151
+ // actions cannot be carried out (Auth Hub's `list_disabled`), and this
152
+ // index row does not say whether a proposal has actions, so `canRun` is
153
+ // left out there. Auth Hub checks the contracts again.
154
+ const multipleChoice =
155
+ !!module.moduleType && module.moduleType !== SINGLE_CHOICE;
156
+ const listDisabled = module.status === 'disabled';
144
157
  for (const proposal of proposals.nodes) {
145
158
  const id = String(proposal.id);
146
159
  const vote = proposal.daoVotesByProposalModuleAndProposalId?.nodes[0];
147
160
  const indexedFromStart =
148
161
  typeof proposal.startHeight === 'number' &&
149
162
  proposal.startHeight >= votesFrom;
163
+ const expiry = expirationOf(proposal.expiration, now);
164
+ // The index keeps `open` until a proposal is carried out or closed:
165
+ // once voting has ended it may have passed or not.
166
+ const ended = proposal.status === 'open' && expiry.votingEnded === true;
150
167
  rows.push({
151
168
  startHeight: proposal.startHeight ?? -1,
152
169
  id,
@@ -158,7 +175,23 @@ function rowsOf(
158
175
  proposalModule: idText(module.address),
159
176
  title: text(proposal.title),
160
177
  status: text(proposal.status, 40),
161
- ...expirationOf(proposal.expiration, now),
178
+ ...expiry,
179
+ allowRevoting:
180
+ typeof proposal.allowRevoting === 'boolean'
181
+ ? proposal.allowRevoting
182
+ : undefined,
183
+ canRun:
184
+ !multipleChoice &&
185
+ !listDisabled &&
186
+ (proposal.status === 'passed' || ended)
187
+ ? true
188
+ : undefined,
189
+ canClose:
190
+ !multipleChoice && (proposal.status === 'rejected' || ended)
191
+ ? true
192
+ : undefined,
193
+ multipleChoice: multipleChoice ? true : undefined,
194
+ listDisabled: listDisabled ? true : undefined,
162
195
  proposer: idText(proposal.proposer),
163
196
  tally: tallyOf(proposal.votes),
164
197
  totalPower: amountText(proposal.totalPower),
@@ -195,7 +228,7 @@ function coreFields(
195
228
  const votes = voter
196
229
  ? ` daoVotesByProposalModuleAndProposalId(filter: ${equalTo('voter', voter)}, first: 1) { nodes { vote power votedAt } }`
197
230
  : '';
198
- return `address name${described ? ' description' : ''} daoProposalModulesByDaoAddress(first: ${MODULES_PER_DAO}, orderBy: PRIMARY_KEY_ASC) { ${PAGE} nodes { address prefix daoProposalsByProposalModule(${filter}first: ${n}, orderBy: PRIMARY_KEY_DESC) { ${PAGE} nodes { id title status proposer startHeight expiration totalPower votes${votes} } } } }`;
231
+ return `address name${described ? ' description' : ''} daoProposalModulesByDaoAddress(first: ${MODULES_PER_DAO}, orderBy: PRIMARY_KEY_ASC) { ${PAGE} nodes { address prefix status moduleType daoProposalsByProposalModule(${filter}first: ${n}, orderBy: PRIMARY_KEY_DESC) { ${PAGE} nodes { id title status proposer startHeight expiration totalPower votes allowRevoting${votes} } } } }`;
199
232
  }
200
233
 
201
234
  /** Rows by start height, newest first (a proposal without one last), then by id. */
@@ -242,8 +275,10 @@ export const daoProposals = defineKind({
242
275
  'Each: dao, daoName, id and `label` (the DAO’s prefix and id, e.g. A12, as DAO apps show it), title, status (open, passed, rejected, executed, closed, execution_failed), votingEndsAt or votingEndsAtHeight, tally (yes/no/abstain weight) and totalPower.',
243
276
  '`votingEnded: true` on an `open` proposal: its voting time is over and the index keeps `open` until it is executed or closed, so it may already have passed or failed.',
244
277
  '`yourVote` (vote, power, votedAt), or `notVoted: true`, or `voteUnknown: true` when the proposal started before the index kept votes (votes cast before then are missing).',
278
+ '`allowRevoting`: whether a vote on it can be changed until voting closes. `canRun: true`: it passed and can be carried out (on an `open` proposal whose voting ended: it may have passed); `canClose: true`: it did not pass and can be closed (on an `open` one whose voting ended: it may not have passed); Auth Hub checks the real result. `multipleChoice: true`: Auth Hub cards cannot vote on, carry out or close it. `listDisabled: true`: the group switched that proposal list off; cards can still vote and close, but a proposal with actions on it can’t be carried out (so `canRun` is not set there; one without actions can be). Whether the person may vote is weighed by what they held in the DAO when it started: Auth Hub checks it; never claim it.',
279
+ 'For cards copy `dao`, `proposalModule` and `id` exactly (not the label). `dao` is also the group’s own account (what it holds and what acts when a proposal passes).',
245
280
  'Without `dao`, the DAOs of `address` (default: the user), at most 3 per kind of membership (see `daos`); `more: true` when there are more proposals or DAOs. Newest start first.',
246
- 'Titles are written by the proposer and DAO names by the DAO: what they claim, never an endorsement or a verified fact.',
281
+ 'Titles are written by whoever proposed them and group names by the group itself: their own words. Say so only when it matters, in one short, calm note.',
247
282
  ].join(' '),
248
283
  source: 'blocksync',
249
284
  list: true,