patram 0.9.0 → 0.10.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.
- package/lib/cli/arguments.types.d.ts +63 -0
- package/lib/cli/commands/query.js +6 -3
- package/lib/cli/help-metadata.js +2 -97
- package/lib/cli/parse-arguments-helpers.js +17 -5
- package/lib/cli/render-help.js +81 -0
- package/lib/config/schema.d.ts +2 -0
- package/lib/config/schema.js +4 -0
- package/lib/find-close-match.d.ts +8 -0
- package/lib/find-close-match.js +98 -0
- package/lib/graph/query/resolve.d.ts +3 -5
- package/lib/graph/query/resolve.js +38 -4
- package/lib/output/layout-stored-queries.js +18 -2
- package/lib/output/list-queries.js +2 -1
- package/lib/output/renderers/json.js +9 -5
- package/lib/output/renderers/plain.js +15 -26
- package/lib/output/renderers/rich.js +22 -26
- package/lib/output/view-model/index.js +5 -18
- package/lib/scan/discover-fields.js +136 -10
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
suggestion?: string;
|
|
51
|
+
} | {
|
|
52
|
+
code: 'unknown_option';
|
|
53
|
+
command_name?: CliCommandName;
|
|
54
|
+
suggestion?: string;
|
|
55
|
+
token: string;
|
|
56
|
+
};
|
|
57
|
+
export type ParseCliArgumentsResult = {
|
|
58
|
+
success: true;
|
|
59
|
+
value: ParsedCliRequest;
|
|
60
|
+
} | {
|
|
61
|
+
error: CliParseError;
|
|
62
|
+
success: false;
|
|
63
|
+
};
|
|
@@ -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 {
|
|
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(
|
|
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(
|
|
145
|
+
io_context.stderr.write(renderCliParseError(where_clause.error));
|
|
143
146
|
|
|
144
147
|
return 1;
|
|
145
148
|
}
|
package/lib/cli/help-metadata.js
CHANGED
|
@@ -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,
|
|
@@ -532,100 +534,3 @@ function listOptionLabels(command_name) {
|
|
|
532
534
|
|
|
533
535
|
return [...option_labels];
|
|
534
536
|
}
|
|
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
|
-
}
|
|
@@ -67,9 +67,7 @@ export function validateHelpCommandLine(command_line) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (command_line.positionals.length > 2) {
|
|
70
|
-
return
|
|
71
|
-
'Help accepts at most one topic or command.',
|
|
72
|
-
);
|
|
70
|
+
return createUnexpectedArgumentError('help', command_line.positionals[2]);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
return null;
|
|
@@ -422,8 +420,9 @@ function validateCommandPositionals(command_name, command_positionals) {
|
|
|
422
420
|
}
|
|
423
421
|
|
|
424
422
|
if (command_positionals.length > command_definition.max_positionals) {
|
|
425
|
-
return
|
|
426
|
-
|
|
423
|
+
return createUnexpectedArgumentError(
|
|
424
|
+
command_name,
|
|
425
|
+
command_positionals[command_definition.max_positionals],
|
|
427
426
|
);
|
|
428
427
|
}
|
|
429
428
|
|
|
@@ -451,3 +450,16 @@ function isKnownCommandOptionName(option_name) {
|
|
|
451
450
|
option_name === 'where'
|
|
452
451
|
);
|
|
453
452
|
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @param {'help' | CliCommandName} command_name
|
|
456
|
+
* @param {string | undefined} token
|
|
457
|
+
* @returns {CliParseError}
|
|
458
|
+
*/
|
|
459
|
+
function createUnexpectedArgumentError(command_name, token) {
|
|
460
|
+
return {
|
|
461
|
+
code: 'unexpected_argument',
|
|
462
|
+
command_name,
|
|
463
|
+
token: token ?? '',
|
|
464
|
+
};
|
|
465
|
+
}
|
package/lib/cli/render-help.js
CHANGED
|
@@ -75,6 +75,20 @@ export function renderCliParseError(parse_error) {
|
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
if (parse_error.code === 'unexpected_argument') {
|
|
79
|
+
return renderUnexpectedArgumentError(
|
|
80
|
+
parse_error.command_name,
|
|
81
|
+
parse_error.token,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (parse_error.code === 'unknown_stored_query') {
|
|
86
|
+
return renderUnknownStoredQueryError(
|
|
87
|
+
parse_error.name,
|
|
88
|
+
parse_error.suggestion,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
return `${parse_error.message}\n`;
|
|
79
93
|
}
|
|
80
94
|
|
|
@@ -326,6 +340,49 @@ function renderMissingRequiredArgumentError(command_name, argument_label) {
|
|
|
326
340
|
]);
|
|
327
341
|
}
|
|
328
342
|
|
|
343
|
+
/**
|
|
344
|
+
* @param {'help' | CliCommandName} command_name
|
|
345
|
+
* @param {string} invalid_token
|
|
346
|
+
* @returns {string}
|
|
347
|
+
*/
|
|
348
|
+
function renderUnexpectedArgumentError(command_name, invalid_token) {
|
|
349
|
+
return joinOutputLines([
|
|
350
|
+
`Unexpected argument: ${invalid_token}`,
|
|
351
|
+
'',
|
|
352
|
+
'Usage:',
|
|
353
|
+
...indentLines(getUnexpectedArgumentUsageLines(command_name)),
|
|
354
|
+
'',
|
|
355
|
+
'Next:',
|
|
356
|
+
` ${renderUnexpectedArgumentNext(command_name)}`,
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @param {string} stored_query_name
|
|
362
|
+
* @param {string | undefined} suggestion
|
|
363
|
+
* @returns {string}
|
|
364
|
+
*/
|
|
365
|
+
function renderUnknownStoredQueryError(stored_query_name, suggestion) {
|
|
366
|
+
if (suggestion) {
|
|
367
|
+
return joinOutputLines([
|
|
368
|
+
`Unknown stored query: ${stored_query_name}`,
|
|
369
|
+
'',
|
|
370
|
+
'Did you mean:',
|
|
371
|
+
` ${suggestion}`,
|
|
372
|
+
'',
|
|
373
|
+
'Next:',
|
|
374
|
+
` patram query ${suggestion}`,
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return joinOutputLines([
|
|
379
|
+
`Unknown stored query: ${stored_query_name}`,
|
|
380
|
+
'',
|
|
381
|
+
'Next:',
|
|
382
|
+
' patram queries',
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
|
|
329
386
|
/**
|
|
330
387
|
* @param {string} invalid_token
|
|
331
388
|
* @param {CliCommandName | CliHelpTopicName | undefined} suggestion
|
|
@@ -378,6 +435,30 @@ function renderCommandHelpPath(command_name) {
|
|
|
378
435
|
return `patram help ${command_name}`;
|
|
379
436
|
}
|
|
380
437
|
|
|
438
|
+
/**
|
|
439
|
+
* @param {'help' | CliCommandName} command_name
|
|
440
|
+
* @returns {string}
|
|
441
|
+
*/
|
|
442
|
+
function renderUnexpectedArgumentNext(command_name) {
|
|
443
|
+
if (command_name === 'help') {
|
|
444
|
+
return 'patram --help';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return renderCommandHelpPath(command_name);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @param {'help' | CliCommandName} command_name
|
|
452
|
+
* @returns {string[]}
|
|
453
|
+
*/
|
|
454
|
+
function getUnexpectedArgumentUsageLines(command_name) {
|
|
455
|
+
if (command_name === 'help') {
|
|
456
|
+
return ['patram help [command]'];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return getCommandDefinition(command_name).usage_lines;
|
|
460
|
+
}
|
|
461
|
+
|
|
381
462
|
/**
|
|
382
463
|
* @param {string[]} lines
|
|
383
464
|
* @returns {string[]}
|
package/lib/config/schema.d.ts
CHANGED
|
@@ -141,6 +141,7 @@ export const patram_repo_config_schema: z.ZodObject<{
|
|
|
141
141
|
prefixes: z.ZodArray<z.ZodString>;
|
|
142
142
|
}, z.core.$strict>>>;
|
|
143
143
|
queries: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
144
|
+
description: z.ZodOptional<z.ZodString>;
|
|
144
145
|
where: z.ZodString;
|
|
145
146
|
}, z.core.$strict>>>;
|
|
146
147
|
relations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -167,6 +168,7 @@ import { z } from 'zod';
|
|
|
167
168
|
* @typedef {z.output<typeof stored_query_schema>} StoredQueryConfig
|
|
168
169
|
*/
|
|
169
170
|
declare const stored_query_schema: z.ZodObject<{
|
|
171
|
+
description: z.ZodOptional<z.ZodString>;
|
|
170
172
|
where: z.ZodString;
|
|
171
173
|
}, z.core.$strict>;
|
|
172
174
|
/**
|
package/lib/config/schema.js
CHANGED
|
@@ -27,6 +27,10 @@ const MIXED_STYLE_VALUES = new Set(['ignore', 'error']);
|
|
|
27
27
|
*/
|
|
28
28
|
const stored_query_schema = z
|
|
29
29
|
.object({
|
|
30
|
+
description: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1, 'Stored query "description" must not be empty.')
|
|
33
|
+
.optional(),
|
|
30
34
|
where: z.string().min(1, 'Stored query "where" must not be empty.'),
|
|
31
35
|
})
|
|
32
36
|
.strict();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the closest candidate above the shared suggestion threshold.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} input_text
|
|
5
|
+
* @param {readonly string[]} candidates
|
|
6
|
+
* @returns {string | undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function findCloseMatch(input_text: string, candidates: readonly string[]): string | undefined;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the closest candidate above the shared suggestion threshold.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} input_text
|
|
5
|
+
* @param {readonly string[]} candidates
|
|
6
|
+
* @returns {string | undefined}
|
|
7
|
+
*/
|
|
8
|
+
export function findCloseMatch(input_text, candidates) {
|
|
9
|
+
let best_candidate;
|
|
10
|
+
let best_score = 0;
|
|
11
|
+
|
|
12
|
+
for (const candidate of candidates) {
|
|
13
|
+
const score = scoreCandidate(input_text, candidate);
|
|
14
|
+
|
|
15
|
+
if (score > best_score) {
|
|
16
|
+
best_candidate = candidate;
|
|
17
|
+
best_score = score;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (best_score < 0.6) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return best_candidate;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} input_text
|
|
30
|
+
* @param {string} candidate
|
|
31
|
+
* @returns {number}
|
|
32
|
+
*/
|
|
33
|
+
function scoreCandidate(input_text, candidate) {
|
|
34
|
+
const max_length = Math.max(input_text.length, candidate.length);
|
|
35
|
+
|
|
36
|
+
if (max_length === 0) {
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
1 - calculateDamerauLevenshteinDistance(input_text, candidate) / max_length
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} left_text
|
|
47
|
+
* @param {string} right_text
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
function calculateDamerauLevenshteinDistance(left_text, right_text) {
|
|
51
|
+
/** @type {number[][]} */
|
|
52
|
+
const matrix = Array.from({ length: left_text.length + 1 }, () =>
|
|
53
|
+
Array.from({ length: right_text.length + 1 }, () => 0),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
for (let left_index = 0; left_index <= left_text.length; left_index += 1) {
|
|
57
|
+
matrix[left_index][0] = left_index;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (
|
|
61
|
+
let right_index = 0;
|
|
62
|
+
right_index <= right_text.length;
|
|
63
|
+
right_index += 1
|
|
64
|
+
) {
|
|
65
|
+
matrix[0][right_index] = right_index;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let left_index = 1; left_index <= left_text.length; left_index += 1) {
|
|
69
|
+
for (
|
|
70
|
+
let right_index = 1;
|
|
71
|
+
right_index <= right_text.length;
|
|
72
|
+
right_index += 1
|
|
73
|
+
) {
|
|
74
|
+
const substitution_cost =
|
|
75
|
+
left_text[left_index - 1] === right_text[right_index - 1] ? 0 : 1;
|
|
76
|
+
|
|
77
|
+
matrix[left_index][right_index] = Math.min(
|
|
78
|
+
matrix[left_index - 1][right_index] + 1,
|
|
79
|
+
matrix[left_index][right_index - 1] + 1,
|
|
80
|
+
matrix[left_index - 1][right_index - 1] + substitution_cost,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
left_index > 1 &&
|
|
85
|
+
right_index > 1 &&
|
|
86
|
+
left_text[left_index - 1] === right_text[right_index - 2] &&
|
|
87
|
+
left_text[left_index - 2] === right_text[right_index - 1]
|
|
88
|
+
) {
|
|
89
|
+
matrix[left_index][right_index] = Math.min(
|
|
90
|
+
matrix[left_index][right_index],
|
|
91
|
+
matrix[left_index - 2][right_index - 2] + substitution_cost,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return matrix[left_text.length][right_text.length];
|
|
98
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
|
|
3
|
-
*/
|
|
4
1
|
/**
|
|
5
2
|
* @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
|
|
6
3
|
*/
|
|
@@ -9,7 +6,7 @@
|
|
|
9
6
|
*
|
|
10
7
|
* @param {PatramRepoConfig} repo_config
|
|
11
8
|
* @param {string[]} command_arguments
|
|
12
|
-
* @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | {
|
|
9
|
+
* @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false }}
|
|
13
10
|
*/
|
|
14
11
|
export function resolveWhereClause(repo_config: PatramRepoConfig, command_arguments: string[]): {
|
|
15
12
|
success: true;
|
|
@@ -18,8 +15,8 @@ export function resolveWhereClause(repo_config: PatramRepoConfig, command_argume
|
|
|
18
15
|
where_clause: string;
|
|
19
16
|
};
|
|
20
17
|
} | {
|
|
18
|
+
error: CliParseError;
|
|
21
19
|
success: false;
|
|
22
|
-
message: string;
|
|
23
20
|
};
|
|
24
21
|
export type QuerySource = {
|
|
25
22
|
kind: "ad_hoc";
|
|
@@ -28,3 +25,4 @@ export type QuerySource = {
|
|
|
28
25
|
name: string;
|
|
29
26
|
};
|
|
30
27
|
import type { PatramRepoConfig } from '../../config/load-patram-config.types.d.ts';
|
|
28
|
+
import type { CliParseError } from '../../cli/arguments.types.d.ts';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @import { CliParseError } from '../../cli/arguments.types.ts';
|
|
2
3
|
* @import { PatramRepoConfig } from '../../config/load-patram-config.types.ts';
|
|
3
4
|
*/
|
|
4
5
|
|
|
6
|
+
import { findCloseMatch } from '../../find-close-match.js';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
|
|
7
10
|
*/
|
|
@@ -11,7 +14,7 @@
|
|
|
11
14
|
*
|
|
12
15
|
* @param {PatramRepoConfig} repo_config
|
|
13
16
|
* @param {string[]} command_arguments
|
|
14
|
-
* @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | {
|
|
17
|
+
* @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false }}
|
|
15
18
|
*/
|
|
16
19
|
export function resolveWhereClause(repo_config, command_arguments) {
|
|
17
20
|
if (command_arguments[0] === '--where') {
|
|
@@ -19,7 +22,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
|
|
|
19
22
|
|
|
20
23
|
if (where_clause.length === 0) {
|
|
21
24
|
return {
|
|
22
|
-
|
|
25
|
+
error: {
|
|
26
|
+
code: 'message',
|
|
27
|
+
message: 'Query requires a where clause.',
|
|
28
|
+
},
|
|
23
29
|
success: false,
|
|
24
30
|
};
|
|
25
31
|
}
|
|
@@ -39,7 +45,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
|
|
|
39
45
|
|
|
40
46
|
if (!stored_query_name) {
|
|
41
47
|
return {
|
|
42
|
-
|
|
48
|
+
error: {
|
|
49
|
+
code: 'message',
|
|
50
|
+
message: 'Query requires "--where" or a stored query name.',
|
|
51
|
+
},
|
|
43
52
|
success: false,
|
|
44
53
|
};
|
|
45
54
|
}
|
|
@@ -48,7 +57,10 @@ export function resolveWhereClause(repo_config, command_arguments) {
|
|
|
48
57
|
|
|
49
58
|
if (!stored_query) {
|
|
50
59
|
return {
|
|
51
|
-
|
|
60
|
+
error: createUnknownStoredQueryError(
|
|
61
|
+
stored_query_name,
|
|
62
|
+
Object.keys(repo_config.queries),
|
|
63
|
+
),
|
|
52
64
|
success: false,
|
|
53
65
|
};
|
|
54
66
|
}
|
|
@@ -64,3 +76,25 @@ export function resolveWhereClause(repo_config, command_arguments) {
|
|
|
64
76
|
},
|
|
65
77
|
};
|
|
66
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} stored_query_name
|
|
82
|
+
* @param {string[]} stored_query_names
|
|
83
|
+
* @returns {CliParseError}
|
|
84
|
+
*/
|
|
85
|
+
function createUnknownStoredQueryError(stored_query_name, stored_query_names) {
|
|
86
|
+
const suggestion = findCloseMatch(stored_query_name, stored_query_names);
|
|
87
|
+
|
|
88
|
+
if (!suggestion) {
|
|
89
|
+
return {
|
|
90
|
+
code: 'unknown_stored_query',
|
|
91
|
+
name: stored_query_name,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
code: 'unknown_stored_query',
|
|
97
|
+
name: stored_query_name,
|
|
98
|
+
suggestion,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -10,7 +10,7 @@ const MIN_TERM_COLUMN_WIDTH = 20;
|
|
|
10
10
|
const STORED_QUERY_COLUMN_GAP = 2;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @typedef {'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain'} StoredQuerySegmentKind
|
|
13
|
+
* @typedef {'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain'} StoredQuerySegmentKind
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -56,7 +56,8 @@ function layoutStoredQuery(output_item, name_column_width, term_column_width) {
|
|
|
56
56
|
name_column_width + STORED_QUERY_COLUMN_GAP,
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/** @type {StoredQuerySegment[][]} */
|
|
60
|
+
const output_lines = term_lines.map((line_segments, line_index) => {
|
|
60
61
|
if (line_index === 0) {
|
|
61
62
|
return [
|
|
62
63
|
{
|
|
@@ -79,6 +80,21 @@ function layoutStoredQuery(output_item, name_column_width, term_column_width) {
|
|
|
79
80
|
...line_segments,
|
|
80
81
|
];
|
|
81
82
|
});
|
|
83
|
+
|
|
84
|
+
if (output_item.description) {
|
|
85
|
+
for (const description_line of output_item.description.split('\n')) {
|
|
86
|
+
output_lines.push([
|
|
87
|
+
{
|
|
88
|
+
kind: 'description',
|
|
89
|
+
text: `${' '.repeat(name_column_width + STORED_QUERY_COLUMN_GAP)}${description_line}`,
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
output_lines.push([]);
|
|
96
|
+
|
|
97
|
+
return output_lines;
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
/**
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* List stored queries in stable name order.
|
|
7
7
|
*
|
|
8
8
|
* @param {Record<string, StoredQueryConfig>} stored_queries
|
|
9
|
-
* @returns {{ name: string, where: string }[]}
|
|
9
|
+
* @returns {{ name: string, where: string, description?: string }[]}
|
|
10
10
|
*/
|
|
11
11
|
export function listQueries(stored_queries) {
|
|
12
12
|
return Object.entries(stored_queries)
|
|
13
13
|
.sort(([left_name], [right_name]) => left_name.localeCompare(right_name))
|
|
14
14
|
.map(([name, stored_query]) => ({
|
|
15
|
+
description: stored_query.description,
|
|
15
16
|
name,
|
|
16
17
|
where: stored_query.where,
|
|
17
18
|
}));
|
|
@@ -92,9 +92,6 @@ function renderJsonRefsOutput(output_view) {
|
|
|
92
92
|
function renderJsonShowOutput(output_view) {
|
|
93
93
|
return `${JSON.stringify(
|
|
94
94
|
{
|
|
95
|
-
document: output_view.document
|
|
96
|
-
? formatJsonNodeItem(output_view.document)
|
|
97
|
-
: undefined,
|
|
98
95
|
incoming_summary: output_view.incoming_summary,
|
|
99
96
|
source: output_view.source,
|
|
100
97
|
resolved_links: output_view.items.map(formatJsonResolvedLink),
|
|
@@ -144,13 +141,20 @@ function formatJsonNodeItem(output_item) {
|
|
|
144
141
|
|
|
145
142
|
/**
|
|
146
143
|
* @param {OutputStoredQueryItem} output_item
|
|
147
|
-
* @returns {{ name: string, where: string }}
|
|
144
|
+
* @returns {{ name: string, where: string, description?: string }}
|
|
148
145
|
*/
|
|
149
146
|
function formatJsonStoredQuery(output_item) {
|
|
150
|
-
|
|
147
|
+
/** @type {{ description?: string, name: string, where: string }} */
|
|
148
|
+
const stored_query = {
|
|
151
149
|
name: output_item.name,
|
|
152
150
|
where: output_item.where,
|
|
153
151
|
};
|
|
152
|
+
|
|
153
|
+
if (output_item.description) {
|
|
154
|
+
stored_query.description = output_item.description;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return stored_query;
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
/**
|
|
@@ -24,7 +24,7 @@ export function renderPlainOutput(output_view) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (output_view.command === 'queries') {
|
|
27
|
-
return renderPlainStoredQueries(output_view
|
|
27
|
+
return renderPlainStoredQueries(output_view);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (output_view.command === 'refs') {
|
|
@@ -69,17 +69,23 @@ function renderPlainEmptyQuery(footer) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* @param {
|
|
72
|
+
* @param {Extract<OutputView, { command: 'queries' }>} output_view
|
|
73
73
|
* @returns {string}
|
|
74
74
|
*/
|
|
75
|
-
function renderPlainStoredQueries(
|
|
76
|
-
if (
|
|
75
|
+
function renderPlainStoredQueries(output_view) {
|
|
76
|
+
if (output_view.items.length === 0) {
|
|
77
77
|
return '';
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
const output_lines = layoutStoredQueries(output_view.items).map(
|
|
81
|
+
formatPlainStoredQueryLine,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (output_view.hints.length === 0) {
|
|
85
|
+
return `${output_lines.join('\n')}\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `${output_lines.join('\n')}\n${output_view.hints.join('\n')}\n`;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
/**
|
|
@@ -88,29 +94,12 @@ function renderPlainStoredQueries(output_items) {
|
|
|
88
94
|
*/
|
|
89
95
|
function renderPlainShowOutput(output_view) {
|
|
90
96
|
const rendered_source = trimTrailingLineBreaks(output_view.rendered_source);
|
|
91
|
-
const document_summary = output_view.document
|
|
92
|
-
? formatPlainNodeItem(output_view.document)
|
|
93
|
-
: '';
|
|
94
97
|
const incoming_summary = renderPlainIncomingSummary(output_view);
|
|
95
98
|
|
|
96
|
-
if (
|
|
97
|
-
document_summary.length === 0 &&
|
|
98
|
-
output_view.items.length === 0 &&
|
|
99
|
-
incoming_summary.length === 0
|
|
100
|
-
) {
|
|
99
|
+
if (output_view.items.length === 0 && incoming_summary.length === 0) {
|
|
101
100
|
return `${rendered_source}\n`;
|
|
102
101
|
}
|
|
103
|
-
|
|
104
|
-
/** @type {string[]} */
|
|
105
|
-
const summary_items = [];
|
|
106
|
-
|
|
107
|
-
if (document_summary.length > 0) {
|
|
108
|
-
summary_items.push(document_summary);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
summary_items.push(...output_view.items.map(formatPlainResolvedLinkItem));
|
|
112
|
-
|
|
113
|
-
const summary_blocks = summary_items.filter((summary_item) => summary_item);
|
|
102
|
+
const summary_blocks = output_view.items.map(formatPlainResolvedLinkItem);
|
|
114
103
|
|
|
115
104
|
if (incoming_summary.length > 0) {
|
|
116
105
|
summary_blocks.push(incoming_summary);
|
|
@@ -33,7 +33,7 @@ export async function renderRichOutput(output_view, render_options) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (output_view.command === 'queries') {
|
|
36
|
-
return renderRichStoredQueries(output_view
|
|
36
|
+
return renderRichStoredQueries(output_view, ansi);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (output_view.command === 'refs') {
|
|
@@ -83,18 +83,24 @@ function renderRichEmptyQuery(footer, ansi) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* @param {
|
|
86
|
+
* @param {Extract<OutputView, { command: 'queries' }>} output_view
|
|
87
87
|
* @param {Ansis} ansi
|
|
88
88
|
* @returns {string}
|
|
89
89
|
*/
|
|
90
|
-
function renderRichStoredQueries(
|
|
91
|
-
if (
|
|
90
|
+
function renderRichStoredQueries(output_view, ansi) {
|
|
91
|
+
if (output_view.items.length === 0) {
|
|
92
92
|
return '';
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
const output_lines = layoutStoredQueries(output_view.items).map(
|
|
96
|
+
(line_segments) => formatRichStoredQueryLine(line_segments, ansi),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (output_view.hints.length === 0) {
|
|
100
|
+
return `${output_lines.join('\n')}\n`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return `${output_lines.join('\n')}\n${output_view.hints.map((hint) => ansi.gray(hint)).join('\n')}\n`;
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
/**
|
|
@@ -107,28 +113,14 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
|
|
|
107
113
|
const rendered_source = trimTrailingLineBreaks(
|
|
108
114
|
await renderRichSource(output_view, render_options),
|
|
109
115
|
);
|
|
110
|
-
const document_summary = output_view.document
|
|
111
|
-
? formatRichNodeItem(output_view.document, ansi)
|
|
112
|
-
: '';
|
|
113
116
|
const incoming_summary = renderRichIncomingSummary(output_view, ansi);
|
|
114
117
|
|
|
115
|
-
if (
|
|
116
|
-
document_summary.length === 0 &&
|
|
117
|
-
output_view.items.length === 0 &&
|
|
118
|
-
incoming_summary.length === 0
|
|
119
|
-
) {
|
|
118
|
+
if (output_view.items.length === 0 && incoming_summary.length === 0) {
|
|
120
119
|
return `${rendered_source}\n`;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (document_summary.length > 0) {
|
|
127
|
-
summary_items.push(document_summary);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
summary_items.push(
|
|
131
|
-
...output_view.items.map((item) => formatRichResolvedLinkItem(item, ansi)),
|
|
122
|
+
const summary_items = output_view.items.map((item) =>
|
|
123
|
+
formatRichResolvedLinkItem(item, ansi),
|
|
132
124
|
);
|
|
133
125
|
|
|
134
126
|
if (incoming_summary.length > 0) {
|
|
@@ -173,7 +165,7 @@ function formatRichNodeItem(output_item, ansi) {
|
|
|
173
165
|
}
|
|
174
166
|
|
|
175
167
|
/**
|
|
176
|
-
* @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }[]} line_segments
|
|
168
|
+
* @param {{ kind: 'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }[]} line_segments
|
|
177
169
|
* @param {Ansis} ansi
|
|
178
170
|
* @returns {string}
|
|
179
171
|
*/
|
|
@@ -232,7 +224,7 @@ function createAnsi(color_enabled) {
|
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
/**
|
|
235
|
-
* @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
|
|
227
|
+
* @param {{ kind: 'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
|
|
236
228
|
* @param {Ansis} ansi
|
|
237
229
|
* @returns {string}
|
|
238
230
|
*/
|
|
@@ -241,6 +233,10 @@ function styleStoredQuerySegment(line_segment, ansi) {
|
|
|
241
233
|
return ansi.green(line_segment.text);
|
|
242
234
|
}
|
|
243
235
|
|
|
236
|
+
if (line_segment.kind === 'description') {
|
|
237
|
+
return ansi.gray(line_segment.text);
|
|
238
|
+
}
|
|
239
|
+
|
|
244
240
|
if (line_segment.kind === 'operator') {
|
|
245
241
|
return ansi.gray(line_segment.text);
|
|
246
242
|
}
|
|
@@ -12,7 +12,7 @@ import { resolveDocumentNodeId } from '../../graph/build-graph-identity.js';
|
|
|
12
12
|
* Create a shared output view from one command result.
|
|
13
13
|
*
|
|
14
14
|
* @param {'query' | 'queries'} command_name
|
|
15
|
-
* @param {GraphNode[] | { name: string, where: string }[]} command_items
|
|
15
|
+
* @param {GraphNode[] | { name: string, where: string, description?: string }[]} command_items
|
|
16
16
|
* @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
|
|
17
17
|
* @returns {OutputView}
|
|
18
18
|
*/
|
|
@@ -41,23 +41,8 @@ export function createOutputView(command_name, command_items, command_options) {
|
|
|
41
41
|
* @returns {ShowOutputView}
|
|
42
42
|
*/
|
|
43
43
|
export function createShowOutputView(show_output, command_options = {}) {
|
|
44
|
-
const shown_document_node = resolveDocumentGraphNode(
|
|
45
|
-
command_options.graph_nodes,
|
|
46
|
-
command_options.document_node_ids,
|
|
47
|
-
show_output.path,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
44
|
return {
|
|
51
45
|
command: 'show',
|
|
52
|
-
document: shown_document_node
|
|
53
|
-
? createOutputNodeItem(
|
|
54
|
-
shown_document_node,
|
|
55
|
-
command_options.derived_summary_evaluator?.evaluate(
|
|
56
|
-
shown_document_node,
|
|
57
|
-
) ?? null,
|
|
58
|
-
command_options.repo_config?.fields ?? {},
|
|
59
|
-
)
|
|
60
|
-
: undefined,
|
|
61
46
|
hints: [],
|
|
62
47
|
incoming_summary: show_output.incoming_summary,
|
|
63
48
|
items: show_output.resolved_links.map((resolved_link) =>
|
|
@@ -144,17 +129,19 @@ function createQueryOutputView(graph_nodes, command_options = {}) {
|
|
|
144
129
|
}
|
|
145
130
|
|
|
146
131
|
/**
|
|
147
|
-
* @param {{ name: string, where: string }[]} stored_queries
|
|
132
|
+
* @param {{ name: string, where: string, description?: string }[]} stored_queries
|
|
148
133
|
* @returns {OutputView}
|
|
149
134
|
*/
|
|
150
135
|
function createStoredQueriesOutputView(stored_queries) {
|
|
151
136
|
return {
|
|
152
137
|
command: 'queries',
|
|
153
|
-
hints:
|
|
138
|
+
hints:
|
|
139
|
+
stored_queries.length === 0 ? [] : ['Hint: patram help query-language'],
|
|
154
140
|
items: stored_queries.map((stored_query) => ({
|
|
155
141
|
kind: 'stored_query',
|
|
156
142
|
name: stored_query.name,
|
|
157
143
|
where: stored_query.where,
|
|
144
|
+
description: stored_query.description,
|
|
158
145
|
})),
|
|
159
146
|
summary: {
|
|
160
147
|
count: stored_queries.length,
|
|
@@ -20,6 +20,11 @@ import { resolve } from 'node:path';
|
|
|
20
20
|
import { DEFAULT_INCLUDE_PATTERNS } from '../config/source-file-defaults.js';
|
|
21
21
|
import { listSourceFiles } from './list-source-files.js';
|
|
22
22
|
import { parseSourceFile } from '../parse/parse-claims.js';
|
|
23
|
+
import {
|
|
24
|
+
matchHiddenDirectiveFields,
|
|
25
|
+
matchVisibleDirectiveFields,
|
|
26
|
+
} from '../parse/markdown/parse-markdown-directives.js';
|
|
27
|
+
import { isPathLikeTarget } from '../parse/claim-helpers.js';
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Field discovery from source claims.
|
|
@@ -71,10 +76,9 @@ export async function discoverFields(
|
|
|
71
76
|
options,
|
|
72
77
|
) {
|
|
73
78
|
const defined_field_names = options?.defined_field_names ?? new Set();
|
|
74
|
-
const source_file_paths =
|
|
75
|
-
DEFAULT_INCLUDE_PATTERNS,
|
|
76
|
-
|
|
77
|
-
);
|
|
79
|
+
const source_file_paths = (
|
|
80
|
+
await listSourceFiles(DEFAULT_INCLUDE_PATTERNS, project_directory)
|
|
81
|
+
).filter((source_file_path) => source_file_path.includes('/'));
|
|
78
82
|
const parse_results = await Promise.all(
|
|
79
83
|
source_file_paths.map(async (source_file_path) => {
|
|
80
84
|
const source_text = await readFile(
|
|
@@ -88,6 +92,7 @@ export async function discoverFields(
|
|
|
88
92
|
source: source_text,
|
|
89
93
|
}).claims,
|
|
90
94
|
path: source_file_path,
|
|
95
|
+
source_text,
|
|
91
96
|
};
|
|
92
97
|
}),
|
|
93
98
|
);
|
|
@@ -107,13 +112,20 @@ export async function discoverFields(
|
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
115
|
+
const allowed_markdown_lines = collectAllowedMarkdownDirectiveLines(
|
|
116
|
+
parse_result.path,
|
|
117
|
+
parse_result.source_text,
|
|
118
|
+
parse_result.claims,
|
|
119
|
+
);
|
|
120
|
+
|
|
110
121
|
return parse_result.claims.flatMap((claim) => {
|
|
111
122
|
if (
|
|
112
123
|
claim.type !== 'directive' ||
|
|
113
124
|
!claim.name ||
|
|
114
125
|
claim.name.startsWith('$') ||
|
|
115
126
|
typeof claim.value !== 'string' ||
|
|
116
|
-
claim.value.length === 0
|
|
127
|
+
claim.value.length === 0 ||
|
|
128
|
+
!shouldIncludeDiscoveryClaim(claim, allowed_markdown_lines)
|
|
117
129
|
) {
|
|
118
130
|
return [];
|
|
119
131
|
}
|
|
@@ -123,6 +135,7 @@ export async function discoverFields(
|
|
|
123
135
|
class_names: new Set(document_classes),
|
|
124
136
|
document_id: claim.document_id,
|
|
125
137
|
name: claim.name,
|
|
138
|
+
normalized_value: normalizeDiscoveryValue(claim.value),
|
|
126
139
|
origin: claim.origin,
|
|
127
140
|
value: claim.value,
|
|
128
141
|
},
|
|
@@ -147,7 +160,9 @@ export async function discoverFields(
|
|
|
147
160
|
const fields = [...field_buckets.values()]
|
|
148
161
|
.map(buildFieldSuggestion)
|
|
149
162
|
.filter(
|
|
150
|
-
(field_suggestion) =>
|
|
163
|
+
(field_suggestion) =>
|
|
164
|
+
!defined_field_names.has(field_suggestion.name) &&
|
|
165
|
+
isPlausibleFieldName(field_suggestion.name),
|
|
151
166
|
)
|
|
152
167
|
.sort((left_suggestion, right_suggestion) =>
|
|
153
168
|
left_suggestion.confidence !== right_suggestion.confidence
|
|
@@ -182,7 +197,10 @@ function buildFieldSuggestion(field_bucket) {
|
|
|
182
197
|
const conflicting_evidence = buildEvidenceReferences(
|
|
183
198
|
field_bucket.observations.filter(
|
|
184
199
|
(field_observation) =>
|
|
185
|
-
scoreFieldValue(
|
|
200
|
+
scoreFieldValue(
|
|
201
|
+
field_observation.normalized_value,
|
|
202
|
+
type_result.name,
|
|
203
|
+
) === 0,
|
|
186
204
|
),
|
|
187
205
|
);
|
|
188
206
|
|
|
@@ -228,12 +246,13 @@ function buildEvidenceReferences(observations) {
|
|
|
228
246
|
function inferFieldMultiplicity(observations) {
|
|
229
247
|
/** @type {Map<string, Set<string>>} */
|
|
230
248
|
const values_by_document = observations.reduce((values, observation) => {
|
|
249
|
+
const normalized_value = observation.normalized_value;
|
|
231
250
|
const current_values = values.get(observation.document_id);
|
|
232
251
|
|
|
233
252
|
if (current_values) {
|
|
234
|
-
current_values.add(
|
|
253
|
+
current_values.add(normalized_value);
|
|
235
254
|
} else {
|
|
236
|
-
values.set(observation.document_id, new Set([
|
|
255
|
+
values.set(observation.document_id, new Set([normalized_value]));
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
return values;
|
|
@@ -369,7 +388,7 @@ function scoreFieldType(observations, field_type_name) {
|
|
|
369
388
|
|
|
370
389
|
const total_score = observations.reduce(
|
|
371
390
|
(sum, observation) =>
|
|
372
|
-
sum + scoreFieldValue(observation.
|
|
391
|
+
sum + scoreFieldValue(observation.normalized_value, field_type_name),
|
|
373
392
|
0,
|
|
374
393
|
);
|
|
375
394
|
|
|
@@ -423,6 +442,7 @@ const FIELD_TYPE_SCORERS = {
|
|
|
423
442
|
* class_names: Set<string>,
|
|
424
443
|
* document_id: string,
|
|
425
444
|
* name: string,
|
|
445
|
+
* normalized_value: string,
|
|
426
446
|
* origin: ClaimOrigin,
|
|
427
447
|
* value: string,
|
|
428
448
|
* }} FieldObservation
|
|
@@ -434,3 +454,109 @@ const FIELD_TYPE_SCORERS = {
|
|
|
434
454
|
* observations: FieldObservation[],
|
|
435
455
|
* }} FieldBucket
|
|
436
456
|
*/
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @param {PatramClaim} claim
|
|
460
|
+
* @param {Set<number> | null} allowed_markdown_lines
|
|
461
|
+
* @returns {boolean}
|
|
462
|
+
*/
|
|
463
|
+
function shouldIncludeDiscoveryClaim(claim, allowed_markdown_lines) {
|
|
464
|
+
if (claim.parser !== 'markdown') {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (claim.markdown_style === 'front_matter') {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return allowed_markdown_lines?.has(claim.origin.line) ?? false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @param {string} file_path
|
|
477
|
+
* @param {string} source_text
|
|
478
|
+
* @param {PatramClaim[]} claims
|
|
479
|
+
* @returns {Set<number> | null}
|
|
480
|
+
*/
|
|
481
|
+
function collectAllowedMarkdownDirectiveLines(file_path, source_text, claims) {
|
|
482
|
+
if (!file_path.endsWith('.md')) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const title_claim = claims.find((claim) => claim.type === 'document.title');
|
|
487
|
+
|
|
488
|
+
if (!title_claim) {
|
|
489
|
+
return new Set();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const lines = source_text.split('\n');
|
|
493
|
+
/** @type {Set<number>} */
|
|
494
|
+
const allowed_lines = new Set();
|
|
495
|
+
|
|
496
|
+
for (
|
|
497
|
+
let line_index = title_claim.origin.line;
|
|
498
|
+
line_index < lines.length;
|
|
499
|
+
line_index += 1
|
|
500
|
+
) {
|
|
501
|
+
const line = lines[line_index];
|
|
502
|
+
|
|
503
|
+
if (line.trim().length === 0) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (isMarkdownDiscoveryDirective(file_path, line, line_index + 1)) {
|
|
508
|
+
allowed_lines.add(line_index + 1);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return allowed_lines;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* @param {string} file_path
|
|
520
|
+
* @param {string} line
|
|
521
|
+
* @param {number} line_number
|
|
522
|
+
* @returns {boolean}
|
|
523
|
+
*/
|
|
524
|
+
function isMarkdownDiscoveryDirective(file_path, line, line_number) {
|
|
525
|
+
return (
|
|
526
|
+
matchVisibleDirectiveFields(file_path, line, line_number) !== null ||
|
|
527
|
+
matchHiddenDirectiveFields(file_path, line, line_number) !== null
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* @param {string} value
|
|
533
|
+
* @returns {string}
|
|
534
|
+
*/
|
|
535
|
+
function normalizeDiscoveryValue(value) {
|
|
536
|
+
const trimmed_value = value.trim();
|
|
537
|
+
const markdown_link_match = trimmed_value.match(
|
|
538
|
+
/^\[([^\]]+)\]\(([^)]+)\)$/du,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
if (markdown_link_match && isPathLikeTarget(markdown_link_match[2])) {
|
|
542
|
+
return markdown_link_match[2];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const code_span_match = trimmed_value.match(/^`([^`]+)`[.,;:]?$/du);
|
|
546
|
+
|
|
547
|
+
if (code_span_match) {
|
|
548
|
+
return code_span_match[1];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return trimmed_value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* @param {string} field_name
|
|
556
|
+
* @returns {boolean}
|
|
557
|
+
*/
|
|
558
|
+
function isPlausibleFieldName(field_name) {
|
|
559
|
+
const field_name_tokens = field_name.split('_');
|
|
560
|
+
|
|
561
|
+
return field_name.length <= 32 && field_name_tokens.length <= 4;
|
|
562
|
+
}
|