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
@@ -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 = await listSourceFiles(
75
- DEFAULT_INCLUDE_PATTERNS,
76
- project_directory,
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) => !defined_field_names.has(field_suggestion.name),
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(field_observation.value, type_result.name) === 0,
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(observation.value);
253
+ current_values.add(normalized_value);
235
254
  } else {
236
- values.set(observation.document_id, new Set([observation.value]));
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.value, field_type_name),
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.9.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",