@brydio/manifest 0.1.0-alpha.35 → 0.1.0-alpha.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brydio/manifest",
3
- "version": "0.1.0-alpha.35",
3
+ "version": "0.1.0-alpha.37",
4
4
  "description": "The shape of a Brydio app's manifest, its collections' schemas, and the checks that say what is wrong with one.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "url": "https://github.com/nakel-ola/brydio-sdk",
11
11
  "directory": "packages/manifest"
12
12
  },
13
- "gitHead": "5bd1e984602ccea955d5755e613fe773ae4899ca",
13
+ "gitHead": "c0364642e34a602b75477af366aa61582e5598f7",
14
14
  "exports": {
15
15
  ".": {
16
16
  "types": "./src/index.d.ts",
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * An anonymous collection (P13): answers nobody can tie back to the person
4
+ * who gave them. Mirrors Brydio's `apps/api/src/apps/manifest/anonymous.ts`;
5
+ * the two change together.
6
+ *
7
+ * An app only declares it: which structured field groups the answers (a
8
+ * form, a survey round: a number, a choice, a date), and how many answers a
9
+ * group must hold before any of them is read. Brydio does the rest in the store, never the app: no writer kept
10
+ * on the record, times cut to the day, no live change told, no update, a
11
+ * create naming its own writer refused, and reads only of a whole group of
12
+ * at least `minimum` answers.
13
+ */
14
+ export declare const ANONYMOUS_LIMITS: {
15
+ /** The fewest answers a group, or a narrowed read of one, may be read at. */
16
+ readonly minimum: 5;
17
+ /** The most an app may ask for. */
18
+ readonly maximum: 1000;
19
+ };
20
+ /** What the store writes as `created_by` on every anonymous record. */
21
+ export declare const ANONYMOUS_WRITER = "anonymous";
22
+ export declare const anonymousSchema: z.ZodObject<{
23
+ group: z.ZodString;
24
+ minimum: z.ZodOptional<z.ZodNumber>;
25
+ }, z.core.$strict>;
26
+ export type AnonymousDeclared = z.infer<typeof anonymousSchema>;
27
+ /** As the host reads it, on `CollectionSpec.anonymous`. */
28
+ export interface AnonymousSpec {
29
+ group: string;
30
+ minimum: number;
31
+ }
32
+ export type AnonymousProblemCode = 'data_anonymous_group' | 'data_anonymous_member' | 'data_anonymous_minimum' | 'data_anonymous_coedit';
33
+ export declare function anonymousProblems(collection: string, declared: {
34
+ schema: Record<string, unknown>;
35
+ anonymous?: AnonymousDeclared;
36
+ }): {
37
+ code: AnonymousProblemCode;
38
+ collection: string;
39
+ field?: string;
40
+ message: string;
41
+ }[];
42
+ export declare function anonymousSpec(declared: AnonymousDeclared): AnonymousSpec;
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod';
2
+ import { FIELD_LIMITS, isStructured, parseFieldType, } from "./field-types.js";
3
+ /**
4
+ * An anonymous collection (P13): answers nobody can tie back to the person
5
+ * who gave them. Mirrors Brydio's `apps/api/src/apps/manifest/anonymous.ts`;
6
+ * the two change together.
7
+ *
8
+ * An app only declares it: which structured field groups the answers (a
9
+ * form, a survey round: a number, a choice, a date), and how many answers a
10
+ * group must hold before any of them is read. Brydio does the rest in the store, never the app: no writer kept
11
+ * on the record, times cut to the day, no live change told, no update, a
12
+ * create naming its own writer refused, and reads only of a whole group of
13
+ * at least `minimum` answers.
14
+ */
15
+ export const ANONYMOUS_LIMITS = {
16
+ /** The fewest answers a group, or a narrowed read of one, may be read at. */
17
+ minimum: 5,
18
+ /** The most an app may ask for. */
19
+ maximum: 1000,
20
+ };
21
+ /** What the store writes as `created_by` on every anonymous record. */
22
+ export const ANONYMOUS_WRITER = 'anonymous';
23
+ export const anonymousSchema = z
24
+ .object({
25
+ /** A structured field of the same collection, never a member: which group an answer belongs to. */
26
+ group: z.string().min(1).max(FIELD_LIMITS.nameChars),
27
+ /** At least 5, the default. */
28
+ minimum: z.number().int().max(ANONYMOUS_LIMITS.maximum).optional(),
29
+ })
30
+ .strict();
31
+ export function anonymousProblems(collection, declared) {
32
+ const anonymous = declared.anonymous;
33
+ if (!anonymous)
34
+ return [];
35
+ const problems = [];
36
+ const types = new Map();
37
+ for (const [field, raw] of Object.entries(declared.schema)) {
38
+ try {
39
+ types.set(field, parseFieldType(raw));
40
+ }
41
+ catch {
42
+ // Refused as a field type already.
43
+ }
44
+ }
45
+ // Kept in plain, so a read can name its group; words never are.
46
+ const group = types.get(anonymous.group);
47
+ if (!group || !isStructured(group) || group.kind === 'member') {
48
+ problems.push({
49
+ code: 'data_anonymous_group',
50
+ collection,
51
+ field: anonymous.group,
52
+ message: `${collection}.anonymous.group must name one of its structured fields (a number, choice, date, boolean, token or project); ${anonymous.group} is not.`,
53
+ });
54
+ }
55
+ if (anonymous.minimum !== undefined && anonymous.minimum < ANONYMOUS_LIMITS.minimum) {
56
+ problems.push({
57
+ code: 'data_anonymous_minimum',
58
+ collection,
59
+ message: `${collection}.anonymous.minimum is at least ${ANONYMOUS_LIMITS.minimum}: fewer answers than that can be told apart.`,
60
+ });
61
+ }
62
+ for (const [field, type] of types) {
63
+ // A member field names a person on the record, which is what this is for not doing.
64
+ if (type.kind === 'member') {
65
+ problems.push({
66
+ code: 'data_anonymous_member',
67
+ collection,
68
+ field,
69
+ message: `${collection} is anonymous, so it can't name a member: ${field} would.`,
70
+ });
71
+ }
72
+ // Writing together shows who is typing, and changes a record after it is given.
73
+ if (type.coedit) {
74
+ problems.push({
75
+ code: 'data_anonymous_coedit',
76
+ collection,
77
+ field,
78
+ message: `${collection} is anonymous, so its answers can't be written together: ${field} would be.`,
79
+ });
80
+ }
81
+ }
82
+ return problems;
83
+ }
84
+ export function anonymousSpec(declared) {
85
+ return { group: declared.group, minimum: declared.minimum ?? ANONYMOUS_LIMITS.minimum };
86
+ }
package/src/grants.d.ts CHANGED
@@ -6,8 +6,16 @@
6
6
  * `apps/api/src/apps/manifest/grants.ts`, and the `grant_unknown` sentence in
7
7
  * `apps/api/src/apps/publishing/app-publish.service.ts`.
8
8
  */
9
- /** The host grants with a name of their own. A connection's is `connection:<name>`. */
10
- export declare const HOST_CAPABILITIES: readonly ["navigate", "message", "members", "projects", "files", "chats", "model", "secrets", "notify", "approvals"];
9
+ /**
10
+ * The host grants with a name of their own. A connection's is `connection:<name>`.
11
+ *
12
+ * `chats` and `chat` differ: `chats` is the typed Brydio API's reach into the
13
+ * person's assistant conversations; `chat` is a handler's `chat.post`, the
14
+ * app posting one of its cards into a workspace room (FO03). `directory` is
15
+ * a handler's `directory.*` and a screen's `useProfile` (FO02); `webhooks`
16
+ * is a handler's `webhooks.send` (FO07).
17
+ */
18
+ export declare const HOST_CAPABILITIES: readonly ["navigate", "message", "members", "projects", "files", "chats", "model", "secrets", "notify", "approvals", "chat", "directory", "webhooks"];
11
19
  /** The older name for `HOST_CAPABILITIES`, kept for what already imports it. */
12
20
  export declare const KNOWN_HOST_GRANTS: readonly string[];
13
21
  /** True for a host grant Brydio knows how to honour: a named capability, or one connection. */
package/src/grants.js CHANGED
@@ -7,8 +7,30 @@
7
7
  * `apps/api/src/apps/publishing/app-publish.service.ts`.
8
8
  */
9
9
  import { BUNDLE_MANIFEST } from "./bundle.js";
10
- /** The host grants with a name of their own. A connection's is `connection:<name>`. */
11
- export const HOST_CAPABILITIES = ['navigate', 'message', 'members', 'projects', 'files', 'chats', 'model', 'secrets', 'notify', 'approvals'];
10
+ /**
11
+ * The host grants with a name of their own. A connection's is `connection:<name>`.
12
+ *
13
+ * `chats` and `chat` differ: `chats` is the typed Brydio API's reach into the
14
+ * person's assistant conversations; `chat` is a handler's `chat.post`, the
15
+ * app posting one of its cards into a workspace room (FO03). `directory` is
16
+ * a handler's `directory.*` and a screen's `useProfile` (FO02); `webhooks`
17
+ * is a handler's `webhooks.send` (FO07).
18
+ */
19
+ export const HOST_CAPABILITIES = [
20
+ 'navigate',
21
+ 'message',
22
+ 'members',
23
+ 'projects',
24
+ 'files',
25
+ 'chats',
26
+ 'model',
27
+ 'secrets',
28
+ 'notify',
29
+ 'approvals',
30
+ 'chat',
31
+ 'directory',
32
+ 'webhooks',
33
+ ];
12
34
  /** The older name for `HOST_CAPABILITIES`, kept for what already imports it. */
13
35
  export const KNOWN_HOST_GRANTS = HOST_CAPABILITIES;
14
36
  const CONNECTION = /^connection:[a-z0-9][a-z0-9_-]{0,59}$/;
package/src/index.d.ts CHANGED
@@ -7,9 +7,10 @@ export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, br
7
7
  export { BRAND_FIELDS, BRAND_LIMITS, brandFilePath, brandPathsOf, brandProblems, brandShapeOf, isBrandPath, pngVerdict, svgColours, toolsProblem, type BrandField, type RequirementCode, type RequirementFiles, type RequirementProblem, } from './requirements.js';
8
8
  export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes, bundleHash, brandOf, bundleProblem, codeOf, isBundlePath, isScriptPath, sizeOf, type BundleFiles, type BundleProblem, type BundleProblemCode, } from './bundle.js';
9
9
  export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeType, isCoedited, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, type DocumentOf, type FieldKind, type FieldType, type FieldValue, type FieldsOf, } from './field-types.js';
10
- export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, type AppManifestWithData, type CollectionSpec, type DataProblem, type DataProblemCode, type DataProblemOptions, type HomeCardSize, type ManifestExtensions, type SecretSpec, } from './schema.js';
10
+ export { appManifestSchema, appToolPrefix, collectionOf, crossAppTool, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, type AppManifestWithData, type CollectionSpec, type DataProblem, type DataProblemCode, type DataProblemOptions, type HomeCardSize, type ManifestExtensions, type SecretSpec, } from './schema.js';
11
11
  export { OPEN_LIMITS, OPEN_TYPES, choicesProblem, definitionOf, definitionTypeProblem, describeField, fieldTypeOf, hasChoices, isPlainOpenType, keyFromName, keyProblem, missingRequired, moveValue, openSchemaProblems, openSpec, openValueProblem, type Moved, type OpenField, type OpenFieldChange, type OpenFieldValue, type OpenProblem, type OpenProblemCode, type OpenSchemaFlag, type OpenType, } from './open-schema.js';
12
12
  export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, type ConfirmEmailDeclared, type ConfirmEmailSpec, type ConfirmProblemCode, } from './confirm-email.js';
13
+ export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, type AnonymousDeclared, type AnonymousProblemCode, type AnonymousSpec, } from './anonymous.js';
13
14
  export { DOCUMENT_LIMITS } from './document-limits.js';
14
15
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
15
16
  export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, type Migration, type MigrationCode, type MigrationProblem, type MigrationStep, type SchemaChange, } from './migrations.js';
package/src/index.js CHANGED
@@ -7,9 +7,10 @@ export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, br
7
7
  export { BRAND_FIELDS, BRAND_LIMITS, brandFilePath, brandPathsOf, brandProblems, brandShapeOf, isBrandPath, pngVerdict, svgColours, toolsProblem, } from "./requirements.js";
8
8
  export { BUNDLE_MANIFEST, BUNDLE_MAX_BYTES, BUNDLE_PATH_MAX_CHARS, bundleBytes, bundleHash, brandOf, bundleProblem, codeOf, isBundlePath, isScriptPath, sizeOf, } from "./bundle.js";
9
9
  export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeType, isCoedited, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, } from "./field-types.js";
10
- export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, } from "./schema.js";
10
+ export { appManifestSchema, appToolPrefix, collectionOf, crossAppTool, collectionsOf, dataProblems, effectivePlacementKey, HOME_CARD_SIZES, labelOf, manifestExtensionsSchema, newAppManifestSchema, SIZED_PLACEMENT_KINDS, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, } from "./schema.js";
11
11
  export { OPEN_LIMITS, OPEN_TYPES, choicesProblem, definitionOf, definitionTypeProblem, describeField, fieldTypeOf, hasChoices, isPlainOpenType, keyFromName, keyProblem, missingRequired, moveValue, openSchemaProblems, openSpec, openValueProblem, } from "./open-schema.js";
12
12
  export { CONFIRM_LIMITS, confirmEmailProblems, confirmEmailSpec, } from "./confirm-email.js";
13
+ export { ANONYMOUS_LIMITS, ANONYMOUS_WRITER, anonymousProblems, anonymousSpec, } from "./anonymous.js";
13
14
  export { DOCUMENT_LIMITS } from "./document-limits.js";
14
15
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
15
16
  export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
@@ -115,7 +115,7 @@ export type SchemaChange = {
115
115
  * Hodler's `diffManifests` to show, and none of them moves a record.
116
116
  */
117
117
  export declare function diffSchemas(from: unknown, to: unknown): SchemaChange[];
118
- export type MigrationCode = 'migration_step_invalid' | 'migration_step_pointless' | 'migration_default_missing' | 'migration_default_invalid' | 'migration_unexplained' | 'migration_type_changed' | 'migration_value_removed';
118
+ export type MigrationCode = 'migration_step_invalid' | 'migration_step_pointless' | 'migration_default_missing' | 'migration_default_invalid' | 'migration_unexplained' | 'migration_type_changed' | 'migration_value_removed' | 'migration_anonymous_changed';
119
119
  export interface MigrationProblem {
120
120
  code: MigrationCode;
121
121
  collection: string;
package/src/migrations.js CHANGED
@@ -181,6 +181,35 @@ export function migrationProblems(from, to, steps) {
181
181
  break;
182
182
  }
183
183
  }
184
+ problems.push(...anonymityChanges(from, to));
185
+ return problems;
186
+ }
187
+ /**
188
+ * A kept collection that becomes anonymous, stops being, or moves its group
189
+ * (P13): no step can say what its records become. Records kept before kept
190
+ * their writers and times, and records kept since were read only by group.
191
+ */
192
+ function anonymityChanges(from, to) {
193
+ const before = record(record(from).data);
194
+ const after = record(record(to).data);
195
+ const problems = [];
196
+ for (const [name, declared] of Object.entries(after)) {
197
+ if (!(name in before))
198
+ continue;
199
+ const was = record(record(before[name]).anonymous).group;
200
+ const now = record(record(declared).anonymous).group;
201
+ if (was === now)
202
+ continue;
203
+ problems.push({
204
+ code: 'migration_anonymous_changed',
205
+ collection: name,
206
+ message: was === undefined
207
+ ? `${name} already keeps records with their writers, so it can't become anonymous; keep anonymous answers in a new collection.`
208
+ : now === undefined
209
+ ? `${name} keeps anonymous answers, so it stays anonymous.`
210
+ : `${name}'s anonymous answers stay grouped by ${String(was)}.`,
211
+ });
212
+ }
184
213
  return problems;
185
214
  }
186
215
  /** The old schema with the steps applied, and every step that could not apply or changed nothing. */
package/src/schema.d.ts CHANGED
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { FieldTypeInvalid, type FieldType } from './field-types.js';
3
3
  import { type OpenProblemCode } from './open-schema.js';
4
4
  import { type ConfirmProblemCode } from './confirm-email.js';
5
+ import { type AnonymousProblemCode, type AnonymousSpec } from './anonymous.js';
5
6
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, type FieldKind, type FieldType, } from './field-types.js';
6
7
  /**
7
8
  * What an app adds to `.brydio/app.json` to be more than a bundle of servers
@@ -101,6 +102,7 @@ declare const extensionShape: {
101
102
  "project-sidebar": "project-sidebar";
102
103
  "workspace-sidebar": "workspace-sidebar";
103
104
  "public-page": "public-page";
105
+ "chat-card": "chat-card";
104
106
  }>;
105
107
  screen: z.ZodString;
106
108
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -157,6 +159,10 @@ declare const extensionShape: {
157
159
  message: z.ZodOptional<z.ZodString>;
158
160
  link: z.ZodOptional<z.ZodBoolean>;
159
161
  }, z.core.$strict>>;
162
+ anonymous: z.ZodOptional<z.ZodObject<{
163
+ group: z.ZodString;
164
+ minimum: z.ZodOptional<z.ZodNumber>;
165
+ }, z.core.$strict>>;
160
166
  }, z.core.$strip>>>;
161
167
  tools: z.ZodOptional<z.ZodObject<{
162
168
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -235,7 +241,7 @@ export interface DataProblem {
235
241
  field?: string;
236
242
  message: string;
237
243
  }
238
- export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | 'data_too_many_collections' | 'data_too_many_fields' | 'data_collection_name_format' | 'data_field_name_format' | 'data_field_reserved' | 'data_label_format' | 'data_label_taken' | 'data_project_field_twice' | 'data_search_unknown_field' | 'data_search_not_text' | 'grant_collection_missing' | 'grant_tool_missing' | 'custom_name_taken' | 'custom_collection_unknown' | 'custom_input_invalid' | 'placement_key_taken' | 'placement_screen_unknown' | 'placement_home_sizes' | 'placement_sizes_not_home' | 'placement_widget_sizes' | 'placement_tab_retired' | 'placement_public_shape' | 'placement_public_nothing' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
244
+ export type DataProblemCode = FieldTypeInvalid['code'] | OpenProblemCode | ConfirmProblemCode | AnonymousProblemCode | 'data_too_many_collections' | 'data_too_many_fields' | 'data_collection_name_format' | 'data_field_name_format' | 'data_field_reserved' | 'data_label_format' | 'data_label_taken' | 'data_project_field_twice' | 'data_search_unknown_field' | 'data_search_not_text' | 'grant_collection_missing' | 'grant_tool_missing' | 'custom_name_taken' | 'custom_collection_unknown' | 'custom_input_invalid' | 'placement_key_taken' | 'placement_screen_unknown' | 'placement_home_sizes' | 'placement_sizes_not_home' | 'placement_widget_sizes' | 'placement_tab_retired' | 'placement_public_shape' | 'placement_public_nothing' | 'placement_chat_card_shape' | 'grant_tool_cross_app_self' | 'placement_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
239
245
  type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
240
246
  /**
241
247
  * Everything wrong with an app's additions that a type cannot say.
@@ -251,6 +257,17 @@ export interface DataProblemOptions {
251
257
  retired?: boolean;
252
258
  }
253
259
  export declare function dataProblems(additions: Additions, options?: DataProblemOptions): DataProblem[];
260
+ /**
261
+ * Another installed app's tool, as `grants.tools` names it (FO07): the name
262
+ * the assistant knows it by, `<slug>__<tool>`. Null for anything else.
263
+ * A handler calls it with `tools.call('<slug>__<tool>', input)`.
264
+ */
265
+ export declare function crossAppTool(name: string): {
266
+ app: string;
267
+ tool: string;
268
+ } | null;
269
+ /** An app's name as the front of its tools' assistant names: `issue-tracker` → `issue_tracker`. */
270
+ export declare const appToolPrefix: (name: string) => string;
254
271
  /** The additions alone, for code that has the rest of the manifest already. */
255
272
  export declare const manifestExtensionsSchema: z.ZodObject<{
256
273
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -262,6 +279,7 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
262
279
  "project-sidebar": "project-sidebar";
263
280
  "workspace-sidebar": "workspace-sidebar";
264
281
  "public-page": "public-page";
282
+ "chat-card": "chat-card";
265
283
  }>;
266
284
  screen: z.ZodString;
267
285
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -318,6 +336,10 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
318
336
  message: z.ZodOptional<z.ZodString>;
319
337
  link: z.ZodOptional<z.ZodBoolean>;
320
338
  }, z.core.$strict>>;
339
+ anonymous: z.ZodOptional<z.ZodObject<{
340
+ group: z.ZodString;
341
+ minimum: z.ZodOptional<z.ZodNumber>;
342
+ }, z.core.$strict>>;
321
343
  }, z.core.$strip>>>;
322
344
  tools: z.ZodOptional<z.ZodObject<{
323
345
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -393,6 +415,7 @@ export declare const storedExtensionsSchema: z.ZodObject<{
393
415
  "project-sidebar": "project-sidebar";
394
416
  "workspace-sidebar": "workspace-sidebar";
395
417
  "public-page": "public-page";
418
+ "chat-card": "chat-card";
396
419
  }>;
397
420
  screen: z.ZodString;
398
421
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -449,6 +472,10 @@ export declare const storedExtensionsSchema: z.ZodObject<{
449
472
  message: z.ZodOptional<z.ZodString>;
450
473
  link: z.ZodOptional<z.ZodBoolean>;
451
474
  }, z.core.$strict>>;
475
+ anonymous: z.ZodOptional<z.ZodObject<{
476
+ group: z.ZodString;
477
+ minimum: z.ZodOptional<z.ZodNumber>;
478
+ }, z.core.$strict>>;
452
479
  }, z.core.$strip>>>;
453
480
  tools: z.ZodOptional<z.ZodObject<{
454
481
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -559,6 +586,7 @@ export declare const appManifestSchema: z.ZodObject<{
559
586
  "project-sidebar": "project-sidebar";
560
587
  "workspace-sidebar": "workspace-sidebar";
561
588
  "public-page": "public-page";
589
+ "chat-card": "chat-card";
562
590
  }>;
563
591
  screen: z.ZodString;
564
592
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -615,6 +643,10 @@ export declare const appManifestSchema: z.ZodObject<{
615
643
  message: z.ZodOptional<z.ZodString>;
616
644
  link: z.ZodOptional<z.ZodBoolean>;
617
645
  }, z.core.$strict>>;
646
+ anonymous: z.ZodOptional<z.ZodObject<{
647
+ group: z.ZodString;
648
+ minimum: z.ZodOptional<z.ZodNumber>;
649
+ }, z.core.$strict>>;
618
650
  }, z.core.$strip>>>;
619
651
  tools: z.ZodOptional<z.ZodObject<{
620
652
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -729,6 +761,7 @@ export declare const newAppManifestSchema: z.ZodObject<{
729
761
  "project-sidebar": "project-sidebar";
730
762
  "workspace-sidebar": "workspace-sidebar";
731
763
  "public-page": "public-page";
764
+ "chat-card": "chat-card";
732
765
  }>;
733
766
  screen: z.ZodString;
734
767
  sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -785,6 +818,10 @@ export declare const newAppManifestSchema: z.ZodObject<{
785
818
  message: z.ZodOptional<z.ZodString>;
786
819
  link: z.ZodOptional<z.ZodBoolean>;
787
820
  }, z.core.$strict>>;
821
+ anonymous: z.ZodOptional<z.ZodObject<{
822
+ group: z.ZodString;
823
+ minimum: z.ZodOptional<z.ZodNumber>;
824
+ }, z.core.$strict>>;
788
825
  }, z.core.$strip>>>;
789
826
  tools: z.ZodOptional<z.ZodObject<{
790
827
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -875,6 +912,8 @@ export interface CollectionSpec {
875
912
  };
876
913
  /** On a companion: the open collection whose fields its records define (P5). */
877
914
  definesFieldsOf?: string;
915
+ /** Answers kept with no writer, read only a whole group of at least `minimum` at a time (P13). */
916
+ anonymous?: AnonymousSpec;
878
917
  }
879
918
  /**
880
919
  * The singular a collection's tools are named with: the manifest's, else the
package/src/schema.js CHANGED
@@ -4,6 +4,7 @@ import { COLLECTION_NAME, FIELD_LIMITS, FIELD_NAME, FieldTypeInvalid, isSearchab
4
4
  import { migrationsSchema } from "./migrations.js";
5
5
  import { openSchemaProblems } from "./open-schema.js";
6
6
  import { confirmEmailProblems, confirmEmailSchema } from "./confirm-email.js";
7
+ import { anonymousProblems, anonymousSchema, anonymousSpec } from "./anonymous.js";
7
8
  export { COLOUR_TOKENS, FIELD_LIMITS, parseFieldType, RESERVED_FIELDS, structuredFields, } from "./field-types.js";
8
9
  /**
9
10
  * What an app adds to `.brydio/app.json` to be more than a bundle of servers
@@ -71,8 +72,12 @@ const placementSchema = z.object({
71
72
  * `children` or `settings` (`placement_public_shape`), and it reaches only
72
73
  * collections marked `publicRead` or `publicSubmit` and tools marked
73
74
  * `public`.
75
+ * `chat-card`: a screen drawn inside a chat message the app posted with
76
+ * a handler's `chat.post` (FO03). Nobody places it by hand, so it has no
77
+ * `sizes`, `children` or `settings` (`placement_chat_card_shape`); its
78
+ * screen sees `placement.kind === 'chat-card'` and the card's `route`.
74
79
  */
75
- kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home', 'public-page']),
80
+ kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home', 'public-page', 'chat-card']),
76
81
  screen: z.string().min(1).max(FIELD_LIMITS.nameChars),
77
82
  /** For `home` and `project-widget` only, and required there: the sizes the screen draws well at. */
78
83
  sizes: z.array(z.enum(HOME_CARD_SIZES)).max(3).optional(),
@@ -138,6 +143,15 @@ const collectionSchema = z.object({
138
143
  * Needs `publicSubmit`. Brydio decides when, how often and from whom.
139
144
  */
140
145
  confirmEmail: confirmEmailSchema.optional(),
146
+ /**
147
+ * Answers nobody can tie to who gave them (P13): `group` names a
148
+ * structured field (a number, choice, date, boolean, token or project)
149
+ * the answers are read by, a whole group at a time and only once it holds
150
+ * `minimum` answers (at least 5, the default). Brydio keeps no writer,
151
+ * cuts times to the day, tells no live change, and refuses an update, a
152
+ * batch, a watch and a create naming its own writer.
153
+ */
154
+ anonymous: anonymousSchema.optional(),
141
155
  });
142
156
  const screenSchema = z.object({
143
157
  /**
@@ -371,6 +385,10 @@ export function dataProblems(additions, options = {}) {
371
385
  for (const [collection, declared] of collections) {
372
386
  problems.push(...confirmEmailProblems(collection, declared));
373
387
  }
388
+ // An anonymous collection groups its answers by one of its structured fields, names nobody (P13).
389
+ for (const [collection, declared] of collections) {
390
+ problems.push(...anonymousProblems(collection, declared));
391
+ }
374
392
  // What the app keeps must be what it asks to keep (A3-F06-S01): a
375
393
  // collection the grants leave out would be data a workspace never agreed to
376
394
  // hold, found only when the first write is refused.
@@ -440,6 +458,14 @@ export function dataProblems(additions, options = {}) {
440
458
  message: `The public page "${placement.screen}" can have no children or settings: it is one screen at its own address.`,
441
459
  });
442
460
  }
461
+ // A chat card is drawn at the message's own size, in a message nobody
462
+ // places by hand (FO03): nothing to size, configure or list.
463
+ if (placement.kind === 'chat-card' && (placement.sizes?.length || placement.children || placement.settings)) {
464
+ problems.push({
465
+ code: 'placement_chat_card_shape',
466
+ message: `The chat card "${placement.screen}" cannot have sizes, children or settings.`,
467
+ });
468
+ }
443
469
  }
444
470
  // A public page with nothing public could show a visitor nothing at all.
445
471
  if ((additions.placements ?? []).some(placement => placement.kind === 'public-page')) {
@@ -537,6 +563,20 @@ function customToolProblems(additions, options) {
537
563
  }));
538
564
  const seen = new Set();
539
565
  const granted = additions.grants?.tools ?? [];
566
+ // Another app's tool, asked for by its assistant name `<slug>__<tool>`
567
+ // (FO07): never an unknown custom tool of this app, but naming the app
568
+ // itself would be a way round its own grants.
569
+ const name = additions.name;
570
+ const own = typeof name === 'string' ? appToolPrefix(name) : null;
571
+ for (const grant of granted) {
572
+ const qualified = crossAppTool(grant);
573
+ if (qualified && own !== null && qualified.app === own) {
574
+ problems.push({
575
+ code: 'grant_tool_cross_app_self',
576
+ message: `${grant} names this app's own tool: ask for ${qualified.tool} instead.`,
577
+ });
578
+ }
579
+ }
540
580
  for (const tool of custom) {
541
581
  if (generated.has(tool.name) || seen.has(tool.name)) {
542
582
  problems.push({
@@ -573,6 +613,22 @@ function customToolProblems(additions, options) {
573
613
  }
574
614
  return problems;
575
615
  }
616
+ /**
617
+ * Another installed app's tool, as `grants.tools` names it (FO07): the name
618
+ * the assistant knows it by, `<slug>__<tool>`. Null for anything else.
619
+ * A handler calls it with `tools.call('<slug>__<tool>', input)`.
620
+ */
621
+ export function crossAppTool(name) {
622
+ const at = name.indexOf('__');
623
+ if (at <= 0 || at + 2 >= name.length)
624
+ return null;
625
+ return { app: name.slice(0, at), tool: name.slice(at + 2) };
626
+ }
627
+ /** An app's name as the front of its tools' assistant names: `issue-tracker` → `issue_tracker`. */
628
+ export const appToolPrefix = (name) => name
629
+ .replace(/[^A-Za-z0-9_]+/g, '_')
630
+ .replace(/_+/g, '_')
631
+ .replace(/^_+|_+$/g, '');
576
632
  /**
577
633
  * Each secret declared once, and asked for (ADR-A24): an app that declares
578
634
  * secrets without the `secrets` host grant could never read one.
@@ -605,7 +661,7 @@ const refuse = (additions, ctx, options = {}) => {
605
661
  ctx.addIssue({
606
662
  code: 'custom',
607
663
  message: problem.message,
608
- path: problem.code === 'grant_tool_missing'
664
+ path: problem.code === 'grant_tool_missing' || problem.code === 'grant_tool_cross_app_self'
609
665
  ? ['grants', 'tools']
610
666
  : problem.code === 'grant_secrets_missing'
611
667
  ? ['grants', 'host']
@@ -681,6 +737,7 @@ export function collectionsOf(manifest) {
681
737
  ? { openSchema: { fields: declared.openSchema.fields, table: declared.openSchema.table ?? null } }
682
738
  : {}),
683
739
  ...(owner ? { definesFieldsOf: owner.name } : {}),
740
+ ...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
684
741
  };
685
742
  });
686
743
  }