@protoyard/blueprint 0.0.0-stage → 1.1.0
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 +1149 -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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic deck layout selection from slide roles, pacing, block contents
|
|
3
|
+
* and optional author overrides. Pure: no Math.random, no Date.now.
|
|
4
|
+
*
|
|
5
|
+
* Uses the read-only `@protoyard/decks/layouts` catalog (AUTHORING + DECKS).
|
|
6
|
+
*/
|
|
7
|
+
import { DECK_LAYOUT_BY_ID, DECK_LAYOUTS, } from '@protoyard/decks/layouts';
|
|
8
|
+
/** Preferred catalog layouts for each outline role (ordered). */
|
|
9
|
+
const ROLE_LAYOUTS = Object.freeze({
|
|
10
|
+
title: ['title-hero'],
|
|
11
|
+
agenda: ['section-divider', 'two-column-split', 'process-steps'],
|
|
12
|
+
close: ['closing-cta', 'quote'],
|
|
13
|
+
ask: ['closing-cta'],
|
|
14
|
+
section: ['section-divider'],
|
|
15
|
+
hook: ['title-hero', 'big-stat', 'quote'],
|
|
16
|
+
problem: ['two-column-split', 'image-left', 'free'],
|
|
17
|
+
solution: ['two-column-split', 'three-column-cards', 'process-steps'],
|
|
18
|
+
'business-model': ['two-column-split', 'three-column-cards'],
|
|
19
|
+
'why-now': ['big-stat', 'two-column-split'],
|
|
20
|
+
pricing: ['comparison-table', 'three-column-cards'],
|
|
21
|
+
metrics: ['big-stat', 'chart'],
|
|
22
|
+
chart: ['chart', 'big-stat'],
|
|
23
|
+
comparison: ['comparison-table'],
|
|
24
|
+
steps: ['process-steps'],
|
|
25
|
+
process: ['process-steps'],
|
|
26
|
+
progress: ['process-steps', 'timeline'],
|
|
27
|
+
timeline: ['timeline'],
|
|
28
|
+
roadmap: ['timeline', 'process-steps'],
|
|
29
|
+
plan: ['timeline', 'process-steps', 'two-column-split'],
|
|
30
|
+
team: ['team-avatars'],
|
|
31
|
+
quote: ['quote'],
|
|
32
|
+
story: ['process-steps', 'timeline', 'free'],
|
|
33
|
+
demo: ['image-centered', 'video-centered', 'image-left'],
|
|
34
|
+
code: ['code-with-callout'],
|
|
35
|
+
concept: ['two-column-split', 'three-column-cards', 'free'],
|
|
36
|
+
context: ['two-column-split', 'free'],
|
|
37
|
+
findings: ['three-column-cards', 'two-column-split', 'chart'],
|
|
38
|
+
method: ['process-steps', 'two-column-split'],
|
|
39
|
+
objectives: ['three-column-cards', 'process-steps'],
|
|
40
|
+
wins: ['three-column-cards', 'big-stat'],
|
|
41
|
+
misses: ['two-column-split', 'free'],
|
|
42
|
+
risks: ['two-column-split', 'free'],
|
|
43
|
+
recommendations: ['process-steps', 'three-column-cards'],
|
|
44
|
+
resources: ['three-column-cards', 'free'],
|
|
45
|
+
recap: ['process-steps', 'section-divider'],
|
|
46
|
+
homework: ['process-steps', 'free'],
|
|
47
|
+
poll: ['free'],
|
|
48
|
+
quiz: ['free'],
|
|
49
|
+
audience: ['team-avatars', 'free'],
|
|
50
|
+
activity: ['process-steps', 'free'],
|
|
51
|
+
example: ['image-right', 'two-column-split', 'free'],
|
|
52
|
+
});
|
|
53
|
+
/** Block types that strongly suggest a layout. */
|
|
54
|
+
const BLOCK_LAYOUTS = Object.freeze({
|
|
55
|
+
chart: ['chart'],
|
|
56
|
+
table: ['comparison-table'],
|
|
57
|
+
comparison: ['comparison-table'],
|
|
58
|
+
steps: ['process-steps'],
|
|
59
|
+
timeline: ['timeline'],
|
|
60
|
+
people: ['team-avatars'],
|
|
61
|
+
quote: ['quote'],
|
|
62
|
+
cards: ['three-column-cards'],
|
|
63
|
+
code: ['code-with-callout'],
|
|
64
|
+
image: ['image-centered', 'image-left', 'image-right'],
|
|
65
|
+
video: ['video-centered', 'video-left'],
|
|
66
|
+
cta: ['closing-cta'],
|
|
67
|
+
stat: ['big-stat'],
|
|
68
|
+
});
|
|
69
|
+
const FALLBACK_BY_PACING = Object.freeze({
|
|
70
|
+
punch: ['big-stat', 'section-divider', 'quote', 'title-hero', 'free'],
|
|
71
|
+
idea: ['two-column-split', 'three-column-cards', 'process-steps', 'free'],
|
|
72
|
+
diagram: ['process-steps', 'timeline', 'chart', 'comparison-table', 'free'],
|
|
73
|
+
story: ['timeline', 'process-steps', 'quote', 'image-full-bleed', 'free'],
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Select one catalog layout per slide. Deterministic for the same inputs.
|
|
77
|
+
* Avoids three identical layouts in a row when another compatible layout fits.
|
|
78
|
+
*/
|
|
79
|
+
export function selectDeckLayouts(slides, options = {}) {
|
|
80
|
+
const catalog = options.catalog ?? DECK_LAYOUTS;
|
|
81
|
+
const byId = new Map(catalog.map((entry) => [entry.id, entry]));
|
|
82
|
+
const aspect = options.aspect ?? '16:9';
|
|
83
|
+
const previous = [];
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const slide of slides) {
|
|
86
|
+
const override = slide.layout?.trim();
|
|
87
|
+
if (override && byId.has(override)) {
|
|
88
|
+
out.push({
|
|
89
|
+
id: slide.id,
|
|
90
|
+
layout: override,
|
|
91
|
+
rationale: `Author override: layout "${override}".`,
|
|
92
|
+
override: true,
|
|
93
|
+
});
|
|
94
|
+
previous.push(override);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const candidates = rankCandidates(slide, byId, aspect);
|
|
98
|
+
let chosen = candidates[0] ?? 'free';
|
|
99
|
+
let avoidedRepeat = false;
|
|
100
|
+
// Avoid three identical layouts in a row when an alternative remains.
|
|
101
|
+
if (previous.length >= 2 &&
|
|
102
|
+
previous[previous.length - 1] === previous[previous.length - 2] &&
|
|
103
|
+
previous[previous.length - 1] === chosen) {
|
|
104
|
+
const alt = candidates.find((id) => id !== chosen);
|
|
105
|
+
if (alt) {
|
|
106
|
+
chosen = alt;
|
|
107
|
+
avoidedRepeat = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const entry = byId.get(chosen);
|
|
111
|
+
const reasons = [
|
|
112
|
+
`Role "${slide.role}" with pacing "${slide.pacing}"`,
|
|
113
|
+
slide.blockTypes && slide.blockTypes.length > 0
|
|
114
|
+
? `blocks [${[...new Set(slide.blockTypes)].sort().join(', ')}]`
|
|
115
|
+
: null,
|
|
116
|
+
entry ? `fits catalog "${entry.id}" (${entry.label})` : null,
|
|
117
|
+
avoidedRepeat ? 'avoided a third identical layout in a row' : null,
|
|
118
|
+
aspect !== '16:9' ? `aspect ${aspect}` : null,
|
|
119
|
+
].filter(Boolean);
|
|
120
|
+
out.push({
|
|
121
|
+
id: slide.id,
|
|
122
|
+
layout: chosen,
|
|
123
|
+
rationale: `${reasons.join('; ')}.`,
|
|
124
|
+
override: false,
|
|
125
|
+
});
|
|
126
|
+
previous.push(chosen);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Minimal valid catalog `props` for a layout, seeded from the slide title.
|
|
132
|
+
* Used when rewriting free outline slides to catalog slides.
|
|
133
|
+
*/
|
|
134
|
+
export function propsForSelectedLayout(layoutId, slide) {
|
|
135
|
+
const entry = DECK_LAYOUT_BY_ID[layoutId] ?? getFromList(layoutId);
|
|
136
|
+
const sample = entry && typeof entry.sample === 'object' && entry.sample !== null
|
|
137
|
+
? structuredClone(entry.sample)
|
|
138
|
+
: {};
|
|
139
|
+
if (layoutId === 'free' || entry?.id === 'free') {
|
|
140
|
+
return { blocks: [] };
|
|
141
|
+
}
|
|
142
|
+
const title = slide.title?.trim();
|
|
143
|
+
if (title) {
|
|
144
|
+
if ('title' in sample)
|
|
145
|
+
sample.title = title;
|
|
146
|
+
else if (layoutId === 'big-stat' && 'label' in sample)
|
|
147
|
+
sample.label = title;
|
|
148
|
+
}
|
|
149
|
+
return sample;
|
|
150
|
+
}
|
|
151
|
+
function getFromList(id) {
|
|
152
|
+
return DECK_LAYOUTS.find((entry) => entry.id === id);
|
|
153
|
+
}
|
|
154
|
+
function rankCandidates(slide, byId, aspect) {
|
|
155
|
+
const pacing = normalizePacing(slide.pacing);
|
|
156
|
+
const roleKey = slide.role.trim().toLowerCase();
|
|
157
|
+
const fromRole = ROLE_LAYOUTS[roleKey] ?? [];
|
|
158
|
+
const fromBlocks = blockHints(slide.blockTypes);
|
|
159
|
+
const fromPacing = FALLBACK_BY_PACING[pacing] ?? FALLBACK_BY_PACING.idea ?? ['free'];
|
|
160
|
+
const ordered = unique([...fromBlocks, ...fromRole, ...fromPacing, 'free']);
|
|
161
|
+
const filtered = ordered.filter((id) => {
|
|
162
|
+
const entry = byId.get(id);
|
|
163
|
+
if (!entry)
|
|
164
|
+
return id === 'free';
|
|
165
|
+
if (entry.pacing.length > 0 && !entry.pacing.includes(pacing)) {
|
|
166
|
+
// Still allow if role strongly requested it
|
|
167
|
+
if (fromRole.includes(id) || fromBlocks.includes(id))
|
|
168
|
+
return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
// Portrait decks: prefer centered/free over wide multi-column when tied.
|
|
174
|
+
if (aspect === '9:16' || aspect === '1:1') {
|
|
175
|
+
filtered.sort((a, b) => portraitScore(b) - portraitScore(a));
|
|
176
|
+
}
|
|
177
|
+
return filtered.length > 0 ? filtered : ['free'];
|
|
178
|
+
}
|
|
179
|
+
function blockHints(types) {
|
|
180
|
+
if (!types || types.length === 0)
|
|
181
|
+
return [];
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const type of types) {
|
|
184
|
+
const layouts = BLOCK_LAYOUTS[type];
|
|
185
|
+
if (layouts)
|
|
186
|
+
out.push(...layouts);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function normalizePacing(value) {
|
|
191
|
+
const v = value.trim().toLowerCase();
|
|
192
|
+
if (v === 'punch' || v === 'idea' || v === 'diagram' || v === 'story')
|
|
193
|
+
return v;
|
|
194
|
+
return 'idea';
|
|
195
|
+
}
|
|
196
|
+
function unique(ids) {
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const id of ids) {
|
|
200
|
+
if (seen.has(id))
|
|
201
|
+
continue;
|
|
202
|
+
seen.add(id);
|
|
203
|
+
out.push(id);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function portraitScore(layoutId) {
|
|
208
|
+
if (layoutId === 'free' || layoutId.includes('centered') || layoutId === 'title-hero')
|
|
209
|
+
return 2;
|
|
210
|
+
if (layoutId === 'section-divider' || layoutId === 'quote' || layoutId === 'big-stat')
|
|
211
|
+
return 1;
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lazy authoring chunk behind `loadAuthoringApis()` (see `index.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Keep this a separate module: a dynamic `import()` of modules that the root
|
|
5
|
+
* entry also re-exports statically would pull them into the importer's first
|
|
6
|
+
* chunk, while a dynamic import of this file keeps them, and the deck layout
|
|
7
|
+
* catalog, in a chunk of their own.
|
|
8
|
+
*/
|
|
9
|
+
export { DECK_LAYOUTS as deckLayouts, describeLayouts as describeDeckLayouts, getDeckLayout, } from '@protoyard/decks/layouts';
|
|
10
|
+
export { lintDeckAssets, lintDeckContent, lintDeckStructure } from './authoring/deck-lint.ts';
|
|
11
|
+
export { propsForSelectedLayout, selectDeckLayouts } from './authoring/index.ts';
|
|
12
|
+
export { validateBlueprintForAuthoring } from './authoring-validation.ts';
|
|
13
|
+
export { blueprintFromBrief, buildDocOutline, buildPageOutline, detectBriefKind, synthesizeContentFromBrief, synthesizeDeckOutline, } from './brief/index.ts';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lazy authoring chunk behind `loadAuthoringApis()` (see `index.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Keep this a separate module: a dynamic `import()` of modules that the root
|
|
5
|
+
* entry also re-exports statically would pull them into the importer's first
|
|
6
|
+
* chunk, while a dynamic import of this file keeps them, and the deck layout
|
|
7
|
+
* catalog, in a chunk of their own.
|
|
8
|
+
*/
|
|
9
|
+
export { DECK_LAYOUTS as deckLayouts, describeLayouts as describeDeckLayouts, getDeckLayout, } from '@protoyard/decks/layouts';
|
|
10
|
+
export { lintDeckAssets, lintDeckContent, lintDeckStructure } from "./authoring/deck-lint.js";
|
|
11
|
+
export { propsForSelectedLayout, selectDeckLayouts } from "./authoring/index.js";
|
|
12
|
+
export { validateBlueprintForAuthoring } from "./authoring-validation.js";
|
|
13
|
+
export { blueprintFromBrief, buildDocOutline, buildPageOutline, detectBriefKind, synthesizeContentFromBrief, synthesizeDeckOutline, } from "./brief/index.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type BlueprintKind, type ContentKind } from './kinds/kind.ts';
|
|
2
|
+
/** A JSON path segment in a blueprint validation diagnostic. */
|
|
3
|
+
export type BlueprintDiagnosticPath = string | number;
|
|
4
|
+
/** One stable, actionable blueprint validation diagnostic. */
|
|
5
|
+
export type BlueprintAuthoringDiagnostic = {
|
|
6
|
+
/** Stable machine-readable category, independent of the Zod error wording. */
|
|
7
|
+
code: string;
|
|
8
|
+
/** Original JSON path segments. */
|
|
9
|
+
path: BlueprintDiagnosticPath[];
|
|
10
|
+
/** JSONPath-style rendering of `path`, with keys quoted for unambiguous use. */
|
|
11
|
+
jsonPath: string;
|
|
12
|
+
/** Concise schema or cross-reference explanation. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** A next step an author can take to resolve this diagnostic. */
|
|
15
|
+
hint: string;
|
|
16
|
+
};
|
|
17
|
+
/** Full blueprint validity and diagnostics. `valid` uses the complete Zod schema. */
|
|
18
|
+
export type BlueprintAuthoringValidation = {
|
|
19
|
+
valid: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The kind the input was validated as: `app` when `kind` is missing (the
|
|
22
|
+
* legacy compatibility rule), `null` when `kind` itself is unusable.
|
|
23
|
+
*/
|
|
24
|
+
kind: BlueprintKind | null;
|
|
25
|
+
diagnostics: BlueprintAuthoringDiagnostic[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Validate untrusted author input of any kind and return path-addressed repair
|
|
29
|
+
* guidance. Each kind runs through the shared kind union
|
|
30
|
+
* (`safeParseAnyBlueprint`): an app (with no `kind`, or `kind: "app"`) through
|
|
31
|
+
* the full app schema, including cross-reference checks, and a deck, page or
|
|
32
|
+
* doc through its strict envelope and body schema. A JSON Schema shape check
|
|
33
|
+
* alone is not enough to establish `valid`.
|
|
34
|
+
*
|
|
35
|
+
* Diagnostics never fetch network assets and never render markdown as HTML.
|
|
36
|
+
* Block and media failures map to stable codes (unknown block type, chart
|
|
37
|
+
* specs, alt text, URL schemes, step/fragment targets) with the original path.
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateBlueprintForAuthoring(input: unknown): Promise<BlueprintAuthoringValidation>;
|
|
40
|
+
/**
|
|
41
|
+
* Map path-addressed block issues (from `@protoyard/blocks` `safeParseBlock` /
|
|
42
|
+
* `safeParseBlocks` / `parseUntrustedBlocks`) into authoring diagnostics.
|
|
43
|
+
* `pathPrefix` is prepended (for example `["deck","slides",0,"blocks"]`) so
|
|
44
|
+
* the JSON path stays relative to the blueprint root. Does not mutate or
|
|
45
|
+
* auto-repair the input.
|
|
46
|
+
*/
|
|
47
|
+
export declare function diagnosticsFromBlockIssues(issues: readonly {
|
|
48
|
+
path: readonly (string | number)[];
|
|
49
|
+
message: string;
|
|
50
|
+
}[], pathPrefix?: readonly BlueprintDiagnosticPath[]): BlueprintAuthoringDiagnostic[];
|
|
51
|
+
/** Turn one Zod issue into a stable authoring diagnostic. */
|
|
52
|
+
export declare function issueToDiagnostic(issue: {
|
|
53
|
+
code: string;
|
|
54
|
+
message: string;
|
|
55
|
+
path: readonly PropertyKey[];
|
|
56
|
+
keys?: readonly PropertyKey[];
|
|
57
|
+
}, contentKind?: ContentKind | null): BlueprintAuthoringDiagnostic;
|
|
58
|
+
export declare function toJsonPath(path: readonly BlueprintDiagnosticPath[]): string;
|
|
59
|
+
/**
|
|
60
|
+
* Stable codes for authoring tools. Block/media messages are matched before
|
|
61
|
+
* generic Zod codes so chart, alt and scheme failures stay specific.
|
|
62
|
+
*/
|
|
63
|
+
export declare function diagnosticCode(zodCode: string, message: string, path?: readonly BlueprintDiagnosticPath[]): string;
|
|
64
|
+
/** Hints for block, media and generic blueprint failures. Never auto-repairs. */
|
|
65
|
+
export declare function blockOrAssetHint(code: string, message: string): string;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { safeParseAnyBlueprint } from "./kinds/envelope.js";
|
|
2
|
+
import { BLUEPRINT_KINDS, detectBlueprintKind, isContentKind, } from "./kinds/kind.js";
|
|
3
|
+
import { BLUEPRINT_BASE_SHAPE } from "./schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* Validate untrusted author input of any kind and return path-addressed repair
|
|
6
|
+
* guidance. Each kind runs through the shared kind union
|
|
7
|
+
* (`safeParseAnyBlueprint`): an app (with no `kind`, or `kind: "app"`) through
|
|
8
|
+
* the full app schema, including cross-reference checks, and a deck, page or
|
|
9
|
+
* doc through its strict envelope and body schema. A JSON Schema shape check
|
|
10
|
+
* alone is not enough to establish `valid`.
|
|
11
|
+
*
|
|
12
|
+
* Diagnostics never fetch network assets and never render markdown as HTML.
|
|
13
|
+
* Block and media failures map to stable codes (unknown block type, chart
|
|
14
|
+
* specs, alt text, URL schemes, step/fragment targets) with the original path.
|
|
15
|
+
*/
|
|
16
|
+
export async function validateBlueprintForAuthoring(input) {
|
|
17
|
+
const detected = detectBlueprintKind(input);
|
|
18
|
+
if (!detected.ok) {
|
|
19
|
+
return {
|
|
20
|
+
valid: false,
|
|
21
|
+
kind: null,
|
|
22
|
+
diagnostics: [kindDiagnostic(input, detected.message, detected.fix)],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const kind = detected.kind;
|
|
26
|
+
const result = await safeParseAnyBlueprint(input);
|
|
27
|
+
if (result.success)
|
|
28
|
+
return { valid: true, kind, diagnostics: [] };
|
|
29
|
+
return {
|
|
30
|
+
valid: false,
|
|
31
|
+
kind,
|
|
32
|
+
diagnostics: result.error.issues.map((issue) => issueToDiagnostic(issue, isContentKind(kind) ? kind : null)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Map path-addressed block issues (from `@protoyard/blocks` `safeParseBlock` /
|
|
37
|
+
* `safeParseBlocks` / `parseUntrustedBlocks`) into authoring diagnostics.
|
|
38
|
+
* `pathPrefix` is prepended (for example `["deck","slides",0,"blocks"]`) so
|
|
39
|
+
* the JSON path stays relative to the blueprint root. Does not mutate or
|
|
40
|
+
* auto-repair the input.
|
|
41
|
+
*/
|
|
42
|
+
export function diagnosticsFromBlockIssues(issues, pathPrefix = []) {
|
|
43
|
+
return issues.map((issue) => {
|
|
44
|
+
const path = [...pathPrefix, ...issue.path];
|
|
45
|
+
const code = diagnosticCode('custom', issue.message, path);
|
|
46
|
+
return {
|
|
47
|
+
code,
|
|
48
|
+
path,
|
|
49
|
+
jsonPath: toJsonPath(path),
|
|
50
|
+
message: issue.message,
|
|
51
|
+
hint: blockOrAssetHint(code, issue.message),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Turn one Zod issue into a stable authoring diagnostic. */
|
|
56
|
+
export function issueToDiagnostic(issue, contentKind = null) {
|
|
57
|
+
const path = [...issue.path];
|
|
58
|
+
const message = issue.message;
|
|
59
|
+
if (contentKind && issue.code === 'custom' && path.length === 1 && path[0] === 'kind') {
|
|
60
|
+
return {
|
|
61
|
+
code: 'kind-unavailable',
|
|
62
|
+
path,
|
|
63
|
+
jsonPath: toJsonPath(path),
|
|
64
|
+
message,
|
|
65
|
+
hint: `Update Protoyard to a build that ships the ${contentKind} engine, or author an app blueprint instead.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const code = diagnosticCode(issue.code, message, path);
|
|
69
|
+
return {
|
|
70
|
+
code,
|
|
71
|
+
path,
|
|
72
|
+
jsonPath: toJsonPath(path),
|
|
73
|
+
message,
|
|
74
|
+
hint: contentKind
|
|
75
|
+
? contentHint(contentKind, code, issue, path, message)
|
|
76
|
+
: blockOrAssetHint(code, message),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** `kind` is not a string, or names a kind this version does not know. */
|
|
80
|
+
function kindDiagnostic(input, message, fix) {
|
|
81
|
+
const kind = input.kind;
|
|
82
|
+
return {
|
|
83
|
+
code: typeof kind === 'string' ? 'unknown-kind' : 'invalid-kind',
|
|
84
|
+
path: ['kind'],
|
|
85
|
+
jsonPath: toJsonPath(['kind']),
|
|
86
|
+
message,
|
|
87
|
+
hint: fix,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const SHARED_KEYS = Object.keys(BLUEPRINT_BASE_SHAPE);
|
|
91
|
+
function contentHint(kind, code, issue, path, message) {
|
|
92
|
+
if (code === 'unknown-key' && path.length === 0) {
|
|
93
|
+
const others = (issue.keys ?? []).filter((key) => BLUEPRINT_KINDS.includes(key));
|
|
94
|
+
if (others.length) {
|
|
95
|
+
return `A ${kind} blueprint has exactly one body, "${kind}". Remove ${others.map((key) => `"${String(key)}"`).join(', ')}, or change "kind" to match it.`;
|
|
96
|
+
}
|
|
97
|
+
return `A ${kind} blueprint accepts only ${[...SHARED_KEYS, 'kind', kind].join(', ')}. App keys such as entities, roles and screens belong to app blueprints: remove them, or drop "kind" to author an app.`;
|
|
98
|
+
}
|
|
99
|
+
if (code === 'invalid-type' && path.length === 1 && path[0] === kind) {
|
|
100
|
+
return `Add the "${kind}" body: an object with the ${kind}'s content.`;
|
|
101
|
+
}
|
|
102
|
+
return blockOrAssetHint(code, message);
|
|
103
|
+
}
|
|
104
|
+
export function toJsonPath(path) {
|
|
105
|
+
let result = '$';
|
|
106
|
+
for (const segment of path) {
|
|
107
|
+
result += typeof segment === 'number' ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Stable codes for authoring tools. Block/media messages are matched before
|
|
113
|
+
* generic Zod codes so chart, alt and scheme failures stay specific.
|
|
114
|
+
*/
|
|
115
|
+
export function diagnosticCode(zodCode, message, path = []) {
|
|
116
|
+
const lower = message.toLowerCase();
|
|
117
|
+
const last = path.length > 0 ? path[path.length - 1] : undefined;
|
|
118
|
+
if (lower.includes('invalid discriminator') ||
|
|
119
|
+
(last === 'type' &&
|
|
120
|
+
(lower.includes('expected') ||
|
|
121
|
+
lower.includes('invalid option') ||
|
|
122
|
+
lower.includes('invalid input')))) {
|
|
123
|
+
return 'unknown-block-type';
|
|
124
|
+
}
|
|
125
|
+
if (lower.includes('describe the image in alt') ||
|
|
126
|
+
lower.includes('mark it decorative') ||
|
|
127
|
+
(last === 'alt' && lower.includes('alt'))) {
|
|
128
|
+
return 'missing-alt-text';
|
|
129
|
+
}
|
|
130
|
+
if (lower.includes('decorative image has no alt')) {
|
|
131
|
+
return 'invalid-alt-text';
|
|
132
|
+
}
|
|
133
|
+
if (lower.includes('http(s), mailto') ||
|
|
134
|
+
lower.includes('https: or mailto:') ||
|
|
135
|
+
lower.includes('no svg or remote') ||
|
|
136
|
+
lower.includes('no remote urls') ||
|
|
137
|
+
lower.includes('relative image path') ||
|
|
138
|
+
lower.includes('relative video path') ||
|
|
139
|
+
lower.includes('use avatar:av-') ||
|
|
140
|
+
lower.includes('base64 data uri') ||
|
|
141
|
+
lower.includes('javascript:') ||
|
|
142
|
+
lower.includes('protocol-relative')) {
|
|
143
|
+
return 'unsupported-url-scheme';
|
|
144
|
+
}
|
|
145
|
+
if (lower.includes('needs one value per label') ||
|
|
146
|
+
lower.includes('chart takes exactly one series') ||
|
|
147
|
+
lower.includes('chart needs non-negative') ||
|
|
148
|
+
lower.includes('chart data is not valid json') ||
|
|
149
|
+
lower.includes('chart data is too large') ||
|
|
150
|
+
lower.includes('invalid chart') ||
|
|
151
|
+
(path.includes('spec') &&
|
|
152
|
+
(lower.includes('series') || lower.includes('label') || lower.includes('kind')))) {
|
|
153
|
+
return 'invalid-chart-spec';
|
|
154
|
+
}
|
|
155
|
+
if (lower.includes('duplicate block id') || lower.includes('first used at')) {
|
|
156
|
+
return 'duplicate-block-id';
|
|
157
|
+
}
|
|
158
|
+
if (lower.includes('no such step') ||
|
|
159
|
+
lower.includes('unknown step') ||
|
|
160
|
+
lower.includes('invalid step') ||
|
|
161
|
+
lower.includes('fragment target') ||
|
|
162
|
+
lower.includes('unknown fragment') ||
|
|
163
|
+
lower.includes('unknown block id') ||
|
|
164
|
+
lower.includes('no such block') ||
|
|
165
|
+
(lower.includes('fragment') && lower.includes('target')) ||
|
|
166
|
+
(last === 'step' &&
|
|
167
|
+
(lower.includes('integer') || lower.includes('max') || lower.includes('min')))) {
|
|
168
|
+
return 'invalid-step-or-fragment-target';
|
|
169
|
+
}
|
|
170
|
+
if (message.startsWith('unknown entity') || message.includes('unknown entity "')) {
|
|
171
|
+
return 'unknown-entity';
|
|
172
|
+
}
|
|
173
|
+
if (message.startsWith('unknown field') ||
|
|
174
|
+
message.includes('unknown field "') ||
|
|
175
|
+
message.includes('is not a field of')) {
|
|
176
|
+
return 'unknown-field';
|
|
177
|
+
}
|
|
178
|
+
if (message.includes('must be one of') || message.includes('not one of')) {
|
|
179
|
+
return 'invalid-reference';
|
|
180
|
+
}
|
|
181
|
+
if (message.startsWith('duplicate ') || message.includes('duplicate ')) {
|
|
182
|
+
return 'duplicate-value';
|
|
183
|
+
}
|
|
184
|
+
if (message.includes('needs an ownerField'))
|
|
185
|
+
return 'missing-owner-field';
|
|
186
|
+
if (message.includes('cannot be') || message.includes('must be '))
|
|
187
|
+
return 'invalid-field-kind';
|
|
188
|
+
if (zodCode === 'unrecognized_keys')
|
|
189
|
+
return 'unknown-key';
|
|
190
|
+
if (zodCode === 'invalid_type')
|
|
191
|
+
return 'invalid-type';
|
|
192
|
+
if (zodCode === 'too_small')
|
|
193
|
+
return 'value-too-small';
|
|
194
|
+
if (zodCode === 'too_big')
|
|
195
|
+
return 'value-too-large';
|
|
196
|
+
if (zodCode === 'invalid_value')
|
|
197
|
+
return 'invalid-value';
|
|
198
|
+
if (zodCode === 'invalid_format')
|
|
199
|
+
return 'invalid-format';
|
|
200
|
+
if (zodCode === 'custom')
|
|
201
|
+
return 'invalid-reference';
|
|
202
|
+
return 'invalid-value';
|
|
203
|
+
}
|
|
204
|
+
/** Hints for block, media and generic blueprint failures. Never auto-repairs. */
|
|
205
|
+
export function blockOrAssetHint(code, message) {
|
|
206
|
+
if (code === 'unknown-block-type') {
|
|
207
|
+
return 'Set "type" to a supported block type (heading, text, image, chart, …). Custom or script types are not allowed.';
|
|
208
|
+
}
|
|
209
|
+
if (code === 'missing-alt-text') {
|
|
210
|
+
return 'Add a short alt description of the image, or set decorative: true when the image is purely decorative.';
|
|
211
|
+
}
|
|
212
|
+
if (code === 'invalid-alt-text') {
|
|
213
|
+
return 'Remove alt when decorative is true, or drop decorative and keep a meaningful alt.';
|
|
214
|
+
}
|
|
215
|
+
if (code === 'unsupported-url-scheme') {
|
|
216
|
+
return 'Use only allowed sources: avatar:av-001…175, a bounded data: URI, a relative asset path, or https:, mailto: and #section links. Remote media, http: links, relative links and javascript: URLs are refused; nothing is fetched during validation.';
|
|
217
|
+
}
|
|
218
|
+
if (code === 'invalid-chart-spec') {
|
|
219
|
+
return 'Fix the chart spec: every series needs one value per label; part-whole charts need one series of non-negative numbers; keep kind, title and labels within the schema bounds.';
|
|
220
|
+
}
|
|
221
|
+
if (code === 'duplicate-block-id') {
|
|
222
|
+
return 'Give each block a unique id within the document (deep links and interactive state key on id).';
|
|
223
|
+
}
|
|
224
|
+
if (code === 'invalid-step-or-fragment-target') {
|
|
225
|
+
return 'Point the step or fragment at an existing block id or a step index in range (0–32). Do not invent targets.';
|
|
226
|
+
}
|
|
227
|
+
return diagnosticHint(code, message);
|
|
228
|
+
}
|
|
229
|
+
function diagnosticHint(code, message) {
|
|
230
|
+
if (code === 'unknown-entity') {
|
|
231
|
+
return 'Use the exact name of an entity declared in entities, or add that entity first.';
|
|
232
|
+
}
|
|
233
|
+
if (code === 'unknown-field') {
|
|
234
|
+
return 'Use the exact field name declared on this entity, or add the field first.';
|
|
235
|
+
}
|
|
236
|
+
if (code === 'unknown-key') {
|
|
237
|
+
return 'Remove this key or replace it with a property supported by the blueprint schema.';
|
|
238
|
+
}
|
|
239
|
+
if (code === 'invalid-type') {
|
|
240
|
+
return 'Change this value to the type required by the schema at this path.';
|
|
241
|
+
}
|
|
242
|
+
if (code === 'value-too-small') {
|
|
243
|
+
return 'Add the required minimum number of items or increase this value to the allowed minimum.';
|
|
244
|
+
}
|
|
245
|
+
if (code === 'value-too-large') {
|
|
246
|
+
return 'Remove items or reduce this value to the allowed maximum.';
|
|
247
|
+
}
|
|
248
|
+
if (code === 'invalid-format') {
|
|
249
|
+
return 'Use the format shown by the schema message, such as kebab-case names or an ISO currency code.';
|
|
250
|
+
}
|
|
251
|
+
if (code === 'duplicate-value') {
|
|
252
|
+
return 'Give each item a unique name or value within its collection.';
|
|
253
|
+
}
|
|
254
|
+
if (code === 'missing-owner-field') {
|
|
255
|
+
return 'Add an ownerField that names a persona field, or remove the :own permission.';
|
|
256
|
+
}
|
|
257
|
+
if (code === 'invalid-field-kind') {
|
|
258
|
+
return 'Choose a field of the required kind, or change this setting to match the existing field.';
|
|
259
|
+
}
|
|
260
|
+
if (code === 'invalid-reference') {
|
|
261
|
+
return 'Update the reference to point to a declared entity, field, role, route, or allowed value.';
|
|
262
|
+
}
|
|
263
|
+
if (message.includes('money is integer cents')) {
|
|
264
|
+
return 'Use an integer amount in cents; for example, 1250 represents 12.50.';
|
|
265
|
+
}
|
|
266
|
+
return 'Adjust this value to satisfy the schema constraint described in the message.';
|
|
267
|
+
}
|
package/dist/avatar.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AVATAR_IDS, type AvatarId, isAvatarId } from '@protoyard/scenarios';
|
|
2
|
+
import type { BlueprintEntity, BlueprintField } from './schema.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Avatar fields: a face from the avatar library stored as its id
|
|
5
|
+
* (`av-001` … `av-175`). Generated rows get seeded faces in the generator;
|
|
6
|
+
* a person changes a face through `EntityService.shuffleAvatar` (a service
|
|
7
|
+
* mutation) or by picking one in the form.
|
|
8
|
+
*/
|
|
9
|
+
export type AvatarField = Extract<BlueprintField, {
|
|
10
|
+
kind: 'avatar';
|
|
11
|
+
}>;
|
|
12
|
+
export { AVATAR_IDS, type AvatarId, isAvatarId };
|
|
13
|
+
/** The entity's avatar fields, in declaration order. */
|
|
14
|
+
export declare function avatarFields(entity: Pick<BlueprintEntity, 'fields'>): AvatarField[];
|
|
15
|
+
/** The face shown next to a record's title: the entity's first avatar field. */
|
|
16
|
+
export declare function primaryAvatarField(entity: Pick<BlueprintEntity, 'fields'>): AvatarField | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The next face after `current` for a stable `key` (for example
|
|
19
|
+
* `customer:<id>:avatar`). Deterministic, with no clock or random source:
|
|
20
|
+
* each key walks the library with its own step, coprime with the library
|
|
21
|
+
* size, so repeated shuffles visit every face once before any repeats and
|
|
22
|
+
* never return `current`. A missing or unknown `current` starts at a
|
|
23
|
+
* key-derived face.
|
|
24
|
+
*/
|
|
25
|
+
export declare function nextAvatar(current: unknown, key: string): AvatarId;
|
package/dist/avatar.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { hashString } from '@protoyard/core';
|
|
2
|
+
import { AVATAR_IDS, isAvatarId } from '@protoyard/scenarios';
|
|
3
|
+
export { AVATAR_IDS, isAvatarId };
|
|
4
|
+
/** The entity's avatar fields, in declaration order. */
|
|
5
|
+
export function avatarFields(entity) {
|
|
6
|
+
return entity.fields.filter((field) => field.kind === 'avatar');
|
|
7
|
+
}
|
|
8
|
+
/** The face shown next to a record's title: the entity's first avatar field. */
|
|
9
|
+
export function primaryAvatarField(entity) {
|
|
10
|
+
return avatarFields(entity)[0];
|
|
11
|
+
}
|
|
12
|
+
const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
|
|
13
|
+
/**
|
|
14
|
+
* The next face after `current` for a stable `key` (for example
|
|
15
|
+
* `customer:<id>:avatar`). Deterministic, with no clock or random source:
|
|
16
|
+
* each key walks the library with its own step, coprime with the library
|
|
17
|
+
* size, so repeated shuffles visit every face once before any repeats and
|
|
18
|
+
* never return `current`. A missing or unknown `current` starts at a
|
|
19
|
+
* key-derived face.
|
|
20
|
+
*/
|
|
21
|
+
export function nextAvatar(current, key) {
|
|
22
|
+
const count = AVATAR_IDS.length;
|
|
23
|
+
const hash = hashString(key);
|
|
24
|
+
let step = 1 + (hash % (count - 1));
|
|
25
|
+
while (gcd(step, count) !== 1)
|
|
26
|
+
step = (step % (count - 1)) + 1;
|
|
27
|
+
const index = isAvatarId(current) ? AVATAR_IDS.indexOf(current) : -1;
|
|
28
|
+
const next = index < 0 ? hash % count : (index + step) % count;
|
|
29
|
+
return AVATAR_IDS[next];
|
|
30
|
+
}
|