patram 0.2.0 → 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.
@@ -1,5 +1,5 @@
1
1
  import type {
2
- KindDefinition,
2
+ ClassDefinition,
3
3
  MappingDefinition,
4
4
  RelationDefinition,
5
5
  } from './patram-config.types.ts';
@@ -8,6 +8,98 @@ export interface StoredQueryConfig {
8
8
  where: string;
9
9
  }
10
10
 
11
+ export type FieldValueTypeName =
12
+ | 'string'
13
+ | 'integer'
14
+ | 'enum'
15
+ | 'path'
16
+ | 'glob'
17
+ | 'date'
18
+ | 'date_time';
19
+
20
+ export interface FieldDisplayConfig {
21
+ hidden?: boolean;
22
+ order?: number;
23
+ }
24
+
25
+ export interface FieldQueryConfig {
26
+ contains?: boolean;
27
+ prefix?: boolean;
28
+ }
29
+
30
+ export interface StringFieldConfig {
31
+ display?: FieldDisplayConfig;
32
+ multiple?: boolean;
33
+ query?: FieldQueryConfig;
34
+ type: 'string';
35
+ }
36
+
37
+ export interface IntegerFieldConfig {
38
+ display?: FieldDisplayConfig;
39
+ multiple?: boolean;
40
+ type: 'integer';
41
+ }
42
+
43
+ export interface EnumFieldConfig {
44
+ display?: FieldDisplayConfig;
45
+ multiple?: boolean;
46
+ type: 'enum';
47
+ values: string[];
48
+ }
49
+
50
+ export interface PathFieldConfig {
51
+ display?: FieldDisplayConfig;
52
+ multiple?: boolean;
53
+ path_class?: string;
54
+ type: 'path';
55
+ }
56
+
57
+ export interface GlobFieldConfig {
58
+ display?: FieldDisplayConfig;
59
+ multiple?: boolean;
60
+ type: 'glob';
61
+ }
62
+
63
+ export interface DateFieldConfig {
64
+ display?: FieldDisplayConfig;
65
+ multiple?: boolean;
66
+ type: 'date';
67
+ }
68
+
69
+ export interface DateTimeFieldConfig {
70
+ display?: FieldDisplayConfig;
71
+ multiple?: boolean;
72
+ type: 'date_time';
73
+ }
74
+
75
+ export type MetadataFieldConfig =
76
+ | DateFieldConfig
77
+ | DateTimeFieldConfig
78
+ | EnumFieldConfig
79
+ | GlobFieldConfig
80
+ | IntegerFieldConfig
81
+ | PathFieldConfig
82
+ | StringFieldConfig;
83
+
84
+ export interface ClassFieldRuleConfig {
85
+ presence: 'required' | 'optional' | 'forbidden';
86
+ }
87
+
88
+ export type DirectiveTypeConfig = MetadataFieldConfig;
89
+ export type MetadataDirectiveRuleConfig = ClassFieldRuleConfig;
90
+
91
+ export interface ClassSchemaConfig {
92
+ document_path_class?: string;
93
+ fields: Record<string, ClassFieldRuleConfig>;
94
+ unknown_fields?: 'ignore' | 'error';
95
+ }
96
+
97
+ export type MetadataSchemaConfig = ClassSchemaConfig;
98
+
99
+ export interface PathClassConfig {
100
+ prefixes: string[];
101
+ }
102
+
11
103
  export type DerivedSummaryScalar = boolean | number | string | null;
12
104
 
13
105
  export interface DerivedSummaryCountFieldConfig {
@@ -34,15 +126,18 @@ export type DerivedSummaryFieldConfig =
34
126
  | DerivedSummarySelectFieldConfig;
35
127
 
36
128
  export interface DerivedSummaryConfig {
129
+ classes: string[];
37
130
  fields: DerivedSummaryFieldConfig[];
38
- kinds: string[];
39
131
  }
40
132
 
41
133
  export interface PatramRepoConfig {
134
+ class_schemas?: Record<string, ClassSchemaConfig>;
135
+ classes?: Record<string, ClassDefinition>;
42
136
  derived_summaries?: Record<string, DerivedSummaryConfig>;
137
+ fields?: Record<string, MetadataFieldConfig>;
43
138
  include: string[];
44
- kinds?: Record<string, KindDefinition>;
45
139
  mappings?: Record<string, MappingDefinition>;
140
+ path_classes?: Record<string, PathClassConfig>;
46
141
  queries: Record<string, StoredQueryConfig>;
47
142
  relations?: Record<string, RelationDefinition>;
48
143
  }
@@ -36,13 +36,14 @@ import { resolvePatramGraphConfig } from './resolve-patram-graph-config.js';
36
36
  * project directory.
37
37
  *
38
38
  * @param {string} project_directory
39
- * @returns {Promise<{ config: PatramRepoConfig, diagnostics: PatramDiagnostic[], graph: BuildGraphResult, source_file_paths: string[] }>}
39
+ * @returns {Promise<{ claims: PatramClaim[], config: PatramRepoConfig, diagnostics: PatramDiagnostic[], graph: BuildGraphResult, source_file_paths: string[] }>}
40
40
  */
41
41
  export async function loadProjectGraph(project_directory) {
42
42
  const load_result = await loadPatramConfig(project_directory);
43
43
 
44
44
  if (load_result.diagnostics.length > 0) {
45
45
  return {
46
+ claims: [],
46
47
  config: {
47
48
  include: [],
48
49
  queries: {},
@@ -74,6 +75,7 @@ export async function loadProjectGraph(project_directory) {
74
75
 
75
76
  if (collect_result.diagnostics.length > 0) {
76
77
  return {
78
+ claims: collect_result.claims,
77
79
  config: repo_config,
78
80
  diagnostics: collect_result.diagnostics,
79
81
  graph: {
@@ -85,6 +87,7 @@ export async function loadProjectGraph(project_directory) {
85
87
  }
86
88
 
87
89
  return {
90
+ claims: collect_result.claims,
88
91
  config: repo_config,
89
92
  diagnostics: [],
90
93
  graph: buildGraph(graph_config, collect_result.claims),
@@ -12,6 +12,11 @@ export interface OutputDerivedSummary {
12
12
  name: string;
13
13
  }
14
14
 
15
+ export interface OutputMetadataField {
16
+ name: string;
17
+ value: string | string[];
18
+ }
19
+
15
20
  export interface OutputViewSummary {
16
21
  count: number;
17
22
  kind: 'resolved_link_list' | 'result_list' | 'stored_query_list';
@@ -26,12 +31,13 @@ export interface QueryOutputViewSummary extends OutputViewSummary {
26
31
 
27
32
  export interface OutputNodeItem {
28
33
  derived_summary?: OutputDerivedSummary;
34
+ fields: Record<string, string | string[]>;
29
35
  id: string;
30
36
  kind: 'node';
31
- node_kind: GraphNode['kind'];
32
- path: string;
33
- status?: string;
37
+ node_kind: string;
38
+ path?: string;
34
39
  title: string;
40
+ visible_fields: OutputMetadataField[];
35
41
  }
36
42
 
37
43
  export interface OutputStoredQueryItem {
@@ -42,10 +48,12 @@ export interface OutputStoredQueryItem {
42
48
 
43
49
  export interface OutputResolvedLinkTarget {
44
50
  derived_summary?: OutputDerivedSummary;
45
- kind?: string;
46
- path: string;
47
- status?: string;
51
+ fields: Record<string, string | string[]>;
52
+ id: string;
53
+ kind: string;
54
+ path?: string;
48
55
  title: string;
56
+ visible_fields: OutputMetadataField[];
49
57
  }
50
58
 
51
59
  export interface OutputResolvedLinkItem {
@@ -1,4 +1,4 @@
1
- export type CliCommandName = 'check' | 'query' | 'queries' | 'show';
1
+ export type CliCommandName = 'check' | 'fields' | 'query' | 'queries' | 'show';
2
2
  export type CliHelpTopicName = 'query-language';
3
3
  export type CliHelpTargetKind = 'root' | 'command' | 'topic';
4
4
 
@@ -218,10 +218,7 @@ function parseAtomicTerm(parser_state) {
218
218
  return failToken(parser_state);
219
219
  }
220
220
 
221
- return (
222
- parseFieldSet(parser_state, field_or_relation_name) ??
223
- parseOperatorTerm(parser_state, start_index, field_or_relation_name)
224
- );
221
+ return parseOperatorTerm(parser_state, start_index, field_or_relation_name);
225
222
  }
226
223
 
227
224
  /**
@@ -259,16 +256,12 @@ function createAggregateTerm(parser_state, aggregate_name, traversal, clauses) {
259
256
 
260
257
  /**
261
258
  * @param {ParserState} parser_state
259
+ * @param {number} start_index
262
260
  * @param {ParsedFieldName | string} field_name
263
261
  * @returns {ParseTermResult | null}
264
262
  */
265
- function parseFieldSet(parser_state, field_name) {
266
- if (!isSupportedFieldName(field_name)) {
267
- return null;
268
- }
269
-
270
- const start_index = parser_state.index;
271
-
263
+ function parseFieldSet(parser_state, start_index, field_name) {
264
+ const operator_start_index = parser_state.index;
272
265
  if (!consumeRequiredWhitespace(parser_state)) {
273
266
  return null;
274
267
  }
@@ -276,7 +269,7 @@ function parseFieldSet(parser_state, field_name) {
276
269
  const operator = parseSetOperator(parser_state);
277
270
 
278
271
  if (!operator) {
279
- parser_state.index = start_index;
272
+ parser_state.index = operator_start_index;
280
273
  return null;
281
274
  }
282
275
 
@@ -287,7 +280,13 @@ function parseFieldSet(parser_state, field_name) {
287
280
  const values = parseList(parser_state);
288
281
 
289
282
  return values
290
- ? success({ field_name, kind: 'field_set', operator, values })
283
+ ? success({
284
+ column: start_index + 1,
285
+ field_name,
286
+ kind: 'field_set',
287
+ operator,
288
+ values,
289
+ })
291
290
  : failToken(parser_state);
292
291
  }
293
292
 
@@ -298,18 +297,46 @@ function parseFieldSet(parser_state, field_name) {
298
297
  * @returns {ParseTermResult}
299
298
  */
300
299
  function parseOperatorTerm(parser_state, start_index, field_or_relation_name) {
301
- const prefix_term = parsePrefixTerm(parser_state, field_or_relation_name);
300
+ const field_set = parseFieldSet(
301
+ parser_state,
302
+ start_index,
303
+ field_or_relation_name,
304
+ );
305
+
306
+ if (field_set) {
307
+ return field_set;
308
+ }
309
+
310
+ const prefix_term = parsePrefixTerm(
311
+ parser_state,
312
+ start_index,
313
+ field_or_relation_name,
314
+ );
302
315
 
303
316
  if (prefix_term) {
304
317
  return prefix_term;
305
318
  }
306
319
 
307
- const contains_term = parseContainsTerm(parser_state, field_or_relation_name);
320
+ const contains_term = parseContainsTerm(
321
+ parser_state,
322
+ start_index,
323
+ field_or_relation_name,
324
+ );
308
325
 
309
326
  if (contains_term) {
310
327
  return contains_term;
311
328
  }
312
329
 
330
+ const comparison_term = parseFieldComparisonTerm(
331
+ parser_state,
332
+ start_index,
333
+ field_or_relation_name,
334
+ );
335
+
336
+ if (comparison_term) {
337
+ return comparison_term;
338
+ }
339
+
313
340
  const equality_term = parseEqualityTerm(
314
341
  parser_state,
315
342
  start_index,
@@ -395,39 +422,51 @@ function parseSetOperator(parser_state) {
395
422
 
396
423
  /**
397
424
  * @param {ParserState} parser_state
425
+ * @param {number} start_index
398
426
  * @param {string} field_name
399
427
  * @returns {ParseTermResult | null}
400
428
  */
401
- function parsePrefixTerm(parser_state, field_name) {
402
- if (field_name !== 'id' && field_name !== 'path') {
403
- return null;
404
- }
405
-
429
+ function parsePrefixTerm(parser_state, start_index, field_name) {
406
430
  if (!consumeOperator(parser_state, '^=')) {
407
431
  return null;
408
432
  }
409
433
 
434
+ skipWhitespace(parser_state);
410
435
  const value = parseBareValue(parser_state);
411
436
 
412
437
  return value
413
- ? success({ field_name, kind: 'field', operator: '^=', value })
438
+ ? success({
439
+ column: start_index + 1,
440
+ field_name,
441
+ kind: 'field',
442
+ operator: '^=',
443
+ value,
444
+ })
414
445
  : failToken(parser_state);
415
446
  }
416
447
 
417
448
  /**
418
449
  * @param {ParserState} parser_state
450
+ * @param {number} start_index
419
451
  * @param {string} field_name
420
452
  * @returns {ParseTermResult | null}
421
453
  */
422
- function parseContainsTerm(parser_state, field_name) {
423
- if (field_name !== 'title' || !consumeOperator(parser_state, '~')) {
454
+ function parseContainsTerm(parser_state, start_index, field_name) {
455
+ if (!consumeOperator(parser_state, '~')) {
424
456
  return null;
425
457
  }
426
458
 
459
+ skipWhitespace(parser_state);
427
460
  const value = parseBareValue(parser_state);
428
461
 
429
462
  return value
430
- ? success({ field_name: 'title', kind: 'field', operator: '~', value })
463
+ ? success({
464
+ column: start_index + 1,
465
+ field_name,
466
+ kind: 'field',
467
+ operator: '~',
468
+ value,
469
+ })
431
470
  : failToken(parser_state);
432
471
  }
433
472
 
@@ -442,14 +481,20 @@ function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
442
481
  return null;
443
482
  }
444
483
 
484
+ skipWhitespace(parser_state);
445
485
  const value = parseBareValue(parser_state);
446
486
 
447
487
  if (!value) {
448
488
  return failToken(parser_state);
449
489
  }
450
490
 
451
- if (isExactMatchField(field_or_relation_name)) {
491
+ if (
492
+ field_or_relation_name.startsWith('$') ||
493
+ field_or_relation_name === 'title' ||
494
+ !value.includes(':')
495
+ ) {
452
496
  return success({
497
+ column: start_index + 1,
453
498
  field_name: field_or_relation_name,
454
499
  kind: 'field',
455
500
  operator: '=',
@@ -457,17 +502,54 @@ function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
457
502
  });
458
503
  }
459
504
 
460
- if (value.includes(':')) {
461
- return success({
462
- column: start_index + 1,
463
- kind: 'relation_target',
464
- relation_name: field_or_relation_name,
465
- target_id: value,
466
- });
505
+ return success({
506
+ column: start_index + 1,
507
+ kind: 'relation_target',
508
+ relation_name: field_or_relation_name,
509
+ target_id: value,
510
+ });
511
+ }
512
+
513
+ /**
514
+ * @param {ParserState} parser_state
515
+ * @param {number} start_index
516
+ * @param {string} field_name
517
+ * @returns {ParseTermResult | null}
518
+ */
519
+ function parseFieldComparisonTerm(parser_state, start_index, field_name) {
520
+ const operator = parseFieldComparisonOperator(parser_state);
521
+
522
+ if (!operator) {
523
+ return null;
467
524
  }
468
525
 
469
- parser_state.index = start_index;
470
- return failToken(parser_state);
526
+ skipWhitespace(parser_state);
527
+ const value = parseBareValue(parser_state);
528
+
529
+ if (!value) {
530
+ return failToken(parser_state);
531
+ }
532
+
533
+ return success({
534
+ column: start_index + 1,
535
+ field_name,
536
+ kind: 'field',
537
+ operator,
538
+ value,
539
+ });
540
+ }
541
+
542
+ /**
543
+ * @param {ParserState} parser_state
544
+ * @returns {'!=' | '<=' | '>=' | '<' | '>' | null}
545
+ */
546
+ function parseFieldComparisonOperator(parser_state) {
547
+ /** @type {Array<'!=' | '<=' | '>=' | '<' | '>'>} */
548
+ const comparisons = ['!=', '<=', '>=', '<', '>'];
549
+
550
+ return (
551
+ comparisons.find((value) => consumeOperator(parser_state, value)) ?? null
552
+ );
471
553
  }
472
554
 
473
555
  /**
@@ -526,7 +608,7 @@ function parseComparison(parser_state) {
526
608
  * @returns {string | null}
527
609
  */
528
610
  function parseIdentifier(parser_state) {
529
- return readMatch(parser_state, /^[a-z_]+/u);
611
+ return readMatch(parser_state, /^\$?[a-z_][a-z0-9_]*/u);
530
612
  }
531
613
 
532
614
  /**
@@ -643,22 +725,6 @@ function isAtEnd(parser_state) {
643
725
  return parser_state.index >= parser_state.where_clause.length;
644
726
  }
645
727
 
646
- /**
647
- * @param {string} field_name
648
- * @returns {field_name is ParsedFieldName}
649
- */
650
- function isSupportedFieldName(field_name) {
651
- return ['id', 'kind', 'path', 'status', 'title'].includes(field_name);
652
- }
653
-
654
- /**
655
- * @param {string} field_name
656
- * @returns {field_name is 'id' | 'kind' | 'path' | 'status'}
657
- */
658
- function isExactMatchField(field_name) {
659
- return ['id', 'kind', 'path', 'status'].includes(field_name);
660
- }
661
-
662
728
  /**
663
729
  * @param {ParsedTerm} term
664
730
  * @returns {ParseTermResult}
@@ -1,15 +1,17 @@
1
1
  import type { PatramDiagnostic } from './load-patram-config.types.ts';
2
2
 
3
- export type ParsedFieldName = 'id' | 'kind' | 'path' | 'status' | 'title';
3
+ export type ParsedFieldName = string;
4
4
 
5
5
  export interface ParsedFieldTerm {
6
+ column: number;
6
7
  field_name: ParsedFieldName;
7
8
  kind: 'field';
8
- operator: '=' | '^=' | '~';
9
+ operator: '!=' | '<' | '<=' | '=' | '>' | '>=' | '^=' | '~';
9
10
  value: string;
10
11
  }
11
12
 
12
13
  export interface ParsedFieldSetTerm {
14
+ column: number;
13
15
  field_name: ParsedFieldName;
14
16
  kind: 'field_set';
15
17
  operator: 'in' | 'not in';
package/lib/patram-cli.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  shouldPageCommandOutput,
11
11
  writeCommandOutput,
12
12
  } from './command-output.js';
13
+ import { discoverFields } from './discover-fields.js';
13
14
  import { listRepoFiles } from './list-source-files.js';
14
15
  import { listQueries } from './list-queries.js';
15
16
  import { loadPatramConfig } from './load-patram-config.js';
@@ -36,6 +37,7 @@ import {
36
37
  createOutputView,
37
38
  createShowOutputView,
38
39
  } from './render-output-view.js';
40
+ import { renderFieldDiscovery } from './render-field-discovery.js';
39
41
  import { resolveWhereClause } from './resolve-where-clause.js';
40
42
  import { resolveOutputMode } from './resolve-output-mode.js';
41
43
  import { loadShowOutput } from './show-document.js';
@@ -43,8 +45,8 @@ import { loadShowOutput } from './show-document.js';
43
45
  /**
44
46
  * Patram command execution flow.
45
47
  *
46
- * Loads repo state and routes `check`, `query`, `queries`, and `show` through
47
- * the shared output pipeline.
48
+ * Loads repo state and routes `check`, `fields`, `query`, `queries`, and
49
+ * `show` through the shared output pipeline.
48
50
  *
49
51
  * Kind: cli
50
52
  * Status: active
@@ -94,6 +96,10 @@ export async function main(cli_arguments, io_context) {
94
96
  return runQueryCommand(parsed_command, io_context);
95
97
  }
96
98
 
99
+ if (parsed_command.command_name === 'fields') {
100
+ return runFieldsCommand(parsed_command, io_context);
101
+ }
102
+
97
103
  if (parsed_command.command_name === 'queries') {
98
104
  return runQueriesCommand(parsed_command, io_context);
99
105
  }
@@ -140,7 +146,12 @@ async function runCheckCommand(parsed_command, io_context) {
140
146
  return 1;
141
147
  }
142
148
 
143
- const diagnostics = checkGraph(project_graph_result.graph, repo_file_paths);
149
+ const diagnostics = checkGraph(
150
+ project_graph_result.graph,
151
+ repo_file_paths,
152
+ project_graph_result.config,
153
+ project_graph_result.claims,
154
+ );
144
155
  const selected_diagnostics = selectCheckTargetDiagnostics(
145
156
  diagnostics,
146
157
  resolved_target,
@@ -205,6 +216,7 @@ async function runQueryCommand(parsed_command, io_context) {
205
216
  const query_result = queryGraph(
206
217
  project_graph_result.graph,
207
218
  where_clause.value.where_clause,
219
+ project_graph_result.config,
208
220
  createQueryPaginationOptions(parsed_command, use_pager),
209
221
  );
210
222
 
@@ -227,12 +239,31 @@ async function runQueryCommand(parsed_command, io_context) {
227
239
  createOutputView('query', query_result.nodes, {
228
240
  derived_summary_evaluator,
229
241
  ...createQueryOutputOptions(parsed_command, query_result, use_pager),
242
+ repo_config: project_graph_result.config,
230
243
  }),
231
244
  );
232
245
 
233
246
  return 0;
234
247
  }
235
248
 
249
+ /**
250
+ * @param {ParsedCliCommandRequest} parsed_command
251
+ * @param {{ stderr: { write(chunk: string): boolean }, stdout: { isTTY?: boolean, write(chunk: string): boolean } }} io_context
252
+ * @returns {Promise<number>}
253
+ */
254
+ async function runFieldsCommand(parsed_command, io_context) {
255
+ const output_mode = resolveOutputMode(parsed_command, {
256
+ is_tty: io_context.stdout.isTTY === true,
257
+ no_color: process.env.NO_COLOR !== undefined,
258
+ term: process.env.TERM,
259
+ });
260
+ const discovery_result = await discoverFields(process.cwd());
261
+
262
+ io_context.stdout.write(renderFieldDiscovery(discovery_result, output_mode));
263
+
264
+ return 0;
265
+ }
266
+
236
267
  /**
237
268
  * @param {ParsedCliCommandRequest} parsed_command
238
269
  * @param {{ stderr: { write(chunk: string): boolean }, stdout: { isTTY?: boolean, write(chunk: string): boolean } }} io_context
@@ -365,6 +396,7 @@ async function runShowCommand(parsed_command, io_context) {
365
396
  createShowOutputView(show_output.value, {
366
397
  derived_summary_evaluator,
367
398
  graph_nodes: project_graph_result.graph.nodes,
399
+ repo_config: project_graph_result.config,
368
400
  }),
369
401
  );
370
402
 
@@ -396,7 +428,7 @@ function createQueryOutputOptions(parsed_command, query_result, use_pager) {
396
428
  const offset = parsed_command.query_offset ?? 0;
397
429
 
398
430
  if (query_result.total_count === 0) {
399
- hints.push('Try: patram query --where "kind=task"');
431
+ hints.push('Try: patram query --where "$class=task"');
400
432
  }
401
433
 
402
434
  if (