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.
- package/lib/cli/arguments.types.d.ts +64 -0
- package/lib/cli/commands/check.js +27 -15
- package/lib/cli/commands/queries.js +189 -1
- package/lib/cli/commands/query.js +6 -3
- package/lib/cli/help-metadata.js +45 -110
- package/lib/cli/parse-arguments-helpers.js +295 -39
- package/lib/cli/render-help.js +87 -0
- package/lib/config/load-patram-config.d.ts +11 -0
- package/lib/config/load-patram-config.js +9 -88
- package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
- package/lib/config/manage-stored-queries-helpers.js +262 -0
- package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
- package/lib/config/manage-stored-queries-jsonc.js +95 -0
- package/lib/config/manage-stored-queries.d.ts +77 -0
- package/lib/config/manage-stored-queries.js +294 -0
- package/lib/config/schema.d.ts +2 -0
- package/lib/config/schema.js +4 -0
- package/lib/config/validate-patram-config-value.d.ts +13 -0
- package/lib/config/validate-patram-config-value.js +119 -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 +9 -5
- package/lib/graph/query/resolve.js +41 -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/resolve-check-target.js +120 -11
- package/lib/output/view-model/index.js +5 -18
- package/lib/patram.d.ts +8 -0
- package/lib/scan/discover-fields.js +136 -10
- package/package.json +2 -1
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/patram.js",
|
|
6
6
|
"types": "./lib/patram.d.ts",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"ansis": "^4.2.0",
|
|
68
68
|
"beautiful-mermaid": "^1.1.3",
|
|
69
69
|
"globby": "^16.1.1",
|
|
70
|
+
"jsonc-parser": "^3.3.1",
|
|
70
71
|
"md4x": "^0.0.25",
|
|
71
72
|
"shiki": "^4.0.2",
|
|
72
73
|
"string-width": "^8.2.0",
|