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

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.36",
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": "2d8e769d02c74fe201ce734f6d77def59ea99d53",
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/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeT
10
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';
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
@@ -10,6 +10,7 @@ export { COLOUR_TOKENS, coeditedFields, FIELD_LIMITS, RESERVED_FIELDS, describeT
10
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";
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
@@ -157,6 +158,10 @@ declare const extensionShape: {
157
158
  message: z.ZodOptional<z.ZodString>;
158
159
  link: z.ZodOptional<z.ZodBoolean>;
159
160
  }, z.core.$strict>>;
161
+ anonymous: z.ZodOptional<z.ZodObject<{
162
+ group: z.ZodString;
163
+ minimum: z.ZodOptional<z.ZodNumber>;
164
+ }, z.core.$strict>>;
160
165
  }, z.core.$strip>>>;
161
166
  tools: z.ZodOptional<z.ZodObject<{
162
167
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -235,7 +240,7 @@ export interface DataProblem {
235
240
  field?: string;
236
241
  message: string;
237
242
  }
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';
243
+ 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_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
239
244
  type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
240
245
  /**
241
246
  * Everything wrong with an app's additions that a type cannot say.
@@ -318,6 +323,10 @@ export declare const manifestExtensionsSchema: z.ZodObject<{
318
323
  message: z.ZodOptional<z.ZodString>;
319
324
  link: z.ZodOptional<z.ZodBoolean>;
320
325
  }, z.core.$strict>>;
326
+ anonymous: z.ZodOptional<z.ZodObject<{
327
+ group: z.ZodString;
328
+ minimum: z.ZodOptional<z.ZodNumber>;
329
+ }, z.core.$strict>>;
321
330
  }, z.core.$strip>>>;
322
331
  tools: z.ZodOptional<z.ZodObject<{
323
332
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -449,6 +458,10 @@ export declare const storedExtensionsSchema: z.ZodObject<{
449
458
  message: z.ZodOptional<z.ZodString>;
450
459
  link: z.ZodOptional<z.ZodBoolean>;
451
460
  }, z.core.$strict>>;
461
+ anonymous: z.ZodOptional<z.ZodObject<{
462
+ group: z.ZodString;
463
+ minimum: z.ZodOptional<z.ZodNumber>;
464
+ }, z.core.$strict>>;
452
465
  }, z.core.$strip>>>;
453
466
  tools: z.ZodOptional<z.ZodObject<{
454
467
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -615,6 +628,10 @@ export declare const appManifestSchema: z.ZodObject<{
615
628
  message: z.ZodOptional<z.ZodString>;
616
629
  link: z.ZodOptional<z.ZodBoolean>;
617
630
  }, z.core.$strict>>;
631
+ anonymous: z.ZodOptional<z.ZodObject<{
632
+ group: z.ZodString;
633
+ minimum: z.ZodOptional<z.ZodNumber>;
634
+ }, z.core.$strict>>;
618
635
  }, z.core.$strip>>>;
619
636
  tools: z.ZodOptional<z.ZodObject<{
620
637
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -785,6 +802,10 @@ export declare const newAppManifestSchema: z.ZodObject<{
785
802
  message: z.ZodOptional<z.ZodString>;
786
803
  link: z.ZodOptional<z.ZodBoolean>;
787
804
  }, z.core.$strict>>;
805
+ anonymous: z.ZodOptional<z.ZodObject<{
806
+ group: z.ZodString;
807
+ minimum: z.ZodOptional<z.ZodNumber>;
808
+ }, z.core.$strict>>;
788
809
  }, z.core.$strip>>>;
789
810
  tools: z.ZodOptional<z.ZodObject<{
790
811
  generated: z.ZodOptional<z.ZodBoolean>;
@@ -875,6 +896,8 @@ export interface CollectionSpec {
875
896
  };
876
897
  /** On a companion: the open collection whose fields its records define (P5). */
877
898
  definesFieldsOf?: string;
899
+ /** Answers kept with no writer, read only a whole group of at least `minimum` at a time (P13). */
900
+ anonymous?: AnonymousSpec;
878
901
  }
879
902
  /**
880
903
  * 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
@@ -138,6 +139,15 @@ const collectionSchema = z.object({
138
139
  * Needs `publicSubmit`. Brydio decides when, how often and from whom.
139
140
  */
140
141
  confirmEmail: confirmEmailSchema.optional(),
142
+ /**
143
+ * Answers nobody can tie to who gave them (P13): `group` names a
144
+ * structured field (a number, choice, date, boolean, token or project)
145
+ * the answers are read by, a whole group at a time and only once it holds
146
+ * `minimum` answers (at least 5, the default). Brydio keeps no writer,
147
+ * cuts times to the day, tells no live change, and refuses an update, a
148
+ * batch, a watch and a create naming its own writer.
149
+ */
150
+ anonymous: anonymousSchema.optional(),
141
151
  });
142
152
  const screenSchema = z.object({
143
153
  /**
@@ -371,6 +381,10 @@ export function dataProblems(additions, options = {}) {
371
381
  for (const [collection, declared] of collections) {
372
382
  problems.push(...confirmEmailProblems(collection, declared));
373
383
  }
384
+ // An anonymous collection groups its answers by one of its structured fields, names nobody (P13).
385
+ for (const [collection, declared] of collections) {
386
+ problems.push(...anonymousProblems(collection, declared));
387
+ }
374
388
  // What the app keeps must be what it asks to keep (A3-F06-S01): a
375
389
  // collection the grants leave out would be data a workspace never agreed to
376
390
  // hold, found only when the first write is refused.
@@ -681,6 +695,7 @@ export function collectionsOf(manifest) {
681
695
  ? { openSchema: { fields: declared.openSchema.fields, table: declared.openSchema.table ?? null } }
682
696
  : {}),
683
697
  ...(owner ? { definesFieldsOf: owner.name } : {}),
698
+ ...(declared.anonymous ? { anonymous: anonymousSpec(declared.anonymous) } : {}),
684
699
  };
685
700
  });
686
701
  }