brand-intent 0.22.0 → 0.23.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/examples/krume/purposes/daily-bread.purpose +24 -16
- package/package.json +1 -1
- package/parser/README.md +1 -0
- package/parser/index.ts +1 -1
- package/parser/purposeParser.ts +151 -16
- package/skills/brand-intent.md +2 -2
- package/skills/brief.md +2 -1
- package/vscode-extension/syntaxes/purpose.tmLanguage.json +12 -5
|
@@ -42,37 +42,45 @@ slot primary
|
|
|
42
42
|
typography: headline
|
|
43
43
|
color: text-primary
|
|
44
44
|
maxLength: 30
|
|
45
|
-
samples:
|
|
46
|
-
- Roggenlaib
|
|
47
|
-
- Weizenmisch
|
|
48
|
-
- Dinkelvollkorn
|
|
49
45
|
|
|
50
46
|
slot secondary
|
|
51
47
|
label: Gewicht & Preis
|
|
52
48
|
typography: kicker
|
|
53
49
|
color: text-secondary
|
|
54
50
|
maxLength: 25
|
|
55
|
-
samples:
|
|
56
|
-
- 750 g · 5,80 €
|
|
57
|
-
- 500 g · 4,20 €
|
|
58
|
-
- 1 kg · 7,50 €
|
|
59
51
|
|
|
60
52
|
slot meta
|
|
61
53
|
label: Beschreibung
|
|
62
54
|
typography: caption
|
|
63
55
|
color: text-secondary
|
|
64
56
|
maxLength: 100
|
|
65
|
-
samples:
|
|
66
|
-
- Roggen 1150, 18 Stunden Teigführung, Kümmel aus dem Wendland.
|
|
67
|
-
- Tipo 1 aus der Drax-Mühle, 72 % Hydration.
|
|
68
|
-
- Saisonale Walnüsse vom Wochenmarkt, Emmer-Vollkorn.
|
|
69
57
|
|
|
70
58
|
slot label
|
|
71
59
|
label: Badge
|
|
72
60
|
typography: label
|
|
73
61
|
color: label-fg
|
|
74
62
|
maxLength: 20
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
63
|
+
|
|
64
|
+
# ── Examples ──
|
|
65
|
+
# Whole pages: all slots written together about one bread.
|
|
66
|
+
|
|
67
|
+
example
|
|
68
|
+
topic: The classic rye loaf, today's bread
|
|
69
|
+
label: Heute frisch
|
|
70
|
+
primary: Roggenlaib
|
|
71
|
+
secondary: 750 g · 5,80 €
|
|
72
|
+
meta: Roggen 1150, 18 Stunden Teigführung, Kümmel aus dem Wendland.
|
|
73
|
+
|
|
74
|
+
example
|
|
75
|
+
topic: The wheat blend, only on Saturdays
|
|
76
|
+
label: Nur samstags
|
|
77
|
+
primary: Weizenmisch
|
|
78
|
+
secondary: 500 g · 4,20 €
|
|
79
|
+
meta: Tipo 1 aus der Drax-Mühle, 72 % Hydration.
|
|
80
|
+
|
|
81
|
+
example
|
|
82
|
+
topic: New in the shop: emmer bread with walnuts
|
|
83
|
+
label: Neu im Laden
|
|
84
|
+
primary: Emmer-Walnuss
|
|
85
|
+
secondary: 1 kg · 7,50 €
|
|
86
|
+
meta: Saisonale Walnüsse vom Wochenmarkt, Emmer-Vollkorn.
|
package/package.json
CHANGED
package/parser/README.md
CHANGED
|
@@ -34,3 +34,4 @@ const brand = parseBrandFile(
|
|
|
34
34
|
## Notes
|
|
35
35
|
|
|
36
36
|
- These parsers throw on validation errors, with the filename and line number included in the error message.
|
|
37
|
+
- Non-fatal findings (deprecated syntax) are returned in `warnings` (`parsePurposeFile`) as `file:line: message`, so implementations can show them in their UI.
|
package/parser/index.ts
CHANGED
|
@@ -69,7 +69,7 @@ export { parseFormatFile } from './formatParser';
|
|
|
69
69
|
export type { ParsedFormat } from './formatParser';
|
|
70
70
|
|
|
71
71
|
export { parsePurposeFile } from './purposeParser';
|
|
72
|
-
export type { ParsedPurpose, ParsedPurposeSlot, CountRange, Density, ListMarker } from './purposeParser';
|
|
72
|
+
export type { ParsedPurpose, ParsedPurposeSlot, ParsedExample, CountRange, Density, ListMarker } from './purposeParser';
|
|
73
73
|
|
|
74
74
|
export { parseCompositionFile } from './compositionParser';
|
|
75
75
|
export type {
|
package/parser/purposeParser.ts
CHANGED
|
@@ -51,17 +51,36 @@ export interface ParsedPurpose {
|
|
|
51
51
|
defaultIcon?: IconRef;
|
|
52
52
|
/** Optional prompt text shown in the empty background placeholder. Default: "Select a background". */
|
|
53
53
|
bgPrompt?: string;
|
|
54
|
+
/** Example pages: all texts of one page written together, one topic each. Built from deprecated
|
|
55
|
+
* slot `samples:` (paired by position, `legacy: true`) when the file has no `example` blocks. */
|
|
56
|
+
examples: ParsedExample[];
|
|
57
|
+
/** Deprecations and other non-fatal findings, as "file:line: message". Implementations show them. */
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One `example` block: a whole page about one topic. */
|
|
62
|
+
export interface ParsedExample {
|
|
63
|
+
/** What the page is about, one sentence. Becomes the page topic. */
|
|
64
|
+
topic?: string;
|
|
65
|
+
/** Identity goal id; overrides the purpose `goal:` for this page. */
|
|
66
|
+
goal?: string;
|
|
67
|
+
/** Date window name from the `.brand` `windows` block; undefined = all year. */
|
|
68
|
+
when?: string;
|
|
69
|
+
/** Text per slot. List items are newline-separated. Slots with `value:` never appear. */
|
|
70
|
+
texts: Partial<Record<SlotId, string>>;
|
|
71
|
+
/** True when built from deprecated slot `samples:`. */
|
|
72
|
+
legacy?: boolean;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
export interface ParsedPurposeSlot {
|
|
57
76
|
id: SlotId;
|
|
58
77
|
label: string;
|
|
59
|
-
/**
|
|
78
|
+
/** @deprecated Slot `samples:` from older files; content lives in `ParsedPurpose.examples`.
|
|
79
|
+
* Empty for files with `example` blocks; `[value]` for a fixed-value slot. */
|
|
60
80
|
samples: string[];
|
|
61
|
-
/** Date window per sample (parallel to `samples`), from a trailing `@name` marker
|
|
62
|
-
* Absent when no sample in the slot has a marker. Names resolve against the `.brand` `windows` block. */
|
|
81
|
+
/** @deprecated Date window per legacy sample (parallel to `samples`), from a trailing `@name` marker. */
|
|
63
82
|
sampleWindows?: (string | undefined)[];
|
|
64
|
-
/** Fixed value — when set, this slot is never AI-generated or shuffled.
|
|
83
|
+
/** Fixed value — when set, this slot is never AI-generated or shuffled. Never part of an example. */
|
|
65
84
|
value?: string;
|
|
66
85
|
maxLength: number;
|
|
67
86
|
/** Editorial word range (`words: 3..8`). Guidance on top of the hard `maxLength`. */
|
|
@@ -159,6 +178,13 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
159
178
|
let defaultIcon: IconRef | undefined;
|
|
160
179
|
let bgPrompt: string | undefined;
|
|
161
180
|
const slots: ParsedPurposeSlot[] = [];
|
|
181
|
+
const warnings: string[] = [];
|
|
182
|
+
const examples: (ParsedExample & { line: number; keyLines: Partial<Record<string, number>> })[] = [];
|
|
183
|
+
// Example block being built, and the slot key whose multi-line value is being read (4-space lines)
|
|
184
|
+
let currentExample: (typeof examples)[number] | null = null;
|
|
185
|
+
let exampleMultiKey: SlotId | null = null;
|
|
186
|
+
// First line of a deprecated slot `samples:` (0 = none)
|
|
187
|
+
let legacySamplesLine = 0;
|
|
162
188
|
// For backward compat: track first # line text for fallback id
|
|
163
189
|
let firstHashText = '';
|
|
164
190
|
|
|
@@ -176,7 +202,6 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
176
202
|
const s = currentSlot;
|
|
177
203
|
if (!s.id) throw new Error(`${fileName}:${lineNum}: slot missing id`);
|
|
178
204
|
if (!s.label) throw new Error(`${fileName}:${lineNum}: slot "${s.id}" missing label`);
|
|
179
|
-
if ((!s.samples || s.samples.length === 0) && !s.iconOnly && !s.value) throw new Error(`${fileName}:${lineNum}: slot "${s.id}" missing samples or value`);
|
|
180
205
|
if (!s.maxLength) throw new Error(`${fileName}:${lineNum}: slot "${s.id}" missing maxLength`);
|
|
181
206
|
if (s.iconOnly) {
|
|
182
207
|
if (s.marker && s.marker !== 'icon') throw new Error(`${fileName}:${lineNum}: slot "${s.id}": iconOnly needs "marker: icon", got "${s.marker}"`);
|
|
@@ -217,6 +242,17 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
217
242
|
currentSlot = null;
|
|
218
243
|
}
|
|
219
244
|
|
|
245
|
+
function finalizeExample() {
|
|
246
|
+
currentExample = null;
|
|
247
|
+
exampleMultiKey = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A top-level line closes the open slot or example block. */
|
|
251
|
+
function closeBlocks(lineNum: number) {
|
|
252
|
+
finalizeSlot(lineNum);
|
|
253
|
+
finalizeExample();
|
|
254
|
+
}
|
|
255
|
+
|
|
220
256
|
for (let i = 0; i < lines.length; i++) {
|
|
221
257
|
const lineNum = i + 1;
|
|
222
258
|
const raw = lines[i];
|
|
@@ -247,7 +283,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
247
283
|
|
|
248
284
|
// scope block header
|
|
249
285
|
if (line === 'scope' && !raw.startsWith(' ')) {
|
|
250
|
-
|
|
286
|
+
closeBlocks(lineNum);
|
|
251
287
|
inScopeBlock = true;
|
|
252
288
|
inContextBlock = false;
|
|
253
289
|
inVoiceBlock = false;
|
|
@@ -257,7 +293,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
257
293
|
|
|
258
294
|
// context block header
|
|
259
295
|
if (line === 'context' && !raw.startsWith(' ')) {
|
|
260
|
-
|
|
296
|
+
closeBlocks(lineNum);
|
|
261
297
|
inContextBlock = true;
|
|
262
298
|
inScopeBlock = false;
|
|
263
299
|
inVoiceBlock = false;
|
|
@@ -266,7 +302,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
266
302
|
|
|
267
303
|
// voice block header
|
|
268
304
|
if (line === 'voice' && !raw.startsWith(' ')) {
|
|
269
|
-
|
|
305
|
+
closeBlocks(lineNum);
|
|
270
306
|
inVoiceBlock = true;
|
|
271
307
|
inScopeBlock = false;
|
|
272
308
|
inContextBlock = false;
|
|
@@ -274,6 +310,44 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
274
310
|
continue;
|
|
275
311
|
}
|
|
276
312
|
|
|
313
|
+
// example block header: one whole page
|
|
314
|
+
if (line === 'example' && !raw.startsWith(' ')) {
|
|
315
|
+
closeBlocks(lineNum);
|
|
316
|
+
inScopeBlock = inContextBlock = inVoiceBlock = false;
|
|
317
|
+
currentExample = { texts: {}, line: lineNum, keyLines: {} };
|
|
318
|
+
examples.push(currentExample);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// example block lines: `key: value`, or bare `key:` followed by 4-space lines (list items, line breaks)
|
|
323
|
+
if (currentExample && raw.startsWith(' ')) {
|
|
324
|
+
if (raw.startsWith(' ')) {
|
|
325
|
+
if (!exampleMultiKey) throw new Error(`${fileName}:${lineNum}: unexpected "${line}" in example (multi-line text follows a bare "slot:" line)`);
|
|
326
|
+
const prev = currentExample.texts[exampleMultiKey];
|
|
327
|
+
currentExample.texts[exampleMultiKey] = prev ? `${prev}\n${line}` : line;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
exampleMultiKey = null;
|
|
331
|
+
const colonIdx = line.indexOf(':');
|
|
332
|
+
if (colonIdx === -1) throw new Error(`${fileName}:${lineNum}: expected "key: value" in example, got "${line}"`);
|
|
333
|
+
const key = line.slice(0, colonIdx).trim();
|
|
334
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
335
|
+
if (key in currentExample.keyLines) throw new Error(`${fileName}:${lineNum}: duplicate "${key}" in example`);
|
|
336
|
+
currentExample.keyLines[key] = lineNum;
|
|
337
|
+
if (key === 'topic' || key === 'goal' || key === 'when') {
|
|
338
|
+
if (!value) throw new Error(`${fileName}:${lineNum}: empty ${key}`);
|
|
339
|
+
if (key === 'when' && !/^[a-z][a-z0-9-]*$/.test(value)) throw new Error(`${fileName}:${lineNum}: invalid window name "${value}" (lowercase kebab-case)`);
|
|
340
|
+
currentExample[key] = value;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!VALID_SLOTS.has(key)) throw new Error(`${fileName}:${lineNum}: unknown example key "${key}" (expected topic, goal, when or a slot id)`);
|
|
344
|
+
if (value) currentExample.texts[key as SlotId] = value;
|
|
345
|
+
else exampleMultiKey = key as SlotId;
|
|
346
|
+
continue;
|
|
347
|
+
} else if (currentExample && !raw.startsWith(' ')) {
|
|
348
|
+
finalizeExample();
|
|
349
|
+
}
|
|
350
|
+
|
|
277
351
|
// scope block properties (2-space indent)
|
|
278
352
|
if (inScopeBlock && raw.startsWith(' ') && !raw.startsWith(' ')) {
|
|
279
353
|
if (line.startsWith('audience:')) {
|
|
@@ -388,14 +462,14 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
388
462
|
|
|
389
463
|
// Backward compat: "icon: warning-circle" (deprecated — use defaultIcon:)
|
|
390
464
|
if (line.startsWith('icon:') && !raw.startsWith(' ')) {
|
|
391
|
-
|
|
465
|
+
warnings.push(`${fileName}:${lineNum}: deprecated "icon:" — use "defaultIcon:" instead`);
|
|
392
466
|
defaultIcon = parseIconRef(line.slice(5), `${fileName}:${lineNum}`);
|
|
393
467
|
continue;
|
|
394
468
|
}
|
|
395
469
|
|
|
396
470
|
// Slot start
|
|
397
471
|
if (line.startsWith('slot ')) {
|
|
398
|
-
|
|
472
|
+
closeBlocks(lineNum);
|
|
399
473
|
const slotId = line.slice(5).trim();
|
|
400
474
|
if (!VALID_SLOTS.has(slotId)) throw new Error(`${fileName}:${lineNum}: unknown slot "${slotId}"`);
|
|
401
475
|
currentSlot = { id: slotId as SlotId, samples: [], typography: {} as ParsedPurposeSlot['typography'] };
|
|
@@ -511,6 +585,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
511
585
|
continue;
|
|
512
586
|
}
|
|
513
587
|
if (prop.startsWith('samples:')) {
|
|
588
|
+
legacySamplesLine ||= lineNum;
|
|
514
589
|
const raw = prop.slice(8).trim();
|
|
515
590
|
if (!raw) {
|
|
516
591
|
// Bare `samples:` — enter multi-line mode
|
|
@@ -532,12 +607,12 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
532
607
|
continue;
|
|
533
608
|
}
|
|
534
609
|
if (prop === 'icons: true' || prop === 'icons:true') {
|
|
535
|
-
|
|
610
|
+
warnings.push(`${fileName}:${lineNum}: deprecated "icons: true" — use "marker: counter" or "marker: icon"`);
|
|
536
611
|
currentSlot.marker = currentSlot.marker ?? 'counter';
|
|
537
612
|
continue;
|
|
538
613
|
}
|
|
539
614
|
if (prop === 'icons: false' || prop === 'icons:false') {
|
|
540
|
-
|
|
615
|
+
warnings.push(`${fileName}:${lineNum}: deprecated "icons: false" — use "marker: none"`);
|
|
541
616
|
currentSlot.marker = currentSlot.marker ?? 'none';
|
|
542
617
|
continue;
|
|
543
618
|
}
|
|
@@ -600,6 +675,7 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
600
675
|
continue;
|
|
601
676
|
}
|
|
602
677
|
if (prop.startsWith('samples:')) {
|
|
678
|
+
legacySamplesLine ||= lineNum;
|
|
603
679
|
const raw = prop.slice(8).trim();
|
|
604
680
|
if (!raw) {
|
|
605
681
|
// Bare `samples:` — enter multi-line mode
|
|
@@ -683,13 +759,13 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
683
759
|
throw new Error(`${fileName}:${lineNum}: unexpected "${line}"`);
|
|
684
760
|
}
|
|
685
761
|
|
|
686
|
-
// Finalize last
|
|
687
|
-
|
|
762
|
+
// Finalize last block
|
|
763
|
+
closeBlocks(lines.length);
|
|
688
764
|
|
|
689
765
|
// Backward compat: if no id: line but we have a first # line, use it as id
|
|
690
766
|
if (!id) {
|
|
691
767
|
if (firstHashText) {
|
|
692
|
-
|
|
768
|
+
warnings.push(`${fileName}: deprecated — add "id: ${firstHashText}" line (first # line used as fallback id)`);
|
|
693
769
|
id = firstHashText;
|
|
694
770
|
} else {
|
|
695
771
|
throw new Error(`${fileName}: missing purpose id (add "id:" line)`);
|
|
@@ -710,5 +786,64 @@ export function parsePurposeFile(content: string, fileName: string): ParsedPurpo
|
|
|
710
786
|
context = contextLines.join('\n').trim();
|
|
711
787
|
}
|
|
712
788
|
|
|
713
|
-
|
|
789
|
+
const parsedExamples = resolveExamples(examples, slots, legacySamplesLine, fileName, warnings);
|
|
790
|
+
|
|
791
|
+
return { id, name, description, preferredCompositions: compositions, slots, density, palette, scope, context, voice, defaultLogo, defaultPhoto, defaultVideo, camera, cameraFacing, framing, goal, analyze, defaultIcon, bgPrompt, examples: parsedExamples, warnings };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Validate `example` blocks against the declared slots, or build examples from deprecated slot
|
|
795
|
+
* `samples:` (paired by position) when the file has none. Mixing both is an error. */
|
|
796
|
+
function resolveExamples(
|
|
797
|
+
examples: (ParsedExample & { line: number; keyLines: Partial<Record<string, number>> })[],
|
|
798
|
+
slots: ParsedPurposeSlot[],
|
|
799
|
+
legacySamplesLine: number,
|
|
800
|
+
fileName: string,
|
|
801
|
+
warnings: string[],
|
|
802
|
+
): ParsedExample[] {
|
|
803
|
+
const byId = new Map(slots.map(s => [s.id, s]));
|
|
804
|
+
const contentSlots = slots.filter(s => !s.value && !s.iconOnly);
|
|
805
|
+
|
|
806
|
+
if (examples.length > 0) {
|
|
807
|
+
if (legacySamplesLine) throw new Error(`${fileName}:${legacySamplesLine}: slot "samples:" next to "example" blocks — move the texts into the examples`);
|
|
808
|
+
for (const ex of examples) {
|
|
809
|
+
const at = (key: string) => `${fileName}:${ex.keyLines[key] ?? ex.line}`;
|
|
810
|
+
const filled = Object.keys(ex.texts) as SlotId[];
|
|
811
|
+
if (filled.length === 0) throw new Error(`${fileName}:${ex.line}: example without slot texts`);
|
|
812
|
+
for (const slotId of filled) {
|
|
813
|
+
const slot = byId.get(slotId);
|
|
814
|
+
if (!slot) throw new Error(`${at(slotId)}: example fills slot "${slotId}", which the purpose does not declare`);
|
|
815
|
+
if (slot.value) throw new Error(`${at(slotId)}: slot "${slotId}" has a fixed value and takes no example text`);
|
|
816
|
+
const text = ex.texts[slotId]!;
|
|
817
|
+
if (text.split('\n').some(l => SAMPLE_WINDOW_MARKER.test(l))) throw new Error(`${at(slotId)}: window marker in example text — use "when:" on the example`);
|
|
818
|
+
if (slot.slotType === 'list') {
|
|
819
|
+
for (const item of text.split('\n')) {
|
|
820
|
+
const { icon } = parseListItem(item);
|
|
821
|
+
if (icon && slot.marker !== 'icon') throw new Error(`${at(slotId)}: slot "${slotId}": item "${item}" names an icon, but the list has no "marker: icon"`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
for (const slot of contentSlots) {
|
|
827
|
+
if (!examples.some(ex => ex.texts[slot.id])) throw new Error(`${fileName}: slot "${slot.id}" is filled by no example`);
|
|
828
|
+
}
|
|
829
|
+
return examples.map(({ line: _line, keyLines: _keyLines, ...ex }) => ex);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const slot of contentSlots) {
|
|
833
|
+
if (slot.samples.length === 0) throw new Error(`${fileName}: slot "${slot.id}" has no text — add "example" blocks or a "value:"`);
|
|
834
|
+
}
|
|
835
|
+
if (!legacySamplesLine) return [];
|
|
836
|
+
warnings.push(`${fileName}:${legacySamplesLine}: deprecated slot "samples:" — write whole pages as "example" blocks`);
|
|
837
|
+
const sampled = slots.filter(s => !s.value && s.samples.length > 0);
|
|
838
|
+
const count = Math.max(...sampled.map(s => s.samples.length));
|
|
839
|
+
return Array.from({ length: count }, (_, i) => {
|
|
840
|
+
const texts: Partial<Record<SlotId, string>> = {};
|
|
841
|
+
let when: string | undefined;
|
|
842
|
+
for (const slot of sampled) {
|
|
843
|
+
const idx = i % slot.samples.length;
|
|
844
|
+
texts[slot.id] = slot.samples[idx];
|
|
845
|
+
when ??= slot.sampleWindows?.[idx];
|
|
846
|
+
}
|
|
847
|
+
return { texts, ...(when ? { when } : {}), legacy: true };
|
|
848
|
+
});
|
|
714
849
|
}
|
package/skills/brand-intent.md
CHANGED
|
@@ -104,7 +104,7 @@ When generating content for a specific purpose, read all layers silently (do not
|
|
|
104
104
|
2. **Read `.brand`** — voice-constraints: sentence-max, headline-pattern, number-format, register
|
|
105
105
|
3. **Apply `scope` from `.purpose`** — audience and pillar filters narrow the upstream context
|
|
106
106
|
4. **Apply `context` from `.purpose`** — slot-specific instructions, follow literally
|
|
107
|
-
5. **Generate per-slot content** — respect maxLength and any `words:`/`sentences:` range, use
|
|
107
|
+
5. **Generate per-slot content** — respect maxLength and any `words:`/`sentences:` range, use the purpose's `example` pages as structural reference (match pattern, not words; all slots of one output share one topic), follow context instructions
|
|
108
108
|
|
|
109
109
|
### Output format:
|
|
110
110
|
|
|
@@ -252,7 +252,7 @@ Read the files and walk the user through them:
|
|
|
252
252
|
- **`krume.identity`** — the strategic layer: essence, voice, values, audiences. Start here.
|
|
253
253
|
- **`krume.brand`** — derived expression: colors, themes, typography, spacing. Every decision traces back to identity.
|
|
254
254
|
- **`formats/`** — canvas dimensions, safe zones, grid. Where content appears.
|
|
255
|
-
- **`purposes/`** — content type with semantic slots,
|
|
255
|
+
- **`purposes/`** — content type with semantic slots, constraints, and example pages. What content does.
|
|
256
256
|
- **`compositions/`** — spatial arrangement of slots. How elements are placed.
|
|
257
257
|
|
|
258
258
|
If `examples/krume/` doesn't exist, tell the user to run `npx brand-intent init` first.
|
package/skills/brief.md
CHANGED
|
@@ -180,7 +180,8 @@ For each purpose, emit **glossed** tags — not verbatim DSL values:
|
|
|
180
180
|
- Scope: {scope audience + pillars}
|
|
181
181
|
- Context: {context prose per slot}
|
|
182
182
|
- Goal: {goal label}, if set
|
|
183
|
-
-
|
|
183
|
+
- Examples: each `example` block as one page — topic (if set), then slot texts. For files with deprecated slot `samples:`, list them per slot and note that they are not paired.
|
|
184
|
+
- Slots: for each slot — id, typography, maxLength, words/sentences range if set. Omit `color:` references (theme-internal detail). Override hints (`hyphenate`, `textAlign`) pass through as-is.
|
|
184
185
|
|
|
185
186
|
## Formats (optional)
|
|
186
187
|
_Role: where content appears. Only emitted if any `.format` files are present._
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
{ "include": "#bg-prompt-field" },
|
|
20
20
|
{ "include": "#context-header" },
|
|
21
21
|
{ "include": "#slot-header" },
|
|
22
|
+
{ "include": "#example-header" },
|
|
22
23
|
{ "include": "#override-header" },
|
|
23
24
|
{ "include": "#override-value" },
|
|
24
25
|
{ "include": "#list-property" },
|
|
@@ -156,6 +157,12 @@
|
|
|
156
157
|
"2": { "name": "entity.name.tag.brand-intent-purpose" }
|
|
157
158
|
}
|
|
158
159
|
},
|
|
160
|
+
"example-header": {
|
|
161
|
+
"match": "^(example)\\s*$",
|
|
162
|
+
"captures": {
|
|
163
|
+
"1": { "name": "keyword.control.brand-intent-purpose" }
|
|
164
|
+
}
|
|
165
|
+
},
|
|
159
166
|
"override-header": {
|
|
160
167
|
"match": "^\\s+(override:)\\s*$",
|
|
161
168
|
"captures": {
|
|
@@ -216,7 +223,7 @@
|
|
|
216
223
|
]
|
|
217
224
|
},
|
|
218
225
|
"property": {
|
|
219
|
-
"match": "^\\s+(label|samples|value|maxLength|words|sentences|font|size|opsz|lineHeight|letterSpacing|align|typography|color|type|editable)(:)\\s+(.*)",
|
|
226
|
+
"match": "^\\s+(label|samples|value|maxLength|words|sentences|font|size|opsz|lineHeight|letterSpacing|align|typography|color|type|editable|topic|goal|when|primary|secondary|detail|meta|cta)(:)\\s+(.*)",
|
|
220
227
|
"captures": {
|
|
221
228
|
"1": { "name": "support.type.property-name.brand-intent-purpose" },
|
|
222
229
|
"2": { "name": "punctuation.separator.brand-intent-purpose" },
|
|
@@ -241,8 +248,8 @@
|
|
|
241
248
|
}
|
|
242
249
|
},
|
|
243
250
|
"sample-icon-continuation": {
|
|
244
|
-
"comment": "[ID] icon prefix on list
|
|
245
|
-
"match": "^(\\s{
|
|
251
|
+
"comment": "[ID] icon prefix on list items (example blocks, legacy sample continuation lines)",
|
|
252
|
+
"match": "^(\\s{4,})(\\[[^\\]]+\\])",
|
|
246
253
|
"captures": {
|
|
247
254
|
"2": { "name": "entity.name.function.brand-intent-purpose" }
|
|
248
255
|
}
|
|
@@ -258,13 +265,13 @@
|
|
|
258
265
|
"comment": "Fallback: color the keyword token on incomplete lines (while typing)",
|
|
259
266
|
"patterns": [
|
|
260
267
|
{
|
|
261
|
-
"match": "^(id:|name:|compositions:|density:|defaultIcon:|scope|voice|palette:|logo:|photo:|video:|camera:|framing:|goal:|analyze:|bg-prompt:|context|slot)(?=\\s|$)",
|
|
268
|
+
"match": "^(id:|name:|compositions:|density:|defaultIcon:|scope|voice|palette:|logo:|photo:|video:|camera:|framing:|goal:|analyze:|bg-prompt:|context|slot|example)(?=\\s|$)",
|
|
262
269
|
"captures": {
|
|
263
270
|
"1": { "name": "keyword.control.brand-intent-purpose" }
|
|
264
271
|
}
|
|
265
272
|
},
|
|
266
273
|
{
|
|
267
|
-
"match": "^\\s+(label|samples|value|maxLength|words|sentences|font|size|opsz|lineHeight|letterSpacing|align|typography|color|type|override|audience|pillars|editable)(?=:|\\s|$)",
|
|
274
|
+
"match": "^\\s+(label|samples|value|maxLength|words|sentences|font|size|opsz|lineHeight|letterSpacing|align|typography|color|type|override|audience|pillars|editable|topic|goal|when|primary|secondary|detail|meta|cta)(?=:|\\s|$)",
|
|
268
275
|
"captures": {
|
|
269
276
|
"1": { "name": "support.type.property-name.brand-intent-purpose" }
|
|
270
277
|
}
|