patram 0.3.0 → 0.4.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,10 +1,9 @@
1
- /** @import * as $k$$l$parse$j$where$j$clause$k$types$k$ts from './parse-where-clause.types.ts'; */
2
1
  /* eslint-disable max-lines */
3
2
  /**
4
3
  * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
5
4
  * @import { ResolvedOutputMode } from './output-view.types.ts';
6
5
  * @import {
7
- * ParsedClause,
6
+ * ParsedExpression,
8
7
  * ParsedRelationTargetTerm,
9
8
  * ParsedRelationTerm,
10
9
  * ParsedTerm,
@@ -38,10 +37,10 @@ import { parseWhereClause } from './parse-where-clause.js';
38
37
  * limit: number | null,
39
38
  * offset: number,
40
39
  * },
40
+ * expression?: ParsedExpression,
41
41
  * inspection_mode: 'explain' | 'lint',
42
42
  * query_source: QuerySource,
43
43
  * where_clause: string,
44
- * clauses?: ParsedClause[],
45
44
  * }} QueryInspectionSuccess
46
45
  */
47
46
 
@@ -75,7 +74,7 @@ export function inspectQuery(
75
74
  const diagnostics = getQuerySemanticDiagnostics(
76
75
  repo_config,
77
76
  resolved_where_clause.query_source,
78
- parse_result.clauses,
77
+ parse_result.expression,
79
78
  );
80
79
 
81
80
  if (diagnostics.length > 0) {
@@ -99,11 +98,11 @@ export function inspectQuery(
99
98
  return {
100
99
  success: true,
101
100
  value: {
102
- clauses: parse_result.clauses,
103
101
  execution: {
104
102
  limit: inspection_options.limit,
105
103
  offset: inspection_options.offset,
106
104
  },
105
+ expression: parse_result.expression,
107
106
  inspection_mode: 'explain',
108
107
  query_source: resolved_where_clause.query_source,
109
108
  where_clause: resolved_where_clause.where_clause,
@@ -143,20 +142,26 @@ export function renderQueryInspection(query_inspection, output_mode) {
143
142
  }
144
143
 
145
144
  /**
145
+ * Collect schema-aware diagnostics for one parsed where clause.
146
+ *
146
147
  * @param {PatramRepoConfig} repo_config
147
148
  * @param {QuerySource} query_source
148
- * @param {ParsedClause[]} clauses
149
+ * @param {ParsedExpression} expression
149
150
  * @returns {PatramDiagnostic[]}
150
151
  */
151
- function collectSemanticDiagnostics(repo_config, query_source, clauses) {
152
+ export function getQuerySemanticDiagnostics(
153
+ repo_config,
154
+ query_source,
155
+ expression,
156
+ ) {
152
157
  const known_relation_names = new Set(
153
158
  Object.keys(repo_config.relations ?? {}),
154
159
  );
155
160
  /** @type {PatramDiagnostic[]} */
156
161
  const diagnostics = [];
157
162
 
158
- collectClauseDiagnostics(
159
- clauses,
163
+ collectExpressionDiagnostics(
164
+ expression,
160
165
  diagnostics,
161
166
  known_relation_names,
162
167
  repo_config.fields ?? {},
@@ -167,44 +172,58 @@ function collectSemanticDiagnostics(repo_config, query_source, clauses) {
167
172
  }
168
173
 
169
174
  /**
170
- * Collect schema-aware diagnostics for one parsed where clause.
171
- *
172
- * @param {PatramRepoConfig} repo_config
173
- * @param {QuerySource} query_source
174
- * @param {ParsedClause[]} clauses
175
- * @returns {PatramDiagnostic[]}
176
- */
177
- export function getQuerySemanticDiagnostics(
178
- repo_config,
179
- query_source,
180
- clauses,
181
- ) {
182
- return collectSemanticDiagnostics(repo_config, query_source, clauses);
183
- }
184
-
185
- /**
186
- * @param {ParsedClause[]} clauses
175
+ * @param {ParsedExpression} expression
187
176
  * @param {PatramDiagnostic[]} diagnostics
188
177
  * @param {Set<string>} known_relation_names
189
178
  * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
190
179
  * @param {string} diagnostic_path
191
180
  */
192
- function collectClauseDiagnostics(
193
- clauses,
181
+ function collectExpressionDiagnostics(
182
+ expression,
194
183
  diagnostics,
195
184
  known_relation_names,
196
185
  known_field_definitions,
197
186
  diagnostic_path,
198
187
  ) {
199
- for (const clause of clauses) {
188
+ if (expression.kind === 'and' || expression.kind === 'or') {
189
+ for (const subexpression of expression.expressions) {
190
+ collectExpressionDiagnostics(
191
+ subexpression,
192
+ diagnostics,
193
+ known_relation_names,
194
+ known_field_definitions,
195
+ diagnostic_path,
196
+ );
197
+ }
198
+
199
+ return;
200
+ }
201
+
202
+ if (expression.kind === 'not') {
203
+ collectExpressionDiagnostics(
204
+ expression.expression,
205
+ diagnostics,
206
+ known_relation_names,
207
+ known_field_definitions,
208
+ diagnostic_path,
209
+ );
210
+
211
+ return;
212
+ }
213
+
214
+ if (expression.kind === 'term') {
200
215
  collectTermDiagnostics(
201
- clause.term,
216
+ expression.term,
202
217
  diagnostics,
203
218
  known_relation_names,
204
219
  known_field_definitions,
205
220
  diagnostic_path,
206
221
  );
222
+
223
+ return;
207
224
  }
225
+
226
+ throw new Error('Unsupported query inspection expression.');
208
227
  }
209
228
 
210
229
  /**
@@ -228,8 +247,8 @@ function collectTermDiagnostics(
228
247
  known_relation_names,
229
248
  diagnostic_path,
230
249
  );
231
- collectClauseDiagnostics(
232
- term.clauses,
250
+ collectExpressionDiagnostics(
251
+ term.expression,
233
252
  diagnostics,
234
253
  known_relation_names,
235
254
  known_field_definitions,
@@ -577,9 +596,9 @@ function formatJsonQueryInspection(query_inspection) {
577
596
  }
578
597
 
579
598
  return {
580
- clauses: query_inspection.clauses,
581
599
  diagnostics: [],
582
600
  execution: query_inspection.execution,
601
+ expression: query_inspection.expression,
583
602
  mode: 'explain',
584
603
  source: query_inspection.query_source,
585
604
  where: query_inspection.where_clause,
@@ -625,9 +644,12 @@ function renderTextQueryInspection(query_inspection, render_options) {
625
644
  : String(query_inspection.execution?.limit ?? ''),
626
645
  ),
627
646
  '',
628
- `${render_options.label('clauses:')}`,
629
- ...formatExplainClauseBlock(
630
- query_inspection.clauses ?? [],
647
+ `${render_options.label('expression:')}`,
648
+ ...formatExplainExpressionBlock(
649
+ query_inspection.expression ?? {
650
+ expressions: [],
651
+ kind: 'and',
652
+ },
631
653
  render_options,
632
654
  '',
633
655
  ),
@@ -637,42 +659,49 @@ function renderTextQueryInspection(query_inspection, render_options) {
637
659
  }
638
660
 
639
661
  /**
640
- * @param {ParsedClause[]} clauses
662
+ * @param {ParsedExpression} expression
641
663
  * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
642
664
  * @param {string} indentation
643
665
  * @returns {string[]}
644
666
  */
645
- function formatExplainClauseBlock(clauses, render_options, indentation) {
646
- /** @type {string[]} */
647
- const output_lines = [];
667
+ function formatExplainExpressionBlock(expression, render_options, indentation) {
668
+ if (expression.kind === 'and') {
669
+ return formatExplainExpressionItems(
670
+ expression.expressions,
671
+ render_options,
672
+ indentation,
673
+ );
674
+ }
648
675
 
649
- clauses.forEach((clause, clause_index) => {
650
- const clause_number = clause_index + 1;
651
- const clause_text = formatClauseSummary(clause);
676
+ return formatExplainExpressionItems(
677
+ [expression],
678
+ render_options,
679
+ indentation,
680
+ );
681
+ }
652
682
 
653
- output_lines.push(`${indentation}${clause_number}. ${clause_text}`);
683
+ /**
684
+ * @param {ParsedExpression[]} expressions
685
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
686
+ * @param {string} indentation
687
+ * @returns {string[]}
688
+ */
689
+ function formatExplainExpressionItems(
690
+ expressions,
691
+ render_options,
692
+ indentation,
693
+ ) {
694
+ /** @type {string[]} */
695
+ const output_lines = [];
654
696
 
655
- if (clause.term.kind !== 'aggregate') {
656
- return;
657
- }
697
+ expressions.forEach((expression, expression_index) => {
698
+ const expression_number = expression_index + 1;
658
699
 
659
700
  output_lines.push(
660
- `${indentation} ${render_options.label('traversal:')} ${formatTraversal(clause.term.traversal)}`,
701
+ `${indentation}${expression_number}. ${formatExpressionSummary(expression)}`,
661
702
  );
662
-
663
- if (clause.term.aggregate_name === 'count') {
664
- output_lines.push(
665
- `${indentation} ${render_options.label('comparison:')} ${clause.term.comparison} ${clause.term.value}`,
666
- );
667
- }
668
-
669
703
  output_lines.push(
670
- `${indentation} ${render_options.label('nested clauses:')}`,
671
- ...formatExplainClauseBlock(
672
- clause.term.clauses,
673
- render_options,
674
- `${indentation} `,
675
- ),
704
+ ...formatExpressionDetailLines(expression, render_options, indentation),
676
705
  );
677
706
  });
678
707
 
@@ -680,17 +709,89 @@ function formatExplainClauseBlock(clauses, render_options, indentation) {
680
709
  }
681
710
 
682
711
  /**
683
- * @param {ParsedClause} clause
712
+ * @param {ParsedExpression} expression
684
713
  * @returns {string}
685
714
  */
686
- function formatClauseSummary(clause) {
687
- const clause_prefix = clause.is_negated ? 'not ' : '';
715
+ function formatExpressionSummary(expression) {
716
+ if (expression.kind === 'and') {
717
+ return 'all of';
718
+ }
688
719
 
689
- if (clause.term.kind === 'aggregate') {
690
- return `${clause_prefix}aggregate ${clause.term.aggregate_name}`;
720
+ if (expression.kind === 'or') {
721
+ return 'any of';
691
722
  }
692
723
 
693
- return `${clause_prefix}${formatTermSummary(clause.term)}`;
724
+ if (expression.kind === 'not') {
725
+ if (expression.expression.kind === 'term') {
726
+ return `not ${formatTermSummary(expression.expression.term)}`;
727
+ }
728
+
729
+ return 'not';
730
+ }
731
+
732
+ if (expression.kind === 'term') {
733
+ return formatTermSummary(expression.term);
734
+ }
735
+
736
+ throw new Error('Unsupported explain expression.');
737
+ }
738
+
739
+ /**
740
+ * @param {ParsedExpression} expression
741
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
742
+ * @param {string} indentation
743
+ * @returns {string[]}
744
+ */
745
+ function formatExpressionDetailLines(expression, render_options, indentation) {
746
+ if (expression.kind === 'and' || expression.kind === 'or') {
747
+ return formatExplainExpressionItems(
748
+ expression.expressions,
749
+ render_options,
750
+ `${indentation} `,
751
+ );
752
+ }
753
+
754
+ if (expression.kind === 'not') {
755
+ if (expression.expression.kind === 'term') {
756
+ return [];
757
+ }
758
+
759
+ return formatExplainExpressionBlock(
760
+ expression.expression,
761
+ render_options,
762
+ `${indentation} `,
763
+ );
764
+ }
765
+
766
+ if (expression.kind !== 'term') {
767
+ throw new Error('Unsupported explain expression details.');
768
+ }
769
+
770
+ if (expression.term.kind !== 'aggregate') {
771
+ return [];
772
+ }
773
+
774
+ /** @type {string[]} */
775
+ const output_lines = [
776
+ `${indentation} ${render_options.label('traversal:')} ${formatTraversal(expression.term.traversal)}`,
777
+ ];
778
+
779
+ if (expression.term.aggregate_name === 'count') {
780
+ output_lines.push(
781
+ `${indentation} ${render_options.label('comparison:')} ${expression.term.comparison} ${expression.term.value}`,
782
+ );
783
+ }
784
+
785
+ output_lines.push(
786
+ `${indentation} ${render_options.label('nested expression:')}`,
787
+ ...formatExplainExpressionBlock(
788
+ expression.term.expression,
789
+ render_options,
790
+ `${indentation} `,
791
+ ),
792
+ );
793
+
794
+ return output_lines;
694
795
  }
695
796
 
696
797
  /**
@@ -698,6 +799,10 @@ function formatClauseSummary(clause) {
698
799
  * @returns {string}
699
800
  */
700
801
  function formatTermSummary(term) {
802
+ if (term.kind === 'aggregate') {
803
+ return `aggregate ${term.aggregate_name}`;
804
+ }
805
+
701
806
  if (term.kind === 'field') {
702
807
  return `${term.field_name} ${term.operator} ${term.value}`;
703
808
  }
@@ -714,7 +819,7 @@ function formatTermSummary(term) {
714
819
  return `${term.relation_name} = ${term.target_id}`;
715
820
  }
716
821
 
717
- throw new Error('Expected a non-aggregate query term.');
822
+ throw new Error('Expected a parsed query term.');
718
823
  }
719
824
 
720
825
  /**
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { Ansis } from 'ansis';
7
7
 
8
+ const MAX_TEXT_EVIDENCE_ROWS = 5;
9
+
8
10
  /**
9
11
  * Render field discovery output.
10
12
  *
@@ -109,21 +111,21 @@ function formatTextFieldSuggestion(field_suggestion, render_options) {
109
111
  );
110
112
 
111
113
  if (field_suggestion.evidence_references.length > 0) {
112
- lines.push(render_options.label(' evidence:'));
113
114
  lines.push(
114
- ...field_suggestion.evidence_references.map(
115
- (evidence_reference) =>
116
- `${render_options.label(' ')}${formatEvidenceReference(evidence_reference)}`,
115
+ ...formatTextEvidenceSection(
116
+ ' evidence:',
117
+ field_suggestion.evidence_references,
118
+ render_options,
117
119
  ),
118
120
  );
119
121
  }
120
122
 
121
123
  if (field_suggestion.conflicting_evidence.length > 0) {
122
- lines.push(render_options.label(' conflicting evidence:'));
123
124
  lines.push(
124
- ...field_suggestion.conflicting_evidence.map(
125
- (evidence_reference) =>
126
- `${render_options.label(' ')}${formatEvidenceReference(evidence_reference)}`,
125
+ ...formatTextEvidenceSection(
126
+ ' conflicting evidence:',
127
+ field_suggestion.conflicting_evidence,
128
+ render_options,
127
129
  ),
128
130
  );
129
131
  }
@@ -131,6 +133,40 @@ function formatTextFieldSuggestion(field_suggestion, render_options) {
131
133
  return lines;
132
134
  }
133
135
 
136
+ /**
137
+ * @param {string} section_title
138
+ * @param {import('./discover-fields.types.ts').FieldDiscoveryEvidenceReference[]} evidence_references
139
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
140
+ * @returns {string[]}
141
+ */
142
+ function formatTextEvidenceSection(
143
+ section_title,
144
+ evidence_references,
145
+ render_options,
146
+ ) {
147
+ /** @type {string[]} */
148
+ const lines = [render_options.label(section_title)];
149
+ const visible_evidence_references = evidence_references.slice(
150
+ 0,
151
+ MAX_TEXT_EVIDENCE_ROWS,
152
+ );
153
+
154
+ lines.push(
155
+ ...visible_evidence_references.map(
156
+ (evidence_reference) =>
157
+ `${render_options.label(' ')}${formatEvidenceReference(evidence_reference)}`,
158
+ ),
159
+ );
160
+
161
+ if (evidence_references.length > MAX_TEXT_EVIDENCE_ROWS) {
162
+ const remaining_count = evidence_references.length - MAX_TEXT_EVIDENCE_ROWS;
163
+
164
+ lines.push(render_options.label(` ${remaining_count} more ...`));
165
+ }
166
+
167
+ return lines;
168
+ }
169
+
134
170
  /**
135
171
  * @param {import('./discover-fields.types.ts').FieldDiscoveryEvidenceReference} evidence_reference
136
172
  * @returns {string}
@@ -11,6 +11,7 @@
11
11
  import { renderJsonOutput } from './render-json-output.js';
12
12
  import { renderPlainOutput } from './render-plain-output.js';
13
13
  import { renderRichOutput } from './render-rich-output.js';
14
+ import { resolveDocumentNodeId } from './build-graph-identity.js';
14
15
 
15
16
  /**
16
17
  * Shared command output views.
@@ -56,12 +57,15 @@ export function createOutputView(command_name, command_items, command_options) {
56
57
  * Create a shared output view for the show command.
57
58
  *
58
59
  * @param {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }} show_output
59
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }=} command_options
60
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, document_node_ids?: BuildGraphResult['document_node_ids'], graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }=} command_options
60
61
  * @returns {ShowOutputView}
61
62
  */
62
63
  export function createShowOutputView(show_output, command_options = {}) {
63
- const shown_document_node =
64
- command_options.graph_nodes?.[`doc:${show_output.path}`];
64
+ const shown_document_node = resolveDocumentGraphNode(
65
+ command_options.graph_nodes,
66
+ command_options.document_node_ids,
67
+ show_output.path,
68
+ );
65
69
 
66
70
  return {
67
71
  command: 'show',
@@ -75,20 +79,9 @@ export function createShowOutputView(show_output, command_options = {}) {
75
79
  )
76
80
  : undefined,
77
81
  hints: [],
78
- items: show_output.resolved_links.map((resolved_link) => ({
79
- kind: 'resolved_link',
80
- label: resolved_link.label,
81
- reference: resolved_link.reference,
82
- target: createResolvedLinkTarget(
83
- resolved_link.target,
84
- command_options.repo_config?.fields ?? {},
85
- command_options.graph_nodes?.[`doc:${resolved_link.target.path}`]
86
- ? (command_options.derived_summary_evaluator?.evaluate(
87
- command_options.graph_nodes[`doc:${resolved_link.target.path}`],
88
- ) ?? null)
89
- : null,
90
- ),
91
- })),
82
+ items: show_output.resolved_links.map((resolved_link) =>
83
+ createResolvedLinkOutputItem(resolved_link, command_options),
84
+ ),
92
85
  path: show_output.path,
93
86
  rendered_source: show_output.rendered_source,
94
87
  source: show_output.source,
@@ -211,9 +204,15 @@ function createOutputNodeItem(graph_node, derived_summary, field_definitions) {
211
204
  * @param {{ kind?: string, path: string, status?: string, title: string }} target
212
205
  * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
213
206
  * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
207
+ * @param {GraphNode | undefined} graph_node
214
208
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
215
209
  */
216
- function createResolvedLinkTarget(target, field_definitions, derived_summary) {
210
+ function createResolvedLinkTarget(
211
+ target,
212
+ field_definitions,
213
+ derived_summary,
214
+ graph_node,
215
+ ) {
217
216
  /** @type {Record<string, string | string[]>} */
218
217
  const fields = {};
219
218
 
@@ -225,7 +224,7 @@ function createResolvedLinkTarget(target, field_definitions, derived_summary) {
225
224
  const resolved_target = {
226
225
  derived_summary: derived_summary ?? undefined,
227
226
  fields,
228
- id: `doc:${target.path}`,
227
+ id: graph_node ? getOutputNodeId(graph_node) : `doc:${target.path}`,
229
228
  kind: target.kind ?? 'document',
230
229
  path: target.path,
231
230
  title: target.title,
@@ -286,6 +285,53 @@ function getOutputNodeId(graph_node) {
286
285
  );
287
286
  }
288
287
 
288
+ /**
289
+ * @param {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }} resolved_link
290
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, document_node_ids?: BuildGraphResult['document_node_ids'], graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }} command_options
291
+ * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkItem}
292
+ */
293
+ function createResolvedLinkOutputItem(resolved_link, command_options) {
294
+ const target_graph_node = resolveDocumentGraphNode(
295
+ command_options.graph_nodes,
296
+ command_options.document_node_ids,
297
+ resolved_link.target.path,
298
+ );
299
+
300
+ return {
301
+ kind: 'resolved_link',
302
+ label: resolved_link.label,
303
+ reference: resolved_link.reference,
304
+ target: createResolvedLinkTarget(
305
+ resolved_link.target,
306
+ command_options.repo_config?.fields ?? {},
307
+ target_graph_node
308
+ ? (command_options.derived_summary_evaluator?.evaluate(
309
+ target_graph_node,
310
+ ) ?? null)
311
+ : null,
312
+ target_graph_node,
313
+ ),
314
+ };
315
+ }
316
+
317
+ /**
318
+ * @param {BuildGraphResult['nodes'] | undefined} graph_nodes
319
+ * @param {BuildGraphResult['document_node_ids'] | undefined} document_node_ids
320
+ * @param {string} document_path
321
+ * @returns {GraphNode | undefined}
322
+ */
323
+ function resolveDocumentGraphNode(
324
+ graph_nodes,
325
+ document_node_ids,
326
+ document_path,
327
+ ) {
328
+ if (!graph_nodes) {
329
+ return undefined;
330
+ }
331
+
332
+ return graph_nodes[resolveDocumentNodeId(document_node_ids, document_path)];
333
+ }
334
+
289
335
  /**
290
336
  * @param {GraphNode} graph_node
291
337
  * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions