patram 0.9.0 → 0.11.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 (33) hide show
  1. package/lib/cli/arguments.types.d.ts +64 -0
  2. package/lib/cli/commands/check.js +27 -15
  3. package/lib/cli/commands/queries.js +189 -1
  4. package/lib/cli/commands/query.js +6 -3
  5. package/lib/cli/help-metadata.js +45 -110
  6. package/lib/cli/parse-arguments-helpers.js +295 -39
  7. package/lib/cli/render-help.js +87 -0
  8. package/lib/config/load-patram-config.d.ts +11 -0
  9. package/lib/config/load-patram-config.js +9 -88
  10. package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
  11. package/lib/config/manage-stored-queries-helpers.js +262 -0
  12. package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
  13. package/lib/config/manage-stored-queries-jsonc.js +95 -0
  14. package/lib/config/manage-stored-queries.d.ts +77 -0
  15. package/lib/config/manage-stored-queries.js +294 -0
  16. package/lib/config/schema.d.ts +2 -0
  17. package/lib/config/schema.js +4 -0
  18. package/lib/config/validate-patram-config-value.d.ts +13 -0
  19. package/lib/config/validate-patram-config-value.js +119 -0
  20. package/lib/find-close-match.d.ts +8 -0
  21. package/lib/find-close-match.js +98 -0
  22. package/lib/graph/query/resolve.d.ts +9 -5
  23. package/lib/graph/query/resolve.js +41 -4
  24. package/lib/output/layout-stored-queries.js +18 -2
  25. package/lib/output/list-queries.js +2 -1
  26. package/lib/output/renderers/json.js +9 -5
  27. package/lib/output/renderers/plain.js +15 -26
  28. package/lib/output/renderers/rich.js +22 -26
  29. package/lib/output/resolve-check-target.js +120 -11
  30. package/lib/output/view-model/index.js +5 -18
  31. package/lib/patram.d.ts +8 -0
  32. package/lib/scan/discover-fields.js +136 -10
  33. package/package.json +2 -1
@@ -0,0 +1,64 @@
1
+ export type CliCommandName = 'check' | 'fields' | 'query' | 'queries' | 'refs' | 'show';
2
+ export type CliHelpTopicName = 'query-language';
3
+ export type CliHelpTargetKind = 'root' | 'command' | 'topic';
4
+ export type CliUnexpectedArgumentCommandName = CliCommandName | 'help';
5
+ export type CliOutputMode = 'default' | 'plain' | 'json';
6
+ export type CliColorMode = 'auto' | 'always' | 'never';
7
+ export interface ParsedCliCommandRequest {
8
+ kind?: 'command';
9
+ color_mode: CliColorMode;
10
+ command_arguments: string[];
11
+ command_name: CliCommandName;
12
+ output_mode: CliOutputMode;
13
+ query_inspection_mode?: 'explain' | 'lint';
14
+ query_limit?: number;
15
+ query_offset?: number;
16
+ }
17
+ export interface ParsedCliHelpRequest {
18
+ kind: 'help';
19
+ target_kind: CliHelpTargetKind;
20
+ target_name?: CliCommandName | CliHelpTopicName;
21
+ }
22
+ export type ParsedCliArguments = ParsedCliCommandRequest;
23
+ export type ParsedCliRequest = ParsedCliCommandRequest | ParsedCliHelpRequest;
24
+ export type CliParseError = {
25
+ code: 'message';
26
+ message: string;
27
+ } | {
28
+ code: 'missing_required_argument';
29
+ argument_label: string;
30
+ command_name: 'query' | 'refs' | 'show';
31
+ } | {
32
+ code: 'unexpected_argument';
33
+ command_name: CliUnexpectedArgumentCommandName;
34
+ token: string;
35
+ } | {
36
+ code: 'option_not_valid_for_command';
37
+ command_name: CliCommandName;
38
+ token: string;
39
+ } | {
40
+ code: 'unknown_command';
41
+ suggestion?: CliCommandName;
42
+ token: string;
43
+ } | {
44
+ code: 'unknown_help_target';
45
+ suggestion?: CliCommandName | CliHelpTopicName;
46
+ token: string;
47
+ } | {
48
+ code: 'unknown_stored_query';
49
+ name: string;
50
+ next_path?: string;
51
+ suggestion?: string;
52
+ } | {
53
+ code: 'unknown_option';
54
+ command_name?: CliCommandName;
55
+ suggestion?: string;
56
+ token: string;
57
+ };
58
+ export type ParseCliArgumentsResult = {
59
+ success: true;
60
+ value: ParsedCliRequest;
61
+ } | {
62
+ error: CliParseError;
63
+ success: false;
64
+ };
@@ -9,13 +9,15 @@ import {
9
9
  renderCheckSuccess,
10
10
  } from '../../output/render-check-output.js';
11
11
  import {
12
- resolveCheckTarget,
13
- selectCheckTargetDiagnostics,
14
- selectCheckTargetSourceFiles,
12
+ resolveCheckTargetProjectDirectory,
13
+ resolveCheckTargets,
14
+ selectCheckTargetsDiagnostics,
15
+ selectCheckTargetsSourceFiles,
15
16
  } from '../../output/resolve-check-target.js';
16
17
  import { listRepoFiles } from '../../scan/list-repo-files.js';
17
18
 
18
19
  import { resolveCommandOutputMode } from '../command-helpers.js';
20
+ import { renderCliParseError } from '../render-help.js';
19
21
 
20
22
  /**
21
23
  * @param {ParsedCliCommandRequest} parsed_command
@@ -24,18 +26,28 @@ import { resolveCommandOutputMode } from '../command-helpers.js';
24
26
  */
25
27
  export async function runCheckCommand(parsed_command, io_context) {
26
28
  const output_mode = resolveCommandOutputMode(parsed_command, io_context);
27
- const resolved_target = await resolveCheckTarget(
28
- parsed_command.command_arguments[0],
29
+ const resolved_targets = await resolveCheckTargets(
30
+ parsed_command.command_arguments,
29
31
  );
30
- const project_graph_result = await loadProjectGraph(
31
- resolved_target.project_directory,
32
- );
33
- const repo_file_paths = await listRepoFiles(
34
- resolved_target.project_directory,
35
- );
36
- const selected_source_file_paths = selectCheckTargetSourceFiles(
32
+ const project_directory =
33
+ resolveCheckTargetProjectDirectory(resolved_targets);
34
+
35
+ if (!project_directory) {
36
+ io_context.stderr.write(
37
+ renderCliParseError({
38
+ code: 'message',
39
+ message: 'Check paths must resolve to the same project root.',
40
+ }),
41
+ );
42
+
43
+ return 1;
44
+ }
45
+
46
+ const project_graph_result = await loadProjectGraph(project_directory);
47
+ const repo_file_paths = await listRepoFiles(project_directory);
48
+ const selected_source_file_paths = selectCheckTargetsSourceFiles(
37
49
  project_graph_result.source_file_paths,
38
- resolved_target,
50
+ resolved_targets,
39
51
  );
40
52
 
41
53
  if (project_graph_result.diagnostics.length > 0) {
@@ -52,9 +64,9 @@ export async function runCheckCommand(parsed_command, io_context) {
52
64
  project_graph_result.config,
53
65
  project_graph_result.claims,
54
66
  );
55
- const selected_diagnostics = selectCheckTargetDiagnostics(
67
+ const selected_diagnostics = selectCheckTargetsDiagnostics(
56
68
  diagnostics,
57
- resolved_target,
69
+ resolved_targets,
58
70
  );
59
71
 
60
72
  if (selected_diagnostics.length > 0) {
@@ -4,12 +4,18 @@
4
4
 
5
5
  import process from 'node:process';
6
6
 
7
+ import { manageStoredQueries } from '../../config/manage-stored-queries.js';
7
8
  import { loadPatramConfig } from '../../config/load-patram-config.js';
9
+ import { renderCheckDiagnostics } from '../../output/render-check-output.js';
8
10
  import { writeCommandOutput } from '../../output/command-output.js';
9
11
  import { listQueries } from '../../output/list-queries.js';
10
12
  import { createOutputView } from '../../output/render-output-view.js';
13
+ import { renderCliParseError } from '../render-help.js';
11
14
 
12
- import { writeDiagnostics } from '../command-helpers.js';
15
+ import {
16
+ resolveCommandOutputMode,
17
+ writeDiagnostics,
18
+ } from '../command-helpers.js';
13
19
 
14
20
  /**
15
21
  * @param {ParsedCliCommandRequest} parsed_command
@@ -17,6 +23,10 @@ import { writeDiagnostics } from '../command-helpers.js';
17
23
  * @returns {Promise<number>}
18
24
  */
19
25
  export async function runQueriesCommand(parsed_command, io_context) {
26
+ if (parsed_command.command_arguments.length > 0) {
27
+ return runQueriesMutationCommand(parsed_command, io_context);
28
+ }
29
+
20
30
  const load_result = await loadPatramConfig(process.cwd());
21
31
 
22
32
  if (load_result.diagnostics.length > 0) {
@@ -39,3 +49,181 @@ export async function runQueriesCommand(parsed_command, io_context) {
39
49
 
40
50
  return 0;
41
51
  }
52
+
53
+ /**
54
+ * @param {ParsedCliCommandRequest} parsed_command
55
+ * @param {{ stderr: { write(chunk: string): boolean }, stdout: { isTTY?: boolean, write(chunk: string): boolean } }} io_context
56
+ * @returns {Promise<number>}
57
+ */
58
+ async function runQueriesMutationCommand(parsed_command, io_context) {
59
+ const mutation_request = parseStoredQueryMutation(
60
+ parsed_command.command_arguments,
61
+ );
62
+ const output_mode = resolveCommandOutputMode(parsed_command, io_context);
63
+
64
+ if (!mutation_request.success) {
65
+ io_context.stderr.write(renderCliParseError(mutation_request.error));
66
+
67
+ return 1;
68
+ }
69
+
70
+ const mutation_result = await manageStoredQueries(
71
+ process.cwd(),
72
+ mutation_request.value,
73
+ );
74
+
75
+ if (!mutation_result.success) {
76
+ if ('error' in mutation_result) {
77
+ io_context.stderr.write(renderCliParseError(mutation_result.error));
78
+
79
+ return 1;
80
+ }
81
+
82
+ io_context.stderr.write(
83
+ renderCheckDiagnostics(mutation_result.diagnostics, output_mode),
84
+ );
85
+
86
+ return 1;
87
+ }
88
+
89
+ io_context.stdout.write(
90
+ renderStoredQueryMutationResult(
91
+ mutation_result.value,
92
+ parsed_command.output_mode,
93
+ ),
94
+ );
95
+
96
+ return 0;
97
+ }
98
+
99
+ /**
100
+ * @param {string[]} command_arguments
101
+ * @returns {{
102
+ * success: true,
103
+ * value:
104
+ * | { action: 'add', description?: string, name: string, where: string }
105
+ * | { action: 'remove', name: string }
106
+ * | {
107
+ * action: 'update',
108
+ * description?: string,
109
+ * name: string,
110
+ * next_name?: string,
111
+ * where?: string,
112
+ * },
113
+ * } | { error: import('../arguments.types.ts').CliParseError, success: false }}
114
+ */
115
+ function parseStoredQueryMutation(command_arguments) {
116
+ const subcommand_name = command_arguments[0];
117
+
118
+ if (subcommand_name === 'add') {
119
+ return {
120
+ success: true,
121
+ value: {
122
+ action: 'add',
123
+ description: readOptionValue(command_arguments, '--desc'),
124
+ name: command_arguments[1],
125
+ where: readRequiredOptionValue(command_arguments, '--query'),
126
+ },
127
+ };
128
+ }
129
+
130
+ if (subcommand_name === 'remove') {
131
+ return {
132
+ success: true,
133
+ value: {
134
+ action: 'remove',
135
+ name: command_arguments[1],
136
+ },
137
+ };
138
+ }
139
+
140
+ if (subcommand_name === 'update') {
141
+ return {
142
+ success: true,
143
+ value: {
144
+ action: 'update',
145
+ description: readOptionValue(command_arguments, '--desc'),
146
+ name: command_arguments[1],
147
+ next_name: readOptionValue(command_arguments, '--name'),
148
+ where: readOptionValue(command_arguments, '--query'),
149
+ },
150
+ };
151
+ }
152
+
153
+ return {
154
+ error: {
155
+ code: 'unexpected_argument',
156
+ command_name: 'queries',
157
+ token: subcommand_name ?? '',
158
+ },
159
+ success: false,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * @param {{
165
+ * action: 'added',
166
+ * name: string,
167
+ * } | {
168
+ * action: 'removed',
169
+ * name: string,
170
+ * } | {
171
+ * action: 'updated',
172
+ * name: string,
173
+ * previous_name?: string,
174
+ * }} mutation_result
175
+ * @param {ParsedCliCommandRequest['output_mode']} output_mode
176
+ * @returns {string}
177
+ */
178
+ function renderStoredQueryMutationResult(mutation_result, output_mode) {
179
+ if (output_mode === 'json') {
180
+ return `${JSON.stringify(mutation_result, null, 2)}\n`;
181
+ }
182
+
183
+ if (
184
+ mutation_result.action === 'updated' &&
185
+ mutation_result.previous_name !== undefined
186
+ ) {
187
+ return `Updated stored query: ${mutation_result.previous_name} -> ${mutation_result.name}\n`;
188
+ }
189
+
190
+ if (mutation_result.action === 'updated') {
191
+ return `Updated stored query: ${mutation_result.name}\n`;
192
+ }
193
+
194
+ if (mutation_result.action === 'added') {
195
+ return `Added stored query: ${mutation_result.name}\n`;
196
+ }
197
+
198
+ return `Removed stored query: ${mutation_result.name}\n`;
199
+ }
200
+
201
+ /**
202
+ * @param {string[]} command_arguments
203
+ * @param {'--desc' | '--name' | '--query'} option_name
204
+ * @returns {string}
205
+ */
206
+ function readRequiredOptionValue(command_arguments, option_name) {
207
+ const option_value = readOptionValue(command_arguments, option_name);
208
+
209
+ if (option_value === undefined) {
210
+ throw new Error(`Expected ${option_name} to be present.`);
211
+ }
212
+
213
+ return option_value;
214
+ }
215
+
216
+ /**
217
+ * @param {string[]} command_arguments
218
+ * @param {'--desc' | '--name' | '--query'} option_name
219
+ * @returns {string | undefined}
220
+ */
221
+ function readOptionValue(command_arguments, option_name) {
222
+ const option_index = command_arguments.indexOf(option_name);
223
+
224
+ if (option_index < 0) {
225
+ return undefined;
226
+ }
227
+
228
+ return command_arguments[option_index + 1];
229
+ }
@@ -12,7 +12,10 @@ import {
12
12
  } from '../../output/command-output.js';
13
13
  import { createDerivedSummaryEvaluator } from '../../output/derived-summary.js';
14
14
  import { renderCheckDiagnostics } from '../../output/render-check-output.js';
15
- import { renderInvalidWhereDiagnostic } from '../render-help.js';
15
+ import {
16
+ renderCliParseError,
17
+ renderInvalidWhereDiagnostic,
18
+ } from '../render-help.js';
16
19
  import { createOutputView } from '../../output/render-output-view.js';
17
20
  import { loadPatramConfig } from '../../config/load-patram-config.js';
18
21
  import { loadProjectGraph } from '../../graph/load-project-graph.js';
@@ -60,7 +63,7 @@ export async function runQueryCommand(parsed_command, io_context) {
60
63
  );
61
64
 
62
65
  if (!where_clause.success) {
63
- io_context.stderr.write(`${where_clause.message}\n`);
66
+ io_context.stderr.write(renderCliParseError(where_clause.error));
64
67
 
65
68
  return 1;
66
69
  }
@@ -139,7 +142,7 @@ async function runQueryInspectionCommand(
139
142
  );
140
143
 
141
144
  if (!where_clause.success) {
142
- io_context.stderr.write(`${where_clause.message}\n`);
145
+ io_context.stderr.write(renderCliParseError(where_clause.error));
143
146
 
144
147
  return 1;
145
148
  }
@@ -6,6 +6,8 @@
6
6
  * } from './arguments.types.ts';
7
7
  */
8
8
 
9
+ import { findCloseMatch } from '../find-close-match.js';
10
+
9
11
  /**
10
12
  * @typedef {{
11
13
  * description: string,
@@ -85,14 +87,15 @@ const COMMAND_DEFINITIONS = {
85
87
  'patram check',
86
88
  'patram check docs',
87
89
  'patram check docs/patram.md',
90
+ 'patram check docs docs/patram.md',
88
91
  ],
89
- extra_positionals_message: 'Check accepts at most one path.',
92
+ extra_positionals_message: 'Check accepts zero or more paths.',
90
93
  help_topics: [],
91
- max_positionals: 1,
94
+ max_positionals: Number.POSITIVE_INFINITY,
92
95
  min_positionals: 0,
93
96
  missing_argument_examples: [],
94
97
  missing_argument_label: null,
95
- missing_usage_lines: ['patram check [path]'],
98
+ missing_usage_lines: ['patram check [path ...]'],
96
99
  option_column_width: 10,
97
100
  options: [
98
101
  {
@@ -108,7 +111,7 @@ const COMMAND_DEFINITIONS = {
108
111
  root_summary: 'Validate a project, directory, or file',
109
112
  summary:
110
113
  'Validate a project, directory, or file and report graph diagnostics.',
111
- usage_lines: ['patram check [path] [options]'],
114
+ usage_lines: ['patram check [path ...] [options]'],
112
115
  },
113
116
  fields: {
114
117
  allowed_option_names: new Set(),
@@ -219,17 +222,40 @@ const COMMAND_DEFINITIONS = {
219
222
  ],
220
223
  },
221
224
  queries: {
222
- allowed_option_names: new Set(),
223
- examples: ['patram queries'],
224
- extra_positionals_message: 'Queries does not accept positional arguments.',
225
+ allowed_option_names: new Set(['desc', 'name', 'query']),
226
+ examples: [
227
+ 'patram queries',
228
+ "patram queries add ready-tasks --query '$class=task and status=ready'",
229
+ "patram queries update ready-tasks --desc 'Show tasks that are ready.'",
230
+ 'patram queries remove ready-tasks',
231
+ ],
232
+ extra_positionals_message:
233
+ 'Queries accepts no positionals unless using add, update, or remove.',
225
234
  help_topics: [],
226
- max_positionals: 0,
235
+ max_positionals: 2,
227
236
  min_positionals: 0,
228
237
  missing_argument_examples: [],
229
238
  missing_argument_label: null,
230
- missing_usage_lines: ['patram queries'],
231
- option_column_width: 10,
239
+ missing_usage_lines: [
240
+ 'patram queries',
241
+ 'patram queries add <name> --query <clause>',
242
+ 'patram queries update <name> [--name <new_name>] [--query <clause>] [--desc <text>]',
243
+ 'patram queries remove <name>',
244
+ ],
245
+ option_column_width: 19,
232
246
  options: [
247
+ {
248
+ description: 'Persist a new stored query',
249
+ label: '--query <clause>',
250
+ },
251
+ {
252
+ description: 'Set or rename the stored query name for update',
253
+ label: '--name <new_name>',
254
+ },
255
+ {
256
+ description: 'Set or clear the stored query description',
257
+ label: '--desc <text>',
258
+ },
233
259
  {
234
260
  description: 'Print plain text output',
235
261
  label: '--plain',
@@ -240,9 +266,15 @@ const COMMAND_DEFINITIONS = {
240
266
  },
241
267
  ],
242
268
  related: ['query'],
243
- root_summary: 'List stored queries',
244
- summary: 'List the stored queries defined in the project configuration.',
245
- usage_lines: ['patram queries [options]'],
269
+ root_summary: 'List and manage stored queries',
270
+ summary:
271
+ 'List stored queries or mutate them through add, update, and remove.',
272
+ usage_lines: [
273
+ 'patram queries [options]',
274
+ 'patram queries add <name> --query <clause> [--desc <text>] [options]',
275
+ 'patram queries update <name> [--name <new_name>] [--query <clause>] [--desc <text>] [options]',
276
+ 'patram queries remove <name> [options]',
277
+ ],
246
278
  },
247
279
  refs: {
248
280
  allowed_option_names: new Set(['where']),
@@ -532,100 +564,3 @@ function listOptionLabels(command_name) {
532
564
 
533
565
  return [...option_labels];
534
566
  }
535
-
536
- /**
537
- * @param {string} input_text
538
- * @param {readonly string[]} candidates
539
- * @returns {string | undefined}
540
- */
541
- function findCloseMatch(input_text, candidates) {
542
- let best_candidate;
543
- let best_score = 0;
544
-
545
- for (const candidate of candidates) {
546
- const score = scoreCandidate(input_text, candidate);
547
-
548
- if (score > best_score) {
549
- best_candidate = candidate;
550
- best_score = score;
551
- }
552
- }
553
-
554
- if (best_score < 0.6) {
555
- return undefined;
556
- }
557
-
558
- return best_candidate;
559
- }
560
-
561
- /**
562
- * @param {string} input_text
563
- * @param {string} candidate
564
- * @returns {number}
565
- */
566
- function scoreCandidate(input_text, candidate) {
567
- const max_length = Math.max(input_text.length, candidate.length);
568
-
569
- if (max_length === 0) {
570
- return 1;
571
- }
572
-
573
- return (
574
- 1 - calculateDamerauLevenshteinDistance(input_text, candidate) / max_length
575
- );
576
- }
577
-
578
- /**
579
- * @param {string} left_text
580
- * @param {string} right_text
581
- * @returns {number}
582
- */
583
- function calculateDamerauLevenshteinDistance(left_text, right_text) {
584
- /** @type {number[][]} */
585
- const matrix = Array.from({ length: left_text.length + 1 }, () =>
586
- Array.from({ length: right_text.length + 1 }, () => 0),
587
- );
588
-
589
- for (let left_index = 0; left_index <= left_text.length; left_index += 1) {
590
- matrix[left_index][0] = left_index;
591
- }
592
-
593
- for (
594
- let right_index = 0;
595
- right_index <= right_text.length;
596
- right_index += 1
597
- ) {
598
- matrix[0][right_index] = right_index;
599
- }
600
-
601
- for (let left_index = 1; left_index <= left_text.length; left_index += 1) {
602
- for (
603
- let right_index = 1;
604
- right_index <= right_text.length;
605
- right_index += 1
606
- ) {
607
- const substitution_cost =
608
- left_text[left_index - 1] === right_text[right_index - 1] ? 0 : 1;
609
-
610
- matrix[left_index][right_index] = Math.min(
611
- matrix[left_index - 1][right_index] + 1,
612
- matrix[left_index][right_index - 1] + 1,
613
- matrix[left_index - 1][right_index - 1] + substitution_cost,
614
- );
615
-
616
- if (
617
- left_index > 1 &&
618
- right_index > 1 &&
619
- left_text[left_index - 1] === right_text[right_index - 2] &&
620
- left_text[left_index - 2] === right_text[right_index - 1]
621
- ) {
622
- matrix[left_index][right_index] = Math.min(
623
- matrix[left_index][right_index],
624
- matrix[left_index - 2][right_index - 2] + 1,
625
- );
626
- }
627
- }
628
- }
629
-
630
- return matrix[left_text.length][right_text.length];
631
- }