@graphpilot-oss/graphpilot 0.0.1 → 0.1.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 (116) hide show
  1. package/CHANGELOG.md +73 -126
  2. package/README.md +359 -101
  3. package/dist/cli.js +20 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/indexer.js +3 -3
  6. package/dist/indexer.js.map +1 -1
  7. package/dist/init.d.ts +28 -0
  8. package/dist/init.js +112 -0
  9. package/dist/init.js.map +1 -0
  10. package/dist/interactions.d.ts +5 -4
  11. package/dist/interactions.js +0 -0
  12. package/dist/interactions.js.map +1 -1
  13. package/dist/mcp.js +126 -46
  14. package/dist/mcp.js.map +1 -1
  15. package/dist/repo-resolve.d.ts +47 -0
  16. package/dist/repo-resolve.js +195 -0
  17. package/dist/repo-resolve.js.map +1 -0
  18. package/dist/storage.js +10 -1
  19. package/dist/storage.js.map +1 -1
  20. package/dist/validation.js +30 -4
  21. package/dist/validation.js.map +1 -1
  22. package/dist/watcher.d.ts +10 -0
  23. package/dist/watcher.js +70 -7
  24. package/dist/watcher.js.map +1 -1
  25. package/examples/README.md +105 -0
  26. package/examples/claude-code/README.md +125 -0
  27. package/examples/claude-code/claude-routing.md +102 -0
  28. package/examples/claude-code/claude_config.json +8 -0
  29. package/examples/cline/.clinerules +39 -0
  30. package/examples/cline/README.md +104 -0
  31. package/examples/cline/cline_mcp_settings.json +10 -0
  32. package/examples/continue/.continuerules +39 -0
  33. package/examples/continue/README.md +98 -0
  34. package/examples/continue/config.json +13 -0
  35. package/examples/cursor/.cursorrules +39 -0
  36. package/examples/cursor/README.md +98 -0
  37. package/examples/cursor/mcp.json +11 -0
  38. package/examples/windsurf/.windsurfrules +39 -0
  39. package/examples/windsurf/README.md +85 -0
  40. package/examples/windsurf/mcp_config.json +8 -0
  41. package/package.json +12 -3
  42. package/.editorconfig +0 -15
  43. package/.github/CODEOWNERS +0 -22
  44. package/.github/FUNDING.yml +0 -1
  45. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  46. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  47. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  48. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  49. package/.github/dependabot.yml +0 -15
  50. package/.github/workflows/ci.yml +0 -62
  51. package/.github/workflows/release.yml +0 -50
  52. package/.prettierignore +0 -19
  53. package/.prettierrc.json +0 -20
  54. package/CODE_OF_CONDUCT.md +0 -83
  55. package/CONTRIBUTING.md +0 -111
  56. package/bench/README.md +0 -544
  57. package/bench/results/agent-tier-2026-05-22.md +0 -28
  58. package/bench/results/agent-tier-summary.md +0 -44
  59. package/bench/results/baseline-tier-2026-05-22.md +0 -23
  60. package/bench/results/baseline.json +0 -810
  61. package/bench/results/baseline.md +0 -28
  62. package/bench/run-agent-tier-automated.ts +0 -234
  63. package/bench/run-agent-tier.md +0 -125
  64. package/bench/run-baseline-tier.ts +0 -200
  65. package/bench/run.ts +0 -210
  66. package/bench/runner-baseline.ts +0 -177
  67. package/bench/runner-graphpilot.ts +0 -131
  68. package/bench/score-agent-tier.ts +0 -191
  69. package/bench/score.ts +0 -59
  70. package/bench/tasks.ts +0 -236
  71. package/dist/provenance.d.ts +0 -74
  72. package/dist/provenance.js +0 -95
  73. package/dist/provenance.js.map +0 -1
  74. package/docs/architecture.md +0 -311
  75. package/docs/limitations.md +0 -156
  76. package/docs/mcp-setup.md +0 -231
  77. package/docs/quickstart.md +0 -202
  78. package/eslint.config.js +0 -148
  79. package/lefthook.yml +0 -81
  80. package/pnpm-workspace.yaml +0 -6
  81. package/scripts/smoke-stdio.mjs +0 -97
  82. package/src/cli.ts +0 -171
  83. package/src/edges.ts +0 -202
  84. package/src/git.ts +0 -255
  85. package/src/graph-schema.ts +0 -229
  86. package/src/impact.ts +0 -218
  87. package/src/indexer.ts +0 -152
  88. package/src/interactions.ts +0 -0
  89. package/src/mcp.ts +0 -652
  90. package/src/parser.ts +0 -138
  91. package/src/provenance.ts +0 -115
  92. package/src/query.ts +0 -148
  93. package/src/redact.ts +0 -122
  94. package/src/storage.ts +0 -115
  95. package/src/symbols.ts +0 -173
  96. package/src/validation.ts +0 -69
  97. package/src/validators.ts +0 -253
  98. package/src/watcher.ts +0 -383
  99. package/tests/edges.test.ts +0 -175
  100. package/tests/fixtures/sample.ts +0 -32
  101. package/tests/git.test.ts +0 -303
  102. package/tests/graph-schema.test.ts +0 -321
  103. package/tests/impact.test.ts +0 -454
  104. package/tests/interactions.test.ts +0 -180
  105. package/tests/lint-policy.test.ts +0 -106
  106. package/tests/mcp-stdio.test.ts +0 -171
  107. package/tests/mcp.test.ts +0 -335
  108. package/tests/parser.test.ts +0 -31
  109. package/tests/provenance.test.ts +0 -132
  110. package/tests/query.test.ts +0 -160
  111. package/tests/redact.test.ts +0 -167
  112. package/tests/security.test.ts +0 -144
  113. package/tests/symbols.test.ts +0 -78
  114. package/tests/validators.test.ts +0 -193
  115. package/tests/watcher.test.ts +0 -250
  116. package/tsconfig.json +0 -18
package/src/symbols.ts DELETED
@@ -1,173 +0,0 @@
1
- import type Parser from 'tree-sitter';
2
- import { walk, type ParsedFile } from './parser.js';
3
- import { redactSecrets } from './redact.js';
4
-
5
- export type SymbolKind =
6
- | 'function'
7
- | 'class'
8
- | 'method'
9
- | 'interface'
10
- | 'type'
11
- | 'variable'
12
- | 'enum';
13
-
14
- export interface SymbolRecord {
15
- id: string;
16
- name: string;
17
- kind: SymbolKind;
18
- file: string;
19
- line: number;
20
- column: number;
21
- endLine: number;
22
- signature: string;
23
- exported: boolean;
24
- parent?: string;
25
- }
26
-
27
- /**
28
- * Extract every symbol-defining node from a parsed file.
29
- * v1 covers: function decls, arrow/function-expr consts, classes, methods,
30
- * interfaces, type aliases, enums.
31
- */
32
- export function extractSymbols(parsed: ParsedFile): SymbolRecord[] {
33
- const out: SymbolRecord[] = [];
34
- for (const node of walk(parsed.tree.rootNode)) {
35
- extractFromNode(node, parsed, out);
36
- }
37
- return out;
38
- }
39
-
40
- function extractFromNode(node: Parser.SyntaxNode, parsed: ParsedFile, out: SymbolRecord[]): void {
41
- switch (node.type) {
42
- case 'function_declaration':
43
- case 'generator_function_declaration': {
44
- const name = node.childForFieldName('name')?.text;
45
- if (!name) return;
46
- out.push(record(node, parsed, name, 'function'));
47
- return;
48
- }
49
- case 'class_declaration': {
50
- const name = node.childForFieldName('name')?.text;
51
- if (!name) return;
52
- out.push(record(node, parsed, name, 'class'));
53
- extractClassMembers(node, parsed, name, out);
54
- return;
55
- }
56
- case 'interface_declaration': {
57
- const name = node.childForFieldName('name')?.text;
58
- if (!name) return;
59
- out.push(record(node, parsed, name, 'interface'));
60
- return;
61
- }
62
- case 'type_alias_declaration': {
63
- const name = node.childForFieldName('name')?.text;
64
- if (!name) return;
65
- out.push(record(node, parsed, name, 'type'));
66
- return;
67
- }
68
- case 'enum_declaration': {
69
- const name = node.childForFieldName('name')?.text;
70
- if (!name) return;
71
- out.push(record(node, parsed, name, 'enum'));
72
- return;
73
- }
74
- case 'variable_declarator': {
75
- // Only emit if value is a function-like (matches Day-2 listFunctions logic).
76
- const valueNode = node.childForFieldName('value');
77
- if (
78
- !valueNode ||
79
- !(
80
- valueNode.type === 'arrow_function' ||
81
- valueNode.type === 'function_expression' ||
82
- valueNode.type === 'function'
83
- )
84
- ) {
85
- return;
86
- }
87
- const name = node.childForFieldName('name')?.text;
88
- if (!name) return;
89
- out.push(record(node, parsed, name, 'variable'));
90
- return;
91
- }
92
- default:
93
- return;
94
- }
95
- }
96
-
97
- function extractClassMembers(
98
- classNode: Parser.SyntaxNode,
99
- parsed: ParsedFile,
100
- className: string,
101
- out: SymbolRecord[],
102
- ): void {
103
- const body = classNode.childForFieldName('body');
104
- if (!body) return;
105
- for (let i = 0; i < body.childCount; i++) {
106
- const member = body.child(i);
107
- if (!member) continue;
108
- if (member.type === 'method_definition' || member.type === 'method_signature') {
109
- const name = member.childForFieldName('name')?.text;
110
- if (!name) continue;
111
- out.push(record(member, parsed, name, 'method', className));
112
- }
113
- }
114
- }
115
-
116
- function record(
117
- node: Parser.SyntaxNode,
118
- parsed: ParsedFile,
119
- name: string,
120
- kind: SymbolKind,
121
- parent?: string,
122
- ): SymbolRecord {
123
- const line = node.startPosition.row + 1;
124
- const column = node.startPosition.column + 1;
125
- const endLine = node.endPosition.row + 1;
126
- const signature = oneLineSignature(node, parsed.source);
127
- const exported = isExported(node);
128
- const id = `${parsed.path}#${parent ? parent + '.' : ''}${name}@${line}`;
129
- return {
130
- id,
131
- name,
132
- kind,
133
- file: parsed.path,
134
- line,
135
- column,
136
- endLine,
137
- signature,
138
- exported,
139
- parent,
140
- };
141
- }
142
-
143
- /**
144
- * Extract a single-line signature from the node. Takes the first line of
145
- * text up to the body/value, capped at 200 chars. Secrets matching known
146
- * patterns (T3 defence) are redacted before the line is returned.
147
- */
148
- function oneLineSignature(node: Parser.SyntaxNode, source: string): string {
149
- // For variable_declarator, climb to the parent lexical/var declaration so we
150
- // capture "export const foo = ..." rather than just "foo = ...".
151
- let target: Parser.SyntaxNode = node;
152
- if (node.type === 'variable_declarator' && node.parent) {
153
- target = node.parent;
154
- }
155
- const raw = source.slice(target.startIndex, target.endIndex);
156
- const firstLine = raw.split('\n')[0].trim();
157
- const capped = firstLine.length > 200 ? firstLine.slice(0, 197) + '...' : firstLine;
158
- // T3: redact known-format secrets (API keys, tokens, JWTs, PEM headers).
159
- return redactSecrets(capped);
160
- }
161
-
162
- /**
163
- * A node is exported if it (or an ancestor variable/lexical decl) is wrapped
164
- * in an `export_statement`.
165
- */
166
- function isExported(node: Parser.SyntaxNode): boolean {
167
- let cur: Parser.SyntaxNode | null = node;
168
- while (cur) {
169
- if (cur.type === 'export_statement') return true;
170
- cur = cur.parent;
171
- }
172
- return false;
173
- }
package/src/validation.ts DELETED
@@ -1,69 +0,0 @@
1
- import { realpathSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { resolve } from 'node:path';
4
-
5
- /** Max bytes per source file we will read. Anything larger is skipped. */
6
- export const MAX_FILE_BYTES = 5 * 1024 * 1024; // 5 MB
7
-
8
- /** Max number of files we will index in one run. Hard fail above this. */
9
- export const MAX_FILES_PER_INDEX = 50_000;
10
-
11
- /**
12
- * System paths we refuse to index. Indexing these by accident would walk the
13
- * whole machine, fill disk, and leak system files into ~/.graphpilot/.
14
- *
15
- * Each entry must match the *realpath* form (after `fs.realpathSync`). macOS
16
- * symlinks /etc, /var, /tmp to /private/*, so we include both forms here.
17
- */
18
- const DANGEROUS_PATHS = new Set([
19
- '/',
20
- '/bin',
21
- '/sbin',
22
- '/usr',
23
- '/usr/bin',
24
- '/usr/local',
25
- '/etc',
26
- '/var',
27
- '/tmp',
28
- '/private',
29
- '/private/etc',
30
- '/private/var',
31
- '/private/tmp',
32
- '/Library',
33
- '/System',
34
- '/Applications',
35
- '/Volumes',
36
- '/Users',
37
- '/home',
38
- 'C:\\Windows',
39
- 'C:\\Windows\\System32',
40
- 'C:\\Program Files',
41
- 'C:\\Program Files (x86)',
42
- ]);
43
-
44
- /**
45
- * Returns null if the path is safe to index, or a human-readable reason string
46
- * if it should be refused.
47
- */
48
- export function validateRootPath(rawPath: string): string | null {
49
- const abs = resolve(rawPath);
50
-
51
- let real: string;
52
- try {
53
- real = realpathSync(abs);
54
- } catch {
55
- return `Path does not exist or is not accessible: ${abs}`;
56
- }
57
-
58
- // Reject any bare Windows drive root (C:\, D:\, etc.) — not just C:\
59
- if (process.platform === 'win32' && /^[A-Za-z]:\\?$/.test(real)) {
60
- return `Refusing to index system path: ${real}`;
61
- }
62
- if (DANGEROUS_PATHS.has(real)) {
63
- return `Refusing to index system path: ${real}`;
64
- }
65
- if (real === homedir()) {
66
- return `Refusing to index your home directory directly (${real}). Pass a specific project subdirectory instead.`;
67
- }
68
- return null;
69
- }
package/src/validators.ts DELETED
@@ -1,253 +0,0 @@
1
- /**
2
- * Hand-rolled validation for MCP tool inputs. No external deps — three tools,
3
- * each with a handful of fields, makes a library overkill.
4
- *
5
- * Every tool validator returns a tagged result:
6
- * { ok: true, value: <typed args> }
7
- * { ok: false, error: <human-readable> }
8
- *
9
- * Rules (defence-in-depth — JSON schema is declared in the tool catalog
10
- * too; this is the second wall):
11
- * - Reject extra unknown keys (defends against agent typos & tampering)
12
- * - Type-check every field
13
- * - Range-check numbers (limit is bounded, no NaN)
14
- * - Length-cap strings (no 10MB symbol names)
15
- * - Strict enums (no surprise direction values)
16
- */
17
-
18
- export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
19
-
20
- function ok<T>(value: T): Result<T> {
21
- return { ok: true, value };
22
- }
23
- function fail<T>(error: string): Result<T> {
24
- return { ok: false, error };
25
- }
26
-
27
- function isPlainObject(x: unknown): x is Record<string, unknown> {
28
- return typeof x === 'object' && x !== null && !Array.isArray(x);
29
- }
30
-
31
- const MAX_STRING_LEN = 2_000;
32
-
33
- function pickString(
34
- obj: Record<string, unknown>,
35
- key: string,
36
- opts: { required?: boolean; max?: number } = {},
37
- ): Result<string | undefined> {
38
- const raw = obj[key];
39
- if (raw === undefined) {
40
- if (opts.required) return fail(`Missing required field: ${key}`);
41
- return ok(undefined);
42
- }
43
- if (typeof raw !== 'string') return fail(`${key} must be a string`);
44
- const max = opts.max ?? MAX_STRING_LEN;
45
- if (raw.length > max) return fail(`${key} exceeds max length of ${max}`);
46
- return ok(raw);
47
- }
48
-
49
- function pickNumber(
50
- obj: Record<string, unknown>,
51
- key: string,
52
- opts: { min?: number; max?: number; integer?: boolean } = {},
53
- ): Result<number | undefined> {
54
- const raw = obj[key];
55
- if (raw === undefined) return ok(undefined);
56
- if (typeof raw !== 'number' || !Number.isFinite(raw)) {
57
- return fail(`${key} must be a finite number`);
58
- }
59
- if (opts.integer && !Number.isInteger(raw)) {
60
- return fail(`${key} must be an integer`);
61
- }
62
- if (opts.min !== undefined && raw < opts.min) {
63
- return fail(`${key} must be >= ${opts.min}`);
64
- }
65
- if (opts.max !== undefined && raw > opts.max) {
66
- return fail(`${key} must be <= ${opts.max}`);
67
- }
68
- return ok(raw);
69
- }
70
-
71
- function pickBoolean(obj: Record<string, unknown>, key: string): Result<boolean | undefined> {
72
- const raw = obj[key];
73
- if (raw === undefined) return ok(undefined);
74
- if (typeof raw !== 'boolean') return fail(`${key} must be a boolean`);
75
- return ok(raw);
76
- }
77
-
78
- function pickEnum<T extends string>(
79
- obj: Record<string, unknown>,
80
- key: string,
81
- allowed: readonly T[],
82
- ): Result<T | undefined> {
83
- const raw = obj[key];
84
- if (raw === undefined) return ok(undefined);
85
- if (typeof raw !== 'string' || !(allowed as readonly string[]).includes(raw)) {
86
- return fail(`${key} must be one of: ${allowed.join(', ')}`);
87
- }
88
- return ok(raw as T);
89
- }
90
-
91
- function rejectExtraKeys(obj: Record<string, unknown>, allowed: readonly string[]): Result<true> {
92
- const extras = Object.keys(obj).filter((k) => !allowed.includes(k));
93
- if (extras.length > 0) {
94
- return fail(`Unknown field(s): ${extras.join(', ')}. Allowed: ${allowed.join(', ')}`);
95
- }
96
- return ok(true);
97
- }
98
-
99
- // ----------------------------------------------------------------------------
100
- // Per-tool validators
101
- // ----------------------------------------------------------------------------
102
-
103
- export interface GpIndexArgs {
104
- path?: string;
105
- }
106
- const GP_INDEX_KEYS = ['path'] as const;
107
-
108
- export function validateGpIndex(input: unknown): Result<GpIndexArgs> {
109
- if (!isPlainObject(input)) return fail('arguments must be an object');
110
- const extras = rejectExtraKeys(input, GP_INDEX_KEYS);
111
- if (!extras.ok) return fail(extras.error);
112
- const path = pickString(input, 'path', { max: 1024 });
113
- if (!path.ok) return fail(path.error);
114
- return ok({ path: path.value });
115
- }
116
-
117
- export interface GpRecallArgs {
118
- query: string;
119
- limit?: number;
120
- substring?: boolean;
121
- path?: string;
122
- }
123
- const GP_RECALL_KEYS = ['query', 'limit', 'substring', 'path'] as const;
124
-
125
- export function validateGpRecall(input: unknown): Result<GpRecallArgs> {
126
- if (!isPlainObject(input)) return fail('arguments must be an object');
127
- const extras = rejectExtraKeys(input, GP_RECALL_KEYS);
128
- if (!extras.ok) return fail(extras.error);
129
-
130
- const query = pickString(input, 'query', { required: true, max: 200 });
131
- if (!query.ok) return fail(query.error);
132
- if (!query.value || query.value.trim() === '') {
133
- return fail('query must be a non-empty string');
134
- }
135
-
136
- const limit = pickNumber(input, 'limit', { min: 1, max: 50, integer: true });
137
- if (!limit.ok) return fail(limit.error);
138
-
139
- const substring = pickBoolean(input, 'substring');
140
- if (!substring.ok) return fail(substring.error);
141
-
142
- const path = pickString(input, 'path', { max: 1024 });
143
- if (!path.ok) return fail(path.error);
144
-
145
- return ok({
146
- query: query.value,
147
- limit: limit.value,
148
- substring: substring.value,
149
- path: path.value,
150
- });
151
- }
152
-
153
- export type CallersDirection = 'callers' | 'callees';
154
-
155
- export interface GpCallersArgs {
156
- symbol: string;
157
- direction?: CallersDirection;
158
- limit?: number;
159
- includeUnresolved?: boolean;
160
- path?: string;
161
- }
162
- const GP_CALLERS_KEYS = ['symbol', 'direction', 'limit', 'includeUnresolved', 'path'] as const;
163
-
164
- export function validateGpCallers(input: unknown): Result<GpCallersArgs> {
165
- if (!isPlainObject(input)) return fail('arguments must be an object');
166
- const extras = rejectExtraKeys(input, GP_CALLERS_KEYS);
167
- if (!extras.ok) return fail(extras.error);
168
-
169
- const symbol = pickString(input, 'symbol', { required: true, max: 500 });
170
- if (!symbol.ok) return fail(symbol.error);
171
- if (!symbol.value || symbol.value.trim() === '') {
172
- return fail('symbol must be a non-empty string');
173
- }
174
-
175
- const direction = pickEnum(input, 'direction', ['callers', 'callees'] as const);
176
- if (!direction.ok) return fail(direction.error);
177
-
178
- const limit = pickNumber(input, 'limit', { min: 1, max: 100, integer: true });
179
- if (!limit.ok) return fail(limit.error);
180
-
181
- const includeUnresolved = pickBoolean(input, 'includeUnresolved');
182
- if (!includeUnresolved.ok) return fail(includeUnresolved.error);
183
-
184
- const path = pickString(input, 'path', { max: 1024 });
185
- if (!path.ok) return fail(path.error);
186
-
187
- return ok({
188
- symbol: symbol.value,
189
- direction: direction.value,
190
- limit: limit.value,
191
- includeUnresolved: includeUnresolved.value,
192
- path: path.value,
193
- });
194
- }
195
-
196
- // Tool name -> validator dispatcher (for the MCP server)
197
- export type ToolName = 'gp_index' | 'gp_recall' | 'gp_callers' | 'gp_impact' | 'gp_stats';
198
-
199
- export interface GpStatsArgs {
200
- path?: string;
201
- }
202
- export function validateGpStats(input: unknown): Result<GpStatsArgs> {
203
- if (!isPlainObject(input)) return fail('arguments must be an object');
204
- const extras = rejectExtraKeys(input, ['path']);
205
- if (!extras.ok) return fail(extras.error);
206
- const path = pickString(input, 'path', { max: 1024 });
207
- if (!path.ok) return fail(path.error);
208
- return ok({ path: path.value });
209
- }
210
-
211
- export interface GpImpactArgs {
212
- symbol: string;
213
- depth?: number;
214
- path?: string;
215
- since?: string;
216
- }
217
- const GP_IMPACT_KEYS = ['symbol', 'depth', 'path', 'since'] as const;
218
-
219
- export function validateGpImpact(input: unknown): Result<GpImpactArgs> {
220
- if (!isPlainObject(input)) return fail('arguments must be an object');
221
- const extras = rejectExtraKeys(input, GP_IMPACT_KEYS);
222
- if (!extras.ok) return fail(extras.error);
223
-
224
- const symbol = pickString(input, 'symbol', { required: true, max: 500 });
225
- if (!symbol.ok) return fail(symbol.error);
226
- if (!symbol.value || symbol.value.trim() === '') {
227
- return fail('symbol must be a non-empty string');
228
- }
229
-
230
- // BFS depth: hard-cap at 5. Deeper traversals explode in big repos and
231
- // rarely add value — depth-3 already covers ~99% of real refactors.
232
- const depth = pickNumber(input, 'depth', { min: 1, max: 5, integer: true });
233
- if (!depth.ok) return fail(depth.error);
234
-
235
- const path = pickString(input, 'path', { max: 1024 });
236
- if (!path.ok) return fail(path.error);
237
-
238
- // `since` accepts a commit SHA (full or short), tag, or branch name.
239
- // Capped at 200 chars — refs are normally under 100, this is a sanity
240
- // bound, not a security one (isomorphic-git handles ref resolution).
241
- const since = pickString(input, 'since', { max: 200 });
242
- if (!since.ok) return fail(since.error);
243
- if (since.value !== undefined && since.value.trim() === '') {
244
- return fail('since must be a non-empty string when provided');
245
- }
246
-
247
- return ok({
248
- symbol: symbol.value,
249
- depth: depth.value,
250
- path: path.value,
251
- since: since.value,
252
- });
253
- }