@protoyard/blueprint 0.0.0-stage → 1.1.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.
- package/LICENSE +21 -0
- package/README.md +1145 -2
- package/dist/aggregate.d.ts +83 -0
- package/dist/aggregate.js +268 -0
- package/dist/app-model.d.ts +76 -0
- package/dist/app-model.js +287 -0
- package/dist/archetypes/index.d.ts +1 -0
- package/dist/archetypes/index.js +1 -0
- package/dist/archetypes/schema.d.ts +126 -0
- package/dist/archetypes/schema.js +77 -0
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +149 -0
- package/dist/authoring/deck-lint.d.ts +32 -0
- package/dist/authoring/deck-lint.js +567 -0
- package/dist/authoring/index.d.ts +1 -0
- package/dist/authoring/index.js +1 -0
- package/dist/authoring/select-deck-layouts.d.ts +50 -0
- package/dist/authoring/select-deck-layouts.js +213 -0
- package/dist/authoring-apis.d.ts +13 -0
- package/dist/authoring-apis.js +13 -0
- package/dist/authoring-validation.d.ts +65 -0
- package/dist/authoring-validation.js +267 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +30 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.js +294 -0
- package/dist/brief/archetypes.d.ts +28 -0
- package/dist/brief/archetypes.js +798 -0
- package/dist/brief/content-synthesize.d.ts +31 -0
- package/dist/brief/content-synthesize.js +210 -0
- package/dist/brief/deck-outlines.d.ts +96 -0
- package/dist/brief/deck-outlines.js +561 -0
- package/dist/brief/draft-bodies.d.ts +29 -0
- package/dist/brief/draft-bodies.js +224 -0
- package/dist/brief/fields.d.ts +19 -0
- package/dist/brief/fields.js +229 -0
- package/dist/brief/index.d.ts +8 -0
- package/dist/brief/index.js +8 -0
- package/dist/brief/kind-detection.d.ts +69 -0
- package/dist/brief/kind-detection.js +353 -0
- package/dist/brief/limits.d.ts +117 -0
- package/dist/brief/limits.js +217 -0
- package/dist/brief/page-doc-outlines.d.ts +80 -0
- package/dist/brief/page-doc-outlines.js +485 -0
- package/dist/brief/synthesize.d.ts +59 -0
- package/dist/brief/synthesize.js +511 -0
- package/dist/brief/text.d.ts +13 -0
- package/dist/brief/text.js +59 -0
- package/dist/canvas-screen.d.ts +31 -0
- package/dist/canvas-screen.js +33 -0
- package/dist/canvas-seed.d.ts +58 -0
- package/dist/canvas-seed.js +82 -0
- package/dist/computed/evaluate.d.ts +11 -0
- package/dist/computed/evaluate.js +101 -0
- package/dist/computed/index.d.ts +4 -0
- package/dist/computed/index.js +3 -0
- package/dist/computed/public.d.ts +14 -0
- package/dist/computed/public.js +19 -0
- package/dist/computed/schema.d.ts +10 -0
- package/dist/computed/schema.js +140 -0
- package/dist/computed/types.d.ts +70 -0
- package/dist/computed/types.js +1 -0
- package/dist/computed/validate.d.ts +6 -0
- package/dist/computed/validate.js +146 -0
- package/dist/csv-guard.d.ts +15 -0
- package/dist/csv-guard.js +29 -0
- package/dist/csv.d.ts +85 -0
- package/dist/csv.js +288 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.js +246 -0
- package/dist/form-schema.d.ts +124 -0
- package/dist/form-schema.js +300 -0
- package/dist/generator-version.d.ts +11 -0
- package/dist/generator-version.js +51 -0
- package/dist/generator.d.ts +34 -0
- package/dist/generator.js +445 -0
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +91 -0
- package/dist/import/csv-shape.d.ts +40 -0
- package/dist/import/csv-shape.js +265 -0
- package/dist/import/json-shape.d.ts +55 -0
- package/dist/import/json-shape.js +491 -0
- package/dist/import/limits.d.ts +13 -0
- package/dist/import/limits.js +14 -0
- package/dist/import/sqlite-schema.d.ts +36 -0
- package/dist/import/sqlite-schema.js +729 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +58 -0
- package/dist/kinds/anchors.d.ts +18 -0
- package/dist/kinds/anchors.js +46 -0
- package/dist/kinds/bodies.d.ts +6 -0
- package/dist/kinds/bodies.js +32 -0
- package/dist/kinds/brand-runtime.d.ts +19 -0
- package/dist/kinds/brand-runtime.js +35 -0
- package/dist/kinds/deck.d.ts +8 -0
- package/dist/kinds/deck.js +34 -0
- package/dist/kinds/doc.d.ts +19 -0
- package/dist/kinds/doc.js +41 -0
- package/dist/kinds/embed/index.d.ts +39 -0
- package/dist/kinds/embed/index.js +91 -0
- package/dist/kinds/envelope.d.ts +159 -0
- package/dist/kinds/envelope.js +78 -0
- package/dist/kinds/index.d.ts +11 -0
- package/dist/kinds/index.js +11 -0
- package/dist/kinds/kind.d.ts +36 -0
- package/dist/kinds/kind.js +55 -0
- package/dist/kinds/limits.d.ts +64 -0
- package/dist/kinds/limits.js +190 -0
- package/dist/kinds/page-doc-runtime.d.ts +37 -0
- package/dist/kinds/page-doc-runtime.js +55 -0
- package/dist/kinds/page.d.ts +16 -0
- package/dist/kinds/page.js +67 -0
- package/dist/kinds/parse.d.ts +18 -0
- package/dist/kinds/parse.js +27 -0
- package/dist/kinds/runtime.d.ts +128 -0
- package/dist/kinds/runtime.js +206 -0
- package/dist/kinds/services.d.ts +49 -0
- package/dist/kinds/services.js +249 -0
- package/dist/kinds/testing.d.ts +9 -0
- package/dist/kinds/testing.js +16 -0
- package/dist/migrations.d.ts +97 -0
- package/dist/migrations.js +351 -0
- package/dist/motion.d.ts +50 -0
- package/dist/motion.js +52 -0
- package/dist/notifications.d.ts +19 -0
- package/dist/notifications.js +177 -0
- package/dist/patch.d.ts +122 -0
- package/dist/patch.js +322 -0
- package/dist/policy.d.ts +23 -0
- package/dist/policy.js +105 -0
- package/dist/runtime-definition.d.ts +39 -0
- package/dist/runtime-definition.js +74 -0
- package/dist/scenarios.d.ts +9 -0
- package/dist/scenarios.js +49 -0
- package/dist/schema/blueprint.schema.json +10191 -0
- package/dist/schema-json.d.ts +6 -0
- package/dist/schema-json.js +36 -0
- package/dist/schema.d.ts +6348 -0
- package/dist/schema.js +1269 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +96 -0
- package/dist/services.d.ts +89 -0
- package/dist/services.js +898 -0
- package/dist/share.d.ts +78 -0
- package/dist/share.js +234 -0
- package/dist/sheet-screen.d.ts +49 -0
- package/dist/sheet-screen.js +91 -0
- package/dist/storage.d.ts +43 -0
- package/dist/storage.js +179 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.js +173 -0
- package/dist/untrusted.d.ts +63 -0
- package/dist/untrusted.js +502 -0
- package/dist/validation-tasks.d.ts +178 -0
- package/dist/validation-tasks.js +199 -0
- package/dist/validation.d.ts +35 -0
- package/dist/validation.js +312 -0
- package/dist/variants.d.ts +112 -0
- package/dist/variants.js +283 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/schema.d.ts +50 -0
- package/dist/workflows/schema.js +34 -0
- package/dist/workflows/transition.d.ts +15 -0
- package/dist/workflows/transition.js +25 -0
- package/dist/workflows/validate.d.ts +17 -0
- package/dist/workflows/validate.js +140 -0
- package/package.json +120 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { screenPath, } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* The app model (ADR-029): a blueprint's routes, navigation, navigation
|
|
4
|
+
* groups and layout as plain data. `protoyard generate` prints it into
|
|
5
|
+
* `src/generated/routes.ts` and `layout.ts`; the player renders it directly.
|
|
6
|
+
* Framework-free: icons are `SCREEN_NAV_ICONS` ids, and each renderer maps
|
|
7
|
+
* them to its own components.
|
|
8
|
+
*/
|
|
9
|
+
/** Screen kinds that render through their own view component instead of the entity page. */
|
|
10
|
+
const VIEW_COMPONENT_KINDS = [
|
|
11
|
+
'board',
|
|
12
|
+
'calendar',
|
|
13
|
+
'map',
|
|
14
|
+
'analytics',
|
|
15
|
+
'timeline',
|
|
16
|
+
'dashboard',
|
|
17
|
+
'settings',
|
|
18
|
+
'sheet',
|
|
19
|
+
'canvas',
|
|
20
|
+
];
|
|
21
|
+
/** Kinds listed under the "Views" navigation group. */
|
|
22
|
+
const VIEW_GROUP_KINDS = [
|
|
23
|
+
'board',
|
|
24
|
+
'calendar',
|
|
25
|
+
'timeline',
|
|
26
|
+
'map',
|
|
27
|
+
'analytics',
|
|
28
|
+
'dashboard',
|
|
29
|
+
'sheet',
|
|
30
|
+
'canvas',
|
|
31
|
+
];
|
|
32
|
+
const DEFAULT_ICONS = {
|
|
33
|
+
dashboard: 'layout-dashboard',
|
|
34
|
+
settings: 'settings',
|
|
35
|
+
board: 'kanban',
|
|
36
|
+
calendar: 'calendar-days',
|
|
37
|
+
timeline: 'chart-gantt',
|
|
38
|
+
map: 'map',
|
|
39
|
+
analytics: 'chart-line',
|
|
40
|
+
sheet: 'table',
|
|
41
|
+
canvas: 'layout-dashboard',
|
|
42
|
+
};
|
|
43
|
+
/** `support-tickets` → `Support tickets`, unless the entity sets `label`. */
|
|
44
|
+
export function entityLabel(entity) {
|
|
45
|
+
if (entity.label)
|
|
46
|
+
return entity.label;
|
|
47
|
+
const words = entity.plural.replace(/-/g, ' ');
|
|
48
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
49
|
+
}
|
|
50
|
+
/** `support-ticket` → `SupportTicket` (names are lowercase kebab-case). */
|
|
51
|
+
function pascal(name) {
|
|
52
|
+
return name
|
|
53
|
+
.split('-')
|
|
54
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
|
57
|
+
const capitalize = (text) => `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
|
|
58
|
+
/** The icon a screen shows in navigation: its `nav.icon`, or the default for its kind. */
|
|
59
|
+
export function screenNavIcon(screen) {
|
|
60
|
+
return screen.nav?.icon ?? DEFAULT_ICONS[screen.type] ?? 'database';
|
|
61
|
+
}
|
|
62
|
+
function screenTitle(screen, blueprint) {
|
|
63
|
+
if (screen.nav?.label)
|
|
64
|
+
return screen.nav.label;
|
|
65
|
+
if (screen.title)
|
|
66
|
+
return screen.title;
|
|
67
|
+
if ('entity' in screen) {
|
|
68
|
+
const entity = blueprint.entities.find((candidate) => candidate.name === screen.entity);
|
|
69
|
+
if (entity)
|
|
70
|
+
return `${entityLabel(entity)} ${screen.type === 'list' ? '' : screen.type}`.trim();
|
|
71
|
+
}
|
|
72
|
+
return capitalize(screen.type);
|
|
73
|
+
}
|
|
74
|
+
function screenComponent(screen, index, previous) {
|
|
75
|
+
const entity = 'entity' in screen ? screen.entity : undefined;
|
|
76
|
+
const base = screen.type === 'dashboard'
|
|
77
|
+
? `Dashboard${index + 1}Screen`
|
|
78
|
+
: screen.type === 'settings'
|
|
79
|
+
? 'SettingsScreen'
|
|
80
|
+
: `${pascal(screen.type)}${entity ? pascal(entity) : index + 1}Screen`;
|
|
81
|
+
let name = base;
|
|
82
|
+
for (let suffix = 2; previous.has(name); suffix += 1)
|
|
83
|
+
name = base.replace(/Screen$/, `${suffix}Screen`);
|
|
84
|
+
previous.add(name);
|
|
85
|
+
return name;
|
|
86
|
+
}
|
|
87
|
+
/** True when a screen renders through its own component rather than the entity page. */
|
|
88
|
+
export function screenHasOwnView(screen) {
|
|
89
|
+
if (screen.type === 'detail')
|
|
90
|
+
return Boolean(screen.tabs?.length);
|
|
91
|
+
return VIEW_COMPONENT_KINDS.includes(screen.type);
|
|
92
|
+
}
|
|
93
|
+
function screenEntries(blueprint) {
|
|
94
|
+
const names = new Set();
|
|
95
|
+
return blueprint.screens.flatMap((screen, index) => {
|
|
96
|
+
const path = screenPath(screen, blueprint.entities);
|
|
97
|
+
if (!path)
|
|
98
|
+
return [];
|
|
99
|
+
const entityName = 'entity' in screen ? screen.entity : undefined;
|
|
100
|
+
const entity = entityName
|
|
101
|
+
? blueprint.entities.find((item) => item.name === entityName)
|
|
102
|
+
: undefined;
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
screen,
|
|
106
|
+
index,
|
|
107
|
+
path,
|
|
108
|
+
component: screenComponent(screen, index, names),
|
|
109
|
+
ownView: screenHasOwnView(screen),
|
|
110
|
+
label: screenTitle(screen, blueprint),
|
|
111
|
+
description: `${capitalize(screen.type)} screen${entity ? ` for ${entityLabel(entity)}` : ''}`,
|
|
112
|
+
...(entityName ? { entity: entityName } : {}),
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function hasDefaultListRoute(blueprint, entity) {
|
|
118
|
+
if (!entity.screenDefaults)
|
|
119
|
+
return true;
|
|
120
|
+
const explicit = blueprint.screens.find((screen) => 'entity' in screen && screen.entity === entity.name && screen.type === 'list');
|
|
121
|
+
if (explicit)
|
|
122
|
+
return screenPath(explicit, blueprint.entities) === `/${entity.plural}`;
|
|
123
|
+
return entity.screenDefaults.views.includes('list');
|
|
124
|
+
}
|
|
125
|
+
function hasDefaultDetailRoute(blueprint, entity) {
|
|
126
|
+
if (!entity.screenDefaults)
|
|
127
|
+
return true;
|
|
128
|
+
const explicit = blueprint.screens.find((screen) => 'entity' in screen && screen.entity === entity.name && screen.type === 'detail');
|
|
129
|
+
if (explicit)
|
|
130
|
+
return screenPath(explicit, blueprint.entities) === `/${entity.plural}/:id`;
|
|
131
|
+
return entity.screenDefaults.views.includes('detail');
|
|
132
|
+
}
|
|
133
|
+
/** The route fields an entry renders with: its own view, or the entity page it belongs to. */
|
|
134
|
+
function entryTarget(entry) {
|
|
135
|
+
if (entry.ownView) {
|
|
136
|
+
return {
|
|
137
|
+
view: 'screen',
|
|
138
|
+
screenIndex: entry.index,
|
|
139
|
+
screenType: entry.screen.type,
|
|
140
|
+
component: entry.component,
|
|
141
|
+
...(entry.entity ? { entity: entry.entity } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
view: 'entity',
|
|
146
|
+
entity: entry.entity ?? '',
|
|
147
|
+
screenIndex: entry.index,
|
|
148
|
+
screenType: entry.screen.type,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function deriveRoutes(blueprint, entries) {
|
|
152
|
+
const rootDashboard = entries.find((entry) => entry.screen.type === 'dashboard' && entry.path === '/');
|
|
153
|
+
// Keyed by path: a later route on the same path replaces the earlier one in place.
|
|
154
|
+
const routes = new Map();
|
|
155
|
+
routes.set('/', rootDashboard
|
|
156
|
+
? {
|
|
157
|
+
name: 'home',
|
|
158
|
+
description: rootDashboard.description,
|
|
159
|
+
path: '/',
|
|
160
|
+
title: rootDashboard.screen.title ?? rootDashboard.label,
|
|
161
|
+
...entryTarget(rootDashboard),
|
|
162
|
+
}
|
|
163
|
+
: {
|
|
164
|
+
name: 'home',
|
|
165
|
+
description: 'Overview with a count per entity',
|
|
166
|
+
path: '/',
|
|
167
|
+
title: 'Home',
|
|
168
|
+
view: 'home',
|
|
169
|
+
});
|
|
170
|
+
for (const entity of blueprint.entities) {
|
|
171
|
+
const label = entityLabel(entity);
|
|
172
|
+
const page = { view: 'entity', entity: entity.name };
|
|
173
|
+
const detail = entries.find((entry) => entry.screen.type === 'detail' &&
|
|
174
|
+
entry.entity === entity.name &&
|
|
175
|
+
Boolean(entry.screen.tabs?.length));
|
|
176
|
+
const list = entries.find((entry) => entry.screen.type === 'list' && entry.entity === entity.name);
|
|
177
|
+
const listPath = `/${entity.plural}`;
|
|
178
|
+
const detailPath = `/${entity.plural}/:id`;
|
|
179
|
+
if (hasDefaultListRoute(blueprint, entity)) {
|
|
180
|
+
routes.set(listPath, {
|
|
181
|
+
name: `${entity.name}-list`,
|
|
182
|
+
description: `${label}: list, create, edit and delete`,
|
|
183
|
+
path: listPath,
|
|
184
|
+
title: label,
|
|
185
|
+
...(list?.path === listPath ? entryTarget(list) : page),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (hasDefaultDetailRoute(blueprint, entity)) {
|
|
189
|
+
routes.set(detailPath, {
|
|
190
|
+
name: `${entity.name}-detail`,
|
|
191
|
+
description: detail
|
|
192
|
+
? `${label}: record detail with tabs`
|
|
193
|
+
: `One ${entity.name.replace(/-/g, ' ')}: edit form`,
|
|
194
|
+
path: detailPath,
|
|
195
|
+
title: label,
|
|
196
|
+
...(detail?.path === detailPath ? entryTarget(detail) : page),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
if (entry.path === '/' && entry.screen.type === 'dashboard')
|
|
202
|
+
continue;
|
|
203
|
+
routes.set(entry.path, {
|
|
204
|
+
name: `screen-${entry.component.replace(/Screen$/, '')}`,
|
|
205
|
+
description: entry.description,
|
|
206
|
+
path: entry.path,
|
|
207
|
+
title: entry.label,
|
|
208
|
+
...entryTarget(entry),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
routes.set('/members', {
|
|
212
|
+
name: 'members',
|
|
213
|
+
description: 'Workspace people and their simulated roles',
|
|
214
|
+
path: '/members',
|
|
215
|
+
title: 'Members',
|
|
216
|
+
view: 'members',
|
|
217
|
+
});
|
|
218
|
+
return [...routes.values()];
|
|
219
|
+
}
|
|
220
|
+
function deriveNav(blueprint, entries) {
|
|
221
|
+
const rootDashboard = entries.find((entry) => entry.screen.type === 'dashboard' && entry.path === '/');
|
|
222
|
+
const visible = entries.filter((entry) => entry.screen.nav?.hidden !== true);
|
|
223
|
+
const homeIcon = rootDashboard ? screenNavIcon(rootDashboard.screen) : 'layout-dashboard';
|
|
224
|
+
const icons = new Set(['database', 'users', homeIcon]);
|
|
225
|
+
for (const { screen } of visible)
|
|
226
|
+
icons.add(screenNavIcon(screen));
|
|
227
|
+
const entityNav = blueprint.entities.flatMap((entity) => {
|
|
228
|
+
if (entries.some((entry) => entry.screen.type === 'list' && entry.entity === entity.name)) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
if (entity.screenDefaults)
|
|
232
|
+
return [];
|
|
233
|
+
return [{ path: `/${entity.plural}`, label: entityLabel(entity), icon: 'database' }];
|
|
234
|
+
});
|
|
235
|
+
const screenNav = visible
|
|
236
|
+
.filter(({ screen, path }) => screen.type !== 'form' && screen.type !== 'detail' && path !== '/')
|
|
237
|
+
.sort((a, b) => (a.screen.nav?.order ?? Number.POSITIVE_INFINITY) -
|
|
238
|
+
(b.screen.nav?.order ?? Number.POSITIVE_INFINITY) || a.index - b.index)
|
|
239
|
+
.map(({ screen, path, label }) => ({ path, label, icon: screenNavIcon(screen) }));
|
|
240
|
+
const nav = [
|
|
241
|
+
{ path: '/', label: rootDashboard?.label ?? 'Home', icon: homeIcon, exact: true },
|
|
242
|
+
...entityNav,
|
|
243
|
+
...screenNav,
|
|
244
|
+
{ path: '/members', label: 'Members', icon: 'users' },
|
|
245
|
+
];
|
|
246
|
+
return { nav, icons: [...icons] };
|
|
247
|
+
}
|
|
248
|
+
function deriveNavGroups(blueprint, entries) {
|
|
249
|
+
const visible = entries.filter((entry) => entry.screen.nav?.hidden !== true);
|
|
250
|
+
const recordPaths = blueprint.entities.flatMap((entity) => {
|
|
251
|
+
if (!hasDefaultListRoute(blueprint, entity))
|
|
252
|
+
return [];
|
|
253
|
+
const list = visible.find(({ screen, entity: name }) => screen.type === 'list' && name === entity.name);
|
|
254
|
+
return [list?.path ?? `/${entity.plural}`];
|
|
255
|
+
});
|
|
256
|
+
const viewPaths = visible
|
|
257
|
+
.filter(({ screen, path }) => path !== '/' && VIEW_GROUP_KINDS.includes(screen.type))
|
|
258
|
+
.map(({ path }) => path);
|
|
259
|
+
const settingsPaths = visible
|
|
260
|
+
.filter(({ screen }) => screen.type === 'settings')
|
|
261
|
+
.map(({ path }) => path);
|
|
262
|
+
return [
|
|
263
|
+
{ label: 'Overview', paths: ['/'] },
|
|
264
|
+
{ label: 'Records', paths: recordPaths },
|
|
265
|
+
{ label: 'Views', paths: viewPaths },
|
|
266
|
+
{ label: 'Workspace', paths: ['/members', ...settingsPaths] },
|
|
267
|
+
].filter((group) => group.paths.length > 0);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Derives the app model from a parsed blueprint. Pure and deterministic: the
|
|
271
|
+
* same blueprint always yields the same model, which matches what
|
|
272
|
+
* `protoyard generate` writes to `routes.ts` and `layout.ts`.
|
|
273
|
+
*/
|
|
274
|
+
export function deriveAppModel(blueprint) {
|
|
275
|
+
const entries = screenEntries(blueprint);
|
|
276
|
+
const { nav, icons } = deriveNav(blueprint, entries);
|
|
277
|
+
return {
|
|
278
|
+
routes: deriveRoutes(blueprint, entries),
|
|
279
|
+
nav,
|
|
280
|
+
navGroups: deriveNavGroups(blueprint, entries),
|
|
281
|
+
layout: {
|
|
282
|
+
id: blueprint.layout?.id ?? 'sidebar',
|
|
283
|
+
...(blueprint.layout?.mobile ? { mobile: blueprint.layout.mobile } : {}),
|
|
284
|
+
},
|
|
285
|
+
icons,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ARCHETYPE_IDS, ARCHETYPE_KIT_ENTRY_POINTS, type ArchetypeKitEntryPoint, ArchetypeSchema, type BlueprintArchetype, type BlueprintArchetypeInput, } from './schema.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ARCHETYPE_IDS, ARCHETYPE_KIT_ENTRY_POINTS, ArchetypeSchema, } from "./schema.js";
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Kit entry points understood by the player. This list is data; it never imports a kit. */
|
|
3
|
+
export declare const ARCHETYPE_KIT_ENTRY_POINTS: readonly ["@protoyard/ai", "@protoyard/ai-ui", "@protoyard/saas", "@protoyard/saas-ui", "@protoyard/analytics", "@protoyard/analytics-ui/grid", "@protoyard/analytics-ui/date-range"];
|
|
4
|
+
export type ArchetypeKitEntryPoint = (typeof ARCHETYPE_KIT_ENTRY_POINTS)[number];
|
|
5
|
+
export declare const ARCHETYPE_IDS: readonly ["assistant", "team-chat", "saas-onboarding", "analytics"];
|
|
6
|
+
/**
|
|
7
|
+
* Portable runtime configuration for one of the player's kit-backed product
|
|
8
|
+
* archetypes. Package identifiers are an allow-list, never module specifiers
|
|
9
|
+
* resolved from arbitrary blueprint text.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ArchetypeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
12
|
+
id: z.ZodLiteral<"assistant">;
|
|
13
|
+
kits: z.ZodTuple<[z.ZodLiteral<"@protoyard/ai">, z.ZodLiteral<"@protoyard/ai-ui">], null>;
|
|
14
|
+
entities: z.ZodObject<{
|
|
15
|
+
conversations: z.ZodString;
|
|
16
|
+
messages: z.ZodString;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
screens: z.ZodTuple<[z.ZodObject<{
|
|
19
|
+
kind: z.ZodLiteral<"assistant-workspace">;
|
|
20
|
+
path: z.ZodString;
|
|
21
|
+
}, z.core.$strict>], null>;
|
|
22
|
+
scenarioDefaults: z.ZodDefault<z.ZodObject<{
|
|
23
|
+
density: z.ZodDefault<z.ZodEnum<{
|
|
24
|
+
empty: "empty";
|
|
25
|
+
sparse: "sparse";
|
|
26
|
+
normal: "normal";
|
|
27
|
+
dense: "dense";
|
|
28
|
+
massive: "massive";
|
|
29
|
+
}>>;
|
|
30
|
+
activity: z.ZodDefault<z.ZodEnum<{
|
|
31
|
+
none: "none";
|
|
32
|
+
normal: "normal";
|
|
33
|
+
low: "low";
|
|
34
|
+
intense: "intense";
|
|
35
|
+
dormant: "dormant";
|
|
36
|
+
reactivated: "reactivated";
|
|
37
|
+
}>>;
|
|
38
|
+
}, z.core.$strict>>;
|
|
39
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
40
|
+
id: z.ZodLiteral<"team-chat">;
|
|
41
|
+
kits: z.ZodTuple<[z.ZodLiteral<"@protoyard/ai">, z.ZodLiteral<"@protoyard/ai-ui">], null>;
|
|
42
|
+
entities: z.ZodObject<{
|
|
43
|
+
rooms: z.ZodString;
|
|
44
|
+
messages: z.ZodString;
|
|
45
|
+
members: z.ZodString;
|
|
46
|
+
}, z.core.$strict>;
|
|
47
|
+
screens: z.ZodTuple<[z.ZodObject<{
|
|
48
|
+
kind: z.ZodLiteral<"team-chat">;
|
|
49
|
+
path: z.ZodString;
|
|
50
|
+
}, z.core.$strict>], null>;
|
|
51
|
+
scenarioDefaults: z.ZodDefault<z.ZodObject<{
|
|
52
|
+
density: z.ZodDefault<z.ZodEnum<{
|
|
53
|
+
empty: "empty";
|
|
54
|
+
sparse: "sparse";
|
|
55
|
+
normal: "normal";
|
|
56
|
+
dense: "dense";
|
|
57
|
+
massive: "massive";
|
|
58
|
+
}>>;
|
|
59
|
+
activity: z.ZodDefault<z.ZodEnum<{
|
|
60
|
+
none: "none";
|
|
61
|
+
normal: "normal";
|
|
62
|
+
low: "low";
|
|
63
|
+
intense: "intense";
|
|
64
|
+
dormant: "dormant";
|
|
65
|
+
reactivated: "reactivated";
|
|
66
|
+
}>>;
|
|
67
|
+
}, z.core.$strict>>;
|
|
68
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
69
|
+
id: z.ZodLiteral<"saas-onboarding">;
|
|
70
|
+
kits: z.ZodTuple<[z.ZodLiteral<"@protoyard/saas">, z.ZodLiteral<"@protoyard/saas-ui">], null>;
|
|
71
|
+
entities: z.ZodObject<{
|
|
72
|
+
workspaces: z.ZodString;
|
|
73
|
+
members: z.ZodString;
|
|
74
|
+
}, z.core.$strict>;
|
|
75
|
+
screens: z.ZodTuple<[z.ZodObject<{
|
|
76
|
+
kind: z.ZodLiteral<"saas-onboarding">;
|
|
77
|
+
path: z.ZodString;
|
|
78
|
+
}, z.core.$strict>], null>;
|
|
79
|
+
scenarioDefaults: z.ZodDefault<z.ZodObject<{
|
|
80
|
+
density: z.ZodDefault<z.ZodEnum<{
|
|
81
|
+
empty: "empty";
|
|
82
|
+
sparse: "sparse";
|
|
83
|
+
normal: "normal";
|
|
84
|
+
dense: "dense";
|
|
85
|
+
massive: "massive";
|
|
86
|
+
}>>;
|
|
87
|
+
activity: z.ZodDefault<z.ZodEnum<{
|
|
88
|
+
none: "none";
|
|
89
|
+
normal: "normal";
|
|
90
|
+
low: "low";
|
|
91
|
+
intense: "intense";
|
|
92
|
+
dormant: "dormant";
|
|
93
|
+
reactivated: "reactivated";
|
|
94
|
+
}>>;
|
|
95
|
+
}, z.core.$strict>>;
|
|
96
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
97
|
+
id: z.ZodLiteral<"analytics">;
|
|
98
|
+
kits: z.ZodTuple<[z.ZodLiteral<"@protoyard/analytics">, z.ZodLiteral<"@protoyard/analytics-ui/grid">, z.ZodLiteral<"@protoyard/analytics-ui/date-range">], null>;
|
|
99
|
+
entities: z.ZodObject<{
|
|
100
|
+
events: z.ZodString;
|
|
101
|
+
metrics: z.ZodString;
|
|
102
|
+
}, z.core.$strict>;
|
|
103
|
+
screens: z.ZodTuple<[z.ZodObject<{
|
|
104
|
+
kind: z.ZodLiteral<"analytics-dashboard">;
|
|
105
|
+
path: z.ZodString;
|
|
106
|
+
}, z.core.$strict>], null>;
|
|
107
|
+
scenarioDefaults: z.ZodDefault<z.ZodObject<{
|
|
108
|
+
density: z.ZodDefault<z.ZodEnum<{
|
|
109
|
+
empty: "empty";
|
|
110
|
+
sparse: "sparse";
|
|
111
|
+
normal: "normal";
|
|
112
|
+
dense: "dense";
|
|
113
|
+
massive: "massive";
|
|
114
|
+
}>>;
|
|
115
|
+
activity: z.ZodDefault<z.ZodEnum<{
|
|
116
|
+
none: "none";
|
|
117
|
+
normal: "normal";
|
|
118
|
+
low: "low";
|
|
119
|
+
intense: "intense";
|
|
120
|
+
dormant: "dormant";
|
|
121
|
+
reactivated: "reactivated";
|
|
122
|
+
}>>;
|
|
123
|
+
}, z.core.$strict>>;
|
|
124
|
+
}, z.core.$strict>], "id">;
|
|
125
|
+
export type BlueprintArchetype = z.infer<typeof ArchetypeSchema>;
|
|
126
|
+
export type BlueprintArchetypeInput = z.input<typeof ArchetypeSchema>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ActivityLevelSchema, DataDensitySchema } from '@protoyard/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** Kit entry points understood by the player. This list is data; it never imports a kit. */
|
|
4
|
+
export const ARCHETYPE_KIT_ENTRY_POINTS = [
|
|
5
|
+
'@protoyard/ai',
|
|
6
|
+
'@protoyard/ai-ui',
|
|
7
|
+
'@protoyard/saas',
|
|
8
|
+
'@protoyard/saas-ui',
|
|
9
|
+
'@protoyard/analytics',
|
|
10
|
+
'@protoyard/analytics-ui/grid',
|
|
11
|
+
'@protoyard/analytics-ui/date-range',
|
|
12
|
+
];
|
|
13
|
+
export const ARCHETYPE_IDS = ['assistant', 'team-chat', 'saas-onboarding', 'analytics'];
|
|
14
|
+
const ArchetypeEntitySlotsSchema = z.strictObject({
|
|
15
|
+
conversations: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
16
|
+
messages: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
17
|
+
});
|
|
18
|
+
const TeamChatEntitySlotsSchema = z.strictObject({
|
|
19
|
+
rooms: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
20
|
+
messages: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
21
|
+
members: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
22
|
+
});
|
|
23
|
+
const SaasEntitySlotsSchema = z.strictObject({
|
|
24
|
+
workspaces: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
25
|
+
members: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
26
|
+
});
|
|
27
|
+
const AnalyticsEntitySlotsSchema = z.strictObject({
|
|
28
|
+
events: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
29
|
+
metrics: z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
|
|
30
|
+
});
|
|
31
|
+
const ScreenRouteSchema = z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(/^\/[a-z0-9/:-]*$/, { message: 'screen path must be a hash route such as "/assistant"' });
|
|
34
|
+
const ScenarioDefaultsSchema = z.strictObject({
|
|
35
|
+
density: DataDensitySchema.default('normal'),
|
|
36
|
+
activity: ActivityLevelSchema.default('normal'),
|
|
37
|
+
});
|
|
38
|
+
const screen = (kind) => z.strictObject({ kind: z.literal(kind), path: ScreenRouteSchema });
|
|
39
|
+
/**
|
|
40
|
+
* Portable runtime configuration for one of the player's kit-backed product
|
|
41
|
+
* archetypes. Package identifiers are an allow-list, never module specifiers
|
|
42
|
+
* resolved from arbitrary blueprint text.
|
|
43
|
+
*/
|
|
44
|
+
export const ArchetypeSchema = z.discriminatedUnion('id', [
|
|
45
|
+
z.strictObject({
|
|
46
|
+
id: z.literal('assistant'),
|
|
47
|
+
kits: z.tuple([z.literal('@protoyard/ai'), z.literal('@protoyard/ai-ui')]),
|
|
48
|
+
entities: ArchetypeEntitySlotsSchema,
|
|
49
|
+
screens: z.tuple([screen('assistant-workspace')]),
|
|
50
|
+
scenarioDefaults: ScenarioDefaultsSchema.default({ density: 'normal', activity: 'normal' }),
|
|
51
|
+
}),
|
|
52
|
+
z.strictObject({
|
|
53
|
+
id: z.literal('team-chat'),
|
|
54
|
+
kits: z.tuple([z.literal('@protoyard/ai'), z.literal('@protoyard/ai-ui')]),
|
|
55
|
+
entities: TeamChatEntitySlotsSchema,
|
|
56
|
+
screens: z.tuple([screen('team-chat')]),
|
|
57
|
+
scenarioDefaults: ScenarioDefaultsSchema.default({ density: 'normal', activity: 'intense' }),
|
|
58
|
+
}),
|
|
59
|
+
z.strictObject({
|
|
60
|
+
id: z.literal('saas-onboarding'),
|
|
61
|
+
kits: z.tuple([z.literal('@protoyard/saas'), z.literal('@protoyard/saas-ui')]),
|
|
62
|
+
entities: SaasEntitySlotsSchema,
|
|
63
|
+
screens: z.tuple([screen('saas-onboarding')]),
|
|
64
|
+
scenarioDefaults: ScenarioDefaultsSchema.default({ density: 'normal', activity: 'normal' }),
|
|
65
|
+
}),
|
|
66
|
+
z.strictObject({
|
|
67
|
+
id: z.literal('analytics'),
|
|
68
|
+
kits: z.tuple([
|
|
69
|
+
z.literal('@protoyard/analytics'),
|
|
70
|
+
z.literal('@protoyard/analytics-ui/grid'),
|
|
71
|
+
z.literal('@protoyard/analytics-ui/date-range'),
|
|
72
|
+
]),
|
|
73
|
+
entities: AnalyticsEntitySlotsSchema,
|
|
74
|
+
screens: z.tuple([screen('analytics-dashboard')]),
|
|
75
|
+
scenarioDefaults: ScenarioDefaultsSchema.default({ density: 'dense', activity: 'normal' }),
|
|
76
|
+
}),
|
|
77
|
+
]);
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type PrototypeRuntime, type Row } from '@protoyard/core';
|
|
2
|
+
import type { Blueprint, BlueprintEntity } from './schema.ts';
|
|
3
|
+
/** Placeholder stored instead of a sensitive field's real value. */
|
|
4
|
+
export declare const AUDIT_REDACTED = "[redacted]";
|
|
5
|
+
export type AuditAction = 'create' | 'update' | 'delete' | 'restore' | 'purge' | 'import';
|
|
6
|
+
export interface AuditChange {
|
|
7
|
+
before: unknown;
|
|
8
|
+
after: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface AuditEntry extends Row {
|
|
11
|
+
id: string;
|
|
12
|
+
entity: string;
|
|
13
|
+
recordId: string;
|
|
14
|
+
actorId: string;
|
|
15
|
+
action: AuditAction;
|
|
16
|
+
at: string;
|
|
17
|
+
/**
|
|
18
|
+
* Monotonic per writer instance so entries that share the same simulated
|
|
19
|
+
* `at` still sort newest-first.
|
|
20
|
+
*/
|
|
21
|
+
seq: number;
|
|
22
|
+
/** Only fields that changed; sensitive values are `AUDIT_REDACTED`. */
|
|
23
|
+
changes: Record<string, AuditChange>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Field-level diff for an audit row. Only changed input properties are listed;
|
|
27
|
+
* `sensitive` fields store {@link AUDIT_REDACTED} instead of the real value.
|
|
28
|
+
* Platform meta columns (`id`, timestamps, soft-delete markers) are omitted.
|
|
29
|
+
*/
|
|
30
|
+
export declare function auditDiff(entity: BlueprintEntity, before: Row | null | undefined, after: Row | null | undefined): Record<string, AuditChange>;
|
|
31
|
+
export interface AuditEvent {
|
|
32
|
+
readonly action: AuditAction;
|
|
33
|
+
readonly entity: BlueprintEntity;
|
|
34
|
+
readonly recordId: string;
|
|
35
|
+
readonly before?: Row | null;
|
|
36
|
+
readonly after?: Row | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Writes audit rows into the caller's open mutation transaction.
|
|
40
|
+
* No-op when the entity does not enable `features.audit`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createAuditWriter(runtime: PrototypeRuntime, blueprint: Blueprint): (event: AuditEvent) => Promise<AuditEntry | null>;
|
|
43
|
+
export interface BlueprintAuditService {
|
|
44
|
+
/** Newest-first history for one record. Requires `<entity>:read`. */
|
|
45
|
+
listForRecord(entityName: string, recordId: string): Promise<AuditEntry[]>;
|
|
46
|
+
/** Newest-first entries across every audited entity (capped). Requires any audited entity read. */
|
|
47
|
+
listRecent(limit?: number): Promise<AuditEntry[]>;
|
|
48
|
+
}
|
|
49
|
+
/** Read API for the shared `auditEntries` table. */
|
|
50
|
+
export declare function createAuditService(runtime: PrototypeRuntime, blueprint: Blueprint): BlueprintAuditService;
|
|
51
|
+
/** True when at least one entity opts into audit logging. */
|
|
52
|
+
export declare function blueprintUsesAudit(blueprint: Blueprint): boolean;
|