@provartesting/provardx-cli 1.5.0 → 1.5.2

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 (78) hide show
  1. package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +18 -6
  2. package/lib/mcp/prompts/guidePrompts.js +21 -11
  3. package/lib/mcp/prompts/guidePrompts.js.map +1 -1
  4. package/lib/mcp/server.d.ts +1 -0
  5. package/lib/mcp/server.js +90 -21
  6. package/lib/mcp/server.js.map +1 -1
  7. package/lib/mcp/tools/antTools.d.ts +20 -0
  8. package/lib/mcp/tools/antTools.js +105 -41
  9. package/lib/mcp/tools/antTools.js.map +1 -1
  10. package/lib/mcp/tools/automationTools.js +65 -35
  11. package/lib/mcp/tools/automationTools.js.map +1 -1
  12. package/lib/mcp/tools/connectionTools.js +100 -30
  13. package/lib/mcp/tools/connectionTools.js.map +1 -1
  14. package/lib/mcp/tools/defectTools.js +12 -7
  15. package/lib/mcp/tools/defectTools.js.map +1 -1
  16. package/lib/mcp/tools/descHelper.d.ts +5 -0
  17. package/lib/mcp/tools/descHelper.js +14 -0
  18. package/lib/mcp/tools/descHelper.js.map +1 -0
  19. package/lib/mcp/tools/nitroXTools.js +72 -32
  20. package/lib/mcp/tools/nitroXTools.js.map +1 -1
  21. package/lib/mcp/tools/orgDescribeTools.d.ts +57 -0
  22. package/lib/mcp/tools/orgDescribeTools.js +329 -0
  23. package/lib/mcp/tools/orgDescribeTools.js.map +1 -0
  24. package/lib/mcp/tools/pageObjectGenerate.js +33 -15
  25. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  26. package/lib/mcp/tools/pageObjectValidate.js +11 -4
  27. package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
  28. package/lib/mcp/tools/projectInspect.js +31 -5
  29. package/lib/mcp/tools/projectInspect.js.map +1 -1
  30. package/lib/mcp/tools/projectValidateFromPath.js +128 -22
  31. package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
  32. package/lib/mcp/tools/propertiesTools.d.ts +12 -0
  33. package/lib/mcp/tools/propertiesTools.js +155 -17
  34. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  35. package/lib/mcp/tools/qualityHubApiTools.js +8 -7
  36. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -1
  37. package/lib/mcp/tools/qualityHubTools.js +108 -37
  38. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  39. package/lib/mcp/tools/rcaTools.d.ts +17 -0
  40. package/lib/mcp/tools/rcaTools.js +69 -15
  41. package/lib/mcp/tools/rcaTools.js.map +1 -1
  42. package/lib/mcp/tools/testCaseGenerate.d.ts +1 -0
  43. package/lib/mcp/tools/testCaseGenerate.js +116 -13
  44. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  45. package/lib/mcp/tools/testCaseStepTools.js +27 -9
  46. package/lib/mcp/tools/testCaseStepTools.js.map +1 -1
  47. package/lib/mcp/tools/testCaseValidate.d.ts +14 -1
  48. package/lib/mcp/tools/testCaseValidate.js +190 -69
  49. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  50. package/lib/mcp/tools/testPlanTools.js +43 -26
  51. package/lib/mcp/tools/testPlanTools.js.map +1 -1
  52. package/lib/mcp/tools/testPlanValidate.js +64 -11
  53. package/lib/mcp/tools/testPlanValidate.js.map +1 -1
  54. package/lib/mcp/tools/testSuiteValidate.js +99 -10
  55. package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
  56. package/lib/mcp/utils/detailLevel.d.ts +9 -0
  57. package/lib/mcp/utils/detailLevel.js +20 -0
  58. package/lib/mcp/utils/detailLevel.js.map +1 -0
  59. package/lib/mcp/utils/fieldMask.d.ts +17 -0
  60. package/lib/mcp/utils/fieldMask.js +75 -0
  61. package/lib/mcp/utils/fieldMask.js.map +1 -0
  62. package/lib/mcp/utils/testCasePlanMode.d.ts +41 -0
  63. package/lib/mcp/utils/testCasePlanMode.js +208 -0
  64. package/lib/mcp/utils/testCasePlanMode.js.map +1 -0
  65. package/lib/mcp/utils/tokenMeta.d.ts +40 -0
  66. package/lib/mcp/utils/tokenMeta.js +90 -0
  67. package/lib/mcp/utils/tokenMeta.js.map +1 -0
  68. package/lib/mcp/utils/validationDiff.d.ts +57 -0
  69. package/lib/mcp/utils/validationDiff.js +191 -0
  70. package/lib/mcp/utils/validationDiff.js.map +1 -0
  71. package/lib/mcp/utils/validationScore.d.ts +15 -0
  72. package/lib/mcp/utils/validationScore.js +31 -0
  73. package/lib/mcp/utils/validationScore.js.map +1 -0
  74. package/lib/mcp/utils/warningCodes.d.ts +10 -0
  75. package/lib/mcp/utils/warningCodes.js +19 -0
  76. package/lib/mcp/utils/warningCodes.js.map +1 -0
  77. package/oclif.manifest.json +1 -1
  78. package/package.json +13 -1
@@ -0,0 +1,208 @@
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 fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { assertPathAllowed } from '../security/pathPolicy.js';
12
+ /**
13
+ * Resolve whether a given test case file is referenced directly via
14
+ * `testCase`/`testCases` or via a `.testinstance` inside a plan.
15
+ *
16
+ * The resolution flow is intentionally best-effort and silent: any file
17
+ * read failure, JSON parse error, or path-policy violation collapses to
18
+ * `mode: 'unknown'` so the caller falls back to default behaviour rather
19
+ * than surfacing a confusing error from the validator.
20
+ */
21
+ export function resolveTestCasePlanMode(opts) {
22
+ const propsPath = readPropertiesFilePath(opts);
23
+ if (!propsPath)
24
+ return { mode: 'unknown' };
25
+ let propsObj;
26
+ try {
27
+ propsObj = JSON.parse(fs.readFileSync(propsPath, 'utf-8'));
28
+ }
29
+ catch {
30
+ return { mode: 'unknown', propertiesFilePath: propsPath };
31
+ }
32
+ const projectPath = typeof propsObj['projectPath'] === 'string' ? propsObj['projectPath'] : null;
33
+ if (!projectPath) {
34
+ // Without a project root we cannot resolve relative `testCase` entries or
35
+ // walk `plans/`. The mode is unknown.
36
+ return { mode: 'unknown', propertiesFilePath: propsPath };
37
+ }
38
+ let resolvedProjectPath;
39
+ try {
40
+ resolvedProjectPath = path.resolve(projectPath);
41
+ assertPathAllowed(resolvedProjectPath, opts.allowedPaths);
42
+ }
43
+ catch {
44
+ return { mode: 'unknown', propertiesFilePath: propsPath };
45
+ }
46
+ const resolvedTestCasePath = path.resolve(opts.testCaseFilePath);
47
+ // A `.testinstance` reference inside any plan wins — plan mode supports
48
+ // data-driven iteration.
49
+ if (isReferencedFromPlanInstance(resolvedProjectPath, resolvedTestCasePath)) {
50
+ return { mode: 'plan', propertiesFilePath: propsPath, projectPath: resolvedProjectPath };
51
+ }
52
+ if (isReferencedDirectly(propsObj, resolvedProjectPath, resolvedTestCasePath)) {
53
+ return { mode: 'direct', propertiesFilePath: propsPath, projectPath: resolvedProjectPath };
54
+ }
55
+ return { mode: 'unknown', propertiesFilePath: propsPath, projectPath: resolvedProjectPath };
56
+ }
57
+ /**
58
+ * Resolve the active properties file path. Prefers an explicit override
59
+ * (used by tests), then `~/.sf/config.json`'s `PROVARDX_PROPERTIES_FILE_PATH`.
60
+ *
61
+ * Both the override and the value read from `~/.sf/config.json` are funnelled
62
+ * through `assertPathAllowed` so a caller cannot trick the resolver into
63
+ * reading a properties file outside the configured `allowedPaths`. Any policy
64
+ * violation collapses to `null` to preserve the helper's best-effort contract.
65
+ */
66
+ function readPropertiesFilePath(opts) {
67
+ if (opts.propertiesFilePathOverride) {
68
+ return enforceAllowed(opts.propertiesFilePathOverride, opts.allowedPaths);
69
+ }
70
+ try {
71
+ const sfConfigPath = opts.sfConfigPathOverride ?? path.join(os.homedir(), '.sf', 'config.json');
72
+ if (!fs.existsSync(sfConfigPath))
73
+ return null;
74
+ const sfConfig = JSON.parse(fs.readFileSync(sfConfigPath, 'utf-8'));
75
+ const propsPath = sfConfig['PROVARDX_PROPERTIES_FILE_PATH'];
76
+ if (!propsPath)
77
+ return null;
78
+ return enforceAllowed(propsPath, opts.allowedPaths);
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Resolve a candidate properties-file path through `assertPathAllowed`, then
86
+ * canonicalize via `fs.realpathSync` when the file exists so a symlink
87
+ * inside an allowed directory cannot redirect the read outside it. Returns
88
+ * `null` on any policy violation, missing file, or unexpected error — the
89
+ * caller treats `null` as "mode unknown" and falls back to default behaviour.
90
+ */
91
+ function enforceAllowed(candidate, allowedPaths) {
92
+ try {
93
+ const resolved = path.resolve(candidate);
94
+ assertPathAllowed(resolved, allowedPaths);
95
+ if (!fs.existsSync(resolved))
96
+ return null;
97
+ try {
98
+ return fs.realpathSync(resolved);
99
+ }
100
+ catch {
101
+ return resolved;
102
+ }
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Does the properties file's top-level `testCase` or `testCases` array
110
+ * reference this test case file? Entries are interpreted relative to
111
+ * `<projectPath>/tests/` (per the Provar runtime convention).
112
+ */
113
+ function isReferencedDirectly(props, projectPath, testCaseFilePath) {
114
+ const entries = [];
115
+ const tc = props['testCase'];
116
+ const tcs = props['testCases'];
117
+ for (const candidate of [tc, tcs]) {
118
+ if (typeof candidate === 'string') {
119
+ entries.push(candidate);
120
+ }
121
+ else if (Array.isArray(candidate)) {
122
+ for (const e of candidate) {
123
+ if (typeof e === 'string')
124
+ entries.push(e);
125
+ }
126
+ }
127
+ }
128
+ if (entries.length === 0)
129
+ return false;
130
+ const testsDir = path.join(projectPath, 'tests');
131
+ const targetNorm = path.resolve(testCaseFilePath).toLowerCase();
132
+ for (const entry of entries) {
133
+ // Provar accepts both bare names ("MyTest") and relative paths
134
+ // ("Module/MyTest.testcase"). Allow either; match with and without the
135
+ // `.testcase` extension.
136
+ const variants = [];
137
+ const trimmed = entry.replace(/^[/\\]+/, '');
138
+ variants.push(path.resolve(testsDir, trimmed));
139
+ if (!/\.testcase$/i.test(trimmed)) {
140
+ variants.push(path.resolve(testsDir, `${trimmed}.testcase`));
141
+ }
142
+ for (const v of variants) {
143
+ if (v.toLowerCase() === targetNorm)
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+ /**
150
+ * Walk `<projectPath>/plans/` for `.testinstance` files referencing this
151
+ * test case via `testCasePath="..."`. Best-effort — any read error skips
152
+ * the offending file.
153
+ */
154
+ function isReferencedFromPlanInstance(projectPath, testCaseFilePath) {
155
+ const plansDir = path.join(projectPath, 'plans');
156
+ if (!fs.existsSync(plansDir))
157
+ return false;
158
+ const testsDir = path.join(projectPath, 'tests');
159
+ const targetNorm = path.resolve(testCaseFilePath).toLowerCase();
160
+ let found = false;
161
+ const walk = (dir) => {
162
+ if (found)
163
+ return;
164
+ let entries;
165
+ try {
166
+ entries = fs.readdirSync(dir, { withFileTypes: true });
167
+ }
168
+ catch {
169
+ return;
170
+ }
171
+ for (const entry of entries) {
172
+ if (found)
173
+ return;
174
+ if (entry.name.startsWith('.') && !entry.name.endsWith('.testinstance'))
175
+ continue;
176
+ const full = path.join(dir, entry.name);
177
+ if (entry.isDirectory()) {
178
+ walk(full);
179
+ continue;
180
+ }
181
+ if (!entry.name.endsWith('.testinstance'))
182
+ continue;
183
+ let content;
184
+ try {
185
+ content = fs.readFileSync(full, 'utf-8');
186
+ }
187
+ catch {
188
+ continue;
189
+ }
190
+ const matches = content.matchAll(/testCasePath=["']([^"']+)["']/g);
191
+ for (const m of matches) {
192
+ const rel = m[1].replace(/\\/g, '/');
193
+ // testCasePath in Provar testinstances is conventionally relative to
194
+ // `<projectPath>/tests/`. Also tolerate paths relative to project root.
195
+ const candidates = [path.resolve(testsDir, rel), path.resolve(projectPath, rel)];
196
+ for (const c of candidates) {
197
+ if (c.toLowerCase() === targetNorm) {
198
+ found = true;
199
+ return;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ };
205
+ walk(plansDir);
206
+ return found;
207
+ }
208
+ //# sourceMappingURL=testCasePlanMode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testCasePlanMode.js","sourceRoot":"","sources":["../../../src/mcp/utils/testCasePlanMode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ type ContentItem = {
2
+ type: 'text';
3
+ text: string;
4
+ };
5
+ export interface ToolResult {
6
+ content: ContentItem[];
7
+ structuredContent?: Record<string, unknown>;
8
+ isError?: boolean;
9
+ }
10
+ interface ToolExtra {
11
+ sessionId?: string;
12
+ }
13
+ export type AnyToolCallback = (args: Record<string, unknown>, extra: ToolExtra) => ToolResult | Promise<ToolResult>;
14
+ interface SessionEntry {
15
+ calls: number;
16
+ totalEstimatedTokens: number;
17
+ }
18
+ export type DepthGuardState = Map<string, SessionEntry>;
19
+ export declare function createDepthGuardState(): DepthGuardState;
20
+ /**
21
+ * Wraps a tool handler to enforce a per-session call budget.
22
+ * Once `limit` calls have been made for a session, every further call returns
23
+ * TOOL_BUDGET_EXCEEDED without invoking the underlying handler.
24
+ * Callers without a sessionId (stdio transports — Claude Desktop, Cursor, etc.)
25
+ * share a single 'anon' bucket so the budget actually limits runaway tool use;
26
+ * giving each anon call a fresh UUID would defeat the purpose of the guard.
27
+ * `provardx_ping` is excluded from wrapping at the call site in server.ts.
28
+ */
29
+ export declare function wrapWithDepthGuard(toolName: string, handler: AnyToolCallback, state: DepthGuardState, limit: number): AnyToolCallback;
30
+ export declare function estimateTokens(payload: unknown): number;
31
+ /**
32
+ * Appends a `_meta` key to `structuredContent` when PROVAR_MCP_EMIT_TOKEN_META=true.
33
+ * The `content[0].text` string is intentionally left unchanged — LLMs read that
34
+ * field, so including meta there would waste tokens on observability data.
35
+ *
36
+ * @param sessionTotalTokens - Cumulative estimated tokens for the session,
37
+ * included only on TOOL_BUDGET_EXCEEDED errors.
38
+ */
39
+ export declare function attachMeta(response: ToolResult, toolName: string, detailLevel: string, sessionTotalTokens?: number): ToolResult;
40
+ export {};
@@ -0,0 +1,90 @@
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
+ const MAX_SESSIONS = 1000;
8
+ export function createDepthGuardState() {
9
+ return new Map();
10
+ }
11
+ function getOrCreateEntry(state, sessionId) {
12
+ if (!state.has(sessionId)) {
13
+ if (state.size >= MAX_SESSIONS) {
14
+ const oldest = state.keys().next().value;
15
+ if (oldest !== undefined)
16
+ state.delete(oldest);
17
+ }
18
+ state.set(sessionId, { calls: 0, totalEstimatedTokens: 0 });
19
+ }
20
+ // Non-null guaranteed by the set above or pre-existing entry.
21
+ return state.get(sessionId);
22
+ }
23
+ /**
24
+ * Wraps a tool handler to enforce a per-session call budget.
25
+ * Once `limit` calls have been made for a session, every further call returns
26
+ * TOOL_BUDGET_EXCEEDED without invoking the underlying handler.
27
+ * Callers without a sessionId (stdio transports — Claude Desktop, Cursor, etc.)
28
+ * share a single 'anon' bucket so the budget actually limits runaway tool use;
29
+ * giving each anon call a fresh UUID would defeat the purpose of the guard.
30
+ * `provardx_ping` is excluded from wrapping at the call site in server.ts.
31
+ */
32
+ export function wrapWithDepthGuard(toolName, handler, state, limit) {
33
+ return async (args, extra) => {
34
+ const sessionId = extra.sessionId ?? 'anon';
35
+ const entry = getOrCreateEntry(state, sessionId);
36
+ if (entry.calls >= limit) {
37
+ const payload = {
38
+ error: 'TOOL_BUDGET_EXCEEDED',
39
+ callsMade: entry.calls,
40
+ limit,
41
+ suggestion: 'Summarize progress and return control to the user.',
42
+ };
43
+ const response = {
44
+ isError: true,
45
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
46
+ structuredContent: payload,
47
+ };
48
+ return attachMeta(response, toolName, 'standard', entry.totalEstimatedTokens);
49
+ }
50
+ entry.calls++;
51
+ const result = await handler(args, extra);
52
+ if (process.env['PROVAR_MCP_EMIT_TOKEN_META'] === 'true') {
53
+ entry.totalEstimatedTokens += estimateTokens(result);
54
+ }
55
+ const detailLevel = typeof args['detail'] === 'string' ? args['detail'] : 'standard';
56
+ return attachMeta(result, toolName, detailLevel);
57
+ };
58
+ }
59
+ // --------------------------------------------------------------------------- //
60
+ // PDX-475 — Token meta attachment (PROVAR_MCP_EMIT_TOKEN_META)
61
+ // --------------------------------------------------------------------------- //
62
+ export function estimateTokens(payload) {
63
+ return Math.ceil(JSON.stringify(payload).length / 4);
64
+ }
65
+ /**
66
+ * Appends a `_meta` key to `structuredContent` when PROVAR_MCP_EMIT_TOKEN_META=true.
67
+ * The `content[0].text` string is intentionally left unchanged — LLMs read that
68
+ * field, so including meta there would waste tokens on observability data.
69
+ *
70
+ * @param sessionTotalTokens - Cumulative estimated tokens for the session,
71
+ * included only on TOOL_BUDGET_EXCEEDED errors.
72
+ */
73
+ export function attachMeta(response, toolName, detailLevel, sessionTotalTokens) {
74
+ if (process.env['PROVAR_MCP_EMIT_TOKEN_META'] !== 'true')
75
+ return response;
76
+ const meta = {
77
+ tool: toolName,
78
+ detailLevel,
79
+ estimatedTokens: estimateTokens(response),
80
+ };
81
+ if (sessionTotalTokens !== undefined) {
82
+ meta['sessionTotalEstimatedTokens'] = sessionTotalTokens;
83
+ }
84
+ const existing = response.structuredContent ?? {};
85
+ return {
86
+ ...response,
87
+ structuredContent: { ...existing, _meta: meta },
88
+ };
89
+ }
90
+ //# sourceMappingURL=tokenMeta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenMeta.js","sourceRoot":"","sources":["../../../src/mcp/utils/tokenMeta.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ export type DiffableViolation = Record<string, unknown>;
2
+ export interface DiffResult {
3
+ added: DiffableViolation[];
4
+ resolved: DiffableViolation[];
5
+ unchanged_count: number;
6
+ run_id: string;
7
+ }
8
+ /**
9
+ * Compute a stable 8-char context hash for a tool + context pair. Used to
10
+ * scope baseline run lookups so that a run_id from context A cannot be diffed
11
+ * against context B (different project, different suite, different file).
12
+ */
13
+ export declare function computeContextHash(toolTag: string, context: string): string;
14
+ /**
15
+ * Resolve the validation storage root for a given tool subdir. Honors the
16
+ * PROVAR_MCP_VALIDATION_DIR env var when set; otherwise falls back to
17
+ * `~/.provardx/validation/<subdir>`. The env override is useful for restricted
18
+ * CI/dev environments where the home directory is read-only or shared.
19
+ */
20
+ export declare function resolveValidationDir(subdir: string): string;
21
+ /** Generate a run ID from a context string (e.g. project path or suite name). */
22
+ export declare function generateRunId(context: string): string;
23
+ /**
24
+ * Check whether any prior runs exist in the given storage directory.
25
+ * Used by calcNextAction to determine the first-run heuristic.
26
+ */
27
+ export declare function hasAnyRun(storageDir: string): boolean;
28
+ /**
29
+ * Save the current violations as a new run in the storage directory.
30
+ * Caps the index at MAX_RUNS by evicting the oldest entry when full.
31
+ * Returns the generated run_id.
32
+ *
33
+ * When `contextHash` is provided, it is recorded alongside the run so that
34
+ * `loadBaselineViolations` can reject a baseline_run_id whose context does
35
+ * not match the calling context (prevents cross-context diffs).
36
+ */
37
+ export declare function saveRun(storageDir: string, runId: string, violations: DiffableViolation[], contextHash?: string): string;
38
+ /**
39
+ * Load the violations array for a given baseline run ID.
40
+ * Returns null if the run is not found in the index (BASELINE_NOT_FOUND).
41
+ * The filename is looked up from the index only — the run_id itself is never
42
+ * used to construct a file path, preventing path traversal.
43
+ *
44
+ * When `expectedContextHash` is provided, the record's `context_hash` must
45
+ * match. Records without a `context_hash` (written by older versions before
46
+ * H3) are treated as a mismatch and are effectively retired within one or
47
+ * two new runs as the FIFO cap evicts them. This guard prevents diffing a
48
+ * baseline from a different file/suite/project against the current context.
49
+ */
50
+ export declare function loadBaselineViolations(storageDir: string, baselineRunId: string, expectedContextHash?: string): DiffableViolation[] | null;
51
+ /**
52
+ * Compute the diff between a baseline and current violations array.
53
+ * Uses (rule_id + applies_to + full message) as the unique key.
54
+ * Duplicate violations (same key, multiple occurrences) are treated as
55
+ * distinct entries — each occurrence is counted separately (multiset semantics).
56
+ */
57
+ export declare function computeDiff(baseline: DiffableViolation[], current: DiffableViolation[]): Omit<DiffResult, 'run_id'>;
@@ -0,0 +1,191 @@
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 fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { createHash } from 'node:crypto';
12
+ const MAX_RUNS = 20;
13
+ const INDEX_FILE = '.runs.json';
14
+ const DEFAULT_ROOT_NAME = '.provardx';
15
+ const VALIDATION_SUBDIR = 'validation';
16
+ // ── Helpers ───────────────────────────────────────────────────────────────────
17
+ /** Stable 8-char hash of a string for use in run IDs. */
18
+ function shortHash(input) {
19
+ return createHash('sha1').update(input).digest('hex').slice(0, 8);
20
+ }
21
+ /** Build a unique key for a violation so additions/resolutions can be detected. */
22
+ function violationKey(v) {
23
+ const rule_id = String(v['rule_id'] ?? '');
24
+ const applies_to = Array.isArray(v['applies_to'])
25
+ ? v['applies_to'].join(',')
26
+ : String(v['applies_to'] ?? '');
27
+ const message = String(v['message'] ?? '');
28
+ return `${rule_id}||${applies_to}||${message}`;
29
+ }
30
+ function loadIndex(storageDir) {
31
+ const indexPath = path.join(storageDir, INDEX_FILE);
32
+ try {
33
+ return JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
34
+ }
35
+ catch {
36
+ return { runs: [] };
37
+ }
38
+ }
39
+ function saveIndex(storageDir, index) {
40
+ const indexPath = path.join(storageDir, INDEX_FILE);
41
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
42
+ }
43
+ // ── Public API ────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Compute a stable 8-char context hash for a tool + context pair. Used to
46
+ * scope baseline run lookups so that a run_id from context A cannot be diffed
47
+ * against context B (different project, different suite, different file).
48
+ */
49
+ export function computeContextHash(toolTag, context) {
50
+ return shortHash(`${toolTag}|${context}`);
51
+ }
52
+ /**
53
+ * Resolve the validation storage root for a given tool subdir. Honors the
54
+ * PROVAR_MCP_VALIDATION_DIR env var when set; otherwise falls back to
55
+ * `~/.provardx/validation/<subdir>`. The env override is useful for restricted
56
+ * CI/dev environments where the home directory is read-only or shared.
57
+ */
58
+ export function resolveValidationDir(subdir) {
59
+ const override = process.env['PROVAR_MCP_VALIDATION_DIR']?.trim();
60
+ if (override)
61
+ return path.join(override, subdir);
62
+ return path.join(os.homedir(), DEFAULT_ROOT_NAME, VALIDATION_SUBDIR, subdir);
63
+ }
64
+ /** Generate a run ID from a context string (e.g. project path or suite name). */
65
+ export function generateRunId(context) {
66
+ const rand = Math.random().toString(36).slice(2, 6);
67
+ return `${Date.now()}-${shortHash(context)}-${rand}`;
68
+ }
69
+ /**
70
+ * Check whether any prior runs exist in the given storage directory.
71
+ * Used by calcNextAction to determine the first-run heuristic.
72
+ */
73
+ export function hasAnyRun(storageDir) {
74
+ const index = loadIndex(storageDir);
75
+ return index.runs.length > 0;
76
+ }
77
+ /**
78
+ * Save the current violations as a new run in the storage directory.
79
+ * Caps the index at MAX_RUNS by evicting the oldest entry when full.
80
+ * Returns the generated run_id.
81
+ *
82
+ * When `contextHash` is provided, it is recorded alongside the run so that
83
+ * `loadBaselineViolations` can reject a baseline_run_id whose context does
84
+ * not match the calling context (prevents cross-context diffs).
85
+ */
86
+ export function saveRun(storageDir, runId, violations, contextHash) {
87
+ fs.mkdirSync(storageDir, { recursive: true });
88
+ const filename = `${runId}.json`;
89
+ fs.writeFileSync(path.join(storageDir, filename), JSON.stringify(violations), 'utf-8');
90
+ const index = loadIndex(storageDir);
91
+ index.runs.push({
92
+ run_id: runId,
93
+ timestamp: Date.now(),
94
+ filename,
95
+ ...(contextHash ? { context_hash: contextHash } : {}),
96
+ });
97
+ // Evict oldest entries when over the cap
98
+ while (index.runs.length > MAX_RUNS) {
99
+ const evicted = index.runs.shift();
100
+ if (evicted) {
101
+ try {
102
+ fs.unlinkSync(path.join(storageDir, evicted.filename));
103
+ }
104
+ catch {
105
+ /* best-effort eviction */
106
+ }
107
+ }
108
+ }
109
+ saveIndex(storageDir, index);
110
+ return runId;
111
+ }
112
+ /**
113
+ * Load the violations array for a given baseline run ID.
114
+ * Returns null if the run is not found in the index (BASELINE_NOT_FOUND).
115
+ * The filename is looked up from the index only — the run_id itself is never
116
+ * used to construct a file path, preventing path traversal.
117
+ *
118
+ * When `expectedContextHash` is provided, the record's `context_hash` must
119
+ * match. Records without a `context_hash` (written by older versions before
120
+ * H3) are treated as a mismatch and are effectively retired within one or
121
+ * two new runs as the FIFO cap evicts them. This guard prevents diffing a
122
+ * baseline from a different file/suite/project against the current context.
123
+ */
124
+ export function loadBaselineViolations(storageDir, baselineRunId, expectedContextHash) {
125
+ const index = loadIndex(storageDir);
126
+ const record = index.runs.find((r) => r.run_id === baselineRunId);
127
+ if (!record)
128
+ return null;
129
+ if (expectedContextHash !== undefined && record.context_hash !== expectedContextHash) {
130
+ return null;
131
+ }
132
+ // Use the filename from the index, not the run_id
133
+ try {
134
+ const content = fs.readFileSync(path.join(storageDir, record.filename), 'utf-8');
135
+ return JSON.parse(content);
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Compute the diff between a baseline and current violations array.
143
+ * Uses (rule_id + applies_to + full message) as the unique key.
144
+ * Duplicate violations (same key, multiple occurrences) are treated as
145
+ * distinct entries — each occurrence is counted separately (multiset semantics).
146
+ */
147
+ export function computeDiff(baseline, current) {
148
+ // Build multiset counts keyed by violation identity
149
+ const baselineCounts = new Map();
150
+ for (const v of baseline) {
151
+ const key = violationKey(v);
152
+ const entry = baselineCounts.get(key);
153
+ if (entry) {
154
+ entry.count++;
155
+ }
156
+ else {
157
+ baselineCounts.set(key, { count: 1, sample: v });
158
+ }
159
+ }
160
+ const currentCounts = new Map();
161
+ for (const v of current) {
162
+ const key = violationKey(v);
163
+ const entry = currentCounts.get(key);
164
+ if (entry) {
165
+ entry.count++;
166
+ }
167
+ else {
168
+ currentCounts.set(key, { count: 1, sample: v });
169
+ }
170
+ }
171
+ const added = [];
172
+ const resolved = [];
173
+ let unchanged_count = 0;
174
+ // Tally additions: occurrences in current that exceed baseline count
175
+ for (const [key, { count: curr, sample }] of currentCounts) {
176
+ const base = baselineCounts.get(key)?.count ?? 0;
177
+ unchanged_count += Math.min(base, curr);
178
+ const addedCount = curr - base;
179
+ for (let i = 0; i < addedCount; i++)
180
+ added.push(sample);
181
+ }
182
+ // Tally resolutions: occurrences in baseline that exceed current count
183
+ for (const [key, { count: base, sample }] of baselineCounts) {
184
+ const curr = currentCounts.get(key)?.count ?? 0;
185
+ const resolvedCount = base - Math.min(base, curr);
186
+ for (let i = 0; i < resolvedCount; i++)
187
+ resolved.push(sample);
188
+ }
189
+ return { added, resolved, unchanged_count };
190
+ }
191
+ //# sourceMappingURL=validationDiff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validationDiff.js","sourceRoot":"","sources":["../../../src/mcp/utils/validationDiff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export type NextAction = 'stop' | 'fix_and_revalidate' | 'inspect_failures';
2
+ /** Fraction of passing tests expressed as 0–100 integer. Returns 0 when total is 0. */
3
+ export declare function calcCompletenessScore(passing: number, total: number): number;
4
+ /**
5
+ * Recommend what the agent should do next based on the completeness score,
6
+ * remaining violation count, and whether any prior runs exist on disk.
7
+ *
8
+ * - `stop` → score is 100 AND no violations remain
9
+ * - `inspect_failures` → first run (no baseline on disk) — review what's failing before trying to fix
10
+ * - `fix_and_revalidate`→ subsequent run — agent knows the failure set, should fix and re-run
11
+ *
12
+ * The secondary `remainingViolationCount` check prevents `stop` from firing when all
13
+ * tests pass but quality or best-practice violations are still present.
14
+ */
15
+ export declare function calcNextAction(score: number, hasBaseline: boolean, remainingViolationCount?: number): NextAction;
@@ -0,0 +1,31 @@
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
+ /** Fraction of passing tests expressed as 0–100 integer. Returns 0 when total is 0. */
8
+ export function calcCompletenessScore(passing, total) {
9
+ if (total === 0)
10
+ return 0;
11
+ return Math.round((passing / total) * 100);
12
+ }
13
+ /**
14
+ * Recommend what the agent should do next based on the completeness score,
15
+ * remaining violation count, and whether any prior runs exist on disk.
16
+ *
17
+ * - `stop` → score is 100 AND no violations remain
18
+ * - `inspect_failures` → first run (no baseline on disk) — review what's failing before trying to fix
19
+ * - `fix_and_revalidate`→ subsequent run — agent knows the failure set, should fix and re-run
20
+ *
21
+ * The secondary `remainingViolationCount` check prevents `stop` from firing when all
22
+ * tests pass but quality or best-practice violations are still present.
23
+ */
24
+ export function calcNextAction(score, hasBaseline, remainingViolationCount = 0) {
25
+ if (score === 100 && remainingViolationCount === 0)
26
+ return 'stop';
27
+ if (!hasBaseline)
28
+ return 'inspect_failures';
29
+ return 'fix_and_revalidate';
30
+ }
31
+ //# sourceMappingURL=validationScore.js.map