@brydio/manifest 0.1.0-alpha.27 → 0.1.0-alpha.29

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 CHANGED
@@ -11,7 +11,7 @@ export const manifest = defineManifest({
11
11
  name: 'checklist',
12
12
  version: '0.1.0',
13
13
  screens: { home: { entry: 'screens/home.js' } },
14
- placements: [{ kind: 'project-tab', screen: 'home', label: 'Checklist' }],
14
+ placements: [{ kind: 'project-widget', screen: 'home', label: 'Checklist', sizes: ['medium', 'large'] }],
15
15
  });
16
16
  ```
17
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brydio/manifest",
3
- "version": "0.1.0-alpha.27",
3
+ "version": "0.1.0-alpha.29",
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": "99e44a5818de8cbe2021c951af544e12eca039c2",
13
+ "gitHead": "353f8a61e02fa687ca7eb2e7a48d5a73d66d08a0",
14
14
  "exports": {
15
15
  ".": {
16
16
  "types": "./src/index.d.ts",
package/src/define.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * name: 'issues',
12
12
  * version: '0.1.0',
13
13
  * screens: { board: { entry: 'screens/board.js' } },
14
- * placements: [{ kind: 'project-tab', screen: 'bord' }], // error: "bord" is not a screen
14
+ * placements: [{ kind: 'project-widget', screen: 'bord', sizes: ['large'] }], // error: "bord" is not a screen
15
15
  * });
16
16
  * ```
17
17
  */
package/src/define.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * name: 'issues',
12
12
  * version: '0.1.0',
13
13
  * screens: { board: { entry: 'screens/board.js' } },
14
- * placements: [{ kind: 'project-tab', screen: 'bord' }], // error: "bord" is not a screen
14
+ * placements: [{ kind: 'project-widget', screen: 'bord', sizes: ['large'] }], // error: "bord" is not a screen
15
15
  * });
16
16
  * ```
17
17
  */
@@ -12,7 +12,7 @@ import { type ZodType } from 'zod';
12
12
  * and everything downstream — the store, the generated tools, the screen's
13
13
  * form — reads that shape, never the manifest's spelling.
14
14
  */
15
- export type FieldKind = 'string' | 'text' | 'enum' | 'member' | 'project' | 'date' | 'number' | 'boolean' | 'string[]' | 'token';
15
+ export type FieldKind = 'string' | 'text' | 'enum' | 'member' | 'project' | 'date' | 'number' | 'boolean' | 'string[]' | 'token' | 'canvas';
16
16
  export interface FieldType {
17
17
  kind: FieldKind;
18
18
  /** Written with a trailing `?`: may be left out on create. */
@@ -27,6 +27,13 @@ export interface FieldType {
27
27
  * changes, and a value with no label reads as itself.
28
28
  */
29
29
  labels?: Readonly<Record<string, string>>;
30
+ /**
31
+ * Written by several people at once (`tasks/docs-wiki` DW01): a `text`
32
+ * field drawn with `bry-rich-text bind` is one fragment of a shared Yjs
33
+ * document the host keeps. Such a field leaves the record's `version`
34
+ * check, and a tool's change to it merges into the live text.
35
+ */
36
+ coedit?: boolean;
30
37
  }
31
38
  /**
32
39
  * The bounds a schema and a record are held to (A3-F01-S03).
@@ -74,8 +81,8 @@ export declare const FIELD_NAME: RegExp;
74
81
  export declare const COLLECTION_NAME: RegExp;
75
82
  /** A field type the manifest wrote that Brydio does not have, or a bad list. */
76
83
  export declare class FieldTypeInvalid extends Error {
77
- readonly code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown';
78
- constructor(code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown', message: string);
84
+ readonly code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown' | 'data_coedit_not_text';
85
+ constructor(code: 'data_field_type_unknown' | 'data_enum_empty' | 'data_enum_too_many' | 'data_enum_value_invalid' | 'data_enum_duplicate' | 'data_default_not_allowed' | 'data_default_on_required' | 'data_default_invalid' | 'data_labels_not_allowed' | 'data_label_invalid' | 'data_label_unknown_value' | 'data_field_key_unknown' | 'data_coedit_not_text', message: string);
79
86
  }
80
87
  /**
81
88
  * One field's manifest spelling, as a type.
@@ -87,6 +94,16 @@ export declare class FieldTypeInvalid extends Error {
87
94
  export declare function parseFieldType(raw: unknown): FieldType;
88
95
  /** How long a choice value's label may be. */
89
96
  export declare const LABEL_CHARS = 60;
97
+ /** True for a field several people write at once, through the co-editing layer. */
98
+ export declare const isCoedited: (type: FieldType) => boolean;
99
+ /**
100
+ * What a drawing's field holds in the record (WB01): a summary the host writes
101
+ * from the live board, so a list, search and the assistant can say what is on
102
+ * it. The drawing itself lives only in the co-edited document.
103
+ */
104
+ export declare const CANVAS_SUMMARY_CHARS = 10000;
105
+ /** A collection's co-edited fields, by name. */
106
+ export declare const coeditedFields: (fields: Readonly<Record<string, FieldType>>) => string[];
90
107
  /** How a choice's value reads to a person: its label, or the value itself. */
91
108
  export declare const labelOfValue: (type: FieldType, value: string) => string;
92
109
  /** True for a type whose value lands in the plain `fields` column. */
@@ -127,7 +144,10 @@ type Optional<T> = T extends `${string}?` ? true : T extends {
127
144
  /** The TypeScript type of one field's value, from its manifest spelling. */
128
145
  export type FieldValue<T> = T extends {
129
146
  type: infer U;
130
- } ? FieldValue<U> : T extends readonly (infer V)[] ? V : T extends 'number' | 'number?' ? number : T extends 'boolean' | 'boolean?' ? boolean : T extends 'string[]' | 'string[]?' ? string[] : T extends 'token' | 'token?' ? (typeof COLOUR_TOKENS)[number] : T extends string ? string : never;
147
+ } ? FieldValue<U> : T extends readonly (infer V)[] ? V : T extends 'number' | 'number?' ? number : T extends 'boolean' | 'boolean?' ? boolean : T extends 'string[]' | 'string[]?' ? string[] : T extends 'token' | 'token?' ? (typeof COLOUR_TOKENS)[number] : T extends 'canvas' | 'canvas?' ? {
148
+ elements: number;
149
+ text: string;
150
+ } : T extends string ? string : never;
131
151
  type Simplify<T> = {
132
152
  [K in keyof T]: T[K];
133
153
  } & {};
@@ -68,7 +68,7 @@ const STRUCTURED = new Set([
68
68
  ]);
69
69
  /** Only the words in these may one day be searched (A3-F01-S02). */
70
70
  const SEARCHABLE = new Set(['string', 'text', 'string[]']);
71
- const SCALARS = new Set(['string', 'text', 'member', 'project', 'date', 'number', 'boolean', 'token']);
71
+ const SCALARS = new Set(['string', 'text', 'member', 'project', 'date', 'number', 'boolean', 'token', 'canvas']);
72
72
  /** A field type the manifest wrote that Brydio does not have, or a bad list. */
73
73
  export class FieldTypeInvalid extends Error {
74
74
  code;
@@ -99,18 +99,24 @@ export function parseFieldType(raw) {
99
99
  return { kind: 'string[]', optional };
100
100
  if (name === 'token')
101
101
  return { kind: 'token', optional, values: COLOUR_TOKENS };
102
+ // A drawing is always co-edited and never required: it starts empty (WB01).
103
+ if (name === 'canvas')
104
+ return { kind: 'canvas', optional: true, coedit: true };
102
105
  if (SCALARS.has(name))
103
106
  return { kind: name, optional };
104
107
  throw new FieldTypeInvalid('data_field_type_unknown', `"${raw}" is not a field type.`);
105
108
  }
106
- const FIELD_KEYS = new Set(['type', 'optional', 'default', 'labels']);
109
+ const FIELD_KEYS = new Set(['type', 'optional', 'default', 'labels', 'coedit']);
107
110
  /** How long a choice value's label may be. */
108
111
  export const LABEL_CHARS = 60;
109
112
  /** The object form: the short form under `type`, and what it may add. */
110
113
  function parseFieldObject(raw) {
111
114
  const unknown = Object.keys(raw).find(key => !FIELD_KEYS.has(key));
112
115
  if (unknown) {
113
- throw new FieldTypeInvalid('data_field_key_unknown', `"${unknown}" is not something a field may say; use type, optional, default and labels.`);
116
+ throw new FieldTypeInvalid('data_field_key_unknown', `"${unknown}" is not something a field may say; use type, optional, default, labels and coedit.`);
117
+ }
118
+ if (raw.coedit !== undefined && typeof raw.coedit !== 'boolean') {
119
+ throw new FieldTypeInvalid('data_field_key_unknown', '"coedit" is true or false.');
114
120
  }
115
121
  if (typeof raw.type !== 'string' && !Array.isArray(raw.type)) {
116
122
  throw new FieldTypeInvalid('data_field_type_unknown', 'A field written as an object names its type under "type".');
@@ -119,7 +125,12 @@ function parseFieldObject(raw) {
119
125
  throw new FieldTypeInvalid('data_field_key_unknown', '"optional" is true or false.');
120
126
  }
121
127
  const inner = parseFieldType(raw.type);
122
- const type = { ...inner, optional: inner.optional || raw.optional === true, ...labelsOf(inner, raw) };
128
+ const type = {
129
+ ...inner,
130
+ optional: inner.optional || raw.optional === true,
131
+ ...labelsOf(inner, raw),
132
+ ...coeditOf(inner, raw),
133
+ };
123
134
  if (!('default' in raw))
124
135
  return type;
125
136
  const value = raw.default;
@@ -157,6 +168,27 @@ function labelsOf(type, raw) {
157
168
  }
158
169
  return { labels: { ...labels } };
159
170
  }
171
+ /** `coedit`, on long text only: nothing else is typed into by several people at once. */
172
+ function coeditOf(type, raw) {
173
+ if (raw.coedit !== true)
174
+ return {};
175
+ if (type.kind !== 'text' && type.kind !== 'canvas') {
176
+ throw new FieldTypeInvalid('data_coedit_not_text', `Only a text or canvas field may be co-edited, not a ${type.kind}.`);
177
+ }
178
+ return { coedit: true };
179
+ }
180
+ /** True for a field several people write at once, through the co-editing layer. */
181
+ export const isCoedited = (type) => type.kind === 'canvas' || (type.kind === 'text' && type.coedit === true);
182
+ /**
183
+ * What a drawing's field holds in the record (WB01): a summary the host writes
184
+ * from the live board, so a list, search and the assistant can say what is on
185
+ * it. The drawing itself lives only in the co-edited document.
186
+ */
187
+ export const CANVAS_SUMMARY_CHARS = 10_000;
188
+ /** A collection's co-edited fields, by name. */
189
+ export const coeditedFields = (fields) => Object.entries(fields)
190
+ .filter(([, type]) => isCoedited(type))
191
+ .map(([field]) => field);
160
192
  /** How a choice's value reads to a person: its label, or the value itself. */
161
193
  export const labelOfValue = (type, value) => type.labels?.[value] ?? value;
162
194
  function parseEnumeration(values) {
@@ -228,6 +260,8 @@ export function valueSchema(type) {
228
260
  return z.boolean();
229
261
  case 'string[]':
230
262
  return z.array(z.string().max(FIELD_LIMITS.stringChars)).max(FIELD_LIMITS.listEntries);
263
+ case 'canvas':
264
+ return z.object({ elements: z.number().int().min(0), text: z.string().max(CANVAS_SUMMARY_CHARS) }).strict();
231
265
  }
232
266
  }
233
267
  /**
@@ -263,6 +297,8 @@ export function valueProblem(field, type, value) {
263
297
  return Array.isArray(value) && value.length > FIELD_LIMITS.listEntries
264
298
  ? `${field} may hold at most ${FIELD_LIMITS.listEntries} entries.`
265
299
  : `${field} must be a list of short texts, each at most ${FIELD_LIMITS.stringChars.toLocaleString('en-GB')} characters.`;
300
+ case 'canvas':
301
+ return `${field} is a drawing: it is changed on its board, never written.`;
266
302
  }
267
303
  }
268
304
  /** A type in a few words, for a tool's description: every allowed value named. */
@@ -288,6 +324,8 @@ export function describeType(type) {
288
324
  return 'true or false';
289
325
  case 'string[]':
290
326
  return 'a list of short texts';
327
+ case 'canvas':
328
+ return 'a drawing, read only: how many shapes it has and the words on it';
291
329
  }
292
330
  }
293
331
  /** ", read as \"todo\" is To do, \"done\" is Done", for a choice with labels. */
package/src/index.d.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, brandImagesSchema } from './base.js';
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
- export { COLOUR_TOKENS, FIELD_LIMITS, RESERVED_FIELDS, describeType, 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, labelOf, manifestExtensionsSchema, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, type AppManifestWithData, type CollectionSpec, type DataProblem, type DataProblemCode, type ManifestExtensions, type SecretSpec, } from './schema.js';
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';
11
11
  export { DOCUMENT_LIMITS } from './document-limits.js';
12
12
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from './grants.js';
13
13
  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
@@ -6,8 +6,8 @@
6
6
  export { APP_NAME_FORMAT, MANIFEST_LIMITS, SEMVER_FORMAT, baseManifestSchema, brandImagesSchema } from "./base.js";
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
- export { COLOUR_TOKENS, FIELD_LIMITS, RESERVED_FIELDS, describeType, isSearchable, isSortable, isStructured, parseFieldType, structuredFields, valueProblem, valueSchema, FieldTypeInvalid, } from "./field-types.js";
10
- export { appManifestSchema, collectionOf, collectionsOf, dataProblems, effectivePlacementKey, labelOf, manifestExtensionsSchema, MAX_APP_SECRETS, MAX_SECRET_CHARS, PLACEMENT_KEY, SECRET_NAME, storedExtensionsSchema, } from "./schema.js";
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";
11
11
  export { DOCUMENT_LIMITS } from "./document-limits.js";
12
12
  export { HOST_CAPABILITIES, KNOWN_HOST_GRANTS, isHostCapability, unknownHostGrant } from "./grants.js";
13
13
  export { diffSchemas, migrationProblems, migrationSchema, migrationStepSchema, migrationsSchema, publishedMigrationProblems, schemaOf, } from "./migrations.js";
package/src/schema.d.ts CHANGED
@@ -33,6 +33,11 @@ export type PlacementSettingSpec = z.infer<typeof placementSettingSchema>;
33
33
  * the limit is for the sidebar's width and a person's patience.
34
34
  */
35
35
  export declare const MAX_FOLDER_DEPTH = 4;
36
+ /** The sizes a Home card or a project widget can be drawn at (CC11-F10). */
37
+ export declare const HOME_CARD_SIZES: readonly ["small", "medium", "large"];
38
+ export type HomeCardSize = (typeof HOME_CARD_SIZES)[number];
39
+ /** The kinds drawn as a card, which say the sizes they draw well at. */
40
+ export declare const SIZED_PLACEMENT_KINDS: readonly ["home", "project-widget"];
36
41
  /** A stable identity for one independently addable placement offering. */
37
42
  export declare const PLACEMENT_KEY: RegExp;
38
43
  export declare function effectivePlacementKey(one: {
@@ -87,12 +92,18 @@ declare const extensionShape: {
87
92
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
88
93
  key: z.ZodOptional<z.ZodString>;
89
94
  kind: z.ZodEnum<{
95
+ home: "home";
96
+ "project-widget": "project-widget";
90
97
  "project-tab": "project-tab";
91
98
  "project-sidebar": "project-sidebar";
92
99
  "workspace-sidebar": "workspace-sidebar";
93
- home: "home";
94
100
  }>;
95
101
  screen: z.ZodString;
102
+ sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
103
+ small: "small";
104
+ medium: "medium";
105
+ large: "large";
106
+ }>>>;
96
107
  label: z.ZodOptional<z.ZodString>;
97
108
  icon: z.ZodOptional<z.ZodString>;
98
109
  settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -207,7 +218,7 @@ export interface DataProblem {
207
218
  field?: string;
208
219
  message: string;
209
220
  }
210
- export type DataProblemCode = FieldTypeInvalid['code'] | '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_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
221
+ export type DataProblemCode = FieldTypeInvalid['code'] | '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_children_too_deep' | 'placement_create_tool_unknown' | 'placement_create_not_write' | 'secret_name_taken' | 'grant_secrets_missing';
211
222
  type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
212
223
  /**
213
224
  * Everything wrong with an app's additions that a type cannot say.
@@ -216,20 +227,30 @@ type Additions = z.infer<z.ZodObject<typeof extensionShape>>;
216
227
  * report; the zod schemas below run it too, so a parse never accepts what
217
228
  * this refuses.
218
229
  */
219
- export declare function dataProblems(additions: Additions, options?: {
230
+ export interface DataProblemOptions {
231
+ /** Off where the grants are the install's record, not the manifest's. */
220
232
  grants?: boolean;
221
- }): DataProblem[];
233
+ /** On for a manifest being written now: refuses kinds that are retired, such as `project-tab`. */
234
+ retired?: boolean;
235
+ }
236
+ export declare function dataProblems(additions: Additions, options?: DataProblemOptions): DataProblem[];
222
237
  /** The additions alone, for code that has the rest of the manifest already. */
223
238
  export declare const manifestExtensionsSchema: z.ZodObject<{
224
239
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
225
240
  key: z.ZodOptional<z.ZodString>;
226
241
  kind: z.ZodEnum<{
242
+ home: "home";
243
+ "project-widget": "project-widget";
227
244
  "project-tab": "project-tab";
228
245
  "project-sidebar": "project-sidebar";
229
246
  "workspace-sidebar": "workspace-sidebar";
230
- home: "home";
231
247
  }>;
232
248
  screen: z.ZodString;
249
+ sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
250
+ small: "small";
251
+ medium: "medium";
252
+ large: "large";
253
+ }>>>;
233
254
  label: z.ZodOptional<z.ZodString>;
234
255
  icon: z.ZodOptional<z.ZodString>;
235
256
  settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -335,12 +356,18 @@ export declare const storedExtensionsSchema: z.ZodObject<{
335
356
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
336
357
  key: z.ZodOptional<z.ZodString>;
337
358
  kind: z.ZodEnum<{
359
+ home: "home";
360
+ "project-widget": "project-widget";
338
361
  "project-tab": "project-tab";
339
362
  "project-sidebar": "project-sidebar";
340
363
  "workspace-sidebar": "workspace-sidebar";
341
- home: "home";
342
364
  }>;
343
365
  screen: z.ZodString;
366
+ sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
367
+ small: "small";
368
+ medium: "medium";
369
+ large: "large";
370
+ }>>>;
344
371
  label: z.ZodOptional<z.ZodString>;
345
372
  icon: z.ZodOptional<z.ZodString>;
346
373
  settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -481,12 +508,174 @@ export declare const appManifestSchema: z.ZodObject<{
481
508
  placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
482
509
  key: z.ZodOptional<z.ZodString>;
483
510
  kind: z.ZodEnum<{
511
+ home: "home";
512
+ "project-widget": "project-widget";
484
513
  "project-tab": "project-tab";
485
514
  "project-sidebar": "project-sidebar";
486
515
  "workspace-sidebar": "workspace-sidebar";
516
+ }>;
517
+ screen: z.ZodString;
518
+ sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
519
+ small: "small";
520
+ medium: "medium";
521
+ large: "large";
522
+ }>>>;
523
+ label: z.ZodOptional<z.ZodString>;
524
+ icon: z.ZodOptional<z.ZodString>;
525
+ settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
526
+ type: z.ZodEnum<{
527
+ string: "string";
528
+ url: "url";
529
+ }>;
530
+ label: z.ZodString;
531
+ required: z.ZodOptional<z.ZodBoolean>;
532
+ placeholder: z.ZodOptional<z.ZodString>;
533
+ }, z.core.$strip>>>;
534
+ children: z.ZodOptional<z.ZodObject<{
535
+ tool: z.ZodString;
536
+ refreshSeconds: z.ZodOptional<z.ZodNumber>;
537
+ cap: z.ZodOptional<z.ZodNumber>;
538
+ noun: z.ZodOptional<z.ZodString>;
539
+ create: z.ZodOptional<z.ZodObject<{
540
+ tool: z.ZodString;
541
+ noun: z.ZodOptional<z.ZodString>;
542
+ titleField: z.ZodOptional<z.ZodString>;
543
+ parentField: z.ZodOptional<z.ZodString>;
544
+ }, z.core.$strip>>;
545
+ nested: z.ZodOptional<z.ZodArray<z.ZodObject<{
546
+ noun: z.ZodOptional<z.ZodString>;
547
+ create: z.ZodOptional<z.ZodObject<{
548
+ tool: z.ZodString;
549
+ noun: z.ZodOptional<z.ZodString>;
550
+ titleField: z.ZodOptional<z.ZodString>;
551
+ parentField: z.ZodOptional<z.ZodString>;
552
+ }, z.core.$strip>>;
553
+ }, z.core.$strip>>>;
554
+ }, z.core.$strip>>;
555
+ }, z.core.$strip>>>;
556
+ data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
557
+ schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
558
+ search: z.ZodOptional<z.ZodArray<z.ZodString>>;
559
+ label: z.ZodOptional<z.ZodString>;
560
+ }, z.core.$strip>>>;
561
+ tools: z.ZodOptional<z.ZodObject<{
562
+ generated: z.ZodOptional<z.ZodBoolean>;
563
+ custom: z.ZodOptional<z.ZodArray<z.ZodObject<{
564
+ name: z.ZodString;
565
+ description: z.ZodString;
566
+ handler: z.ZodString;
567
+ input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
568
+ write: z.ZodOptional<z.ZodBoolean>;
569
+ collection: z.ZodOptional<z.ZodString>;
570
+ }, z.core.$strip>>>;
571
+ }, z.core.$strip>>;
572
+ screens: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
573
+ entry: z.ZodString;
574
+ }, z.core.$strip>>>;
575
+ grants: z.ZodOptional<z.ZodObject<{
576
+ tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
577
+ collections: z.ZodOptional<z.ZodArray<z.ZodString>>;
578
+ host: z.ZodOptional<z.ZodArray<z.ZodString>>;
579
+ }, z.core.$strip>>;
580
+ secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
581
+ name: z.ZodString;
582
+ label: z.ZodString;
583
+ description: z.ZodOptional<z.ZodString>;
584
+ required: z.ZodOptional<z.ZodBoolean>;
585
+ scope: z.ZodOptional<z.ZodEnum<{
586
+ instance: "instance";
587
+ install: "install";
588
+ }>>;
589
+ }, z.core.$strip>>>;
590
+ migrations: z.ZodOptional<z.ZodArray<z.ZodObject<{
591
+ version: z.ZodString;
592
+ steps: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
593
+ op: z.ZodLiteral<"add">;
594
+ collection: z.ZodString;
595
+ field: z.ZodString;
596
+ default: z.ZodOptional<z.ZodUnknown>;
597
+ }, z.core.$strip>, z.ZodObject<{
598
+ op: z.ZodLiteral<"rename">;
599
+ collection: z.ZodString;
600
+ from: z.ZodString;
601
+ to: z.ZodString;
602
+ }, z.core.$strip>, z.ZodObject<{
603
+ op: z.ZodLiteral<"drop">;
604
+ collection: z.ZodString;
605
+ field: z.ZodString;
606
+ }, z.core.$strip>, z.ZodObject<{
607
+ op: z.ZodLiteral<"dropCollection">;
608
+ collection: z.ZodString;
609
+ }, z.core.$strip>, z.ZodObject<{
610
+ op: z.ZodLiteral<"replace">;
611
+ collection: z.ZodString;
612
+ field: z.ZodString;
613
+ from: z.ZodString;
614
+ to: z.ZodString;
615
+ }, z.core.$strip>], "op">>;
616
+ }, z.core.$strip>>>;
617
+ sdk: z.ZodOptional<z.ZodString>;
618
+ }, z.core.$strip>;
619
+ /**
620
+ * A manifest being written now, as `brydio validate`, `build` and `dev` read
621
+ * it: the same, and nothing retired (`placement_tab_retired`) — which a
622
+ * published version may still hold.
623
+ */
624
+ export declare const newAppManifestSchema: z.ZodObject<{
625
+ name: z.ZodString;
626
+ version: z.ZodString;
627
+ displayName: z.ZodOptional<z.ZodString>;
628
+ summary: z.ZodOptional<z.ZodString>;
629
+ description: z.ZodOptional<z.ZodString>;
630
+ author: z.ZodOptional<z.ZodObject<{
631
+ name: z.ZodString;
632
+ email: z.ZodOptional<z.ZodString>;
633
+ url: z.ZodOptional<z.ZodString>;
634
+ }, z.core.$strip>>;
635
+ homepage: z.ZodOptional<z.ZodString>;
636
+ repository: z.ZodOptional<z.ZodString>;
637
+ license: z.ZodOptional<z.ZodString>;
638
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString>>;
639
+ links: z.ZodOptional<z.ZodObject<{
640
+ privacy: z.ZodOptional<z.ZodString>;
641
+ terms: z.ZodOptional<z.ZodString>;
642
+ support: z.ZodOptional<z.ZodString>;
643
+ }, z.core.$strip>>;
644
+ logo: z.ZodOptional<z.ZodObject<{
645
+ color: z.ZodString;
646
+ mono: z.ZodString;
647
+ }, z.core.$strip>>;
648
+ icon: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
649
+ color: z.ZodString;
650
+ mono: z.ZodString;
651
+ }, z.core.$strip>]>>;
652
+ brandColor: z.ZodOptional<z.ZodString>;
653
+ brandColorDark: z.ZodOptional<z.ZodString>;
654
+ defaultPrompts: z.ZodOptional<z.ZodArray<z.ZodString>>;
655
+ skills: z.ZodOptional<z.ZodString>;
656
+ servers: z.ZodOptional<z.ZodString>;
657
+ integrations: z.ZodOptional<z.ZodString>;
658
+ requires: z.ZodOptional<z.ZodObject<{
659
+ servers: z.ZodDefault<z.ZodArray<z.ZodString>>;
660
+ integrations: z.ZodDefault<z.ZodArray<z.ZodString>>;
661
+ builtin: z.ZodDefault<z.ZodArray<z.ZodString>>;
662
+ }, z.core.$strip>>;
663
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
664
+ placements: z.ZodOptional<z.ZodArray<z.ZodObject<{
665
+ key: z.ZodOptional<z.ZodString>;
666
+ kind: z.ZodEnum<{
487
667
  home: "home";
668
+ "project-widget": "project-widget";
669
+ "project-tab": "project-tab";
670
+ "project-sidebar": "project-sidebar";
671
+ "workspace-sidebar": "workspace-sidebar";
488
672
  }>;
489
673
  screen: z.ZodString;
674
+ sizes: z.ZodOptional<z.ZodArray<z.ZodEnum<{
675
+ small: "small";
676
+ medium: "medium";
677
+ large: "large";
678
+ }>>>;
490
679
  label: z.ZodOptional<z.ZodString>;
491
680
  icon: z.ZodOptional<z.ZodString>;
492
681
  settings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
package/src/schema.js CHANGED
@@ -48,6 +48,10 @@ const folderLevelSchema = z.object({
48
48
  noun: z.string().min(1).max(60).optional(),
49
49
  create: folderCreateSchema.optional(),
50
50
  });
51
+ /** The sizes a Home card or a project widget can be drawn at (CC11-F10). */
52
+ export const HOME_CARD_SIZES = ['small', 'medium', 'large'];
53
+ /** The kinds drawn as a card, which say the sizes they draw well at. */
54
+ export const SIZED_PLACEMENT_KINDS = ['home', 'project-widget'];
51
55
  /** A stable identity for one independently addable placement offering. */
52
56
  export const PLACEMENT_KEY = /^[a-z][a-z0-9-]{0,39}$/;
53
57
  export function effectivePlacementKey(one) {
@@ -55,8 +59,16 @@ export function effectivePlacementKey(one) {
55
59
  }
56
60
  const placementSchema = z.object({
57
61
  key: z.string().regex(PLACEMENT_KEY).optional(),
58
- kind: z.enum(['project-tab', 'project-sidebar', 'workspace-sidebar', 'home']),
62
+ /**
63
+ * `home`: a card on a person's Home, drawn by `screen` at one of `sizes`.
64
+ * `project-widget`: a card on a project's page, drawn at one of `sizes`.
65
+ * `project-tab` is retired (30 Sep 2026): still read from a published
66
+ * manifest, refused in a new one (`placement_tab_retired`).
67
+ */
68
+ kind: z.enum(['project-tab', 'project-widget', 'project-sidebar', 'workspace-sidebar', 'home']),
59
69
  screen: z.string().min(1).max(FIELD_LIMITS.nameChars),
70
+ /** For `home` and `project-widget` only, and required there: the sizes the screen draws well at. */
71
+ sizes: z.array(z.enum(HOME_CARD_SIZES)).max(3).optional(),
60
72
  label: z.string().min(1).max(60).optional(),
61
73
  icon: z.string().max(60).optional(),
62
74
  settings: z.record(z.string().regex(FIELD_NAME), placementSettingSchema).optional(),
@@ -179,13 +191,6 @@ const extensionShape = {
179
191
  */
180
192
  sdk: z.string().max(MANIFEST_LIMITS.versionChars).regex(SEMVER_FORMAT).optional(),
181
193
  };
182
- /**
183
- * Everything wrong with an app's additions that a type cannot say.
184
- *
185
- * Exported on its own so E5's validator can fold the codes into its import
186
- * report; the zod schemas below run it too, so a parse never accepts what
187
- * this refuses.
188
- */
189
194
  export function dataProblems(additions, options = {}) {
190
195
  const problems = [];
191
196
  const collections = Object.entries(additions.data ?? {});
@@ -336,6 +341,31 @@ export function dataProblems(additions, options = {}) {
336
341
  message: `A ${placement.kind} placement opens "${placement.screen}", which is not one of the app's screens.`,
337
342
  });
338
343
  }
344
+ // A card must say how big it can be drawn (CC11-F10-S01).
345
+ if (placement.kind === 'home' && !placement.sizes?.length) {
346
+ problems.push({
347
+ code: 'placement_home_sizes',
348
+ message: `The Home card "${placement.screen}" must say which sizes it draws at: small, medium or large.`,
349
+ });
350
+ }
351
+ if (placement.kind === 'project-widget' && !placement.sizes?.length) {
352
+ problems.push({
353
+ code: 'placement_widget_sizes',
354
+ message: `The project widget "${placement.screen}" must say which sizes it draws at: small, medium or large.`,
355
+ });
356
+ }
357
+ if (!SIZED_PLACEMENT_KINDS.includes(placement.kind) && placement.sizes?.length) {
358
+ problems.push({
359
+ code: 'placement_sizes_not_home',
360
+ message: `Only a Home card or a project widget has sizes; the ${placement.kind} placement "${placement.screen}" cannot.`,
361
+ });
362
+ }
363
+ if (options.retired && placement.kind === 'project-tab') {
364
+ problems.push({
365
+ code: 'placement_tab_retired',
366
+ message: 'Project tabs are retired; declare a project-widget with sizes instead.',
367
+ });
368
+ }
339
369
  }
340
370
  problems.push(...folderProblems(additions));
341
371
  return problems;
@@ -517,6 +547,14 @@ export const manifestExtensionsSchema = z.object(extensionShape).superRefine((ad
517
547
  export const storedExtensionsSchema = z.object(extensionShape).superRefine((additions, ctx) => refuse(additions, ctx, { grants: false }));
518
548
  /** E5's manifest with the additions: the whole `.brydio/app.json` of an app. */
519
549
  export const appManifestSchema = manifestSchema.extend(extensionShape).superRefine((additions, ctx) => refuse(additions, ctx));
550
+ /**
551
+ * A manifest being written now, as `brydio validate`, `build` and `dev` read
552
+ * it: the same, and nothing retired (`placement_tab_retired`) — which a
553
+ * published version may still hold.
554
+ */
555
+ export const newAppManifestSchema = manifestSchema
556
+ .extend(extensionShape)
557
+ .superRefine((additions, ctx) => refuse(additions, ctx, { retired: true }));
520
558
  /**
521
559
  * The singular a collection's tools are named with: the manifest's, else the
522
560
  * collection's name with a trailing "s" taken off (`issues` → `issue`).
package/src/validate.d.ts CHANGED
@@ -22,6 +22,12 @@ export interface ManifestValidation {
22
22
  manifest?: AppManifestWithData;
23
23
  problems: ManifestProblem[];
24
24
  }
25
- export declare function validateManifest(source: unknown): ManifestValidation;
25
+ export declare function validateManifest(source: unknown,
26
+ /** `retired`: a manifest being written now, which may not use a retired kind such as `project-tab`. */
27
+ options?: {
28
+ retired?: boolean;
29
+ }): ManifestValidation;
26
30
  /** Parses the text of `.brydio/app.json` and validates it. */
27
- export declare function validateManifestText(text: string): ManifestValidation;
31
+ export declare function validateManifestText(text: string, options?: {
32
+ retired?: boolean;
33
+ }): ManifestValidation;
package/src/validate.js CHANGED
@@ -1,6 +1,8 @@
1
- import { appManifestSchema } from "./schema.js";
2
- export function validateManifest(source) {
3
- const parsed = appManifestSchema.safeParse(source);
1
+ import { appManifestSchema, newAppManifestSchema } from "./schema.js";
2
+ export function validateManifest(source,
3
+ /** `retired`: a manifest being written now, which may not use a retired kind such as `project-tab`. */
4
+ options = {}) {
5
+ const parsed = (options.retired ? newAppManifestSchema : appManifestSchema).safeParse(source);
4
6
  if (parsed.success)
5
7
  return { ok: true, manifest: parsed.data, problems: [] };
6
8
  return {
@@ -14,7 +16,7 @@ export function validateManifest(source) {
14
16
  };
15
17
  }
16
18
  /** Parses the text of `.brydio/app.json` and validates it. */
17
- export function validateManifestText(text) {
19
+ export function validateManifestText(text, options = {}) {
18
20
  let json;
19
21
  try {
20
22
  json = JSON.parse(text);
@@ -25,5 +27,5 @@ export function validateManifestText(text) {
25
27
  problems: [{ code: 'manifest_not_json', message: `The manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}` }],
26
28
  };
27
29
  }
28
- return validateManifest(json);
30
+ return validateManifest(json, options);
29
31
  }