Skip to content

Commit 8d5cc36

Browse files
committed
Milestone 4: add TradingView Pine verification workflows
1 parent e5eedc5 commit 8d5cc36

File tree

6 files changed

+322
-0
lines changed

6 files changed

+322
-0
lines changed

docs/CHAT_CONTINUITY_IMPLEMENTATION_PLAN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,12 +1073,14 @@ node scripts/test-session-intent-state.js
10731073
- extracted deterministic TradingView alert workflow shaping to `src/main/tradingview/alert-workflows.js`
10741074
- extracted TradingView chart verification plus timeframe/symbol/watchlist workflow shaping to `src/main/tradingview/chart-verification.js`
10751075
- extracted verification-first TradingView drawing/object-tree surface workflow shaping to `src/main/tradingview/drawing-workflows.js`
1076+
- extracted verification-first TradingView Pine Editor surface workflow shaping to `src/main/tradingview/pine-workflows.js`
10761077
- extracted reusable post-key observation checkpoint helpers to `src/main/ai-service/observation-checkpoints.js`
10771078
- added direct module regressions in `scripts/test-tradingview-app-profile.js` and `scripts/test-tradingview-verification.js`
10781079
- added direct indicator-workflow regression coverage in `scripts/test-tradingview-indicator-workflows.js`
10791080
- added direct alert-workflow regression coverage in `scripts/test-tradingview-alert-workflows.js`
10801081
- added direct chart-verification regression coverage in `scripts/test-tradingview-chart-verification.js`
10811082
- added direct drawing-workflow regression coverage in `scripts/test-tradingview-drawing-workflows.js`
1083+
- added direct Pine workflow regression coverage in `scripts/test-tradingview-pine-workflows.js`
10821084

10831085
**Objective**
10841086
- formalize reusable TradingView workflow modules around alerts, indicators, and chart verification

scripts/test-bug-fixes.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ test('ai-service gates TradingView follow-up typing on post-key observation chec
254254
const tradingViewAlertPath = path.join(__dirname, '..', 'src', 'main', 'tradingview', 'alert-workflows.js');
255255
const tradingViewChartPath = path.join(__dirname, '..', 'src', 'main', 'tradingview', 'chart-verification.js');
256256
const tradingViewDrawingPath = path.join(__dirname, '..', 'src', 'main', 'tradingview', 'drawing-workflows.js');
257+
const tradingViewPinePath = path.join(__dirname, '..', 'src', 'main', 'tradingview', 'pine-workflows.js');
257258
const fs = require('fs');
258259

259260
const aiServiceContent = fs.readFileSync(aiServicePath, 'utf8');
@@ -263,6 +264,7 @@ test('ai-service gates TradingView follow-up typing on post-key observation chec
263264
const tradingViewAlertContent = fs.readFileSync(tradingViewAlertPath, 'utf8');
264265
const tradingViewChartContent = fs.readFileSync(tradingViewChartPath, 'utf8');
265266
const tradingViewDrawingContent = fs.readFileSync(tradingViewDrawingPath, 'utf8');
267+
const tradingViewPineContent = fs.readFileSync(tradingViewPinePath, 'utf8');
266268

267269
assert(aiServiceContent.includes("require('./ai-service/observation-checkpoints')"), 'ai-service should consume the extracted observation checkpoint helper module');
268270
assert(observationCheckpointContent.includes('inferKeyObservationCheckpoint'), 'Observation checkpoint module should infer TradingView post-key checkpoints');
@@ -274,6 +276,7 @@ test('ai-service gates TradingView follow-up typing on post-key observation chec
274276
assert(aiServiceContent.includes("require('./tradingview/alert-workflows')"), 'ai-service should consume the extracted TradingView alert workflow helper');
275277
assert(aiServiceContent.includes("require('./tradingview/chart-verification')"), 'ai-service should consume the extracted TradingView chart verification helper');
276278
assert(aiServiceContent.includes("require('./tradingview/drawing-workflows')"), 'ai-service should consume the extracted TradingView drawing workflow helper');
279+
assert(aiServiceContent.includes("require('./tradingview/pine-workflows')"), 'ai-service should consume the extracted TradingView Pine workflow helper');
277280
assert(tradingViewVerificationContent.includes("classification === 'panel-open'"), 'TradingView checkpoints should recognize panel-open flows such as Pine or DOM');
278281
assert(tradingViewVerificationContent.includes('pine editor'), 'TradingView checkpoints should ground Pine Editor workflows');
279282
assert(tradingViewVerificationContent.includes('depth of market'), 'TradingView checkpoints should ground DOM workflows');
@@ -287,6 +290,8 @@ test('ai-service gates TradingView follow-up typing on post-key observation chec
287290
assert(tradingViewChartContent.includes("key: 'enter'"), 'TradingView chart verification workflows should confirm timeframe changes with enter');
288291
assert(tradingViewDrawingContent.includes("target: 'object-tree'"), 'TradingView drawing workflows should encode object-tree verification metadata');
289292
assert(tradingViewDrawingContent.includes("kind: intent.verifyKind"), 'TradingView drawing workflows should preserve verification-first surface contracts');
293+
assert(tradingViewPineContent.includes("target: 'pine-editor'"), 'TradingView Pine workflows should encode pine-editor verification metadata');
294+
assert(tradingViewPineContent.includes('requiresObservedChange'), 'TradingView Pine workflows should gate follow-up typing on observed panel changes');
290295
});
291296

292297
test('ai-service treats TradingView DOM order-entry actions as high risk', () => {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env node
2+
3+
const assert = require('assert');
4+
const path = require('path');
5+
6+
const {
7+
inferTradingViewPineIntent,
8+
buildTradingViewPineWorkflowActions,
9+
maybeRewriteTradingViewPineWorkflow
10+
} = require(path.join(__dirname, '..', 'src', 'main', 'tradingview', 'pine-workflows.js'));
11+
12+
function test(name, fn) {
13+
try {
14+
fn();
15+
console.log(`PASS ${name}`);
16+
} catch (error) {
17+
console.error(`FAIL ${name}`);
18+
console.error(error.stack || error.message);
19+
process.exitCode = 1;
20+
}
21+
}
22+
23+
test('inferTradingViewPineIntent recognizes Pine Editor surface requests', () => {
24+
const intent = inferTradingViewPineIntent('open pine editor in tradingview', [
25+
{ type: 'key', key: 'ctrl+e' }
26+
]);
27+
28+
assert(intent, 'intent should be inferred');
29+
assert.strictEqual(intent.appName, 'TradingView');
30+
assert.strictEqual(intent.surfaceTarget, 'pine-editor');
31+
assert.strictEqual(intent.verifyKind, 'panel-visible');
32+
});
33+
34+
test('buildTradingViewPineWorkflowActions wraps the opener with panel verification', () => {
35+
const actions = buildTradingViewPineWorkflowActions({
36+
appName: 'TradingView',
37+
surfaceTarget: 'pine-editor',
38+
verifyKind: 'panel-visible',
39+
openerIndex: 0,
40+
requiresObservedChange: true
41+
}, [
42+
{ type: 'key', key: 'ctrl+e', reason: 'Open Pine Editor' },
43+
{ type: 'type', text: 'strategy("test")', reason: 'Type script' }
44+
]);
45+
46+
assert.strictEqual(actions[0].type, 'bring_window_to_front');
47+
assert.strictEqual(actions[2].type, 'key');
48+
assert.strictEqual(actions[2].verify.kind, 'panel-visible');
49+
assert.strictEqual(actions[2].verify.target, 'pine-editor');
50+
assert.strictEqual(actions[2].verify.requiresObservedChange, true);
51+
assert.strictEqual(actions[4].type, 'type');
52+
});
53+
54+
test('maybeRewriteTradingViewPineWorkflow rewrites low-signal Pine Editor opener plans', () => {
55+
const rewritten = maybeRewriteTradingViewPineWorkflow([
56+
{ type: 'key', key: 'ctrl+e' },
57+
{ type: 'type', text: 'plot(close)' }
58+
], {
59+
userMessage: 'open pine editor in tradingview and type plot(close)'
60+
});
61+
62+
assert(Array.isArray(rewritten), 'pine rewrite should return an action array');
63+
assert.strictEqual(rewritten[0].type, 'bring_window_to_front');
64+
assert.strictEqual(rewritten[2].type, 'key');
65+
assert.strictEqual(rewritten[2].verify.target, 'pine-editor');
66+
assert.strictEqual(rewritten[2].verify.requiresObservedChange, true);
67+
assert.strictEqual(rewritten[4].type, 'type');
68+
assert.strictEqual(rewritten[4].text, 'plot(close)');
69+
});
70+
71+
test('TradingView Pine workflow does not hijack authoring-only prompts', () => {
72+
const rewritten = maybeRewriteTradingViewPineWorkflow([
73+
{ type: 'key', key: 'ctrl+e' }
74+
], {
75+
userMessage: 'write a pine script for tradingview'
76+
});
77+
78+
assert.strictEqual(rewritten, null, 'authoring-only prompts should not be auto-rewritten into an opener flow');
79+
});

scripts/test-windows-observation-flow.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,25 @@ async function run() {
246246
assert.strictEqual(rewritten[4].text, 'trend line');
247247
});
248248

249+
await testAsync('low-signal TradingView Pine Editor request wraps the opener with bounded panel verification', async () => {
250+
const rewritten = aiService.rewriteActionsForReliability([
251+
{ type: 'key', key: 'ctrl+e' },
252+
{ type: 'type', text: 'plot(close)' }
253+
], {
254+
userMessage: 'open pine editor in tradingview and type plot(close)'
255+
});
256+
257+
assert(Array.isArray(rewritten), 'pine rewrite should return an action array');
258+
assert.strictEqual(rewritten[0].type, 'bring_window_to_front');
259+
assert.strictEqual(rewritten[0].processName, 'tradingview');
260+
assert.strictEqual(rewritten[2].type, 'key');
261+
assert.strictEqual(rewritten[2].verify.kind, 'panel-visible');
262+
assert.strictEqual(rewritten[2].verify.target, 'pine-editor');
263+
assert.strictEqual(rewritten[2].verify.requiresObservedChange, true);
264+
assert.strictEqual(rewritten[4].type, 'type');
265+
assert.strictEqual(rewritten[4].text, 'plot(close)');
266+
});
267+
249268
await testAsync('TradingView alert accelerator blocks follow-up typing when no dialog change is observed', async () => {
250269
const executed = [];
251270
const foregroundSequence = [
@@ -810,6 +829,57 @@ async function run() {
810829
});
811830
});
812831

832+
await testAsync('explicit TradingView Pine Editor contracts gate typing on observed panel change', async () => {
833+
const executed = [];
834+
const foregroundSequence = [
835+
{ success: true, hwnd: 777, title: 'TradingView', processName: 'tradingview', windowKind: 'main' },
836+
{ success: true, hwnd: 777, title: 'Pine Editor - TradingView', processName: 'tradingview', windowKind: 'main' },
837+
{ success: true, hwnd: 777, title: 'Pine Editor - TradingView', processName: 'tradingview', windowKind: 'main' },
838+
{ success: true, hwnd: 777, title: 'Pine Editor - TradingView', processName: 'tradingview', windowKind: 'main' }
839+
];
840+
841+
await withPatchedSystemAutomation({
842+
resolveWindowHandle: async (action) => action?.processName === 'tradingview' ? 777 : 0,
843+
getForegroundWindowInfo: async () => foregroundSequence.shift() || { success: true, hwnd: 777, title: 'Pine Editor - TradingView', processName: 'tradingview', windowKind: 'main' },
844+
focusWindow: async () => ({ success: true }),
845+
getRunningProcessesByNames: async () => ([{ pid: 4242, processName: 'tradingview', mainWindowTitle: 'TradingView', startTime: '2026-03-23T00:00:00Z' }])
846+
}, async () => {
847+
const execResult = await aiService.executeActions({
848+
thought: 'Open TradingView Pine Editor and type a script',
849+
verification: 'TradingView should show the Pine Editor before typing',
850+
actions: [
851+
{ type: 'focus_window', title: 'TradingView', processName: 'tradingview' },
852+
{
853+
type: 'key',
854+
key: 'ctrl+e',
855+
reason: 'Open TradingView Pine Editor',
856+
verify: {
857+
kind: 'panel-visible',
858+
appName: 'TradingView',
859+
target: 'pine-editor',
860+
keywords: ['pine', 'pine editor', 'script'],
861+
requiresObservedChange: true
862+
}
863+
},
864+
{ type: 'type', text: 'plot(close)', reason: 'Type Pine script' }
865+
]
866+
}, null, null, {
867+
userMessage: 'open pine editor in tradingview and type plot(close)',
868+
actionExecutor: async (action) => {
869+
executed.push(action.type);
870+
return { success: true, action: action.type, message: 'executed' };
871+
}
872+
});
873+
874+
assert.strictEqual(execResult.success, true, 'Execution should proceed after the Pine Editor surface is observed');
875+
assert.deepStrictEqual(executed, ['focus_window', 'key', 'type'], 'Typing should continue only after the Pine panel transition is verified');
876+
assert.strictEqual(execResult.observationCheckpoints.length, 1, 'A post-key observation checkpoint should be returned');
877+
assert.strictEqual(execResult.observationCheckpoints[0].verified, true, 'The Pine checkpoint should pass after panel observation');
878+
assert.strictEqual(execResult.observationCheckpoints[0].classification, 'panel-open', 'Pine Editor should verify as a panel-open checkpoint');
879+
assert.strictEqual(execResult.observationCheckpoints[0].foreground.hwnd, 777, 'Checkpoint should preserve the TradingView main window handle');
880+
});
881+
});
882+
813883
await testAsync('TradingView DOM order-entry actions are elevated to high risk', async () => {
814884
const safety = aiService.analyzeActionSafety(
815885
{ type: 'click', reason: 'Place limit order from DOM order book' },

src/main/ai-service.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ const {
108108
const {
109109
maybeRewriteTradingViewDrawingWorkflow
110110
} = require('./tradingview/drawing-workflows');
111+
const {
112+
maybeRewriteTradingViewPineWorkflow
113+
} = require('./tradingview/pine-workflows');
111114
const {
112115
createObservationCheckpointRuntime
113116
} = require('./ai-service/observation-checkpoints');
@@ -3068,6 +3071,11 @@ function rewriteActionsForReliability(actions, context = {}) {
30683071
return tradingViewDrawingRewrite;
30693072
}
30703073

3074+
const tradingViewPineRewrite = maybeRewriteTradingViewPineWorkflow(actions, { userMessage });
3075+
if (tradingViewPineRewrite) {
3076+
return tradingViewPineRewrite;
3077+
}
3078+
30713079
const tradingViewIndicatorRewrite = maybeRewriteTradingViewIndicatorWorkflow(actions, { userMessage });
30723080
if (tradingViewIndicatorRewrite) {
30733081
return tradingViewIndicatorRewrite;
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
const { buildVerifyTargetHintFromAppName } = require('./app-profile');
2+
const { extractTradingViewObservationKeywords } = require('./verification');
3+
4+
function normalizeTextForMatch(value) {
5+
return String(value || '')
6+
.toLowerCase()
7+
.replace(/[^a-z0-9]+/g, ' ')
8+
.trim();
9+
}
10+
11+
function mergeUnique(values = []) {
12+
return Array.from(new Set((Array.isArray(values) ? values : [values])
13+
.flat()
14+
.map((value) => String(value || '').trim())
15+
.filter(Boolean)));
16+
}
17+
18+
function inferPineSurfaceTarget(raw = '') {
19+
const normalized = normalizeTextForMatch(raw);
20+
if (!normalized) return null;
21+
22+
if (/\bpine logs\b/.test(normalized)) {
23+
return { target: 'pine-logs', kind: 'panel-visible' };
24+
}
25+
if (/\bprofiler\b/.test(normalized)) {
26+
return { target: 'pine-profiler', kind: 'panel-visible' };
27+
}
28+
if (/\bversion history\b/.test(normalized)) {
29+
return { target: 'pine-version-history', kind: 'panel-visible' };
30+
}
31+
if (/\bpine editor\b|\bpine\b|\bscript\b|\bscripts\b/.test(normalized)) {
32+
return { target: 'pine-editor', kind: 'panel-visible' };
33+
}
34+
35+
return null;
36+
}
37+
38+
function inferTradingViewPineIntent(userMessage = '', actions = []) {
39+
const raw = String(userMessage || '').trim();
40+
if (!raw) return null;
41+
42+
const mentionsTradingView = /\btradingview|trading view\b/i.test(raw)
43+
|| (Array.isArray(actions) && actions.some((action) => /tradingview/i.test(String(action?.title || '')) || /tradingview/i.test(String(action?.processName || ''))));
44+
if (!mentionsTradingView) return null;
45+
46+
const mentionsPineSurface = /\bpine editor\b|\bpine logs\b|\bprofiler\b|\bversion history\b|\bpine\s+script\b|\bpine\b/i.test(raw);
47+
const mentionsSafeOpenIntent = /\b(open|show|focus|switch|activate|bring up|display|launch)\b/i.test(raw);
48+
const mentionsUnsafeAuthoringOnly = /\b(write|create|generate|build|draft)\b/i.test(raw) && !mentionsSafeOpenIntent;
49+
50+
if (!mentionsPineSurface || mentionsUnsafeAuthoringOnly) {
51+
return null;
52+
}
53+
54+
const openerTypes = new Set(['key', 'click', 'double_click', 'right_click']);
55+
const openerIndex = Array.isArray(actions)
56+
? actions.findIndex((action) => openerTypes.has(action?.type))
57+
: -1;
58+
if (openerIndex < 0) return null;
59+
60+
const nextAction = openerIndex >= 0 ? actions[openerIndex + 1] || null : null;
61+
const surface = inferPineSurfaceTarget(raw);
62+
if (!surface) return null;
63+
64+
const existingWorkflowSignal = Array.isArray(actions) && actions.some((action) => /pine/.test(String(action?.verify?.target || '')));
65+
66+
return {
67+
appName: 'TradingView',
68+
surfaceTarget: surface.target,
69+
verifyKind: surface.kind,
70+
openerIndex,
71+
existingWorkflowSignal,
72+
requiresObservedChange: nextAction?.type === 'type',
73+
reason: surface.target === 'pine-logs'
74+
? 'Open TradingView Pine Logs with verification'
75+
: surface.target === 'pine-profiler'
76+
? 'Open TradingView Pine Profiler with verification'
77+
: surface.target === 'pine-version-history'
78+
? 'Open TradingView Pine version history with verification'
79+
: 'Open TradingView Pine Editor with verification'
80+
};
81+
}
82+
83+
function buildTradingViewPineWorkflowActions(intent = {}, actions = []) {
84+
if (!Array.isArray(actions) || intent.openerIndex < 0 || intent.openerIndex >= actions.length) return null;
85+
86+
const opener = actions[intent.openerIndex];
87+
const verifyTarget = buildVerifyTargetHintFromAppName(intent.appName || 'TradingView');
88+
const expectedKeywords = mergeUnique([
89+
'pine',
90+
'pine editor',
91+
intent.surfaceTarget,
92+
extractTradingViewObservationKeywords(`open ${intent.surfaceTarget} in tradingview`),
93+
verifyTarget.pineKeywords,
94+
verifyTarget.dialogKeywords,
95+
verifyTarget.titleHints
96+
]);
97+
98+
const rewritten = [
99+
{
100+
type: 'bring_window_to_front',
101+
title: 'TradingView',
102+
processName: 'tradingview',
103+
reason: 'Focus TradingView before the Pine workflow',
104+
verifyTarget
105+
},
106+
{ type: 'wait', ms: 650 },
107+
{
108+
...opener,
109+
reason: opener?.reason || intent.reason,
110+
verify: opener?.verify || {
111+
kind: intent.verifyKind,
112+
appName: 'TradingView',
113+
target: intent.surfaceTarget,
114+
keywords: expectedKeywords,
115+
requiresObservedChange: !!intent.requiresObservedChange
116+
},
117+
verifyTarget
118+
}
119+
];
120+
121+
if (!rewritten[2].verifyTarget) {
122+
rewritten[2].verifyTarget = verifyTarget;
123+
}
124+
125+
const trailing = actions.slice(intent.openerIndex + 1)
126+
.filter((action) => action && typeof action === 'object' && action.type !== 'screenshot');
127+
128+
if (trailing.length > 0 && trailing[0]?.type !== 'wait') {
129+
rewritten.push({ type: 'wait', ms: 220 });
130+
}
131+
132+
return rewritten.concat(trailing);
133+
}
134+
135+
function maybeRewriteTradingViewPineWorkflow(actions, context = {}) {
136+
if (!Array.isArray(actions) || actions.length === 0) return null;
137+
138+
const intent = inferTradingViewPineIntent(context.userMessage || '', actions);
139+
if (!intent || intent.existingWorkflowSignal || intent.openerIndex < 0) return null;
140+
141+
const lowSignalTypes = new Set(['bring_window_to_front', 'focus_window', 'key', 'click', 'double_click', 'right_click', 'type', 'wait', 'screenshot']);
142+
const lowSignal = actions.every((action) => lowSignalTypes.has(action?.type));
143+
const tinyOrFragmented = actions.length <= 4;
144+
const screenshotFirst = actions[0]?.type === 'screenshot';
145+
const lacksPineVerification = !actions.some((action) => /pine/.test(String(action?.verify?.target || '')));
146+
147+
if (!lowSignal || (!tinyOrFragmented && !screenshotFirst && !lacksPineVerification)) {
148+
return null;
149+
}
150+
151+
return buildTradingViewPineWorkflowActions(intent, actions);
152+
}
153+
154+
module.exports = {
155+
inferTradingViewPineIntent,
156+
buildTradingViewPineWorkflowActions,
157+
maybeRewriteTradingViewPineWorkflow
158+
};

0 commit comments

Comments
 (0)