@provartesting/provardx-cli 1.5.0-dev.1 → 1.5.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.
Files changed (129) hide show
  1. package/README.md +163 -13
  2. package/bin/mcp-start.js +74 -0
  3. package/lib/commands/provar/auth/clear.d.ts +7 -0
  4. package/lib/commands/provar/auth/clear.js +36 -0
  5. package/lib/commands/provar/auth/clear.js.map +1 -0
  6. package/lib/commands/provar/auth/login.d.ts +10 -0
  7. package/lib/commands/provar/auth/login.js +90 -0
  8. package/lib/commands/provar/auth/login.js.map +1 -0
  9. package/lib/commands/provar/auth/rotate.d.ts +7 -0
  10. package/lib/commands/provar/auth/rotate.js +42 -0
  11. package/lib/commands/provar/auth/rotate.js.map +1 -0
  12. package/lib/commands/provar/auth/status.d.ts +7 -0
  13. package/lib/commands/provar/auth/status.js +107 -0
  14. package/lib/commands/provar/auth/status.js.map +1 -0
  15. package/lib/commands/provar/mcp/start.d.ts +2 -0
  16. package/lib/commands/provar/mcp/start.js +14 -1
  17. package/lib/commands/provar/mcp/start.js.map +1 -1
  18. package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
  19. package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
  20. package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
  21. package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +175 -0
  22. package/lib/mcp/licensing/algasClient.js +14 -5
  23. package/lib/mcp/licensing/algasClient.js.map +1 -1
  24. package/lib/mcp/licensing/ideDetection.d.ts +0 -12
  25. package/lib/mcp/licensing/ideDetection.js +1 -73
  26. package/lib/mcp/licensing/ideDetection.js.map +1 -1
  27. package/lib/mcp/licensing/licenseCache.js +7 -1
  28. package/lib/mcp/licensing/licenseCache.js.map +1 -1
  29. package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
  30. package/lib/mcp/licensing/licenseValidator.js +11 -4
  31. package/lib/mcp/licensing/licenseValidator.js.map +1 -1
  32. package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
  33. package/lib/mcp/prompts/guidePrompts.js +324 -0
  34. package/lib/mcp/prompts/guidePrompts.js.map +1 -0
  35. package/lib/mcp/prompts/index.d.ts +2 -0
  36. package/lib/mcp/prompts/index.js +23 -0
  37. package/lib/mcp/prompts/index.js.map +1 -0
  38. package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
  39. package/lib/mcp/prompts/loopPrompts.js +435 -0
  40. package/lib/mcp/prompts/loopPrompts.js.map +1 -0
  41. package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
  42. package/lib/mcp/prompts/migrationPrompts.js +207 -0
  43. package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
  44. package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
  45. package/lib/mcp/security/pathPolicy.d.ts +5 -0
  46. package/lib/mcp/security/pathPolicy.js +58 -3
  47. package/lib/mcp/security/pathPolicy.js.map +1 -1
  48. package/lib/mcp/server.d.ts +17 -0
  49. package/lib/mcp/server.js +151 -6
  50. package/lib/mcp/server.js.map +1 -1
  51. package/lib/mcp/tools/antTools.d.ts +15 -0
  52. package/lib/mcp/tools/antTools.js +347 -170
  53. package/lib/mcp/tools/antTools.js.map +1 -1
  54. package/lib/mcp/tools/automationTools.d.ts +18 -8
  55. package/lib/mcp/tools/automationTools.js +332 -176
  56. package/lib/mcp/tools/automationTools.js.map +1 -1
  57. package/lib/mcp/tools/bestPracticesEngine.js +161 -23
  58. package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
  59. package/lib/mcp/tools/connectionTools.d.ts +4 -0
  60. package/lib/mcp/tools/connectionTools.js +172 -0
  61. package/lib/mcp/tools/connectionTools.js.map +1 -0
  62. package/lib/mcp/tools/defectTools.d.ts +1 -1
  63. package/lib/mcp/tools/defectTools.js +56 -50
  64. package/lib/mcp/tools/defectTools.js.map +1 -1
  65. package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
  66. package/lib/mcp/tools/hierarchyValidate.js +127 -42
  67. package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
  68. package/lib/mcp/tools/nitroXTools.d.ts +23 -0
  69. package/lib/mcp/tools/nitroXTools.js +823 -0
  70. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  71. package/lib/mcp/tools/pageObjectGenerate.js +132 -57
  72. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  73. package/lib/mcp/tools/pageObjectValidate.js +136 -46
  74. package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
  75. package/lib/mcp/tools/projectInspect.js +51 -30
  76. package/lib/mcp/tools/projectInspect.js.map +1 -1
  77. package/lib/mcp/tools/projectValidateFromPath.js +70 -49
  78. package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
  79. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  80. package/lib/mcp/tools/propertiesTools.js +332 -78
  81. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  82. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  83. package/lib/mcp/tools/qualityHubApiTools.js +138 -0
  84. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  85. package/lib/mcp/tools/qualityHubTools.js +219 -70
  86. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  87. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  88. package/lib/mcp/tools/rcaTools.js +189 -56
  89. package/lib/mcp/tools/rcaTools.js.map +1 -1
  90. package/lib/mcp/tools/sfSpawn.d.ts +25 -3
  91. package/lib/mcp/tools/sfSpawn.js +154 -6
  92. package/lib/mcp/tools/sfSpawn.js.map +1 -1
  93. package/lib/mcp/tools/testCaseGenerate.js +226 -78
  94. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  95. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  96. package/lib/mcp/tools/testCaseStepTools.js +226 -0
  97. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  98. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  99. package/lib/mcp/tools/testCaseValidate.js +307 -46
  100. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  101. package/lib/mcp/tools/testPlanTools.d.ts +1 -0
  102. package/lib/mcp/tools/testPlanTools.js +299 -59
  103. package/lib/mcp/tools/testPlanTools.js.map +1 -1
  104. package/lib/mcp/tools/testPlanValidate.js +56 -18
  105. package/lib/mcp/tools/testPlanValidate.js.map +1 -1
  106. package/lib/mcp/tools/testSuiteValidate.js +37 -11
  107. package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
  108. package/lib/mcp/update/updateChecker.d.ts +14 -0
  109. package/lib/mcp/update/updateChecker.js +228 -0
  110. package/lib/mcp/update/updateChecker.js.map +1 -0
  111. package/lib/services/auth/credentials.d.ts +21 -0
  112. package/lib/services/auth/credentials.js +75 -0
  113. package/lib/services/auth/credentials.js.map +1 -0
  114. package/lib/services/auth/loginFlow.d.ts +68 -0
  115. package/lib/services/auth/loginFlow.js +216 -0
  116. package/lib/services/auth/loginFlow.js.map +1 -0
  117. package/lib/services/projectValidation.d.ts +5 -2
  118. package/lib/services/projectValidation.js +83 -31
  119. package/lib/services/projectValidation.js.map +1 -1
  120. package/lib/services/qualityHub/client.d.ts +161 -0
  121. package/lib/services/qualityHub/client.js +226 -0
  122. package/lib/services/qualityHub/client.js.map +1 -0
  123. package/messages/sf.provar.auth.clear.md +16 -0
  124. package/messages/sf.provar.auth.login.md +31 -0
  125. package/messages/sf.provar.auth.rotate.md +23 -0
  126. package/messages/sf.provar.auth.status.md +16 -0
  127. package/messages/sf.provar.mcp.start.md +83 -48
  128. package/oclif.manifest.json +299 -2
  129. package/package.json +23 -12
@@ -0,0 +1,68 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ export declare const CALLBACK_PORTS: number[];
3
+ /**
4
+ * Generate a PKCE code_verifier / code_challenge pair (S256 method, as required by Cognito).
5
+ */
6
+ export declare function generatePkce(): {
7
+ verifier: string;
8
+ challenge: string;
9
+ };
10
+ /**
11
+ * Generate a random nonce for OIDC replay-attack prevention.
12
+ * Required by the OpenID Connect spec when requesting an id_token.
13
+ */
14
+ export declare function generateNonce(): string;
15
+ /**
16
+ * Generate a random state value for CSRF protection.
17
+ * Required by Cognito Managed Login even though it is optional per the OAuth 2.0 spec.
18
+ */
19
+ export declare function generateState(): string;
20
+ /**
21
+ * Try each registered callback port in order; return the first that is free.
22
+ */
23
+ export declare function findAvailablePort(): Promise<number>;
24
+ /**
25
+ * Return the platform-specific command and argument list for opening a URL
26
+ * in the system browser. The URL is passed as an argument — never interpolated
27
+ * into a shell string — to avoid command injection. Exported so tests can
28
+ * assert the correct command is chosen without actually spawning a process.
29
+ */
30
+ export declare function getBrowserCommand(url: string, platform?: NodeJS.Platform): {
31
+ cmd: string;
32
+ args: string[];
33
+ };
34
+ export declare function openBrowser(url: string): void;
35
+ /**
36
+ * Spin up a temporary localhost HTTP server that accepts exactly one callback
37
+ * from Cognito's Hosted UI, extracts the auth code, and shuts down.
38
+ */
39
+ export declare function listenForCallback(port: number, expectedState?: string): Promise<string>;
40
+ export interface CognitoTokens {
41
+ access_token: string;
42
+ id_token: string;
43
+ token_type: string;
44
+ expires_in: number;
45
+ }
46
+ /**
47
+ * Exchange a PKCE auth code for Cognito tokens via the standard token endpoint.
48
+ * Uses the Authorization Code + PKCE grant — no client secret required.
49
+ */
50
+ export declare function exchangeCodeForTokens(opts: {
51
+ code: string;
52
+ redirectUri: string;
53
+ clientId: string;
54
+ verifier: string;
55
+ tokenEndpoint: string;
56
+ }): Promise<CognitoTokens>;
57
+ /**
58
+ * The login command calls loginFlowClient.X() so tests can replace properties with stubs.
59
+ */
60
+ export declare const loginFlowClient: {
61
+ generatePkce: typeof generatePkce;
62
+ generateNonce: typeof generateNonce;
63
+ generateState: typeof generateState;
64
+ findAvailablePort: typeof findAvailablePort;
65
+ openBrowser: typeof openBrowser;
66
+ listenForCallback: (port: number, expectedState?: string) => Promise<string>;
67
+ exchangeCodeForTokens: typeof exchangeCodeForTokens;
68
+ };
@@ -0,0 +1,216 @@
1
+ /*
2
+ * Copyright (c) 2024 Provar Limited.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ /* eslint-disable camelcase */
8
+ import crypto from 'node:crypto';
9
+ import http from 'node:http';
10
+ import https from 'node:https';
11
+ import { spawn } from 'node:child_process';
12
+ import { URL } from 'node:url';
13
+ // All three ports must be pre-registered in both the Cognito App Client and the Salesforce External Client Application (SF ECA).
14
+ // Both providers require redirect_uri to exactly match a registered callback URL — no wildcards.
15
+ export const CALLBACK_PORTS = [1717, 7890, 8080];
16
+ // ── PKCE ─────────────────────────────────────────────────────────────────────
17
+ /**
18
+ * Generate a PKCE code_verifier / code_challenge pair (S256 method, as required by Cognito).
19
+ */
20
+ export function generatePkce() {
21
+ const verifier = crypto.randomBytes(32).toString('base64url');
22
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
23
+ return { verifier, challenge };
24
+ }
25
+ /**
26
+ * Generate a random nonce for OIDC replay-attack prevention.
27
+ * Required by the OpenID Connect spec when requesting an id_token.
28
+ */
29
+ export function generateNonce() {
30
+ return crypto.randomBytes(16).toString('base64url');
31
+ }
32
+ /**
33
+ * Generate a random state value for CSRF protection.
34
+ * Required by Cognito Managed Login even though it is optional per the OAuth 2.0 spec.
35
+ */
36
+ export function generateState() {
37
+ return crypto.randomBytes(16).toString('base64url');
38
+ }
39
+ // ── Port selection ────────────────────────────────────────────────────────────
40
+ /**
41
+ * Try each registered callback port in order; return the first that is free.
42
+ */
43
+ export async function findAvailablePort() {
44
+ for (const port of CALLBACK_PORTS) {
45
+ // Sequential by design — we need the first free registered port, not all of them.
46
+ // eslint-disable-next-line no-await-in-loop
47
+ if (await isPortFree(port))
48
+ return port;
49
+ }
50
+ throw new Error('Could not bind to any registered callback port (1717, 7890, 8080). ' +
51
+ 'Check that no other process is using these ports and try again.');
52
+ }
53
+ function isPortFree(port) {
54
+ return new Promise((resolve) => {
55
+ const probe = http.createServer();
56
+ probe.once('error', () => resolve(false));
57
+ probe.listen(port, '127.0.0.1', () => {
58
+ probe.close(() => resolve(true));
59
+ });
60
+ });
61
+ }
62
+ // ── Browser open ──────────────────────────────────────────────────────────────
63
+ /**
64
+ * Return the platform-specific command and argument list for opening a URL
65
+ * in the system browser. The URL is passed as an argument — never interpolated
66
+ * into a shell string — to avoid command injection. Exported so tests can
67
+ * assert the correct command is chosen without actually spawning a process.
68
+ */
69
+ export function getBrowserCommand(url, platform = process.platform) {
70
+ switch (platform) {
71
+ case 'darwin':
72
+ return { cmd: 'open', args: [url] };
73
+ case 'win32':
74
+ // Pass the URL via $args[0] so it is never interpolated into the -Command
75
+ // string — avoids quote-breaking and injection risk from special characters.
76
+ return { cmd: 'powershell.exe', args: ['-NoProfile', '-Command', 'Start-Process $args[0]', '-args', url] };
77
+ default:
78
+ return { cmd: 'xdg-open', args: [url] };
79
+ }
80
+ }
81
+ export function openBrowser(url) {
82
+ // detached:true + stdio:'ignore' + unref() is the standard Node.js pattern for
83
+ // fire-and-forget child processes — the event loop will not wait for them to exit.
84
+ const { cmd, args } = getBrowserCommand(url);
85
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
86
+ // Suppress unhandled-error crashes if the browser executable is not found.
87
+ // The login URL is already printed to the terminal so the user can open it manually.
88
+ child.on('error', () => {
89
+ /* intentional no-op */
90
+ });
91
+ child.unref();
92
+ }
93
+ // ── Localhost callback server ─────────────────────────────────────────────────
94
+ /**
95
+ * Spin up a temporary localhost HTTP server that accepts exactly one callback
96
+ * from Cognito's Hosted UI, extracts the auth code, and shuts down.
97
+ */
98
+ export function listenForCallback(port, expectedState) {
99
+ return new Promise((resolve, reject) => {
100
+ // Track open sockets so we can forcibly destroy them on shutdown.
101
+ // This is a Node 18.0/18.1 fallback for server.closeAllConnections(), which
102
+ // was added in Node 18.2. Without it a browser that ignores Connection:close
103
+ // could keep the event loop alive after server.close() returns.
104
+ const openSockets = new Set();
105
+ const closeServer = (srv) => {
106
+ srv.close();
107
+ if (typeof srv.closeAllConnections === 'function') {
108
+ srv.closeAllConnections();
109
+ }
110
+ else {
111
+ openSockets.forEach((s) => s.destroy());
112
+ }
113
+ };
114
+ const server = http.createServer((req, res) => {
115
+ const parsed = new URL(req.url ?? '/', `http://localhost:${port}`);
116
+ const code = parsed.searchParams.get('code');
117
+ const error = parsed.searchParams.get('error');
118
+ const description = parsed.searchParams.get('error_description');
119
+ const callbackState = parsed.searchParams.get('state');
120
+ if (expectedState && callbackState !== expectedState) {
121
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
122
+ res.end('<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
123
+ '<h2 style="color:#c23934">Authentication failed</h2>' +
124
+ '<p>Invalid state parameter — possible CSRF attack. Please try again.</p>' +
125
+ '</body></html>');
126
+ closeServer(server);
127
+ reject(new Error('OAuth callback state mismatch — possible CSRF. Try again.'));
128
+ return;
129
+ }
130
+ // 'Connection: close' tells the browser to close the TCP connection after
131
+ // this response so server.close() has no lingering keep-alive sockets to
132
+ // wait for, allowing the Node.js event loop to exit promptly.
133
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', Connection: 'close' });
134
+ res.end('<html><body style="font-family:sans-serif;padding:2rem;max-width:480px">' +
135
+ '<h2 style="color:#0070d2">Authentication complete</h2>' +
136
+ '<p>You can close this tab and return to the terminal.</p>' +
137
+ '</body></html>');
138
+ closeServer(server);
139
+ if (code) {
140
+ resolve(code);
141
+ }
142
+ else {
143
+ reject(new Error(description ?? error ?? 'No authorisation code received from identity provider'));
144
+ }
145
+ });
146
+ server.on('connection', (socket) => {
147
+ openSockets.add(socket);
148
+ socket.once('close', () => openSockets.delete(socket));
149
+ });
150
+ server.listen(port, '127.0.0.1');
151
+ server.on('error', (err) => reject(err));
152
+ });
153
+ }
154
+ /**
155
+ * Exchange a PKCE auth code for Cognito tokens via the standard token endpoint.
156
+ * Uses the Authorization Code + PKCE grant — no client secret required.
157
+ */
158
+ export async function exchangeCodeForTokens(opts) {
159
+ const body = new URLSearchParams({
160
+ grant_type: 'authorization_code',
161
+ code: opts.code,
162
+ redirect_uri: opts.redirectUri,
163
+ client_id: opts.clientId,
164
+ code_verifier: opts.verifier,
165
+ }).toString();
166
+ const { status, responseBody } = await httpsPost(opts.tokenEndpoint, body, {
167
+ 'Content-Type': 'application/x-www-form-urlencoded',
168
+ });
169
+ if (status !== 200) {
170
+ throw new Error(`Token exchange failed (${status}): ${responseBody}`);
171
+ }
172
+ return JSON.parse(responseBody);
173
+ }
174
+ // ── Internal HTTPS helper ─────────────────────────────────────────────────────
175
+ const REQUEST_TIMEOUT_MS = 30000;
176
+ function httpsPost(url, body, headers) {
177
+ return new Promise((resolve, reject) => {
178
+ const parsed = new URL(url);
179
+ const req = https.request({
180
+ hostname: parsed.hostname,
181
+ port: parsed.port || undefined,
182
+ path: parsed.pathname + parsed.search,
183
+ method: 'POST',
184
+ headers: {
185
+ ...headers,
186
+ 'Content-Length': Buffer.byteLength(body).toString(),
187
+ },
188
+ }, (res) => {
189
+ let data = '';
190
+ res.on('data', (chunk) => {
191
+ data += chunk.toString('utf-8');
192
+ });
193
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, responseBody: data }));
194
+ });
195
+ req.setTimeout(REQUEST_TIMEOUT_MS, () => {
196
+ req.destroy(new Error(`Token exchange timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
197
+ });
198
+ req.on('error', reject);
199
+ req.write(body);
200
+ req.end();
201
+ });
202
+ }
203
+ // ── Indirection object (sinon-stubbable) ──────────────────────────────────────
204
+ /**
205
+ * The login command calls loginFlowClient.X() so tests can replace properties with stubs.
206
+ */
207
+ export const loginFlowClient = {
208
+ generatePkce,
209
+ generateNonce,
210
+ generateState,
211
+ findAvailablePort,
212
+ openBrowser,
213
+ listenForCallback: listenForCallback,
214
+ exchangeCodeForTokens,
215
+ };
216
+ //# sourceMappingURL=loginFlow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loginFlow.js","sourceRoot":"","sources":["../../../src/services/auth/loginFlow.ts"],"names":[],"mappings":"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"}
@@ -62,6 +62,8 @@ export interface ProjectValidationResult {
62
62
  uncovered_test_cases: string[];
63
63
  };
64
64
  saved_to: string | null;
65
+ /** Directories within plans/ that are missing a .planitem file and will be silently ignored by the Provar runner. */
66
+ plan_integrity_warnings?: string[];
65
67
  /** Set when save_results was requested but the write failed (disk full, permissions, etc.). */
66
68
  save_error?: string;
67
69
  }
@@ -88,11 +90,12 @@ export declare function readProjectContext(projectPath: string): {
88
90
  context: ProjectContext;
89
91
  };
90
92
  export declare function resolveTestInstance(instancePath: string, projectPath: string): TestCaseInput | null;
91
- export declare function readSuiteDirectory(dirPath: string, name: string, projectPath: string, depth?: number, coveredPaths?: Set<string>, idMap?: Map<string, string>): TestSuiteInput;
92
- export declare function readPlanDirectory(planPath: string, name: string, projectPath: string, coveredPaths?: Set<string>, idMap?: Map<string, string>): TestPlanInput;
93
+ export declare function readSuiteDirectory(dirPath: string, name: string, projectPath: string, depth?: number, coveredPaths?: Set<string>, idMap?: Map<string, string>, planIntegrityWarnings?: string[]): TestSuiteInput;
94
+ export declare function readPlanDirectory(planPath: string, name: string, projectPath: string, coveredPaths?: Set<string>, idMap?: Map<string, string>, planIntegrityWarnings?: string[]): TestPlanInput;
93
95
  export declare function readPlansDir(projectPath: string): {
94
96
  plans: TestPlanInput[];
95
97
  coveredPaths: Set<string>;
98
+ planIntegrityWarnings: string[];
96
99
  };
97
100
  /**
98
101
  * Collects all .testcase file basenames (without extension) found under tests/.
@@ -70,7 +70,9 @@ export function resolveProjectRoot(givenPath) {
70
70
  candidates.push(sub);
71
71
  }
72
72
  }
73
- catch { /* skip */ }
73
+ catch {
74
+ /* skip */
75
+ }
74
76
  if (candidates.length === 1)
75
77
  return { root: candidates[0], candidates: [] };
76
78
  return { root: givenPath, candidates }; // caller handles 0 or multiple
@@ -127,7 +129,9 @@ export function readProjectContext(projectPath) {
127
129
  unencryptedSecretCount++;
128
130
  }
129
131
  }
130
- catch { /* skip */ }
132
+ catch {
133
+ /* skip */
134
+ }
131
135
  }
132
136
  return {
133
137
  projectName,
@@ -158,16 +162,21 @@ function resolveTestInstanceFull(instancePath, projectPath) {
158
162
  // Bounds check: only read test case files within the project directory
159
163
  const tcInBounds = tcFullPath === projResolved || tcFullPath.startsWith(projResolved + path.sep);
160
164
  // Derive name from the bounds-checked resolved path to prevent injection via crafted testCasePath
161
- const tcName = tcInBounds
162
- ? path.basename(tcFullPath, '.testcase')
163
- : path.basename(testCasePath, '.testcase');
165
+ const tcName = tcInBounds ? path.basename(tcFullPath, '.testcase') : path.basename(testCasePath, '.testcase');
164
166
  if (tcInBounds && fs.existsSync(tcFullPath)) {
165
167
  try {
166
168
  xml_content = fs.readFileSync(tcFullPath, 'utf-8');
167
169
  }
168
- catch { /* xml_content stays undefined */ }
170
+ catch {
171
+ /* xml_content stays undefined */
172
+ }
169
173
  }
170
- return { testCase: { name: tcName, xml_content }, testCasePath, testCaseId };
174
+ // Only expose testCasePath/testCaseId when in-bounds out-of-bounds paths must not affect coverage totals
175
+ return {
176
+ testCase: { name: tcName, xml_content },
177
+ testCasePath: tcInBounds ? testCasePath : null,
178
+ testCaseId: tcInBounds ? testCaseId : null,
179
+ };
171
180
  }
172
181
  catch {
173
182
  return { testCase: null, testCasePath: null, testCaseId: null };
@@ -188,7 +197,7 @@ function accumulateCoveredPath(testCasePath, testCaseId, coveredPaths, idMap) {
188
197
  coveredPaths.add(resolved);
189
198
  }
190
199
  }
191
- export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, coveredPaths, idMap) {
200
+ export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, coveredPaths, idMap, planIntegrityWarnings) {
192
201
  const testCases = [];
193
202
  const testSuites = [];
194
203
  if (depth > MAX_SUITE_DEPTH)
@@ -200,7 +209,13 @@ export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, covere
200
209
  continue;
201
210
  const fullPath = path.join(dirPath, entry.name);
202
211
  if (entry.isDirectory() && !entry.name.startsWith('.')) {
203
- testSuites.push(readSuiteDirectory(fullPath, entry.name, projectPath, depth + 1, coveredPaths, idMap));
212
+ const subHasPlanItem = fs.existsSync(path.join(fullPath, '.planitem'));
213
+ if (planIntegrityWarnings && !subHasPlanItem) {
214
+ const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
215
+ planIntegrityWarnings.push(`${rel}/ is missing a .planitem file — test instances in this suite will be invisible to the runner.`);
216
+ continue;
217
+ }
218
+ testSuites.push(readSuiteDirectory(fullPath, entry.name, projectPath, depth + 1, subHasPlanItem ? coveredPaths : undefined, subHasPlanItem ? idMap : undefined, planIntegrityWarnings));
204
219
  }
205
220
  else if (entry.name.endsWith('.testinstance')) {
206
221
  const { testCase, testCasePath, testCaseId } = resolveTestInstanceFull(fullPath, projectPath);
@@ -211,10 +226,12 @@ export function readSuiteDirectory(dirPath, name, projectPath, depth = 0, covere
211
226
  }
212
227
  }
213
228
  }
214
- catch { /* skip */ }
229
+ catch {
230
+ /* skip */
231
+ }
215
232
  return { name, test_cases: testCases, test_suites: testSuites };
216
233
  }
217
- export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idMap) {
234
+ export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idMap, planIntegrityWarnings) {
218
235
  const testCases = [];
219
236
  const testSuites = [];
220
237
  try {
@@ -224,7 +241,13 @@ export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idM
224
241
  continue;
225
242
  const fullPath = path.join(planPath, entry.name);
226
243
  if (entry.isDirectory() && !entry.name.startsWith('.')) {
227
- testSuites.push(readSuiteDirectory(fullPath, entry.name, projectPath, 0, coveredPaths, idMap));
244
+ const suiteHasPlanItem = fs.existsSync(path.join(fullPath, '.planitem'));
245
+ if (planIntegrityWarnings && !suiteHasPlanItem) {
246
+ const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
247
+ planIntegrityWarnings.push(`${rel}/ is missing a .planitem file — test instances in this suite will be invisible to the runner.`);
248
+ continue;
249
+ }
250
+ testSuites.push(readSuiteDirectory(fullPath, entry.name, projectPath, 0, suiteHasPlanItem ? coveredPaths : undefined, suiteHasPlanItem ? idMap : undefined, planIntegrityWarnings));
228
251
  }
229
252
  else if (entry.name.endsWith('.testinstance')) {
230
253
  const { testCase, testCasePath, testCaseId } = resolveTestInstanceFull(fullPath, projectPath);
@@ -235,14 +258,17 @@ export function readPlanDirectory(planPath, name, projectPath, coveredPaths, idM
235
258
  }
236
259
  }
237
260
  }
238
- catch { /* skip */ }
261
+ catch {
262
+ /* skip */
263
+ }
239
264
  return { name, test_cases: testCases, test_suites: testSuites };
240
265
  }
241
266
  export function readPlansDir(projectPath) {
242
267
  const plansDir = path.join(projectPath, 'plans');
243
268
  const coveredPaths = new Set();
269
+ const planIntegrityWarnings = [];
244
270
  if (!fs.existsSync(plansDir))
245
- return { plans: [], coveredPaths };
271
+ return { plans: [], coveredPaths, planIntegrityWarnings };
246
272
  // Build UUID→path map once so the plan walk can resolve testCaseId fallbacks
247
273
  // without a separate pass over the tests/ directory later.
248
274
  const idMap = buildTestCaseIdMap(projectPath);
@@ -253,11 +279,17 @@ export function readPlansDir(projectPath) {
253
279
  if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules')
254
280
  continue;
255
281
  const planPath = path.join(plansDir, entry.name);
256
- plans.push(readPlanDirectory(planPath, entry.name, projectPath, coveredPaths, idMap));
282
+ const planHasPlanItem = fs.existsSync(path.join(planPath, '.planitem'));
283
+ if (!planHasPlanItem) {
284
+ planIntegrityWarnings.push(`plans/${entry.name}/ is missing a .planitem file — this plan will not be recognised by the Provar runner.`);
285
+ }
286
+ plans.push(readPlanDirectory(planPath, entry.name, projectPath, planHasPlanItem ? coveredPaths : undefined, planHasPlanItem ? idMap : undefined, planIntegrityWarnings));
257
287
  }
258
288
  }
259
- catch { /* skip */ }
260
- return { plans, coveredPaths };
289
+ catch {
290
+ /* skip */
291
+ }
292
+ return { plans, coveredPaths, planIntegrityWarnings };
261
293
  }
262
294
  /**
263
295
  * Builds a map of testcase UUID (registryId / id / guid) → project-relative path.
@@ -284,15 +316,20 @@ function buildTestCaseIdMap(projectPath) {
284
316
  const rel = path.relative(projectPath, fullPath).replace(/\\/g, '/');
285
317
  for (const attr of ['registryId', 'id', 'guid']) {
286
318
  const m = content.match(new RegExp(`${attr}=["']([^"']+)["']`));
287
- if (m?.[1] && !idMap.has(m[1]))
319
+ // Skip id="1" new generator emits this literal and it is not UUID-unique
320
+ if (m?.[1] && m[1] !== '1' && !idMap.has(m[1]))
288
321
  idMap.set(m[1], rel);
289
322
  }
290
323
  }
291
- catch { /* skip */ }
324
+ catch {
325
+ /* skip */
326
+ }
292
327
  }
293
328
  }
294
329
  }
295
- catch { /* skip */ }
330
+ catch {
331
+ /* skip */
332
+ }
296
333
  }
297
334
  walk(testsDir);
298
335
  return idMap;
@@ -320,7 +357,9 @@ export function collectAllTestCaseNames(projectPath) {
320
357
  names.push(path.basename(entry.name, '.testcase'));
321
358
  }
322
359
  }
323
- catch { /* skip */ }
360
+ catch {
361
+ /* skip */
362
+ }
324
363
  }
325
364
  walk(testsDir);
326
365
  return names;
@@ -360,11 +399,15 @@ export function collectCoveredPathsFromDisk(projectPath) {
360
399
  covered.add(resolvedPath);
361
400
  }
362
401
  }
363
- catch { /* skip */ }
402
+ catch {
403
+ /* skip */
404
+ }
364
405
  }
365
406
  }
366
407
  }
367
- catch { /* skip */ }
408
+ catch {
409
+ /* skip */
410
+ }
368
411
  }
369
412
  walk(plansDir);
370
413
  return covered;
@@ -390,7 +433,9 @@ export function findUncoveredTestCases(projectPath, coveredPaths) {
390
433
  }
391
434
  }
392
435
  }
393
- catch { /* skip */ }
436
+ catch {
437
+ /* skip */
438
+ }
394
439
  }
395
440
  walk(testsDir);
396
441
  return uncovered.sort();
@@ -426,7 +471,7 @@ function tcIssuesToQhViolations(tc) {
426
471
  appliesTo: issue.applies_to,
427
472
  });
428
473
  }
429
- for (const bp of (tc.best_practices_violations ?? [])) {
474
+ for (const bp of tc.best_practices_violations ?? []) {
430
475
  violations.push({
431
476
  number: num++,
432
477
  ruleId: bp.rule_id,
@@ -530,7 +575,13 @@ export function buildQhReport(result, projectName) {
530
575
  return {
531
576
  reportInfo: {
532
577
  name: `VR-LOCAL-${now.toISOString().replace(/[:.]/g, '-').slice(0, 19)}`,
533
- generatedAt: now.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
578
+ generatedAt: now.toLocaleDateString('en-US', {
579
+ year: 'numeric',
580
+ month: 'short',
581
+ day: 'numeric',
582
+ hour: '2-digit',
583
+ minute: '2-digit',
584
+ }),
534
585
  exportedAt: now.toISOString(),
535
586
  source: 'provar-mcp-local',
536
587
  },
@@ -551,9 +602,7 @@ export function buildQhReport(result, projectName) {
551
602
  };
552
603
  }
553
604
  export function saveResults(projectPath, resultsDir, report, projectName) {
554
- const targetDir = resultsDir
555
- ? path.resolve(resultsDir)
556
- : path.join(projectPath, 'provardx', 'validation');
605
+ const targetDir = resultsDir ? path.resolve(resultsDir) : path.join(projectPath, 'provardx', 'validation');
557
606
  fs.mkdirSync(targetDir, { recursive: true });
558
607
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
559
608
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
@@ -582,7 +631,9 @@ export function validateProjectFromPath(options) {
582
631
  }
583
632
  const { root: projectRoot, candidates } = resolveProjectRoot(resolved);
584
633
  if (candidates.length > 1) {
585
- throw new ProjectValidationError('AMBIGUOUS_PROJECT', `Multiple Provar projects found under "${resolved}". Specify the exact project directory: ${candidates.map((c) => path.basename(c)).join(', ')}`);
634
+ throw new ProjectValidationError('AMBIGUOUS_PROJECT', `Multiple Provar projects found under "${resolved}". Specify the exact project directory: ${candidates
635
+ .map((c) => path.basename(c))
636
+ .join(', ')}`);
586
637
  }
587
638
  if (!fs.existsSync(path.join(projectRoot, '.testproject'))) {
588
639
  throw new ProjectValidationError('NOT_A_PROJECT', `No Provar project found at "${projectRoot}". Ensure the path points to a directory containing a .testproject file.`);
@@ -592,7 +643,7 @@ export function validateProjectFromPath(options) {
592
643
  const { projectName, context } = readProjectContext(projectRoot);
593
644
  // 2. Read plan hierarchy from plans/ directory; covered paths are computed
594
645
  // as a byproduct of the walk — no second traversal needed.
595
- const { plans: testPlans, coveredPaths } = readPlansDir(projectRoot);
646
+ const { plans: testPlans, coveredPaths, planIntegrityWarnings } = readPlansDir(projectRoot);
596
647
  // 3. Validate
597
648
  const input = {
598
649
  name: projectName,
@@ -672,6 +723,7 @@ export function validateProjectFromPath(options) {
672
723
  uncovered_test_cases: uncoveredTestCases,
673
724
  },
674
725
  saved_to: savedTo,
726
+ ...(planIntegrityWarnings.length > 0 ? { plan_integrity_warnings: planIntegrityWarnings } : {}),
675
727
  save_error: saveError,
676
728
  };
677
729
  }