patram 0.1.1 → 0.3.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 (49) hide show
  1. package/lib/build-graph-identity.js +57 -24
  2. package/lib/build-graph.js +383 -17
  3. package/lib/build-graph.types.ts +5 -2
  4. package/lib/check-directive-metadata.js +516 -0
  5. package/lib/check-directive-value.js +282 -0
  6. package/lib/check-graph.js +24 -5
  7. package/lib/cli-help-metadata.js +580 -0
  8. package/lib/derived-summary.js +280 -0
  9. package/lib/directive-diagnostics.js +38 -0
  10. package/lib/directive-type-rules.js +133 -0
  11. package/lib/discover-fields.js +427 -0
  12. package/lib/discover-fields.types.ts +52 -0
  13. package/lib/format-derived-summary-row.js +9 -0
  14. package/lib/format-node-header.js +21 -0
  15. package/lib/format-output-item-block.js +22 -0
  16. package/lib/format-output-metadata.js +54 -0
  17. package/lib/layout-stored-queries.js +96 -2
  18. package/lib/load-patram-config.js +754 -18
  19. package/lib/load-patram-config.types.ts +128 -2
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +29 -6
  22. package/lib/parse-cli-arguments-helpers.js +263 -90
  23. package/lib/parse-cli-arguments.js +160 -8
  24. package/lib/parse-cli-arguments.types.ts +49 -4
  25. package/lib/parse-where-clause.js +670 -209
  26. package/lib/parse-where-clause.types.ts +72 -0
  27. package/lib/patram-cli.js +180 -21
  28. package/lib/patram-config.js +31 -31
  29. package/lib/patram-config.types.ts +10 -4
  30. package/lib/patram.js +6 -0
  31. package/lib/query-graph.js +444 -113
  32. package/lib/query-inspection.js +798 -0
  33. package/lib/render-check-output.js +1 -1
  34. package/lib/render-cli-help.js +419 -0
  35. package/lib/render-field-discovery.js +148 -0
  36. package/lib/render-json-output.js +66 -14
  37. package/lib/render-output-view.js +272 -22
  38. package/lib/render-plain-output.js +31 -86
  39. package/lib/render-rich-output.js +34 -87
  40. package/lib/resolve-patram-graph-config.js +15 -9
  41. package/lib/resolve-where-clause.js +18 -3
  42. package/lib/show-document.js +51 -7
  43. package/lib/tagged-fenced-block-error.js +17 -0
  44. package/lib/tagged-fenced-block-markdown.js +111 -0
  45. package/lib/tagged-fenced-block-metadata.js +97 -0
  46. package/lib/tagged-fenced-block-parser.js +292 -0
  47. package/lib/tagged-fenced-blocks.js +100 -0
  48. package/lib/tagged-fenced-blocks.types.ts +38 -0
  49. package/package.json +12 -7
@@ -0,0 +1,580 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import {
4
+ * CliCommandName,
5
+ * CliHelpTopicName,
6
+ * } from './parse-cli-arguments.types.ts';
7
+ */
8
+
9
+ /**
10
+ * @typedef {{
11
+ * description: string,
12
+ * label: string,
13
+ * }} CliHelpOption
14
+ */
15
+
16
+ /**
17
+ * @typedef {{
18
+ * allowed_option_names: Set<string>,
19
+ * examples: string[],
20
+ * extra_positionals_message: string,
21
+ * help_topics: CliHelpTopicName[],
22
+ * max_positionals: number,
23
+ * min_positionals: number,
24
+ * missing_argument_examples: string[],
25
+ * missing_argument_label: string | null,
26
+ * missing_usage_lines: string[],
27
+ * option_column_width: number,
28
+ * options: CliHelpOption[],
29
+ * related: CliCommandName[],
30
+ * root_summary: string,
31
+ * summary: string,
32
+ * syntax_lines?: string[],
33
+ * usage_lines: string[],
34
+ * }} CliCommandDefinition
35
+ */
36
+
37
+ /**
38
+ * @typedef {{
39
+ * examples: string[],
40
+ * lead: string,
41
+ * operators: CliHelpOption[],
42
+ * relation_terms: CliHelpOption[],
43
+ * terms: string[],
44
+ * usage_lines: string[],
45
+ * }} CliHelpTopicDefinition
46
+ */
47
+
48
+ export const COMMAND_NAMES = /** @type {const} */ ([
49
+ 'check',
50
+ 'fields',
51
+ 'query',
52
+ 'queries',
53
+ 'show',
54
+ ]);
55
+
56
+ export const HELP_TOPIC_NAMES = /** @type {const} */ (['query-language']);
57
+
58
+ export const GLOBAL_OPTION_NAMES = new Set([
59
+ 'help',
60
+ 'plain',
61
+ 'json',
62
+ 'color',
63
+ 'no-color',
64
+ ]);
65
+
66
+ const ROOT_HELP_SUMMARY = 'Patram explores docs and how they link to sources.';
67
+ const ROOT_HELP_USAGE_LINES = [
68
+ 'patram <command> [options]',
69
+ 'patram help [command]',
70
+ ];
71
+ const ROOT_HELP_GLOBAL_OPTIONS = [
72
+ '--help',
73
+ '--plain',
74
+ '--json',
75
+ '--color <auto|always|never>',
76
+ '--no-color',
77
+ ];
78
+
79
+ /** @type {Record<CliCommandName, CliCommandDefinition>} */
80
+ const COMMAND_DEFINITIONS = {
81
+ check: {
82
+ allowed_option_names: new Set(),
83
+ examples: [
84
+ 'patram check',
85
+ 'patram check docs',
86
+ 'patram check docs/patram.md',
87
+ ],
88
+ extra_positionals_message: 'Check accepts at most one path.',
89
+ help_topics: [],
90
+ max_positionals: 1,
91
+ min_positionals: 0,
92
+ missing_argument_examples: [],
93
+ missing_argument_label: null,
94
+ missing_usage_lines: ['patram check [path]'],
95
+ option_column_width: 10,
96
+ options: [
97
+ {
98
+ description: 'Print plain text output',
99
+ label: '--plain',
100
+ },
101
+ {
102
+ description: 'Print JSON output',
103
+ label: '--json',
104
+ },
105
+ ],
106
+ related: ['show', 'query'],
107
+ root_summary: 'Validate a project, directory, or file',
108
+ summary:
109
+ 'Validate a project, directory, or file and report graph diagnostics.',
110
+ usage_lines: ['patram check [path] [options]'],
111
+ },
112
+ fields: {
113
+ allowed_option_names: new Set(),
114
+ examples: ['patram fields', 'patram fields --json'],
115
+ extra_positionals_message: 'Fields does not accept positional arguments.',
116
+ help_topics: [],
117
+ max_positionals: 0,
118
+ min_positionals: 0,
119
+ missing_argument_examples: [],
120
+ missing_argument_label: null,
121
+ missing_usage_lines: ['patram fields'],
122
+ option_column_width: 10,
123
+ options: [
124
+ {
125
+ description: 'Print plain text output',
126
+ label: '--plain',
127
+ },
128
+ {
129
+ description: 'Print JSON output',
130
+ label: '--json',
131
+ },
132
+ ],
133
+ related: ['query', 'check'],
134
+ root_summary: 'Discover likely field schema from source claims',
135
+ summary:
136
+ 'Discover likely metadata fields, multiplicity, and class usage from source claims.',
137
+ usage_lines: ['patram fields [options]'],
138
+ },
139
+ query: {
140
+ allowed_option_names: new Set([
141
+ 'explain',
142
+ 'limit',
143
+ 'lint',
144
+ 'offset',
145
+ 'where',
146
+ ]),
147
+ examples: [
148
+ 'patram query active-plans',
149
+ 'patram query --where "tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md"',
150
+ 'patram query --where "status not in [done, dropped, superseded]"',
151
+ 'patram query --where "$class=plan and none(in:tracked_in, $class=decision)"',
152
+ 'patram query --where "count(in:decided_by, $class=task) = 0"',
153
+ 'patram query ready-tasks --explain',
154
+ 'patram query --where "$class=decision and status=accepted and count(in:decided_by, $class=task) = 0" --lint',
155
+ 'patram query active-plans --limit 10 --offset 20',
156
+ ],
157
+ extra_positionals_message:
158
+ 'Query accepts either "--where" or a stored query name.',
159
+ help_topics: ['query-language'],
160
+ max_positionals: 1,
161
+ min_positionals: 0,
162
+ missing_argument_examples: [
163
+ 'patram query active-plans',
164
+ 'patram query --where "tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md"',
165
+ ],
166
+ missing_argument_label: '<name> or --where "<clause>"',
167
+ missing_usage_lines: [
168
+ 'patram query <name> [options]',
169
+ 'patram query --where "<clause>" [options]',
170
+ ],
171
+ option_column_width: 19,
172
+ options: [
173
+ {
174
+ description: 'Run an ad hoc query instead of a stored query',
175
+ label: '--where <clause>',
176
+ },
177
+ {
178
+ description: 'Skip the first N matches',
179
+ label: '--offset <number>',
180
+ },
181
+ {
182
+ description: 'Limit the number of matches',
183
+ label: '--limit <number>',
184
+ },
185
+ {
186
+ description: 'Explain the resolved query without rendering results',
187
+ label: '--explain',
188
+ },
189
+ {
190
+ description: 'Validate syntax and relation references only',
191
+ label: '--lint',
192
+ },
193
+ {
194
+ description: 'Print plain text output',
195
+ label: '--plain',
196
+ },
197
+ {
198
+ description: 'Print JSON output',
199
+ label: '--json',
200
+ },
201
+ ],
202
+ related: ['queries', 'show'],
203
+ root_summary: 'Run a stored query or an ad hoc where clause',
204
+ summary:
205
+ 'Run a stored query or an ad hoc where clause against graph nodes.',
206
+ syntax_lines: [
207
+ '$id=<value> | $class=<value> | $path=<value> | status=<value>',
208
+ '$id^=<prefix> | $path^=<prefix> | title~<text>',
209
+ '<field> in [<value>, ...] | <field> not in [<value>, ...]',
210
+ '<relation>:* | <relation>=<target-id>',
211
+ 'any(<traversal>, <term> and <term>)',
212
+ 'none(<traversal>, <term> and <term>)',
213
+ 'count(<traversal>, <term> and <term>) <comparison> <number>',
214
+ ],
215
+ usage_lines: [
216
+ 'patram query <name> [options]',
217
+ 'patram query --where "<clause>" [options]',
218
+ ],
219
+ },
220
+ queries: {
221
+ allowed_option_names: new Set(),
222
+ examples: ['patram queries'],
223
+ extra_positionals_message: 'Queries does not accept positional arguments.',
224
+ help_topics: [],
225
+ max_positionals: 0,
226
+ min_positionals: 0,
227
+ missing_argument_examples: [],
228
+ missing_argument_label: null,
229
+ missing_usage_lines: ['patram queries'],
230
+ option_column_width: 10,
231
+ options: [
232
+ {
233
+ description: 'Print plain text output',
234
+ label: '--plain',
235
+ },
236
+ {
237
+ description: 'Print JSON output',
238
+ label: '--json',
239
+ },
240
+ ],
241
+ related: ['query'],
242
+ root_summary: 'List stored queries',
243
+ summary: 'List the stored queries defined in the project configuration.',
244
+ usage_lines: ['patram queries [options]'],
245
+ },
246
+ show: {
247
+ allowed_option_names: new Set(),
248
+ examples: ['patram show docs/patram.md', 'patram show lib/patram-cli.js'],
249
+ extra_positionals_message: 'Show accepts exactly one file path.',
250
+ help_topics: [],
251
+ max_positionals: 1,
252
+ min_positionals: 1,
253
+ missing_argument_examples: [
254
+ 'patram show docs/patram.md',
255
+ 'patram show lib/patram-cli.js',
256
+ ],
257
+ missing_argument_label: '<file>',
258
+ missing_usage_lines: ['patram show <file>'],
259
+ option_column_width: 10,
260
+ options: [
261
+ {
262
+ description: 'Print plain text output',
263
+ label: '--plain',
264
+ },
265
+ {
266
+ description: 'Print JSON output',
267
+ label: '--json',
268
+ },
269
+ ],
270
+ related: ['query', 'check'],
271
+ root_summary: 'Print a file with resolved links',
272
+ summary:
273
+ 'Print one source file with indexed links resolved against the graph.',
274
+ usage_lines: ['patram show <file> [options]'],
275
+ },
276
+ };
277
+
278
+ /** @type {Record<CliHelpTopicName, CliHelpTopicDefinition>} */
279
+ const HELP_TOPIC_DEFINITIONS = {
280
+ 'query-language': {
281
+ examples: [
282
+ '$class=decision and status=accepted',
283
+ '$path^=docs/plans/',
284
+ 'title~query',
285
+ 'tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md',
286
+ 'implements_command=command:query',
287
+ 'status not in [done, dropped, superseded]',
288
+ 'any(in:tracked_in, $class=task and status in [pending, ready, in_progress, blocked])',
289
+ 'none(in:tracked_in, $class=decision)',
290
+ 'count(in:decided_by, $class=task) = 0',
291
+ 'not uses_term=term:graph',
292
+ ],
293
+ lead: 'Query language filters graph nodes with field, relation, traversal, and aggregate terms.',
294
+ operators: [
295
+ {
296
+ description: 'Exact field match or exact count comparison',
297
+ label: '=',
298
+ },
299
+ {
300
+ description: 'Prefix match for structural id and path',
301
+ label: '^=',
302
+ },
303
+ {
304
+ description: 'Contains text for title',
305
+ label: '~',
306
+ },
307
+ {
308
+ description: 'Set membership for supported fields',
309
+ label: 'in',
310
+ },
311
+ {
312
+ description: 'Set exclusion for supported fields',
313
+ label: 'not in',
314
+ },
315
+ {
316
+ description: 'Negate one term',
317
+ label: 'not',
318
+ },
319
+ {
320
+ description: 'Combine terms',
321
+ label: 'and',
322
+ },
323
+ {
324
+ description: 'Count comparisons',
325
+ label: '!= < > >= <=',
326
+ },
327
+ ],
328
+ relation_terms: [
329
+ {
330
+ description: 'Match nodes with at least one outgoing relation',
331
+ label: '<relation>:*',
332
+ },
333
+ {
334
+ description: 'Match nodes linked to an exact target id',
335
+ label: '<relation>=<target-id>',
336
+ },
337
+ {
338
+ description: 'Traverse one incoming relation hop',
339
+ label: 'in:<relation>',
340
+ },
341
+ {
342
+ description: 'Traverse one outgoing relation hop',
343
+ label: 'out:<relation>',
344
+ },
345
+ ],
346
+ terms: [
347
+ 'Exact match: $id, $class, $path, status',
348
+ 'Prefix match: $id, $path',
349
+ 'Contains text: title',
350
+ 'Set membership: $id, $class, $path, status, title',
351
+ ],
352
+ usage_lines: [
353
+ '<field>=<value>',
354
+ '$id^=<prefix>',
355
+ '$path^=<prefix>',
356
+ 'title~<text>',
357
+ '<field> in [<value>, ...]',
358
+ '<field> not in [<value>, ...]',
359
+ '<relation>:*',
360
+ '<relation>=<target-id>',
361
+ 'any(<traversal>, <term> and <term>)',
362
+ 'none(<traversal>, <term> and <term>)',
363
+ 'count(<traversal>, <term> and <term>) <comparison> <number>',
364
+ 'not <term>',
365
+ '<term> and <term>',
366
+ ],
367
+ },
368
+ };
369
+
370
+ /**
371
+ * @returns {{ global_options: string[], summary: string, usage_lines: string[] }}
372
+ */
373
+ export function getRootHelpDefinition() {
374
+ return {
375
+ global_options: ROOT_HELP_GLOBAL_OPTIONS,
376
+ summary: ROOT_HELP_SUMMARY,
377
+ usage_lines: ROOT_HELP_USAGE_LINES,
378
+ };
379
+ }
380
+
381
+ /**
382
+ * @param {CliCommandName} command_name
383
+ * @returns {CliCommandDefinition}
384
+ */
385
+ export function getCommandDefinition(command_name) {
386
+ return COMMAND_DEFINITIONS[command_name];
387
+ }
388
+
389
+ /**
390
+ * @param {CliHelpTopicName} help_topic_name
391
+ * @returns {CliHelpTopicDefinition}
392
+ */
393
+ export function getHelpTopicDefinition(help_topic_name) {
394
+ return HELP_TOPIC_DEFINITIONS[help_topic_name];
395
+ }
396
+
397
+ /**
398
+ * @param {string | undefined} command_name
399
+ * @returns {command_name is CliCommandName}
400
+ */
401
+ export function isCommandName(command_name) {
402
+ return COMMAND_NAMES.includes(
403
+ /** @type {CliCommandName} */ (command_name ?? ''),
404
+ );
405
+ }
406
+
407
+ /**
408
+ * @param {string | undefined} help_topic_name
409
+ * @returns {help_topic_name is CliHelpTopicName}
410
+ */
411
+ export function isHelpTopicName(help_topic_name) {
412
+ return HELP_TOPIC_NAMES.includes(
413
+ /** @type {CliHelpTopicName} */ (help_topic_name ?? ''),
414
+ );
415
+ }
416
+
417
+ /**
418
+ * @param {string} input_text
419
+ * @returns {CliCommandName | undefined}
420
+ */
421
+ export function findCommandSuggestion(input_text) {
422
+ return /** @type {CliCommandName | undefined} */ (
423
+ findCloseMatch(input_text, COMMAND_NAMES)
424
+ );
425
+ }
426
+
427
+ /**
428
+ * @param {string} input_text
429
+ * @returns {CliCommandName | CliHelpTopicName | undefined}
430
+ */
431
+ export function findHelpTargetSuggestion(input_text) {
432
+ return /** @type {CliCommandName | CliHelpTopicName | undefined} */ (
433
+ findCloseMatch(input_text, [...COMMAND_NAMES, ...HELP_TOPIC_NAMES])
434
+ );
435
+ }
436
+
437
+ /**
438
+ * @param {string} input_text
439
+ * @param {CliCommandName | undefined} command_name
440
+ * @returns {string | undefined}
441
+ */
442
+ export function findOptionSuggestion(input_text, command_name) {
443
+ const candidates = listOptionLabels(command_name);
444
+
445
+ return findCloseMatch(input_text, candidates);
446
+ }
447
+
448
+ /**
449
+ * @returns {CliCommandName[]}
450
+ */
451
+ export function listCommandNames() {
452
+ return [...COMMAND_NAMES];
453
+ }
454
+
455
+ /**
456
+ * @returns {CliHelpTopicName[]}
457
+ */
458
+ export function listHelpTopicNames() {
459
+ return [...HELP_TOPIC_NAMES];
460
+ }
461
+
462
+ /**
463
+ * @param {CliCommandName | undefined} command_name
464
+ * @returns {string[]}
465
+ */
466
+ function listOptionLabels(command_name) {
467
+ const option_labels = new Set(
468
+ [...GLOBAL_OPTION_NAMES].map((option_name) => `--${option_name}`),
469
+ );
470
+
471
+ if (command_name) {
472
+ for (const option_name of getCommandDefinition(command_name)
473
+ .allowed_option_names) {
474
+ option_labels.add(`--${option_name}`);
475
+ }
476
+ } else {
477
+ option_labels.add('--limit');
478
+ option_labels.add('--offset');
479
+ option_labels.add('--where');
480
+ }
481
+
482
+ return [...option_labels];
483
+ }
484
+
485
+ /**
486
+ * @param {string} input_text
487
+ * @param {readonly string[]} candidates
488
+ * @returns {string | undefined}
489
+ */
490
+ function findCloseMatch(input_text, candidates) {
491
+ let best_candidate;
492
+ let best_score = 0;
493
+
494
+ for (const candidate of candidates) {
495
+ const score = scoreCandidate(input_text, candidate);
496
+
497
+ if (score > best_score) {
498
+ best_candidate = candidate;
499
+ best_score = score;
500
+ }
501
+ }
502
+
503
+ if (best_score < 0.6) {
504
+ return undefined;
505
+ }
506
+
507
+ return best_candidate;
508
+ }
509
+
510
+ /**
511
+ * @param {string} input_text
512
+ * @param {string} candidate
513
+ * @returns {number}
514
+ */
515
+ function scoreCandidate(input_text, candidate) {
516
+ const max_length = Math.max(input_text.length, candidate.length);
517
+
518
+ if (max_length === 0) {
519
+ return 1;
520
+ }
521
+
522
+ return (
523
+ 1 - calculateDamerauLevenshteinDistance(input_text, candidate) / max_length
524
+ );
525
+ }
526
+
527
+ /**
528
+ * @param {string} left_text
529
+ * @param {string} right_text
530
+ * @returns {number}
531
+ */
532
+ function calculateDamerauLevenshteinDistance(left_text, right_text) {
533
+ /** @type {number[][]} */
534
+ const matrix = Array.from({ length: left_text.length + 1 }, () =>
535
+ Array(right_text.length + 1).fill(0),
536
+ );
537
+
538
+ for (let left_index = 0; left_index <= left_text.length; left_index += 1) {
539
+ matrix[left_index][0] = left_index;
540
+ }
541
+
542
+ for (
543
+ let right_index = 0;
544
+ right_index <= right_text.length;
545
+ right_index += 1
546
+ ) {
547
+ matrix[0][right_index] = right_index;
548
+ }
549
+
550
+ for (let left_index = 1; left_index <= left_text.length; left_index += 1) {
551
+ for (
552
+ let right_index = 1;
553
+ right_index <= right_text.length;
554
+ right_index += 1
555
+ ) {
556
+ const substitution_cost =
557
+ left_text[left_index - 1] === right_text[right_index - 1] ? 0 : 1;
558
+
559
+ matrix[left_index][right_index] = Math.min(
560
+ matrix[left_index - 1][right_index] + 1,
561
+ matrix[left_index][right_index - 1] + 1,
562
+ matrix[left_index - 1][right_index - 1] + substitution_cost,
563
+ );
564
+
565
+ if (
566
+ left_index > 1 &&
567
+ right_index > 1 &&
568
+ left_text[left_index - 1] === right_text[right_index - 2] &&
569
+ left_text[left_index - 2] === right_text[right_index - 1]
570
+ ) {
571
+ matrix[left_index][right_index] = Math.min(
572
+ matrix[left_index][right_index],
573
+ matrix[left_index - 2][right_index - 2] + 1,
574
+ );
575
+ }
576
+ }
577
+ }
578
+
579
+ return matrix[left_text.length][right_text.length];
580
+ }