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,3 +1,4 @@
1
+ /** @import * as $k$$l$parse$j$where$j$clause$k$types$k$ts from './parse-where-clause.types.ts'; */
1
2
  /* eslint-disable max-lines */
2
3
  /**
3
4
  * @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
@@ -19,6 +20,10 @@ import { parseWhereClause } from './parse-where-clause.js';
19
20
  * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
20
21
  */
21
22
 
23
+ /**
24
+ * @typedef {import('./parse-where-clause.types.ts').ParsedFieldTerm | import('./parse-where-clause.types.ts').ParsedFieldSetTerm} FieldDiagnosticTerm
25
+ */
26
+
22
27
  /**
23
28
  * @typedef {{ query_source: QuerySource, where_clause: string }} ResolvedWhereClause
24
29
  */
@@ -67,7 +72,7 @@ export function inspectQuery(
67
72
  };
68
73
  }
69
74
 
70
- const diagnostics = collectRelationDiagnostics(
75
+ const diagnostics = getQuerySemanticDiagnostics(
71
76
  repo_config,
72
77
  resolved_where_clause.query_source,
73
78
  parse_result.clauses,
@@ -143,12 +148,10 @@ export function renderQueryInspection(query_inspection, output_mode) {
143
148
  * @param {ParsedClause[]} clauses
144
149
  * @returns {PatramDiagnostic[]}
145
150
  */
146
- function collectRelationDiagnostics(repo_config, query_source, clauses) {
147
- if (!repo_config.relations) {
148
- return [];
149
- }
150
-
151
- const known_relation_names = new Set(Object.keys(repo_config.relations));
151
+ function collectSemanticDiagnostics(repo_config, query_source, clauses) {
152
+ const known_relation_names = new Set(
153
+ Object.keys(repo_config.relations ?? {}),
154
+ );
152
155
  /** @type {PatramDiagnostic[]} */
153
156
  const diagnostics = [];
154
157
 
@@ -156,22 +159,41 @@ function collectRelationDiagnostics(repo_config, query_source, clauses) {
156
159
  clauses,
157
160
  diagnostics,
158
161
  known_relation_names,
162
+ repo_config.fields ?? {},
159
163
  formatQueryDiagnosticPath(query_source),
160
164
  );
161
165
 
162
166
  return diagnostics;
163
167
  }
164
168
 
169
+ /**
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
+
165
185
  /**
166
186
  * @param {ParsedClause[]} clauses
167
187
  * @param {PatramDiagnostic[]} diagnostics
168
188
  * @param {Set<string>} known_relation_names
189
+ * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
169
190
  * @param {string} diagnostic_path
170
191
  */
171
192
  function collectClauseDiagnostics(
172
193
  clauses,
173
194
  diagnostics,
174
195
  known_relation_names,
196
+ known_field_definitions,
175
197
  diagnostic_path,
176
198
  ) {
177
199
  for (const clause of clauses) {
@@ -179,6 +201,7 @@ function collectClauseDiagnostics(
179
201
  clause.term,
180
202
  diagnostics,
181
203
  known_relation_names,
204
+ known_field_definitions,
182
205
  diagnostic_path,
183
206
  );
184
207
  }
@@ -188,12 +211,14 @@ function collectClauseDiagnostics(
188
211
  * @param {ParsedTerm} term
189
212
  * @param {PatramDiagnostic[]} diagnostics
190
213
  * @param {Set<string>} known_relation_names
214
+ * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
191
215
  * @param {string} diagnostic_path
192
216
  */
193
217
  function collectTermDiagnostics(
194
218
  term,
195
219
  diagnostics,
196
220
  known_relation_names,
221
+ known_field_definitions,
197
222
  diagnostic_path,
198
223
  ) {
199
224
  if (term.kind === 'aggregate') {
@@ -207,6 +232,7 @@ function collectTermDiagnostics(
207
232
  term.clauses,
208
233
  diagnostics,
209
234
  known_relation_names,
235
+ known_field_definitions,
210
236
  diagnostic_path,
211
237
  );
212
238
 
@@ -233,7 +259,256 @@ function collectTermDiagnostics(
233
259
  diagnostic_path,
234
260
  'relation clause',
235
261
  );
262
+
263
+ return;
264
+ }
265
+
266
+ collectFieldDiagnostics(
267
+ /** @type {FieldDiagnosticTerm} */ (term),
268
+ diagnostics,
269
+ known_field_definitions,
270
+ diagnostic_path,
271
+ );
272
+ }
273
+
274
+ /**
275
+ * @param {FieldDiagnosticTerm} term
276
+ * @param {PatramDiagnostic[]} diagnostics
277
+ * @param {Record<string, import('./load-patram-config.types.ts').MetadataFieldConfig>} known_field_definitions
278
+ * @param {string} diagnostic_path
279
+ */
280
+ function collectFieldDiagnostics(
281
+ term,
282
+ diagnostics,
283
+ known_field_definitions,
284
+ diagnostic_path,
285
+ ) {
286
+ const field_diagnostics_collector = getFieldDiagnosticsCollector(
287
+ term.field_name,
288
+ );
289
+
290
+ if (field_diagnostics_collector) {
291
+ field_diagnostics_collector(term, diagnostics, diagnostic_path);
292
+ return;
293
+ }
294
+
295
+ if (term.field_name.startsWith('$')) {
296
+ diagnostics.push(
297
+ createQueryDiagnostic(
298
+ diagnostic_path,
299
+ term.column,
300
+ `Reserved field "${term.field_name}" is not available.`,
301
+ 'query.reserved_field',
302
+ ),
303
+ );
304
+
305
+ return;
236
306
  }
307
+
308
+ const field_definition = known_field_definitions[term.field_name];
309
+
310
+ if (!field_definition) {
311
+ diagnostics.push(
312
+ createQueryDiagnostic(
313
+ diagnostic_path,
314
+ term.column,
315
+ `Unknown field "${term.field_name}".`,
316
+ 'query.unknown_field',
317
+ ),
318
+ );
319
+
320
+ return;
321
+ }
322
+
323
+ if (term.kind === 'field_set') {
324
+ if (term.operator === 'in' || term.operator === 'not in') {
325
+ return;
326
+ }
327
+ }
328
+
329
+ if (term.kind === 'field') {
330
+ const operator_diagnostic = getMetadataFieldOperatorDiagnostic(
331
+ term,
332
+ field_definition,
333
+ diagnostic_path,
334
+ );
335
+
336
+ if (operator_diagnostic) {
337
+ diagnostics.push(operator_diagnostic);
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * @param {string} field_name
344
+ * @returns {((term: FieldDiagnosticTerm, diagnostics: PatramDiagnostic[], diagnostic_path: string) => void) | null}
345
+ */
346
+ function getFieldDiagnosticsCollector(field_name) {
347
+ if (field_name === 'title') {
348
+ return collectTitleFieldDiagnostics;
349
+ }
350
+
351
+ if (
352
+ field_name === '$id' ||
353
+ field_name === '$class' ||
354
+ field_name === '$path'
355
+ ) {
356
+ return collectStructuralFieldDiagnostics;
357
+ }
358
+
359
+ return null;
360
+ }
361
+
362
+ /**
363
+ * @param {FieldDiagnosticTerm} term
364
+ * @param {PatramDiagnostic[]} diagnostics
365
+ * @param {string} diagnostic_path
366
+ */
367
+ function collectTitleFieldDiagnostics(term, diagnostics, diagnostic_path) {
368
+ const operator = term.kind === 'field' ? term.operator : term.operator;
369
+ const allowed = new Set(['=', '!=', 'in', 'not in', '~']);
370
+
371
+ if (allowed.has(operator)) {
372
+ return;
373
+ }
374
+
375
+ diagnostics.push(
376
+ createQueryDiagnostic(
377
+ diagnostic_path,
378
+ term.column,
379
+ `Field "title" does not support the "${operator}" operator.`,
380
+ 'query.invalid_operator',
381
+ ),
382
+ );
383
+ }
384
+
385
+ /**
386
+ * @param {FieldDiagnosticTerm} term
387
+ * @param {PatramDiagnostic[]} diagnostics
388
+ * @param {string} diagnostic_path
389
+ */
390
+ function collectStructuralFieldDiagnostics(term, diagnostics, diagnostic_path) {
391
+ const operator = term.kind === 'field' ? term.operator : term.operator;
392
+ const allowed_by_field = new Map([
393
+ ['$class', new Set(['=', '!=', 'in', 'not in'])],
394
+ ['$id', new Set(['=', '!=', 'in', 'not in', '^='])],
395
+ ['$path', new Set(['=', '!=', 'in', 'not in', '^='])],
396
+ ]);
397
+ const allowed = allowed_by_field.get(term.field_name);
398
+
399
+ if (!allowed || allowed.has(operator)) {
400
+ return;
401
+ }
402
+
403
+ diagnostics.push(
404
+ createQueryDiagnostic(
405
+ diagnostic_path,
406
+ term.column,
407
+ `Field "${term.field_name}" does not support the "${operator}" operator.`,
408
+ 'query.invalid_operator',
409
+ ),
410
+ );
411
+ }
412
+
413
+ /**
414
+ * @param {import('./parse-where-clause.types.ts').ParsedFieldTerm} term
415
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
416
+ * @param {string} diagnostic_path
417
+ * @returns {PatramDiagnostic | null}
418
+ */
419
+ function getMetadataFieldOperatorDiagnostic(
420
+ term,
421
+ field_definition,
422
+ diagnostic_path,
423
+ ) {
424
+ if (supportsMetadataFieldOperator(term.operator, field_definition)) {
425
+ return null;
426
+ }
427
+
428
+ return createQueryDiagnostic(
429
+ diagnostic_path,
430
+ term.column,
431
+ getUnsupportedOperatorMessage(term.field_name, term.operator),
432
+ 'query.invalid_operator',
433
+ );
434
+ }
435
+
436
+ /**
437
+ * @param {string} operator
438
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
439
+ * @returns {boolean}
440
+ */
441
+ function supportsMetadataFieldOperator(operator, field_definition) {
442
+ if (operator === '=' || operator === '!=') {
443
+ return true;
444
+ }
445
+
446
+ if (operator === '^=') {
447
+ return supportsPrefixOperator(field_definition);
448
+ }
449
+
450
+ if (operator === '~') {
451
+ return supportsContainsOperator(field_definition);
452
+ }
453
+
454
+ return supportsOrderedComparison(operator, field_definition.type);
455
+ }
456
+
457
+ /**
458
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
459
+ * @returns {boolean}
460
+ */
461
+ function supportsPrefixOperator(field_definition) {
462
+ if (field_definition.type === 'path') {
463
+ return true;
464
+ }
465
+
466
+ return (
467
+ field_definition.type === 'string' &&
468
+ field_definition.query?.prefix === true
469
+ );
470
+ }
471
+
472
+ /**
473
+ * @param {import('./load-patram-config.types.ts').MetadataFieldConfig} field_definition
474
+ * @returns {boolean}
475
+ */
476
+ function supportsContainsOperator(field_definition) {
477
+ return (
478
+ field_definition.type === 'string' &&
479
+ field_definition.query?.contains === true
480
+ );
481
+ }
482
+
483
+ /**
484
+ * @param {string} operator
485
+ * @param {string} field_type
486
+ * @returns {boolean}
487
+ */
488
+ function supportsOrderedComparison(operator, field_type) {
489
+ if (
490
+ operator !== '<' &&
491
+ operator !== '<=' &&
492
+ operator !== '>' &&
493
+ operator !== '>='
494
+ ) {
495
+ return false;
496
+ }
497
+
498
+ return (
499
+ field_type === 'integer' ||
500
+ field_type === 'date' ||
501
+ field_type === 'date_time'
502
+ );
503
+ }
504
+
505
+ /**
506
+ * @param {string} field_name
507
+ * @param {string} operator
508
+ * @returns {string}
509
+ */
510
+ function getUnsupportedOperatorMessage(field_name, operator) {
511
+ return `Field "${field_name}" does not support the "${operator}" operator.`;
237
512
  }
238
513
 
239
514
  /**
@@ -290,7 +565,6 @@ function collectTraversalDiagnostic(
290
565
 
291
566
  /**
292
567
  * @param {QueryInspectionSuccess} query_inspection
293
- * @returns {object}
294
568
  */
295
569
  function formatJsonQueryInspection(query_inspection) {
296
570
  if (query_inspection.inspection_mode === 'lint') {
@@ -489,11 +763,12 @@ function formatQueryDiagnosticPath(query_source) {
489
763
  * @param {string} diagnostic_path
490
764
  * @param {number} column
491
765
  * @param {string} message
766
+ * @param {PatramDiagnostic['code']} [code]
492
767
  * @returns {PatramDiagnostic}
493
768
  */
494
- function createQueryDiagnostic(diagnostic_path, column, message) {
769
+ function createQueryDiagnostic(diagnostic_path, column, message, code) {
495
770
  return {
496
- code: 'query.unknown_relation',
771
+ code: code ?? 'query.unknown_relation',
497
772
  column,
498
773
  level: 'error',
499
774
  line: 1,
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @import { ResolvedOutputMode } from './output-view.types.ts';
3
+ * @import { FieldDiscoveryResult, FieldDiscoverySuggestion } from './discover-fields.types.ts';
4
+ */
5
+
6
+ import { Ansis } from 'ansis';
7
+
8
+ /**
9
+ * Render field discovery output.
10
+ *
11
+ * @param {FieldDiscoveryResult} discovery_result
12
+ * @param {ResolvedOutputMode} output_mode
13
+ * @returns {string}
14
+ */
15
+ export function renderFieldDiscovery(discovery_result, output_mode) {
16
+ if (output_mode.renderer_name === 'json') {
17
+ return `${JSON.stringify(formatJsonFieldDiscovery(discovery_result), null, 2)}\n`;
18
+ }
19
+
20
+ return renderTextFieldDiscovery(discovery_result, output_mode);
21
+ }
22
+
23
+ /**
24
+ * @param {FieldDiscoveryResult} discovery_result
25
+ * @returns {{ fields: Array<Record<string, unknown>>, summary: FieldDiscoveryResult['summary'] }}
26
+ */
27
+ function formatJsonFieldDiscovery(discovery_result) {
28
+ return {
29
+ fields: discovery_result.fields.map(formatJsonFieldSuggestion),
30
+ summary: discovery_result.summary,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * @param {FieldDiscoverySuggestion} field_suggestion
36
+ * @returns {Record<string, unknown>}
37
+ */
38
+ function formatJsonFieldSuggestion(field_suggestion) {
39
+ return {
40
+ confidence: field_suggestion.confidence,
41
+ conflicting_evidence: field_suggestion.conflicting_evidence,
42
+ evidence_references: field_suggestion.evidence_references,
43
+ likely_class_usage: field_suggestion.likely_class_usage,
44
+ likely_multiplicity: field_suggestion.likely_multiplicity,
45
+ likely_type: field_suggestion.likely_type,
46
+ name: field_suggestion.name,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * @param {FieldDiscoveryResult} discovery_result
52
+ * @param {ResolvedOutputMode} output_mode
53
+ * @returns {string}
54
+ */
55
+ function renderTextFieldDiscovery(discovery_result, output_mode) {
56
+ const ansi = new Ansis(
57
+ output_mode.renderer_name === 'rich' && output_mode.color_enabled ? 3 : 0,
58
+ );
59
+ /** @type {string[]} */
60
+ const output_lines = [];
61
+
62
+ output_lines.push(
63
+ output_mode.renderer_name === 'rich'
64
+ ? ansi.green('Field discovery')
65
+ : 'Field discovery',
66
+ );
67
+
68
+ output_lines.push(
69
+ output_mode.renderer_name === 'rich'
70
+ ? ansi.gray(
71
+ `Found ${discovery_result.summary.count} suggested fields from ${discovery_result.summary.source_file_count} source files.`,
72
+ )
73
+ : `Found ${discovery_result.summary.count} suggested fields from ${discovery_result.summary.source_file_count} source files.`,
74
+ );
75
+
76
+ for (const field_suggestion of discovery_result.fields) {
77
+ output_lines.push(
78
+ '',
79
+ ...formatTextFieldSuggestion(field_suggestion, {
80
+ header: (value) =>
81
+ output_mode.renderer_name === 'rich' ? ansi.green(value) : value,
82
+ label: (value) =>
83
+ output_mode.renderer_name === 'rich' ? ansi.gray(value) : value,
84
+ }),
85
+ );
86
+ }
87
+
88
+ if (discovery_result.fields.length === 0) {
89
+ output_lines.push('', 'No field candidates discovered.');
90
+ }
91
+
92
+ return `${output_lines.join('\n')}\n`;
93
+ }
94
+
95
+ /**
96
+ * @param {FieldDiscoverySuggestion} field_suggestion
97
+ * @param {{ header: (value: string) => string, label: (value: string) => string }} render_options
98
+ * @returns {string[]}
99
+ */
100
+ function formatTextFieldSuggestion(field_suggestion, render_options) {
101
+ /** @type {string[]} */
102
+ const lines = [render_options.header(field_suggestion.name)];
103
+
104
+ lines.push(
105
+ `${render_options.label(' likely type:')} ${field_suggestion.likely_type.name}`,
106
+ `${render_options.label(' likely multiplicity:')} ${field_suggestion.likely_multiplicity.name}`,
107
+ `${render_options.label(' likely class usage:')} ${field_suggestion.likely_class_usage.classes.join(', ')}`,
108
+ `${render_options.label(' confidence:')} ${formatConfidence(field_suggestion.confidence)}`,
109
+ );
110
+
111
+ if (field_suggestion.evidence_references.length > 0) {
112
+ lines.push(render_options.label(' evidence:'));
113
+ lines.push(
114
+ ...field_suggestion.evidence_references.map(
115
+ (evidence_reference) =>
116
+ `${render_options.label(' ')}${formatEvidenceReference(evidence_reference)}`,
117
+ ),
118
+ );
119
+ }
120
+
121
+ if (field_suggestion.conflicting_evidence.length > 0) {
122
+ lines.push(render_options.label(' conflicting evidence:'));
123
+ lines.push(
124
+ ...field_suggestion.conflicting_evidence.map(
125
+ (evidence_reference) =>
126
+ `${render_options.label(' ')}${formatEvidenceReference(evidence_reference)}`,
127
+ ),
128
+ );
129
+ }
130
+
131
+ return lines;
132
+ }
133
+
134
+ /**
135
+ * @param {import('./discover-fields.types.ts').FieldDiscoveryEvidenceReference} evidence_reference
136
+ * @returns {string}
137
+ */
138
+ function formatEvidenceReference(evidence_reference) {
139
+ return `${evidence_reference.path}:${evidence_reference.line}:${evidence_reference.column} ${JSON.stringify(evidence_reference.value)}`;
140
+ }
141
+
142
+ /**
143
+ * @param {number} confidence
144
+ * @returns {string}
145
+ */
146
+ function formatConfidence(confidence) {
147
+ return confidence.toFixed(2);
148
+ }
@@ -55,19 +55,19 @@ export function renderJsonOutput(output_view) {
55
55
 
56
56
  /**
57
57
  * @param {OutputNodeItem} output_item
58
- * @returns {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, id: string, kind: string, title: string, path: string, status?: string }}
58
+ * @returns {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }}
59
59
  */
60
60
  function formatJsonQueryItem(output_item) {
61
- /** @type {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, id: string, kind: string, title: string, path: string, status?: string }} */
61
+ /** @type {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }} */
62
62
  const query_item = {
63
- id: output_item.id,
64
- kind: output_item.node_kind,
63
+ $class: output_item.node_kind,
64
+ $id: output_item.id,
65
+ fields: output_item.fields,
65
66
  title: output_item.title,
66
- path: output_item.path,
67
67
  };
68
68
 
69
- if (output_item.status) {
70
- query_item.status = output_item.status;
69
+ if (output_item.path) {
70
+ query_item.$path = output_item.path;
71
71
  }
72
72
 
73
73
  if (output_item.derived_summary) {
@@ -96,25 +96,23 @@ function formatJsonStoredQuery(output_item) {
96
96
 
97
97
  /**
98
98
  * @param {OutputResolvedLinkItem} output_item
99
- * @returns {{ label: string, reference: number, target: { derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind?: string, path: string, status?: string, title: string } }}
99
+ * @returns {{ label: string, reference: number, target: { '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string } }}
100
100
  */
101
101
  function formatJsonResolvedLink(output_item) {
102
- /** @type {{ label: string, reference: number, target: { derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind?: string, path: string, status?: string, title: string } }} */
102
+ /** @type {{ label: string, reference: number, target: { '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string } }} */
103
103
  const resolved_link = {
104
104
  reference: output_item.reference,
105
105
  label: output_item.label,
106
106
  target: {
107
+ $class: output_item.target.kind,
108
+ $id: output_item.target.id,
109
+ fields: output_item.target.fields,
107
110
  title: output_item.target.title,
108
- path: output_item.target.path,
109
111
  },
110
112
  };
111
113
 
112
- if (output_item.target.kind) {
113
- resolved_link.target.kind = output_item.target.kind;
114
- }
115
-
116
- if (output_item.target.status) {
117
- resolved_link.target.status = output_item.target.status;
114
+ if (output_item.target.path) {
115
+ resolved_link.target.$path = output_item.target.path;
118
116
  }
119
117
 
120
118
  if (output_item.target.derived_summary) {
@@ -133,18 +131,19 @@ function formatJsonResolvedLink(output_item) {
133
131
 
134
132
  /**
135
133
  * @param {OutputNodeItem} output_item
136
- * @returns {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind: string, title: string, path: string, status?: string }}
134
+ * @returns {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }}
137
135
  */
138
136
  function formatJsonShowDocument(output_item) {
139
- /** @type {{ derived?: Record<string, boolean | number | string | null>, derived_summary?: string, kind: string, title: string, path: string, status?: string }} */
137
+ /** @type {{ '$class': string, '$id': string, '$path'?: string, derived?: Record<string, boolean | number | string | null>, derived_summary?: string, fields: Record<string, string | string[]>, title: string }} */
140
138
  const document_summary = {
141
- kind: output_item.node_kind,
142
- path: output_item.path,
139
+ $class: output_item.node_kind,
140
+ $id: output_item.id,
141
+ fields: output_item.fields,
143
142
  title: output_item.title,
144
143
  };
145
144
 
146
- if (output_item.status) {
147
- document_summary.status = output_item.status;
145
+ if (output_item.path) {
146
+ document_summary.$path = output_item.path;
148
147
  }
149
148
 
150
149
  if (output_item.derived_summary) {