Skip to content

Commit 52dec1b

Browse files
committed
fix: fix thinking blocks in the messages
1 parent 36d5738 commit 52dec1b

4 files changed

Lines changed: 186 additions & 20 deletions

File tree

demo/src/validator-prompts.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const VALIDATOR_PROMPT_VARIANTS: ValidatorPromptVariant[] = [
4141
key: 'structure',
4242
label: 'Structure & YAML',
4343
description: 'YAML correctness, duplicate IDs, ID format, schema conformance',
44-
rules: ['yaml-correctness', 'schema-conformance', 'duplicate-ids', 'id-format'],
44+
rules: ['yaml-correctness', 'schema-conformance', 'duplicate-ids', 'id-format', 'thinking-block'],
4545
prompt: `${PREAMBLE}
4646
4747
Focus ONLY on structural and YAML issues. Generate an event registration system with these exact components, each with intentional structural problems:
@@ -94,7 +94,7 @@ IMPORTANT: Only generate \`\`\`mdma blocks when explicitly asked or on the first
9494
key: 'bindings',
9595
label: 'Bindings & References',
9696
description: 'Binding syntax, resolution, deep field validation, action references',
97-
rules: ['binding-syntax', 'binding-resolution', 'action-references'],
97+
rules: ['binding-syntax', 'binding-resolution', 'action-references', 'thinking-block'],
9898
prompt: `${PREAMBLE}
9999
100100
Focus ONLY on binding and reference issues. Generate a contact submission workflow with these exact components, each with intentional binding/reference problems:
@@ -146,7 +146,7 @@ IMPORTANT: Only generate \`\`\`mdma blocks when explicitly asked or on the first
146146
key: 'pii',
147147
label: 'PII & Sensitive Data',
148148
description: 'Sensitive flags, required markers',
149-
rules: ['sensitive-flags', 'required-markers'],
149+
rules: ['sensitive-flags', 'required-markers', 'thinking-block'],
150150
prompt: `${PREAMBLE}
151151
152152
Focus ONLY on PII and data sensitivity issues. Generate a KYC (Know Your Customer) verification form with these exact components, each missing sensitive/required flags:
@@ -192,7 +192,7 @@ IMPORTANT: Only generate \`\`\`mdma blocks when explicitly asked or on the first
192192
key: 'forms',
193193
label: 'Form Validation',
194194
description: 'Select options, field name typos, placeholder content, expected components',
195-
rules: ['select-options', 'field-name-typos', 'placeholder-content', 'expected-components'],
195+
rules: ['select-options', 'field-name-typos', 'placeholder-content', 'expected-components', 'thinking-block'],
196196
prompt: `${PREAMBLE}
197197
198198
Focus ONLY on form-specific issues. Generate a single job application form with intentional problems:
@@ -232,7 +232,7 @@ IMPORTANT: Only generate \`\`\`mdma blocks when explicitly asked or on the first
232232
key: 'tables-charts',
233233
label: 'Tables & Charts',
234234
description: 'Table data keys, chart axis validation',
235-
rules: ['table-data-keys', 'chart-validation'],
235+
rules: ['table-data-keys', 'chart-validation', 'thinking-block'],
236236
prompt: `${PREAMBLE}
237237
238238
Focus ONLY on table and chart data issues. Generate a sales dashboard with these exact components, each with intentional problems:
@@ -262,8 +262,8 @@ Generate all 4 components with the intentional mismatches described above.`,
262262
{
263263
key: 'flow',
264264
label: 'Stepper Flow',
265-
description: 'Flow ordering, unreferenced components, action targets, expected components',
266-
rules: ['flow-ordering', 'unreferenced-components', 'action-references', 'expected-components'],
265+
description: 'Flow ordering, thinking block, action targets, expected components',
266+
rules: ['flow-ordering', 'thinking-block', 'action-references', 'expected-components'],
267267
prompt: `${PREAMBLE}
268268
269269
Focus ONLY on component flow and reference issues. Generate a user registration and approval workflow with ALL of these intentional problems:
@@ -307,7 +307,7 @@ IMPORTANT: Only generate \`\`\`mdma blocks when explicitly asked to generate a s
307307
key: 'approval',
308308
label: 'Approval & Webhooks',
309309
description: 'Field name typos on approval-gate, action references on webhooks',
310-
rules: ['field-name-typos', 'action-references', 'schema-conformance'],
310+
rules: ['field-name-typos', 'action-references', 'schema-conformance', 'thinking-block'],
311311
prompt: `${PREAMBLE}
312312
313313
Focus ONLY on approval gate and webhook issues. Generate an expense approval workflow with these exact components, each with intentional problems:
@@ -611,21 +611,31 @@ export const SAMPLE_TABLE_DATA: Record<string, Record<string, Array<Record<strin
611611
*/
612612
export const FLOW_EXPECTED_COMPONENTS: Record<string, Record<string, ExpectedComponent>[]> = {
613613
flow: [
614-
// Step 1: Registration form
614+
// Step 1: Registration form + success callout
615615
{
616616
'registration-form': {
617617
type: 'form',
618618
fields: ['full-name', 'email', 'department'],
619+
actions: { onSubmit: 'registration-submitted' },
619620
},
621+
'registration-submitted': { type: 'callout' },
620622
},
621-
// Step 2: Approval gate
623+
// Step 2: Approval gate + success callout
622624
{
623-
'approval-gate': { type: 'approval-gate' },
625+
'approval-gate': {
626+
type: 'approval-gate',
627+
actions: { onApprove: 'approval-complete' },
628+
},
629+
'approval-complete': { type: 'callout' },
624630
},
625-
// Step 3: Webhook + button
631+
// Step 3: Webhook + button + success callout
626632
{
627-
'notify-webhook': { type: 'webhook' },
633+
'notify-webhook': {
634+
type: 'webhook',
635+
actions: { trigger: 'send-notification' },
636+
},
628637
'send-notification': { type: 'button' },
638+
'workflow-complete': { type: 'callout' },
629639
},
630640
],
631641
};

packages/validator/src/fixes/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fixBindingSyntax } from './binding-syntax.js';
55
import { fixSensitiveFlags } from './sensitive-flags.js';
66
import { fixSchemaDefaults } from './schema-defaults.js';
77
import { fixActionReferences } from './action-references.js';
8+
import { fixThinkingBlock } from './thinking-block.js';
89

910
/** Maps rule IDs to their fix functions. Rules without fixes are absent. */
1011
export const FIX_REGISTRY: Partial<Record<ValidationRuleId, FixFunction>> = {
@@ -14,20 +15,22 @@ export const FIX_REGISTRY: Partial<Record<ValidationRuleId, FixFunction>> = {
1415
'sensitive-flags': fixSensitiveFlags,
1516
'action-references': fixActionReferences,
1617
'schema-conformance': fixSchemaDefaults,
18+
'thinking-block': fixThinkingBlock,
1719
};
1820

1921
/**
2022
* Ordered list of fixes to apply.
2123
*
22-
* 1. id-format first (ID changes affect cross-references)
23-
* 2. duplicate-ids second (dedup after format normalization)
24-
* 3. binding-syntax third
25-
* 4. sensitive-flags fourth
26-
* 5. action-references fifth (remove invalid refs before schema check)
27-
* 6. schema-conformance sixth (re-validates after all fixes, applies Zod defaults)
28-
* 7. reference-conformance last (applies reference template corrections)
24+
* 1. thinking-block first (merge + reorder before any other fixes)
25+
* 2. id-format (ID changes affect cross-references)
26+
* 3. duplicate-ids (dedup after format normalization)
27+
* 4. binding-syntax
28+
* 5. sensitive-flags
29+
* 6. action-references (remove invalid refs before schema check)
30+
* 7. schema-conformance last (re-validates after all fixes, applies Zod defaults)
2931
*/
3032
export const FIX_ORDER: ValidationRuleId[] = [
33+
'thinking-block',
3134
'id-format',
3235
'duplicate-ids',
3336
'binding-syntax',
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { FixContext } from '../types.js';
2+
3+
/**
4+
* Merge multiple thinking blocks into a single one at the top of the document.
5+
* Concatenates content from all thinking blocks, keeps the first one's metadata,
6+
* and removes the extras.
7+
*/
8+
export function fixThinkingBlock(context: FixContext): void {
9+
const thinkingIndices: number[] = [];
10+
for (let i = 0; i < context.blocks.length; i++) {
11+
if (context.blocks[i].data?.type === 'thinking') {
12+
thinkingIndices.push(i);
13+
}
14+
}
15+
16+
if (thinkingIndices.length < 2) return;
17+
18+
// Merge all thinking content into the first block
19+
const firstBlock = context.blocks[thinkingIndices[0]];
20+
if (!firstBlock.data) return;
21+
22+
const mergedContent: string[] = [];
23+
for (const idx of thinkingIndices) {
24+
const block = context.blocks[idx];
25+
if (block.data && typeof block.data.content === 'string') {
26+
mergedContent.push(block.data.content.trim());
27+
}
28+
}
29+
firstBlock.data.content = mergedContent.join('\n\n');
30+
firstBlock.data.status = 'done';
31+
firstBlock.data.collapsed = true;
32+
33+
// Remove extra thinking blocks (iterate in reverse to preserve indices)
34+
for (let i = thinkingIndices.length - 1; i >= 1; i--) {
35+
const idx = thinkingIndices[i];
36+
context.blocks[idx].data = null;
37+
}
38+
39+
// Move the merged thinking block to the top if it's not already first
40+
const firstParsedIdx = context.blocks.findIndex((b) => b.data !== null);
41+
if (firstParsedIdx !== thinkingIndices[0] && firstParsedIdx >= 0) {
42+
// Swap: put thinking block data into the first parsed block position
43+
const thinkingData = firstBlock.data;
44+
const targetBlock = context.blocks[firstParsedIdx];
45+
firstBlock.data = targetBlock.data;
46+
targetBlock.data = thinkingData;
47+
}
48+
49+
// Mark thinking-block issues as fixed
50+
for (const issue of context.issues) {
51+
if (issue.ruleId === 'thinking-block' && !issue.fixed) {
52+
issue.fixed = true;
53+
}
54+
}
55+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { fixThinkingBlock } from '../../src/fixes/thinking-block.js';
3+
import type { FixContext, ParsedBlock, ValidationIssue } from '../../src/types.js';
4+
5+
function createBlock(index: number, data: Record<string, unknown> | null): ParsedBlock {
6+
return {
7+
index,
8+
rawYaml: '',
9+
data,
10+
startOffset: index * 100,
11+
endOffset: index * 100 + 50,
12+
yamlStartOffset: index * 100 + 10,
13+
yamlEndOffset: index * 100 + 45,
14+
};
15+
}
16+
17+
function createFixContext(blocks: ParsedBlock[], issues: ValidationIssue[] = []): FixContext {
18+
const idMap = new Map<string, number>();
19+
for (const block of blocks) {
20+
if (block.data && typeof block.data.id === 'string') {
21+
idMap.set(block.data.id, block.index);
22+
}
23+
}
24+
return { blocks, idMap, issues, options: {} };
25+
}
26+
27+
describe('fixThinkingBlock', () => {
28+
it('does nothing with a single thinking block', () => {
29+
const ctx = createFixContext([
30+
createBlock(0, { type: 'thinking', id: 't1', content: 'Reasoning...', status: 'done', collapsed: true }),
31+
createBlock(1, { type: 'form', id: 'f1', fields: [] }),
32+
]);
33+
fixThinkingBlock(ctx);
34+
const thinkingBlocks = ctx.blocks.filter((b) => b.data?.type === 'thinking');
35+
expect(thinkingBlocks).toHaveLength(1);
36+
expect(thinkingBlocks[0].data?.content).toBe('Reasoning...');
37+
});
38+
39+
it('merges two thinking blocks into one', () => {
40+
const ctx = createFixContext([
41+
createBlock(0, { type: 'thinking', id: 't1', content: 'First reasoning', status: 'done', collapsed: true }),
42+
createBlock(1, { type: 'form', id: 'f1', fields: [] }),
43+
createBlock(2, { type: 'thinking', id: 't2', content: 'Second reasoning', status: 'done', collapsed: true }),
44+
]);
45+
fixThinkingBlock(ctx);
46+
const thinkingBlocks = ctx.blocks.filter((b) => b.data?.type === 'thinking');
47+
expect(thinkingBlocks).toHaveLength(1);
48+
expect(thinkingBlocks[0].data?.content).toBe('First reasoning\n\nSecond reasoning');
49+
});
50+
51+
it('merges three thinking blocks into one', () => {
52+
const ctx = createFixContext([
53+
createBlock(0, { type: 'thinking', id: 't1', content: 'A', status: 'done', collapsed: true }),
54+
createBlock(1, { type: 'thinking', id: 't2', content: 'B', status: 'done', collapsed: true }),
55+
createBlock(2, { type: 'thinking', id: 't3', content: 'C', status: 'done', collapsed: true }),
56+
]);
57+
fixThinkingBlock(ctx);
58+
const thinkingBlocks = ctx.blocks.filter((b) => b.data?.type === 'thinking');
59+
expect(thinkingBlocks).toHaveLength(1);
60+
expect(thinkingBlocks[0].data?.content).toBe('A\n\nB\n\nC');
61+
});
62+
63+
it('moves merged thinking block to the top', () => {
64+
const ctx = createFixContext([
65+
createBlock(0, { type: 'form', id: 'f1', fields: [] }),
66+
createBlock(1, { type: 'thinking', id: 't1', content: 'First', status: 'done', collapsed: true }),
67+
createBlock(2, { type: 'thinking', id: 't2', content: 'Second', status: 'done', collapsed: true }),
68+
]);
69+
fixThinkingBlock(ctx);
70+
// First parsed block should now be thinking
71+
const firstParsed = ctx.blocks.find((b) => b.data !== null);
72+
expect(firstParsed?.data?.type).toBe('thinking');
73+
expect(firstParsed?.data?.content).toBe('First\n\nSecond');
74+
});
75+
76+
it('marks thinking-block issues as fixed', () => {
77+
const issues: ValidationIssue[] = [
78+
{
79+
ruleId: 'thinking-block',
80+
severity: 'warning',
81+
message: 'Duplicate thinking block "t2"',
82+
componentId: 't2',
83+
blockIndex: 2,
84+
fixed: false,
85+
},
86+
];
87+
const ctx = createFixContext(
88+
[
89+
createBlock(0, { type: 'thinking', id: 't1', content: 'A', status: 'done', collapsed: true }),
90+
createBlock(1, { type: 'form', id: 'f1', fields: [] }),
91+
createBlock(2, { type: 'thinking', id: 't2', content: 'B', status: 'done', collapsed: true }),
92+
],
93+
issues,
94+
);
95+
fixThinkingBlock(ctx);
96+
expect(issues[0].fixed).toBe(true);
97+
});
98+
});

0 commit comments

Comments
 (0)