@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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1145 -2
  3. package/dist/aggregate.d.ts +83 -0
  4. package/dist/aggregate.js +268 -0
  5. package/dist/app-model.d.ts +76 -0
  6. package/dist/app-model.js +287 -0
  7. package/dist/archetypes/index.d.ts +1 -0
  8. package/dist/archetypes/index.js +1 -0
  9. package/dist/archetypes/schema.d.ts +126 -0
  10. package/dist/archetypes/schema.js +77 -0
  11. package/dist/audit.d.ts +52 -0
  12. package/dist/audit.js +149 -0
  13. package/dist/authoring/deck-lint.d.ts +32 -0
  14. package/dist/authoring/deck-lint.js +567 -0
  15. package/dist/authoring/index.d.ts +1 -0
  16. package/dist/authoring/index.js +1 -0
  17. package/dist/authoring/select-deck-layouts.d.ts +50 -0
  18. package/dist/authoring/select-deck-layouts.js +213 -0
  19. package/dist/authoring-apis.d.ts +13 -0
  20. package/dist/authoring-apis.js +13 -0
  21. package/dist/authoring-validation.d.ts +65 -0
  22. package/dist/authoring-validation.js +267 -0
  23. package/dist/avatar.d.ts +25 -0
  24. package/dist/avatar.js +30 -0
  25. package/dist/brand.d.ts +70 -0
  26. package/dist/brand.js +294 -0
  27. package/dist/brief/archetypes.d.ts +28 -0
  28. package/dist/brief/archetypes.js +798 -0
  29. package/dist/brief/content-synthesize.d.ts +31 -0
  30. package/dist/brief/content-synthesize.js +210 -0
  31. package/dist/brief/deck-outlines.d.ts +96 -0
  32. package/dist/brief/deck-outlines.js +561 -0
  33. package/dist/brief/draft-bodies.d.ts +29 -0
  34. package/dist/brief/draft-bodies.js +224 -0
  35. package/dist/brief/fields.d.ts +19 -0
  36. package/dist/brief/fields.js +229 -0
  37. package/dist/brief/index.d.ts +8 -0
  38. package/dist/brief/index.js +8 -0
  39. package/dist/brief/kind-detection.d.ts +69 -0
  40. package/dist/brief/kind-detection.js +353 -0
  41. package/dist/brief/limits.d.ts +117 -0
  42. package/dist/brief/limits.js +217 -0
  43. package/dist/brief/page-doc-outlines.d.ts +80 -0
  44. package/dist/brief/page-doc-outlines.js +485 -0
  45. package/dist/brief/synthesize.d.ts +59 -0
  46. package/dist/brief/synthesize.js +511 -0
  47. package/dist/brief/text.d.ts +13 -0
  48. package/dist/brief/text.js +59 -0
  49. package/dist/canvas-screen.d.ts +31 -0
  50. package/dist/canvas-screen.js +33 -0
  51. package/dist/canvas-seed.d.ts +58 -0
  52. package/dist/canvas-seed.js +82 -0
  53. package/dist/computed/evaluate.d.ts +11 -0
  54. package/dist/computed/evaluate.js +101 -0
  55. package/dist/computed/index.d.ts +4 -0
  56. package/dist/computed/index.js +3 -0
  57. package/dist/computed/public.d.ts +14 -0
  58. package/dist/computed/public.js +19 -0
  59. package/dist/computed/schema.d.ts +10 -0
  60. package/dist/computed/schema.js +140 -0
  61. package/dist/computed/types.d.ts +70 -0
  62. package/dist/computed/types.js +1 -0
  63. package/dist/computed/validate.d.ts +6 -0
  64. package/dist/computed/validate.js +146 -0
  65. package/dist/csv-guard.d.ts +15 -0
  66. package/dist/csv-guard.js +29 -0
  67. package/dist/csv.d.ts +85 -0
  68. package/dist/csv.js +288 -0
  69. package/dist/files.d.ts +35 -0
  70. package/dist/files.js +246 -0
  71. package/dist/form-schema.d.ts +124 -0
  72. package/dist/form-schema.js +300 -0
  73. package/dist/generator-version.d.ts +11 -0
  74. package/dist/generator-version.js +51 -0
  75. package/dist/generator.d.ts +34 -0
  76. package/dist/generator.js +445 -0
  77. package/dist/identity.d.ts +34 -0
  78. package/dist/identity.js +91 -0
  79. package/dist/import/csv-shape.d.ts +40 -0
  80. package/dist/import/csv-shape.js +265 -0
  81. package/dist/import/json-shape.d.ts +55 -0
  82. package/dist/import/json-shape.js +491 -0
  83. package/dist/import/limits.d.ts +13 -0
  84. package/dist/import/limits.js +14 -0
  85. package/dist/import/sqlite-schema.d.ts +36 -0
  86. package/dist/import/sqlite-schema.js +729 -0
  87. package/dist/index.d.ts +60 -0
  88. package/dist/index.js +58 -0
  89. package/dist/kinds/anchors.d.ts +18 -0
  90. package/dist/kinds/anchors.js +46 -0
  91. package/dist/kinds/bodies.d.ts +6 -0
  92. package/dist/kinds/bodies.js +32 -0
  93. package/dist/kinds/brand-runtime.d.ts +19 -0
  94. package/dist/kinds/brand-runtime.js +35 -0
  95. package/dist/kinds/deck.d.ts +8 -0
  96. package/dist/kinds/deck.js +34 -0
  97. package/dist/kinds/doc.d.ts +19 -0
  98. package/dist/kinds/doc.js +41 -0
  99. package/dist/kinds/embed/index.d.ts +39 -0
  100. package/dist/kinds/embed/index.js +91 -0
  101. package/dist/kinds/envelope.d.ts +159 -0
  102. package/dist/kinds/envelope.js +78 -0
  103. package/dist/kinds/index.d.ts +11 -0
  104. package/dist/kinds/index.js +11 -0
  105. package/dist/kinds/kind.d.ts +36 -0
  106. package/dist/kinds/kind.js +55 -0
  107. package/dist/kinds/limits.d.ts +64 -0
  108. package/dist/kinds/limits.js +190 -0
  109. package/dist/kinds/page-doc-runtime.d.ts +37 -0
  110. package/dist/kinds/page-doc-runtime.js +55 -0
  111. package/dist/kinds/page.d.ts +16 -0
  112. package/dist/kinds/page.js +67 -0
  113. package/dist/kinds/parse.d.ts +18 -0
  114. package/dist/kinds/parse.js +27 -0
  115. package/dist/kinds/runtime.d.ts +128 -0
  116. package/dist/kinds/runtime.js +206 -0
  117. package/dist/kinds/services.d.ts +49 -0
  118. package/dist/kinds/services.js +249 -0
  119. package/dist/kinds/testing.d.ts +9 -0
  120. package/dist/kinds/testing.js +16 -0
  121. package/dist/migrations.d.ts +97 -0
  122. package/dist/migrations.js +351 -0
  123. package/dist/motion.d.ts +50 -0
  124. package/dist/motion.js +52 -0
  125. package/dist/notifications.d.ts +19 -0
  126. package/dist/notifications.js +177 -0
  127. package/dist/patch.d.ts +122 -0
  128. package/dist/patch.js +322 -0
  129. package/dist/policy.d.ts +23 -0
  130. package/dist/policy.js +105 -0
  131. package/dist/runtime-definition.d.ts +39 -0
  132. package/dist/runtime-definition.js +74 -0
  133. package/dist/scenarios.d.ts +9 -0
  134. package/dist/scenarios.js +49 -0
  135. package/dist/schema/blueprint.schema.json +10191 -0
  136. package/dist/schema-json.d.ts +6 -0
  137. package/dist/schema-json.js +36 -0
  138. package/dist/schema.d.ts +6348 -0
  139. package/dist/schema.js +1269 -0
  140. package/dist/search.d.ts +16 -0
  141. package/dist/search.js +96 -0
  142. package/dist/services.d.ts +89 -0
  143. package/dist/services.js +898 -0
  144. package/dist/share.d.ts +78 -0
  145. package/dist/share.js +234 -0
  146. package/dist/sheet-screen.d.ts +49 -0
  147. package/dist/sheet-screen.js +91 -0
  148. package/dist/storage.d.ts +43 -0
  149. package/dist/storage.js +179 -0
  150. package/dist/stream.d.ts +26 -0
  151. package/dist/stream.js +173 -0
  152. package/dist/untrusted.d.ts +63 -0
  153. package/dist/untrusted.js +502 -0
  154. package/dist/validation-tasks.d.ts +178 -0
  155. package/dist/validation-tasks.js +199 -0
  156. package/dist/validation.d.ts +35 -0
  157. package/dist/validation.js +312 -0
  158. package/dist/variants.d.ts +112 -0
  159. package/dist/variants.js +283 -0
  160. package/dist/workflows/index.d.ts +3 -0
  161. package/dist/workflows/index.js +3 -0
  162. package/dist/workflows/schema.d.ts +50 -0
  163. package/dist/workflows/schema.js +34 -0
  164. package/dist/workflows/transition.d.ts +15 -0
  165. package/dist/workflows/transition.js +25 -0
  166. package/dist/workflows/validate.d.ts +17 -0
  167. package/dist/workflows/validate.js +140 -0
  168. package/package.json +120 -3
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Brief kind detection (ADR-036): decides whether a prose brief asks for an
3
+ * app, a deck, a page or a doc before any app archetype is scored.
4
+ *
5
+ * The rules are deliberately conservative, because `app` is what every brief
6
+ * meant before kinds existed:
7
+ *
8
+ * 1. An explicit kind always wins.
9
+ * 2. A content kind is chosen only on a strong cue ("slides", "keynote",
10
+ * "landing page", "report", "one-pager"…). Weak cues ("talk", "hero",
11
+ * "article") are reported as a suggestion but never switch the kind.
12
+ * 3. A cue followed by a tool noun ("slide deck builder", "report generator")
13
+ * describes an app that makes that content, so it counts for `app`.
14
+ * 4. Strong cues for two content kinds, or an app noun ("app", "dashboard",
15
+ * "platform"…) before the first strong cue, make the brief ambiguous: the
16
+ * result is `app`, with a reason that names the conflict and how to pick.
17
+ *
18
+ * Matching is on whole lowercase words (`normalize`), longest phrase first,
19
+ * so the same brief always gives the same result.
20
+ */
21
+ import { BLUEPRINT_KINDS, DEFAULT_BLUEPRINT_KIND, isBlueprintKind, unknownKindMessage, } from "../kinds/kind.js";
22
+ import { MAX_BRIEF_LENGTH } from "./limits.js";
23
+ import { normalize } from "./text.js";
24
+ /** Briefs longer than this are read up to the limit (matches `MAX_BRIEF_LENGTH`). */
25
+ const MAX_DETECTION_LENGTH = MAX_BRIEF_LENGTH;
26
+ /** Content cues per kind, in normalized words ("one-pager" normalizes to "one pager"). */
27
+ export const KIND_CUES = {
28
+ deck: {
29
+ strong: [
30
+ 'slides',
31
+ 'slide deck',
32
+ 'slide decks',
33
+ 'slideshow',
34
+ 'slide show',
35
+ 'deck',
36
+ 'decks',
37
+ 'pitch deck',
38
+ 'keynote',
39
+ 'presentation',
40
+ 'presentations',
41
+ 'powerpoint',
42
+ ],
43
+ weak: [
44
+ 'slide',
45
+ 'talk',
46
+ 'pitch',
47
+ 'qbr',
48
+ 'webinar',
49
+ 'lecture',
50
+ 'all hands',
51
+ 'town hall',
52
+ 'demo day',
53
+ ],
54
+ },
55
+ page: {
56
+ strong: [
57
+ 'landing page',
58
+ 'landing pages',
59
+ 'one page site',
60
+ 'one page website',
61
+ 'single page site',
62
+ 'single page website',
63
+ 'marketing page',
64
+ 'marketing site',
65
+ 'marketing website',
66
+ 'homepage',
67
+ 'home page',
68
+ 'launch page',
69
+ 'waitlist page',
70
+ 'coming soon page',
71
+ 'pricing page',
72
+ 'product page',
73
+ 'sales page',
74
+ 'splash page',
75
+ 'microsite',
76
+ 'portfolio site',
77
+ 'portfolio website',
78
+ ],
79
+ weak: ['website', 'hero section', 'call to action', 'testimonials', 'waitlist', 'signup page'],
80
+ },
81
+ doc: {
82
+ // Bare "report" and "document" are often verbs ("sensors report telemetry"),
83
+ // so only a determiner or a qualifier makes them strong.
84
+ strong: [
85
+ 'a report',
86
+ 'the report',
87
+ 'our report',
88
+ 'this report',
89
+ 'annual report',
90
+ 'quarterly report',
91
+ 'monthly report',
92
+ 'weekly report',
93
+ 'weekly reports',
94
+ 'monthly reports',
95
+ 'status report',
96
+ 'progress report',
97
+ 'research report',
98
+ 'incident report',
99
+ 'investor report',
100
+ 'board report',
101
+ 'impact report',
102
+ 'one pager',
103
+ 'one pagers',
104
+ 'one page report',
105
+ 'one page summary',
106
+ 'write a report',
107
+ 'write the report',
108
+ 'write a one page',
109
+ 'whitepaper',
110
+ 'white paper',
111
+ 'memo',
112
+ 'memos',
113
+ 'handbook',
114
+ 'playbook',
115
+ 'proposal',
116
+ 'design doc',
117
+ 'design document',
118
+ 'a document',
119
+ 'the document',
120
+ 'write up',
121
+ 'writeup',
122
+ 'case study',
123
+ 'postmortem',
124
+ 'post mortem',
125
+ 'rfc',
126
+ ],
127
+ weak: [
128
+ 'report',
129
+ 'reports',
130
+ 'document',
131
+ 'article',
132
+ 'essay',
133
+ 'summary',
134
+ 'guide',
135
+ 'manual',
136
+ 'documentation',
137
+ 'faq',
138
+ 'readme',
139
+ ],
140
+ },
141
+ };
142
+ /** Nouns that name an app; before the first content cue, they keep the brief an app. */
143
+ export const APP_CUES = [
144
+ 'app',
145
+ 'apps',
146
+ 'application',
147
+ 'applications',
148
+ 'web app',
149
+ 'mobile app',
150
+ 'dashboard',
151
+ 'dashboards',
152
+ 'portal',
153
+ 'platform',
154
+ 'saas',
155
+ 'software',
156
+ 'system',
157
+ 'tracker',
158
+ 'crm',
159
+ 'erp',
160
+ 'admin panel',
161
+ 'back office',
162
+ 'internal tool',
163
+ 'wiki',
164
+ 'knowledge base',
165
+ 'workspace',
166
+ ];
167
+ /** A content cue followed by one of these names a tool that makes the content (an app). */
168
+ const TOOL_NOUNS = [
169
+ 'dashboard',
170
+ 'dashboards',
171
+ 'portal',
172
+ 'app',
173
+ 'apps',
174
+ 'application',
175
+ 'builder',
176
+ 'builders',
177
+ 'editor',
178
+ 'editors',
179
+ 'maker',
180
+ 'makers',
181
+ 'generator',
182
+ 'generators',
183
+ 'creator',
184
+ 'creators',
185
+ 'tool',
186
+ 'tools',
187
+ 'software',
188
+ 'platform',
189
+ 'manager',
190
+ 'management',
191
+ 'tracker',
192
+ 'system',
193
+ ];
194
+ /** Phrases where a cue word means something else; matches inside them are ignored. */
195
+ const FALSE_FRIENDS = [
196
+ 'deck of cards',
197
+ 'card deck',
198
+ 'card decks',
199
+ 'boat deck',
200
+ 'sun deck',
201
+ 'deck chair',
202
+ 'pool deck',
203
+ 'presentation layer',
204
+ ];
205
+ /** Every occurrence of every phrase as a word span, overlapping allowed. */
206
+ function findSpans(words, phrases) {
207
+ const spans = [];
208
+ for (const phrase of phrases) {
209
+ const parts = phrase.split(' ');
210
+ for (let i = 0; i + parts.length <= words.length; i++) {
211
+ if (parts.every((part, j) => words[i + j] === part)) {
212
+ spans.push({ start: i, end: i + parts.length, phrase });
213
+ }
214
+ }
215
+ }
216
+ return spans;
217
+ }
218
+ const overlaps = (a, b) => a.start < b.end && b.start < a.end;
219
+ function collectCues(words) {
220
+ const blocked = findSpans(words, FALSE_FRIENDS);
221
+ const candidates = [];
222
+ for (const kind of BLUEPRINT_KINDS) {
223
+ if (kind === 'app') {
224
+ for (const span of findSpans(words, APP_CUES)) {
225
+ candidates.push({ ...span, kind, strength: 'strong' });
226
+ }
227
+ continue;
228
+ }
229
+ for (const strength of ['strong', 'weak']) {
230
+ for (const span of findSpans(words, KIND_CUES[kind][strength])) {
231
+ candidates.push({ ...span, kind, strength });
232
+ }
233
+ }
234
+ }
235
+ // Longest phrase first (then earliest, then strongest), so "pitch deck" beats
236
+ // "pitch" and "deck", and each word belongs to at most one cue.
237
+ candidates.sort((a, b) => b.end - b.start - (a.end - a.start) ||
238
+ a.start - b.start ||
239
+ (a.strength === b.strength ? 0 : a.strength === 'strong' ? -1 : 1));
240
+ const accepted = [];
241
+ for (const candidate of candidates) {
242
+ if (blocked.some((span) => overlaps(span, candidate)))
243
+ continue;
244
+ if (accepted.some((span) => overlaps(span, candidate)))
245
+ continue;
246
+ accepted.push(candidate);
247
+ }
248
+ return accepted
249
+ .map((cue) => {
250
+ // "report generator", "slide deck builder": an app that makes the content.
251
+ const next = words[cue.end];
252
+ const tool = cue.kind !== 'app' && next !== undefined && TOOL_NOUNS.includes(next);
253
+ return tool
254
+ ? { kind: 'app', phrase: `${cue.phrase} ${next}`, strength: 'strong', index: cue.start }
255
+ : { kind: cue.kind, phrase: cue.phrase, strength: cue.strength, index: cue.start };
256
+ })
257
+ .sort((a, b) => a.index - b.index);
258
+ }
259
+ const quoted = (cues) => [...new Set(cues.map((cue) => `"${cue.phrase}"`))].join(', ');
260
+ const CONTENT_ORDER = ['deck', 'page', 'doc'];
261
+ /**
262
+ * Detects the kind a brief asks for. Deterministic: the same brief and options
263
+ * always give the same result. Throws a `RangeError` for an explicit kind this
264
+ * version does not know.
265
+ */
266
+ export function detectBriefKind(brief, options = {}) {
267
+ const words = normalize(brief.slice(0, MAX_DETECTION_LENGTH)).split(' ').filter(Boolean);
268
+ const cues = collectCues(words);
269
+ if (options.kind !== undefined) {
270
+ if (!isBlueprintKind(options.kind))
271
+ throw new RangeError(unknownKindMessage(options.kind));
272
+ return {
273
+ kind: options.kind,
274
+ confidence: 'explicit',
275
+ ambiguous: false,
276
+ reason: `Kind "${options.kind}" was chosen explicitly.`,
277
+ cues,
278
+ suggestion: null,
279
+ };
280
+ }
281
+ const strongBy = (kind) => cues.filter((cue) => cue.kind === kind && cue.strength === 'strong');
282
+ const contentStrong = CONTENT_ORDER.filter((kind) => strongBy(kind).length > 0);
283
+ const appCues = strongBy('app');
284
+ if (contentStrong.length === 0) {
285
+ const weak = cues.filter((cue) => cue.kind !== 'app' && cue.strength === 'weak');
286
+ if (weak.length === 0) {
287
+ return {
288
+ kind: DEFAULT_BLUEPRINT_KIND,
289
+ confidence: 'fallback',
290
+ ambiguous: false,
291
+ reason: appCues.length > 0
292
+ ? `App cues (${quoted(appCues)}) and no deck, page or doc cue: this is an app.`
293
+ : 'No deck, page or doc cue: app is the default kind.',
294
+ cues,
295
+ suggestion: null,
296
+ };
297
+ }
298
+ // Weak cues only lean one way; the earliest weak cue breaks a tie.
299
+ const counts = CONTENT_ORDER.map((kind) => ({
300
+ kind,
301
+ count: weak.filter((cue) => cue.kind === kind).length,
302
+ first: weak.find((cue) => cue.kind === kind)?.index ?? Number.POSITIVE_INFINITY,
303
+ })).sort((a, b) => b.count - a.count || a.first - b.first);
304
+ const lean = counts[0].kind;
305
+ return {
306
+ kind: DEFAULT_BLUEPRINT_KIND,
307
+ confidence: 'fallback',
308
+ ambiguous: false,
309
+ reason: `Only weak ${lean} cues (${quoted(weak.filter((cue) => cue.kind === lean))}): kept app. Pass kind "${lean}" to make a ${lean}.`,
310
+ cues,
311
+ suggestion: lean,
312
+ };
313
+ }
314
+ if (contentStrong.length > 1) {
315
+ const listed = contentStrong.map((kind) => `${kind} (${quoted(strongBy(kind))})`).join(' and ');
316
+ const first = cues.find((cue) => cue.strength === 'strong' && contentStrong.includes(cue.kind));
317
+ return {
318
+ kind: DEFAULT_BLUEPRINT_KIND,
319
+ confidence: 'fallback',
320
+ ambiguous: true,
321
+ reason: `Cues for ${listed} conflict: kept app. Pass the kind you want explicitly.`,
322
+ cues,
323
+ suggestion: first?.kind ?? null,
324
+ };
325
+ }
326
+ const kind = contentStrong[0];
327
+ const kindCues = strongBy(kind);
328
+ const firstContent = kindCues[0].index;
329
+ const appBefore = appCues.filter((cue) => cue.index < firstContent);
330
+ if (appBefore.length > 0) {
331
+ return {
332
+ kind: DEFAULT_BLUEPRINT_KIND,
333
+ confidence: 'fallback',
334
+ ambiguous: true,
335
+ reason: `App cues (${quoted(appBefore)}) come before the ${kind} cues (${quoted(kindCues)}): kept app. Pass kind "${kind}" to make a ${kind}.`,
336
+ cues,
337
+ suggestion: kind,
338
+ };
339
+ }
340
+ return {
341
+ kind,
342
+ confidence: appCues.length > 0 ? 'medium' : 'high',
343
+ ambiguous: false,
344
+ reason: appCues.length > 0
345
+ ? `${capitalize(kind)} cues (${quoted(kindCues)}) lead the brief; app words (${quoted(appCues)}) only describe the subject.`
346
+ : `${capitalize(kind)} cues: ${quoted(kindCues)}.`,
347
+ cues,
348
+ suggestion: null,
349
+ };
350
+ }
351
+ function capitalize(text) {
352
+ return text.charAt(0).toUpperCase() + text.slice(1);
353
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Authoring bounds for briefs and generated outlines (all four kinds).
3
+ *
4
+ * The brief character cap matches historical app synthesis. Slide, section,
5
+ * block and text ceilings agree with the untrusted block contract
6
+ * (`@protoyard/blocks` BLOCK_CONTENT_LIMITS / BLOCK_LIMITS) so a synthesizer
7
+ * never emits drafts that the kind/block validators would reject for size.
8
+ *
9
+ * Oversized **briefs** are truncated with a stable notice (synthesis still
10
+ * runs). Oversized **outline structure** yields stable diagnostics; optional
11
+ * `truncate: true` clips deterministically instead of failing.
12
+ */
13
+ import type { BlueprintKind } from '../kinds/kind.ts';
14
+ /** Longer briefs are cut here; synthesizers read keywords, not essays. */
15
+ export declare const MAX_BRIEF_LENGTH = 5000;
16
+ /** Same cap for deck / page / doc content briefs. */
17
+ export declare const MAX_CONTENT_BRIEF_LENGTH = 5000;
18
+ /**
19
+ * Outline structure and text ceilings. Numbers mirror block content limits
20
+ * (markdown 4_000, short body 600, blocks-per-column 12) and keep generated
21
+ * decks/pages within a practical authoring range.
22
+ */
23
+ export declare const OUTLINE_LIMITS: Readonly<{
24
+ /** Maximum slides in one generated or agent-authored deck outline. */
25
+ maxSlides: 64;
26
+ /** Maximum top-level sections in a page or doc outline. */
27
+ maxSections: 64;
28
+ /**
29
+ * Blocks nested under one slide or section (matches blocks-per-column
30
+ * capacity so a slide always fits).
31
+ */
32
+ maxBlocksPerUnit: 12;
33
+ /** Title / heading string length. */
34
+ maxTitleLength: 80;
35
+ /** Short body / bullet / card copy (block shortMarkdown). */
36
+ maxBodyLength: 600;
37
+ /** Long text / markdown block body (block markdown). */
38
+ maxTextLength: 4000;
39
+ /** Description / rationale line on the envelope. */
40
+ maxDescriptionLength: 280;
41
+ }>;
42
+ export type OutlineLimits = {
43
+ [Key in keyof typeof OUTLINE_LIMITS]: number;
44
+ };
45
+ export type BriefLimitCode = 'brief-too-long' | 'too-many-slides' | 'too-many-sections' | 'too-many-blocks' | 'title-too-long' | 'body-too-long' | 'text-too-long';
46
+ export type BriefLimitIssue = {
47
+ code: BriefLimitCode;
48
+ path: (string | number)[];
49
+ message: string;
50
+ fix: string;
51
+ limit: number;
52
+ actual: number;
53
+ };
54
+ export type BoundBriefInput = {
55
+ /** Brief text ready for synthesis (never longer than the cap). */
56
+ brief: string;
57
+ /** True when the original was longer than the cap. */
58
+ truncated: boolean;
59
+ /** Original length before truncation. */
60
+ originalLength: number;
61
+ /** Documented notices for callers to surface (stable wording). */
62
+ notices: string[];
63
+ };
64
+ /** One slide or section unit inside a generated outline. */
65
+ export type OutlineUnitInput = {
66
+ id?: string;
67
+ title?: string;
68
+ body?: string;
69
+ text?: string;
70
+ blocks?: unknown[];
71
+ };
72
+ /** Structure checked before synthesis or agent draft validation. */
73
+ export type OutlineBoundsInput = {
74
+ kind: BlueprintKind;
75
+ title?: string;
76
+ description?: string;
77
+ slides?: OutlineUnitInput[];
78
+ sections?: OutlineUnitInput[];
79
+ };
80
+ export type CheckOutlineBoundsResult = {
81
+ ok: true;
82
+ issues: [];
83
+ } | {
84
+ ok: false;
85
+ issues: BriefLimitIssue[];
86
+ };
87
+ export type ApplyOutlineBoundsResult = {
88
+ outline: OutlineBoundsInput;
89
+ truncated: boolean;
90
+ notices: string[];
91
+ issues: BriefLimitIssue[];
92
+ };
93
+ /**
94
+ * Enforce the brief character cap. Always returns a string ≤ the limit;
95
+ * never throws. Deterministic for the same input and limit.
96
+ */
97
+ export declare function boundBriefInput(brief: string, options?: {
98
+ maxLength?: number;
99
+ }): BoundBriefInput;
100
+ /**
101
+ * Validate an outline structure against the documented ceilings.
102
+ * Does not mutate input. Empty collections are allowed (scaffolding).
103
+ */
104
+ export declare function checkOutlineBounds(input: OutlineBoundsInput): CheckOutlineBoundsResult;
105
+ /**
106
+ * Deterministically clip an outline to the ceilings. Prefer `checkOutlineBounds`
107
+ * when you want hard failures; use this when synthesis should continue with a
108
+ * documented truncation notice.
109
+ */
110
+ export declare function applyOutlineBounds(input: OutlineBoundsInput): ApplyOutlineBoundsResult;
111
+ /**
112
+ * Convenience for synthesizers: bound the brief, then return source + notices.
113
+ * Same behavior as historical `brief.slice(0, MAX_BRIEF_LENGTH)`.
114
+ */
115
+ export declare function prepareBriefForSynthesis(brief: string, options?: {
116
+ maxLength?: number;
117
+ }): BoundBriefInput;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Authoring bounds for briefs and generated outlines (all four kinds).
3
+ *
4
+ * The brief character cap matches historical app synthesis. Slide, section,
5
+ * block and text ceilings agree with the untrusted block contract
6
+ * (`@protoyard/blocks` BLOCK_CONTENT_LIMITS / BLOCK_LIMITS) so a synthesizer
7
+ * never emits drafts that the kind/block validators would reject for size.
8
+ *
9
+ * Oversized **briefs** are truncated with a stable notice (synthesis still
10
+ * runs). Oversized **outline structure** yields stable diagnostics; optional
11
+ * `truncate: true` clips deterministically instead of failing.
12
+ */
13
+ /** Longer briefs are cut here; synthesizers read keywords, not essays. */
14
+ export const MAX_BRIEF_LENGTH = 5_000;
15
+ /** Same cap for deck / page / doc content briefs. */
16
+ export const MAX_CONTENT_BRIEF_LENGTH = MAX_BRIEF_LENGTH;
17
+ /**
18
+ * Outline structure and text ceilings. Numbers mirror block content limits
19
+ * (markdown 4_000, short body 600, blocks-per-column 12) and keep generated
20
+ * decks/pages within a practical authoring range.
21
+ */
22
+ export const OUTLINE_LIMITS = Object.freeze({
23
+ /** Maximum slides in one generated or agent-authored deck outline. */
24
+ maxSlides: 64,
25
+ /** Maximum top-level sections in a page or doc outline. */
26
+ maxSections: 64,
27
+ /**
28
+ * Blocks nested under one slide or section (matches blocks-per-column
29
+ * capacity so a slide always fits).
30
+ */
31
+ maxBlocksPerUnit: 12,
32
+ /** Title / heading string length. */
33
+ maxTitleLength: 80,
34
+ /** Short body / bullet / card copy (block shortMarkdown). */
35
+ maxBodyLength: 600,
36
+ /** Long text / markdown block body (block markdown). */
37
+ maxTextLength: 4_000,
38
+ /** Description / rationale line on the envelope. */
39
+ maxDescriptionLength: 280,
40
+ });
41
+ function noticeBriefTruncated(original, limit) {
42
+ return `Brief truncated from ${original} to ${limit} characters (MAX_BRIEF_LENGTH).`;
43
+ }
44
+ /**
45
+ * Enforce the brief character cap. Always returns a string ≤ the limit;
46
+ * never throws. Deterministic for the same input and limit.
47
+ */
48
+ export function boundBriefInput(brief, options = {}) {
49
+ const limit = options.maxLength ?? MAX_BRIEF_LENGTH;
50
+ const source = typeof brief === 'string' ? brief : '';
51
+ const originalLength = source.length;
52
+ if (originalLength <= limit) {
53
+ return { brief: source, truncated: false, originalLength, notices: [] };
54
+ }
55
+ return {
56
+ brief: source.slice(0, limit),
57
+ truncated: true,
58
+ originalLength,
59
+ notices: [noticeBriefTruncated(originalLength, limit)],
60
+ };
61
+ }
62
+ function issue(code, path, limit, actual, message, fix) {
63
+ return { code, path, message, fix, limit, actual };
64
+ }
65
+ function checkString(value, path, limit, code, label) {
66
+ if (value === undefined)
67
+ return null;
68
+ if (value.length <= limit)
69
+ return null;
70
+ return issue(code, path, limit, value.length, `${label} is ${value.length} characters; the limit is ${limit}.`, `Shorten ${label.toLowerCase()} to at most ${limit} characters.`);
71
+ }
72
+ function checkUnits(kind, units, collection, maxUnits) {
73
+ const issues = [];
74
+ if (!units)
75
+ return issues;
76
+ if (units.length > maxUnits) {
77
+ issues.push(issue(collection === 'slides' ? 'too-many-slides' : 'too-many-sections', [collection], maxUnits, units.length, `${kind} outline has ${units.length} ${collection}; the limit is ${maxUnits}.`, `Reduce ${collection} to at most ${maxUnits}.`));
78
+ }
79
+ const limit = OUTLINE_LIMITS.maxBlocksPerUnit;
80
+ units.forEach((unit, index) => {
81
+ const pathBase = [collection, index];
82
+ const titleIssue = checkString(unit.title, [...pathBase, 'title'], OUTLINE_LIMITS.maxTitleLength, 'title-too-long', 'Title');
83
+ if (titleIssue)
84
+ issues.push(titleIssue);
85
+ const bodyIssue = checkString(unit.body, [...pathBase, 'body'], OUTLINE_LIMITS.maxBodyLength, 'body-too-long', 'Body');
86
+ if (bodyIssue)
87
+ issues.push(bodyIssue);
88
+ const textIssue = checkString(unit.text, [...pathBase, 'text'], OUTLINE_LIMITS.maxTextLength, 'text-too-long', 'Text');
89
+ if (textIssue)
90
+ issues.push(textIssue);
91
+ const blockCount = Array.isArray(unit.blocks) ? unit.blocks.length : 0;
92
+ if (blockCount > limit) {
93
+ issues.push(issue('too-many-blocks', [...pathBase, 'blocks'], limit, blockCount, `${collection.slice(0, -1)} has ${blockCount} blocks; the limit is ${limit}.`, `Keep at most ${limit} blocks per ${collection.slice(0, -1)}.`));
94
+ }
95
+ });
96
+ return issues;
97
+ }
98
+ /**
99
+ * Validate an outline structure against the documented ceilings.
100
+ * Does not mutate input. Empty collections are allowed (scaffolding).
101
+ */
102
+ export function checkOutlineBounds(input) {
103
+ const issues = [];
104
+ const titleIssue = checkString(input.title, ['title'], OUTLINE_LIMITS.maxTitleLength, 'title-too-long', 'Title');
105
+ if (titleIssue)
106
+ issues.push(titleIssue);
107
+ const descriptionIssue = checkString(input.description, ['description'], OUTLINE_LIMITS.maxDescriptionLength, 'body-too-long', 'Description');
108
+ if (descriptionIssue)
109
+ issues.push(descriptionIssue);
110
+ if (input.kind === 'deck') {
111
+ issues.push(...checkUnits('deck', input.slides, 'slides', OUTLINE_LIMITS.maxSlides));
112
+ if (input.sections && input.sections.length > 0) {
113
+ issues.push(issue('too-many-sections', ['sections'], 0, input.sections.length, 'Deck outlines use slides, not sections.', 'Move content into slides and omit sections.'));
114
+ }
115
+ }
116
+ else if (input.kind === 'page' || input.kind === 'doc') {
117
+ issues.push(...checkUnits(input.kind, input.sections, 'sections', OUTLINE_LIMITS.maxSections));
118
+ if (input.slides && input.slides.length > 0) {
119
+ issues.push(issue('too-many-slides', ['slides'], 0, input.slides.length, `${input.kind} outlines use sections, not slides.`, 'Move content into sections and omit slides.'));
120
+ }
121
+ }
122
+ else {
123
+ // app: no slide/section outline; only envelope strings if present
124
+ if (input.slides && input.slides.length > 0) {
125
+ issues.push(issue('too-many-slides', ['slides'], 0, input.slides.length, 'App briefs do not take a slide outline.', 'Omit slides for kind app.'));
126
+ }
127
+ if (input.sections && input.sections.length > 0) {
128
+ issues.push(issue('too-many-sections', ['sections'], 0, input.sections.length, 'App briefs do not take a section outline.', 'Omit sections for kind app.'));
129
+ }
130
+ }
131
+ return issues.length === 0 ? { ok: true, issues: [] } : { ok: false, issues };
132
+ }
133
+ function clipString(value, limit) {
134
+ if (value === undefined)
135
+ return undefined;
136
+ return value.length <= limit ? value : value.slice(0, limit);
137
+ }
138
+ function clipUnits(units, maxUnits) {
139
+ if (!units)
140
+ return { units: undefined, clippedUnits: 0, clippedFields: 0 };
141
+ let clippedFields = 0;
142
+ const sliced = units.slice(0, maxUnits);
143
+ const clippedUnits = units.length - sliced.length;
144
+ const next = sliced.map((unit) => {
145
+ const title = clipString(unit.title, OUTLINE_LIMITS.maxTitleLength);
146
+ const body = clipString(unit.body, OUTLINE_LIMITS.maxBodyLength);
147
+ const text = clipString(unit.text, OUTLINE_LIMITS.maxTextLength);
148
+ if (title !== unit.title || body !== unit.body || text !== unit.text)
149
+ clippedFields += 1;
150
+ const blocks = Array.isArray(unit.blocks)
151
+ ? unit.blocks.slice(0, OUTLINE_LIMITS.maxBlocksPerUnit)
152
+ : unit.blocks;
153
+ if (Array.isArray(unit.blocks) && unit.blocks.length > OUTLINE_LIMITS.maxBlocksPerUnit) {
154
+ clippedFields += 1;
155
+ }
156
+ return { ...unit, title, body, text, blocks };
157
+ });
158
+ return { units: next, clippedUnits, clippedFields };
159
+ }
160
+ /**
161
+ * Deterministically clip an outline to the ceilings. Prefer `checkOutlineBounds`
162
+ * when you want hard failures; use this when synthesis should continue with a
163
+ * documented truncation notice.
164
+ */
165
+ export function applyOutlineBounds(input) {
166
+ const notices = [];
167
+ let truncated = false;
168
+ const outline = {
169
+ kind: input.kind,
170
+ title: clipString(input.title, OUTLINE_LIMITS.maxTitleLength),
171
+ description: clipString(input.description, OUTLINE_LIMITS.maxDescriptionLength),
172
+ };
173
+ if (outline.title !== input.title || outline.description !== input.description) {
174
+ truncated = true;
175
+ notices.push('Envelope title/description clipped to outline text limits.');
176
+ }
177
+ if (input.kind === 'deck') {
178
+ const { units, clippedUnits, clippedFields } = clipUnits(input.slides, OUTLINE_LIMITS.maxSlides);
179
+ outline.slides = units;
180
+ if (clippedUnits > 0) {
181
+ truncated = true;
182
+ notices.push(`Slides truncated from ${(input.slides?.length ?? 0).toString()} to ${OUTLINE_LIMITS.maxSlides}.`);
183
+ }
184
+ if (clippedFields > 0) {
185
+ truncated = true;
186
+ notices.push('Slide titles, bodies or blocks clipped to outline limits.');
187
+ }
188
+ }
189
+ else if (input.kind === 'page' || input.kind === 'doc') {
190
+ const { units, clippedUnits, clippedFields } = clipUnits(input.sections, OUTLINE_LIMITS.maxSections);
191
+ outline.sections = units;
192
+ if (clippedUnits > 0) {
193
+ truncated = true;
194
+ notices.push(`Sections truncated from ${(input.sections?.length ?? 0).toString()} to ${OUTLINE_LIMITS.maxSections}.`);
195
+ }
196
+ if (clippedFields > 0) {
197
+ truncated = true;
198
+ notices.push('Section titles, bodies or blocks clipped to outline limits.');
199
+ }
200
+ }
201
+ // Re-check so callers always get a full issue list for anything still wrong
202
+ // (e.g. deck with leftover sections field).
203
+ const checked = checkOutlineBounds(outline);
204
+ return {
205
+ outline,
206
+ truncated,
207
+ notices,
208
+ issues: checked.ok ? [] : checked.issues,
209
+ };
210
+ }
211
+ /**
212
+ * Convenience for synthesizers: bound the brief, then return source + notices.
213
+ * Same behavior as historical `brief.slice(0, MAX_BRIEF_LENGTH)`.
214
+ */
215
+ export function prepareBriefForSynthesis(brief, options = {}) {
216
+ return boundBriefInput(brief, options);
217
+ }