@lokascript/framework 2.0.0 → 2.2.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/dist/aot/index.d.ts +1 -0
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/registry-bridge.d.ts +27 -0
- package/dist/aot/registry-bridge.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/dispatcher.d.ts +22 -0
- package/dist/api/dispatcher.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +41 -0
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +1438 -21
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +247 -10
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +74 -10
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/tokenization/index.js +133 -0
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
- package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +3 -1
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
- package/dist/core/types.d.ts +81 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +40 -0
- package/dist/core/types.js.map +1 -1
- package/dist/feedback/confidence-gate.d.ts +28 -0
- package/dist/feedback/confidence-gate.d.ts.map +1 -0
- package/dist/feedback/feedback-formatter.d.ts +21 -0
- package/dist/feedback/feedback-formatter.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +11 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/pattern-tracker.d.ts +47 -0
- package/dist/feedback/pattern-tracker.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +113 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/generation/index.js +2 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +3264 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3223 -132
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +86 -0
- package/dist/ir/explicit-parser.d.ts.map +1 -0
- package/dist/ir/explicit-renderer.d.ts +34 -0
- package/dist/ir/explicit-renderer.d.ts.map +1 -0
- package/dist/ir/from-interchange.d.ts +51 -0
- package/dist/ir/from-interchange.d.ts.map +1 -0
- package/dist/ir/index.d.ts +31 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +1934 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/json-schema.d.ts +40 -0
- package/dist/ir/json-schema.d.ts.map +1 -0
- package/dist/ir/protocol-json.d.ts +73 -0
- package/dist/ir/protocol-json.d.ts.map +1 -0
- package/dist/ir/references.d.ts +19 -0
- package/dist/ir/references.d.ts.map +1 -0
- package/dist/ir/to-runtime-ast.d.ts +53 -0
- package/dist/ir/to-runtime-ast.d.ts.map +1 -0
- package/dist/ir/types.d.ts +151 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/parsing/index.js +74 -10
- package/dist/parsing/index.js.map +1 -1
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/prompt-generator.d.ts +49 -0
- package/dist/prompts/prompt-generator.d.ts.map +1 -0
- package/dist/prompts/types.d.ts +64 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +17 -0
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +8673 -8260
- package/dist/testing/index.js.map +1 -1
- package/dist/training/index.d.ts +11 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/jsonl-writer.d.ts +42 -0
- package/dist/training/jsonl-writer.d.ts.map +1 -0
- package/dist/training/schema-synthesizer.d.ts +20 -0
- package/dist/training/schema-synthesizer.d.ts.map +1 -0
- package/dist/training/types.d.ts +73 -0
- package/dist/training/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/aot/index.ts +1 -0
- package/src/aot/registry-bridge.test.ts +104 -0
- package/src/aot/registry-bridge.ts +66 -0
- package/src/api/create-dsl.test.ts +306 -0
- package/src/api/create-dsl.ts +21 -0
- package/src/api/dispatcher.test.ts +66 -0
- package/src/api/dispatcher.ts +77 -9
- package/src/api/domain-registry.ts +120 -4
- package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
- package/src/core/pattern-matching/pattern-matcher.ts +102 -19
- package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
- package/src/core/tokenization/morphology/index.ts +3 -1
- package/src/core/types.ts +147 -2
- package/src/feedback/confidence-gate.test.ts +111 -0
- package/src/feedback/confidence-gate.ts +70 -0
- package/src/feedback/feedback-formatter.test.ts +198 -0
- package/src/feedback/feedback-formatter.ts +298 -0
- package/src/feedback/index.ts +24 -0
- package/src/feedback/pattern-tracker.test.ts +198 -0
- package/src/feedback/pattern-tracker.ts +140 -0
- package/src/feedback/types.ts +149 -0
- package/src/generation/pattern-generator.ts +2 -2
- package/src/index.ts +29 -0
- package/src/integration/llm-lse-integration.test.ts +457 -0
- package/src/ir/conformance.test.ts +120 -0
- package/src/ir/explicit-parser.test.ts +612 -0
- package/src/ir/explicit-parser.ts +793 -0
- package/src/ir/explicit-renderer.test.ts +229 -0
- package/src/ir/explicit-renderer.ts +198 -0
- package/src/ir/from-interchange.test.ts +853 -0
- package/src/ir/from-interchange.ts +594 -0
- package/src/ir/index.ts +77 -0
- package/src/ir/json-schema.test.ts +281 -0
- package/src/ir/json-schema.ts +299 -0
- package/src/ir/protocol-json.test.ts +1187 -0
- package/src/ir/protocol-json.ts +751 -0
- package/src/ir/references.test.ts +38 -0
- package/src/ir/references.ts +33 -0
- package/src/ir/round-trip.test.ts +557 -0
- package/src/ir/to-runtime-ast.ts +203 -0
- package/src/ir/types.ts +191 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/prompt-generator.test.ts +386 -0
- package/src/prompts/prompt-generator.ts +531 -0
- package/src/prompts/types.ts +89 -0
- package/src/schema/command-schema.ts +19 -0
- package/src/training/index.ts +13 -0
- package/src/training/jsonl-writer.test.ts +138 -0
- package/src/training/jsonl-writer.ts +67 -0
- package/src/training/schema-synthesizer.test.ts +250 -0
- package/src/training/schema-synthesizer.ts +306 -0
- package/src/training/types.ts +103 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Confidence-Aware Disambiguation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { needsDisambiguation, buildDisambiguation } from './confidence-gate';
|
|
7
|
+
import { createCommandNode, createSelector, createLiteral } from '../core/types';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// needsDisambiguation()
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
describe('needsDisambiguation', () => {
|
|
14
|
+
it('returns false for high confidence', () => {
|
|
15
|
+
expect(needsDisambiguation(0.9)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns false for very low confidence', () => {
|
|
19
|
+
expect(needsDisambiguation(0.3)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns true for borderline confidence', () => {
|
|
23
|
+
expect(needsDisambiguation(0.6)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns true at exact low threshold', () => {
|
|
27
|
+
expect(needsDisambiguation(0.5)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns false at exact high threshold', () => {
|
|
31
|
+
expect(needsDisambiguation(0.7)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('supports custom thresholds', () => {
|
|
35
|
+
expect(needsDisambiguation(0.45, 0.4, 0.6)).toBe(true);
|
|
36
|
+
expect(needsDisambiguation(0.3, 0.4, 0.6)).toBe(false);
|
|
37
|
+
expect(needsDisambiguation(0.7, 0.4, 0.6)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// buildDisambiguation()
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
describe('buildDisambiguation', () => {
|
|
46
|
+
const nodeA = createCommandNode('toggle', { patient: createSelector('#button') });
|
|
47
|
+
const nodeB = createCommandNode('toggle', { patient: createSelector('.button') });
|
|
48
|
+
|
|
49
|
+
it('builds a disambiguation request', () => {
|
|
50
|
+
const result = buildDisambiguation('toggle button', [
|
|
51
|
+
{ action: 'toggle', confidence: 0.62, node: nodeA, description: 'toggle class on #button' },
|
|
52
|
+
{ action: 'toggle', confidence: 0.58, node: nodeB, description: 'toggle .button class' },
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
expect(result.input).toBe('toggle button');
|
|
56
|
+
expect(result.candidates.length).toBe(2);
|
|
57
|
+
expect(result.question).toContain('0.62');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('sorts candidates by confidence (highest first)', () => {
|
|
61
|
+
const result = buildDisambiguation('test', [
|
|
62
|
+
{ action: 'add', confidence: 0.5, node: nodeB },
|
|
63
|
+
{ action: 'toggle', confidence: 0.65, node: nodeA },
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
expect(result.candidates[0].action).toBe('toggle');
|
|
67
|
+
expect(result.candidates[1].action).toBe('add');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes bracket syntax in candidates', () => {
|
|
71
|
+
const result = buildDisambiguation('test', [
|
|
72
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA },
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
expect(result.candidates[0].explicit).toContain('[toggle');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('generates option labels (a, b, c)', () => {
|
|
79
|
+
const result = buildDisambiguation('test', [
|
|
80
|
+
{ action: 'a', confidence: 0.6, node: nodeA },
|
|
81
|
+
{ action: 'b', confidence: 0.55, node: nodeB },
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
expect(result.question).toContain('(a)');
|
|
85
|
+
expect(result.question).toContain('(b)');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes Which did you mean? prompt', () => {
|
|
89
|
+
const result = buildDisambiguation('test', [
|
|
90
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA },
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
expect(result.question).toContain('Which did you mean?');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('uses custom description when provided', () => {
|
|
97
|
+
const result = buildDisambiguation('test', [
|
|
98
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA, description: 'Custom description' },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
expect(result.candidates[0].description).toBe('Custom description');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('generates default description when not provided', () => {
|
|
105
|
+
const result = buildDisambiguation('test', [
|
|
106
|
+
{ action: 'toggle', confidence: 0.6, node: nodeA },
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
expect(result.candidates[0].description).toContain('toggle');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence-Aware Disambiguation
|
|
3
|
+
*
|
|
4
|
+
* When parse confidence is borderline (between low and high thresholds),
|
|
5
|
+
* generates a disambiguation request with alternative interpretations
|
|
6
|
+
* instead of blindly accepting or rejecting.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SemanticNode } from '../core/types';
|
|
10
|
+
import { renderExplicit } from '../ir/explicit-renderer';
|
|
11
|
+
import type { DisambiguationRequest, DisambiguationCandidate } from './types';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Public API
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check whether a confidence score falls in the disambiguation range.
|
|
19
|
+
*
|
|
20
|
+
* @returns true if the confidence is ambiguous (between low and high thresholds)
|
|
21
|
+
*/
|
|
22
|
+
export function needsDisambiguation(
|
|
23
|
+
confidence: number,
|
|
24
|
+
low: number = 0.5,
|
|
25
|
+
high: number = 0.7
|
|
26
|
+
): boolean {
|
|
27
|
+
return confidence >= low && confidence < high;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a disambiguation request from candidate interpretations.
|
|
32
|
+
*
|
|
33
|
+
* @param input - The original ambiguous input
|
|
34
|
+
* @param candidates - Alternative interpretations with confidence scores
|
|
35
|
+
*/
|
|
36
|
+
export function buildDisambiguation(
|
|
37
|
+
input: string,
|
|
38
|
+
candidates: ReadonlyArray<{
|
|
39
|
+
action: string;
|
|
40
|
+
confidence: number;
|
|
41
|
+
node: SemanticNode;
|
|
42
|
+
description?: string;
|
|
43
|
+
}>
|
|
44
|
+
): DisambiguationRequest {
|
|
45
|
+
const sortedCandidates = [...candidates].sort((a, b) => b.confidence - a.confidence);
|
|
46
|
+
|
|
47
|
+
const disambiguationCandidates: DisambiguationCandidate[] = sortedCandidates.map((c, i) => ({
|
|
48
|
+
action: c.action,
|
|
49
|
+
confidence: c.confidence,
|
|
50
|
+
explicit: renderExplicit(c.node),
|
|
51
|
+
description: c.description || `${c.action} (option ${String.fromCharCode(97 + i)})`,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const optionLines = disambiguationCandidates.map(
|
|
55
|
+
(c, i) =>
|
|
56
|
+
` (${String.fromCharCode(97 + i)}) ${c.explicit} — ${c.description} (conf: ${c.confidence.toFixed(2)})`
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const question = [
|
|
60
|
+
`Your input parsed with confidence ${sortedCandidates[0]?.confidence.toFixed(2) ?? '?'}.`,
|
|
61
|
+
...optionLines,
|
|
62
|
+
'Which did you mean?',
|
|
63
|
+
].join('\n');
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
input,
|
|
67
|
+
candidates: disambiguationCandidates,
|
|
68
|
+
question,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Feedback Formatter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { buildFeedback } from './feedback-formatter';
|
|
7
|
+
import type { Diagnostic } from '../generation/diagnostics';
|
|
8
|
+
import type { SchemaLookup } from '../ir/types';
|
|
9
|
+
import { defineCommand, defineRole } from '../schema/command-schema';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Test Helpers
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
const fetchSchema = defineCommand({
|
|
16
|
+
action: 'fetch',
|
|
17
|
+
description: 'Fetch data from a URL',
|
|
18
|
+
category: 'source',
|
|
19
|
+
primaryRole: 'source',
|
|
20
|
+
roles: [
|
|
21
|
+
defineRole({
|
|
22
|
+
role: 'source',
|
|
23
|
+
description: 'URL to fetch from',
|
|
24
|
+
required: true,
|
|
25
|
+
expectedTypes: ['expression'],
|
|
26
|
+
}),
|
|
27
|
+
defineRole({
|
|
28
|
+
role: 'style',
|
|
29
|
+
description: 'Response format',
|
|
30
|
+
required: false,
|
|
31
|
+
expectedTypes: ['expression'],
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const schemaLookup: SchemaLookup = {
|
|
37
|
+
getSchema(action: string) {
|
|
38
|
+
if (action === 'fetch') return fetchSchema;
|
|
39
|
+
return undefined;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function makeDiagnostic(overrides?: Partial<Diagnostic>): Diagnostic {
|
|
44
|
+
return {
|
|
45
|
+
message: 'Test error',
|
|
46
|
+
severity: 'error',
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// buildFeedback()
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
describe('buildFeedback', () => {
|
|
56
|
+
it('accepts input with no errors', () => {
|
|
57
|
+
const result = buildFeedback('[fetch source:/api]', 'explicit', []);
|
|
58
|
+
expect(result.accepted).toBe(true);
|
|
59
|
+
expect(result.diagnostics.length).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects input with errors', () => {
|
|
63
|
+
const result = buildFeedback('[fetch]', 'explicit', [makeDiagnostic()]);
|
|
64
|
+
expect(result.accepted).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('preserves input info', () => {
|
|
68
|
+
const result = buildFeedback('[fetch source:/api]', 'explicit', []);
|
|
69
|
+
expect(result.input.format).toBe('explicit');
|
|
70
|
+
expect(result.input.text).toBe('[fetch source:/api]');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles JSON input format', () => {
|
|
74
|
+
const result = buildFeedback('{"action":"fetch"}', 'json', []);
|
|
75
|
+
expect(result.input.format).toBe('json');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- Diagnostic Classification ---
|
|
79
|
+
|
|
80
|
+
it('classifies missing_role fix type', () => {
|
|
81
|
+
const diag = makeDiagnostic({ code: 'missing-role', message: 'Required role source missing' });
|
|
82
|
+
const result = buildFeedback('[fetch]', 'explicit', [diag]);
|
|
83
|
+
expect(result.diagnostics[0].fixType).toBe('missing_role');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('classifies unknown_command fix type', () => {
|
|
87
|
+
const diag = makeDiagnostic({ code: 'unknown-command', message: 'Unknown command xyz' });
|
|
88
|
+
const result = buildFeedback('[xyz]', 'explicit', [diag]);
|
|
89
|
+
expect(result.diagnostics[0].fixType).toBe('unknown_command');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('classifies unknown_role fix type', () => {
|
|
93
|
+
const diag = makeDiagnostic({ code: 'unknown-role', message: 'Unknown role target' });
|
|
94
|
+
const result = buildFeedback('[fetch target:/api]', 'explicit', [diag]);
|
|
95
|
+
expect(result.diagnostics[0].fixType).toBe('unknown_role');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('classifies invalid_type fix type', () => {
|
|
99
|
+
const diag = makeDiagnostic({ code: 'invalid-type', message: 'Invalid type for role' });
|
|
100
|
+
const result = buildFeedback('[fetch source:123]', 'explicit', [diag]);
|
|
101
|
+
expect(result.diagnostics[0].fixType).toBe('invalid_type');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('classifies syntax_error fix type', () => {
|
|
105
|
+
const diag = makeDiagnostic({ code: 'parse-error', message: 'Unexpected token' });
|
|
106
|
+
const result = buildFeedback('[fetch', 'explicit', [diag]);
|
|
107
|
+
expect(result.diagnostics[0].fixType).toBe('syntax_error');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('handles diagnostics with no code', () => {
|
|
111
|
+
const diag = makeDiagnostic({ code: undefined, message: 'Something went wrong' });
|
|
112
|
+
const result = buildFeedback('[fetch]', 'explicit', [diag]);
|
|
113
|
+
expect(result.diagnostics[0].code).toBe('unknown');
|
|
114
|
+
expect(result.diagnostics[0].fixType).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// --- Hints ---
|
|
118
|
+
|
|
119
|
+
it('generates hints for missing_role with schema', () => {
|
|
120
|
+
const diag = makeDiagnostic({ code: 'missing-role', message: 'Missing role source' });
|
|
121
|
+
const result = buildFeedback('[fetch]', 'explicit', [diag], schemaLookup, 'fetch');
|
|
122
|
+
expect(result.hints.length).toBeGreaterThan(0);
|
|
123
|
+
expect(result.hints[0]).toContain("'source'");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('generates hints for unknown_role with schema', () => {
|
|
127
|
+
const diag = makeDiagnostic({ code: 'unknown-role', message: 'Unknown role target' });
|
|
128
|
+
const result = buildFeedback('[fetch target:/api]', 'explicit', [diag], schemaLookup, 'fetch');
|
|
129
|
+
expect(result.hints.some(h => h.includes("'source'"))).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('generates generic hint for unknown_command', () => {
|
|
133
|
+
const diag = makeDiagnostic({ code: 'unknown-command', message: 'Unknown command' });
|
|
134
|
+
const result = buildFeedback('[xyz]', 'explicit', [diag]);
|
|
135
|
+
expect(result.hints.some(h => h.includes('not recognized'))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('deduplicates hints', () => {
|
|
139
|
+
const diags = [
|
|
140
|
+
makeDiagnostic({ code: 'missing-role', message: 'Missing role source' }),
|
|
141
|
+
makeDiagnostic({ code: 'missing-role', message: 'Missing role source again' }),
|
|
142
|
+
];
|
|
143
|
+
const result = buildFeedback('[fetch]', 'explicit', diags, schemaLookup, 'fetch');
|
|
144
|
+
const unique = new Set(result.hints);
|
|
145
|
+
expect(unique.size).toBe(result.hints.length);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- Schema Info ---
|
|
149
|
+
|
|
150
|
+
it('includes schema info when action and schemaLookup provided', () => {
|
|
151
|
+
const result = buildFeedback('[fetch]', 'explicit', [makeDiagnostic()], schemaLookup, 'fetch');
|
|
152
|
+
expect(result.schema).toBeDefined();
|
|
153
|
+
expect(result.schema!.action).toBe('fetch');
|
|
154
|
+
expect(result.schema!.requiredRoles).toContain('source');
|
|
155
|
+
expect(result.schema!.optionalRoles).toContain('style');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('omits schema when no schemaLookup', () => {
|
|
159
|
+
const result = buildFeedback('[fetch]', 'explicit', [makeDiagnostic()], undefined, 'fetch');
|
|
160
|
+
expect(result.schema).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('omits schema when action not found', () => {
|
|
164
|
+
const result = buildFeedback('[xyz]', 'explicit', [makeDiagnostic()], schemaLookup, 'xyz');
|
|
165
|
+
expect(result.schema).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// --- Corrected Example ---
|
|
169
|
+
|
|
170
|
+
it('generates corrected example on error with schema', () => {
|
|
171
|
+
const diag = makeDiagnostic({ code: 'missing-role', message: 'Missing role' });
|
|
172
|
+
const result = buildFeedback('[fetch]', 'explicit', [diag], schemaLookup, 'fetch');
|
|
173
|
+
expect(result.correctedExample).toBeDefined();
|
|
174
|
+
expect(result.correctedExample!.explicit).toContain('[fetch');
|
|
175
|
+
expect(result.correctedExample!.json.action).toBe('fetch');
|
|
176
|
+
expect(result.correctedExample!.json.roles['source']).toBeDefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('omits corrected example when accepted', () => {
|
|
180
|
+
const result = buildFeedback('[fetch source:/api]', 'explicit', [], schemaLookup, 'fetch');
|
|
181
|
+
expect(result.correctedExample).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// --- Warnings/Info ---
|
|
185
|
+
|
|
186
|
+
it('accepts with warnings (no errors)', () => {
|
|
187
|
+
const diag = makeDiagnostic({ severity: 'warning', message: 'Low confidence' });
|
|
188
|
+
const result = buildFeedback('[fetch source:/api]', 'explicit', [diag]);
|
|
189
|
+
expect(result.accepted).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('includes info-level diagnostics', () => {
|
|
193
|
+
const diag = makeDiagnostic({ severity: 'info', message: 'Suggestion' });
|
|
194
|
+
const result = buildFeedback('[fetch source:/api]', 'explicit', [diag]);
|
|
195
|
+
expect(result.diagnostics.length).toBe(1);
|
|
196
|
+
expect(result.diagnostics[0].severity).toBe('info');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Formatter
|
|
3
|
+
*
|
|
4
|
+
* Maps diagnostics to machine-actionable LSEFeedback with hints,
|
|
5
|
+
* corrected examples, and schema context. Enables LLMs to self-correct
|
|
6
|
+
* based on structured error information.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Diagnostic } from '../generation/diagnostics';
|
|
10
|
+
import type { CommandSchema } from '../schema/command-schema';
|
|
11
|
+
import type { SchemaLookup, SemanticJSON, SemanticJSONValue } from '../ir/types';
|
|
12
|
+
import { renderExplicit } from '../ir/explicit-renderer';
|
|
13
|
+
import { createCommandNode } from '../core/types';
|
|
14
|
+
import type { LSEFeedback, FeedbackDiagnostic, FixType } from './types';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Public API
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build structured feedback from diagnostics and optional schema context.
|
|
22
|
+
*
|
|
23
|
+
* @param input - The original input text
|
|
24
|
+
* @param inputFormat - Format of the input
|
|
25
|
+
* @param diagnostics - Error diagnostics from validation/parsing
|
|
26
|
+
* @param schemaLookup - Optional schema lookup for generating hints and corrections
|
|
27
|
+
* @param action - The attempted command action (if identifiable)
|
|
28
|
+
*/
|
|
29
|
+
export function buildFeedback(
|
|
30
|
+
input: string,
|
|
31
|
+
inputFormat: 'explicit' | 'json' | 'natural',
|
|
32
|
+
diagnostics: readonly Diagnostic[],
|
|
33
|
+
schemaLookup?: SchemaLookup,
|
|
34
|
+
action?: string
|
|
35
|
+
): LSEFeedback {
|
|
36
|
+
const hasErrors = diagnostics.some(d => d.severity === 'error');
|
|
37
|
+
const feedbackDiagnostics = diagnostics.map(d => toFeedbackDiagnostic(d));
|
|
38
|
+
const hints = generateHints(feedbackDiagnostics, schemaLookup, action);
|
|
39
|
+
|
|
40
|
+
let schemaInfo: LSEFeedback['schema'] | undefined;
|
|
41
|
+
let correctedExample: LSEFeedback['correctedExample'] | undefined;
|
|
42
|
+
|
|
43
|
+
if (action && schemaLookup) {
|
|
44
|
+
const schema = schemaLookup.getSchema(action);
|
|
45
|
+
if (schema) {
|
|
46
|
+
schemaInfo = extractSchemaInfo(schema);
|
|
47
|
+
if (hasErrors) {
|
|
48
|
+
correctedExample = generateCorrectedExample(schema);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
accepted: !hasErrors,
|
|
55
|
+
input: { format: inputFormat, text: input },
|
|
56
|
+
diagnostics: feedbackDiagnostics,
|
|
57
|
+
hints,
|
|
58
|
+
...(schemaInfo && { schema: schemaInfo }),
|
|
59
|
+
...(correctedExample && { correctedExample }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Diagnostic Classification
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Map a framework Diagnostic to a FeedbackDiagnostic with fix type.
|
|
69
|
+
*/
|
|
70
|
+
function toFeedbackDiagnostic(d: Diagnostic): FeedbackDiagnostic {
|
|
71
|
+
const fixType = classifyFixType(d);
|
|
72
|
+
return {
|
|
73
|
+
severity: d.severity,
|
|
74
|
+
code: d.code || 'unknown',
|
|
75
|
+
message: d.message,
|
|
76
|
+
...(d.suggestions?.[0] && { suggestion: d.suggestions[0] }),
|
|
77
|
+
...(fixType && { fixType }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Classify a diagnostic into a machine-actionable fix type.
|
|
83
|
+
*/
|
|
84
|
+
function classifyFixType(d: Diagnostic): FixType | undefined {
|
|
85
|
+
const code = (d.code || '').toLowerCase();
|
|
86
|
+
const msg = d.message.toLowerCase();
|
|
87
|
+
|
|
88
|
+
// Missing required role
|
|
89
|
+
if (
|
|
90
|
+
code.includes('missing-role') ||
|
|
91
|
+
code.includes('missing_role') ||
|
|
92
|
+
msg.includes('required role') ||
|
|
93
|
+
msg.includes('missing role')
|
|
94
|
+
) {
|
|
95
|
+
return 'missing_role';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Invalid value type
|
|
99
|
+
if (
|
|
100
|
+
code.includes('invalid-type') ||
|
|
101
|
+
code.includes('invalid_type') ||
|
|
102
|
+
code.includes('type-mismatch') ||
|
|
103
|
+
msg.includes('invalid type') ||
|
|
104
|
+
msg.includes('expected type')
|
|
105
|
+
) {
|
|
106
|
+
return 'invalid_type';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Unknown command
|
|
110
|
+
if (
|
|
111
|
+
code.includes('unknown-command') ||
|
|
112
|
+
code.includes('unknown_command') ||
|
|
113
|
+
msg.includes('unknown command') ||
|
|
114
|
+
(msg.includes('not recognized') && msg.includes('command'))
|
|
115
|
+
) {
|
|
116
|
+
return 'unknown_command';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Unknown role
|
|
120
|
+
if (
|
|
121
|
+
code.includes('unknown-role') ||
|
|
122
|
+
code.includes('unknown_role') ||
|
|
123
|
+
msg.includes('unknown role') ||
|
|
124
|
+
(msg.includes('not recognized') && msg.includes('role'))
|
|
125
|
+
) {
|
|
126
|
+
return 'unknown_role';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Syntax error (catch-all for parse errors)
|
|
130
|
+
if (
|
|
131
|
+
code.includes('parse') ||
|
|
132
|
+
code.includes('syntax') ||
|
|
133
|
+
msg.includes('syntax error') ||
|
|
134
|
+
msg.includes('parse error') ||
|
|
135
|
+
msg.includes('unexpected')
|
|
136
|
+
) {
|
|
137
|
+
return 'syntax_error';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// Hint Generation
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
function generateHints(
|
|
148
|
+
diagnostics: readonly FeedbackDiagnostic[],
|
|
149
|
+
schemaLookup?: SchemaLookup,
|
|
150
|
+
action?: string
|
|
151
|
+
): string[] {
|
|
152
|
+
const hints: string[] = [];
|
|
153
|
+
const schema = action && schemaLookup ? schemaLookup.getSchema(action) : undefined;
|
|
154
|
+
|
|
155
|
+
for (const d of diagnostics) {
|
|
156
|
+
if (d.severity !== 'error') continue;
|
|
157
|
+
|
|
158
|
+
switch (d.fixType) {
|
|
159
|
+
case 'missing_role': {
|
|
160
|
+
if (schema) {
|
|
161
|
+
const required = schema.roles
|
|
162
|
+
.filter(r => r.required)
|
|
163
|
+
.map(r => `'${r.role}' (${r.description}, type: ${r.expectedTypes.join('|')})`)
|
|
164
|
+
.join(', ');
|
|
165
|
+
hints.push(`Required roles for '${action}': ${required}`);
|
|
166
|
+
} else {
|
|
167
|
+
hints.push('Check that all required roles are present.');
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case 'unknown_command': {
|
|
173
|
+
hints.push(
|
|
174
|
+
`Command not recognized. Ensure you're using a valid command from the domain schema.`
|
|
175
|
+
);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case 'unknown_role': {
|
|
180
|
+
if (schema) {
|
|
181
|
+
const validRoles = schema.roles.map(r => `'${r.role}'`).join(', ');
|
|
182
|
+
hints.push(`Valid roles for '${action}': ${validRoles}`);
|
|
183
|
+
} else {
|
|
184
|
+
hints.push('Use only roles defined in the command schema.');
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'invalid_type': {
|
|
190
|
+
hints.push(
|
|
191
|
+
'Check value types: selectors start with #.[@*, strings use quotes, numbers are plain digits.'
|
|
192
|
+
);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case 'syntax_error': {
|
|
197
|
+
hints.push('Ensure bracket syntax: [command role:value ...]. No spaces around the colon.');
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
default: {
|
|
202
|
+
if (d.suggestion) {
|
|
203
|
+
hints.push(d.suggestion);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Deduplicate
|
|
210
|
+
return [...new Set(hints)];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Schema Info Extraction
|
|
215
|
+
// =============================================================================
|
|
216
|
+
|
|
217
|
+
function extractSchemaInfo(schema: CommandSchema): LSEFeedback['schema'] {
|
|
218
|
+
const requiredRoles: string[] = [];
|
|
219
|
+
const optionalRoles: string[] = [];
|
|
220
|
+
const roleDescriptions: Record<string, string> = {};
|
|
221
|
+
|
|
222
|
+
for (const role of schema.roles) {
|
|
223
|
+
if (role.required) {
|
|
224
|
+
requiredRoles.push(role.role);
|
|
225
|
+
} else {
|
|
226
|
+
optionalRoles.push(role.role);
|
|
227
|
+
}
|
|
228
|
+
roleDescriptions[role.role] = role.description;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
action: schema.action,
|
|
233
|
+
requiredRoles,
|
|
234
|
+
optionalRoles,
|
|
235
|
+
roleDescriptions,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// =============================================================================
|
|
240
|
+
// Corrected Example Generation
|
|
241
|
+
// =============================================================================
|
|
242
|
+
|
|
243
|
+
function generateCorrectedExample(schema: CommandSchema): { explicit: string; json: SemanticJSON } {
|
|
244
|
+
const roles = new Map<string, unknown>();
|
|
245
|
+
const jsonRoles: Record<string, SemanticJSONValue> = {};
|
|
246
|
+
|
|
247
|
+
for (const role of schema.roles) {
|
|
248
|
+
if (!role.required) continue;
|
|
249
|
+
const sample = sampleDefaultValue(role.role, role.expectedTypes[0] || 'expression');
|
|
250
|
+
roles.set(role.role, toSemanticValue(sample));
|
|
251
|
+
jsonRoles[role.role] = sample as SemanticJSONValue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const node = createCommandNode(schema.action, roles as never);
|
|
255
|
+
const explicit = renderExplicit(node);
|
|
256
|
+
const json: SemanticJSON = { action: schema.action, roles: jsonRoles };
|
|
257
|
+
|
|
258
|
+
return { explicit, json };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function sampleDefaultValue(
|
|
262
|
+
roleName: string,
|
|
263
|
+
expectedType: string
|
|
264
|
+
): { type: string; value: string } {
|
|
265
|
+
switch (expectedType) {
|
|
266
|
+
case 'selector':
|
|
267
|
+
return { type: 'selector', value: `#${roleName}` };
|
|
268
|
+
case 'expression':
|
|
269
|
+
return { type: 'expression', value: `<${roleName}-value>` };
|
|
270
|
+
case 'literal':
|
|
271
|
+
return { type: 'literal', value: `<${roleName}-value>` };
|
|
272
|
+
case 'reference':
|
|
273
|
+
return { type: 'reference', value: 'me' };
|
|
274
|
+
default:
|
|
275
|
+
return { type: 'expression', value: `<${roleName}-value>` };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function toSemanticValue(sample: { type: string; value: string }): {
|
|
280
|
+
type: string;
|
|
281
|
+
value: string;
|
|
282
|
+
raw?: string;
|
|
283
|
+
selectorKind?: string;
|
|
284
|
+
dataType?: string;
|
|
285
|
+
} {
|
|
286
|
+
switch (sample.type) {
|
|
287
|
+
case 'selector':
|
|
288
|
+
return { type: 'selector', value: sample.value, selectorKind: 'id' };
|
|
289
|
+
case 'expression':
|
|
290
|
+
return { type: 'expression', value: sample.value, raw: sample.value };
|
|
291
|
+
case 'literal':
|
|
292
|
+
return { type: 'literal', value: sample.value, dataType: 'string' };
|
|
293
|
+
case 'reference':
|
|
294
|
+
return { type: 'reference', value: sample.value };
|
|
295
|
+
default:
|
|
296
|
+
return { type: 'literal', value: sample.value, dataType: 'string' };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Loop
|
|
3
|
+
*
|
|
4
|
+
* Structured error feedback, confidence-aware disambiguation,
|
|
5
|
+
* and pattern hit-rate tracking for the LLM ↔ LSE integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
LSEFeedback,
|
|
10
|
+
FeedbackDiagnostic,
|
|
11
|
+
FixType,
|
|
12
|
+
DisambiguationRequest,
|
|
13
|
+
DisambiguationCandidate,
|
|
14
|
+
PatternEvent,
|
|
15
|
+
HitRate,
|
|
16
|
+
PatternTrackerSummary,
|
|
17
|
+
TopFailure,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
export { buildFeedback } from './feedback-formatter';
|
|
21
|
+
|
|
22
|
+
export { needsDisambiguation, buildDisambiguation } from './confidence-gate';
|
|
23
|
+
|
|
24
|
+
export { PatternTracker } from './pattern-tracker';
|