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.
- package/lib/build-graph-identity.js +20 -19
- package/lib/build-graph.js +369 -16
- package/lib/build-graph.types.ts +5 -2
- package/lib/check-directive-metadata.js +516 -0
- package/lib/check-directive-value.js +282 -0
- package/lib/check-graph.js +24 -5
- package/lib/cli-help-metadata.js +44 -16
- package/lib/derived-summary.js +10 -8
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +427 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/format-node-header.js +9 -7
- package/lib/format-output-metadata.js +15 -23
- package/lib/layout-stored-queries.js +6 -60
- package/lib/load-patram-config.js +433 -96
- package/lib/load-patram-config.types.ts +98 -3
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +14 -6
- package/lib/parse-cli-arguments.types.ts +1 -1
- package/lib/parse-where-clause.js +117 -51
- package/lib/parse-where-clause.types.ts +4 -2
- package/lib/patram-cli.js +36 -4
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/query-graph.js +241 -22
- package/lib/query-inspection.js +285 -10
- package/lib/render-field-discovery.js +148 -0
- package/lib/render-json-output.js +21 -22
- package/lib/render-output-view.js +240 -19
- package/lib/render-plain-output.js +1 -1
- package/lib/render-rich-output.js +1 -1
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/show-document.js +51 -7
- package/package.json +5 -5
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/* eslint-disable max-lines, max-lines-per-function */
|
|
2
|
+
/**
|
|
3
|
+
* @import { ClaimOrigin, PatramClaim } from './parse-claims.types.ts';
|
|
4
|
+
* @import {
|
|
5
|
+
* DiscoveredFieldMultiplicity,
|
|
6
|
+
* DiscoveredFieldTypeName,
|
|
7
|
+
* FieldDiscoveryClassUsage,
|
|
8
|
+
* FieldDiscoveryEvidenceReference,
|
|
9
|
+
* FieldDiscoveryMultiplicitySuggestion,
|
|
10
|
+
* FieldDiscoveryResult,
|
|
11
|
+
* FieldDiscoverySuggestion,
|
|
12
|
+
* FieldDiscoveryTypeSuggestion,
|
|
13
|
+
* } from './discover-fields.types.ts';
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import process from 'node:process';
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { DEFAULT_INCLUDE_PATTERNS } from './source-file-defaults.js';
|
|
21
|
+
import { listSourceFiles } from './list-source-files.js';
|
|
22
|
+
import { parseSourceFile } from './parse-claims.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Field discovery from source claims.
|
|
26
|
+
*
|
|
27
|
+
* Scans the repository source files directly, infers likely metadata fields,
|
|
28
|
+
* and reports advisory suggestions without requiring repo config to load.
|
|
29
|
+
*
|
|
30
|
+
* Kind: discovery
|
|
31
|
+
* Status: active
|
|
32
|
+
* Tracked in: ../docs/plans/v1/field-model-redesign.md
|
|
33
|
+
* Decided by: ../docs/decisions/field-discovery-workflow.md
|
|
34
|
+
* @patram
|
|
35
|
+
* @see {@link ./render-field-discovery.js}
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const TYPE_NAME_ORDER = /** @type {const} */ ([
|
|
39
|
+
'integer',
|
|
40
|
+
'date_time',
|
|
41
|
+
'date',
|
|
42
|
+
'glob',
|
|
43
|
+
'path',
|
|
44
|
+
'enum',
|
|
45
|
+
'string',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const INTEGER_PATTERN = /^-?\d+$/du;
|
|
49
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/du;
|
|
50
|
+
const DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/du;
|
|
51
|
+
const ENUM_PATTERN = /^[a-z0-9_][a-z0-9_-]*$/du;
|
|
52
|
+
const PATH_PATTERN = /^[a-z0-9_.-]+\.[a-z0-9]+$/du;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {FieldDiscoveryClassUsage & { confidence: number }} InferredFieldClassUsage
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {(value: string) => number} FieldTypeScorer
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Discover likely field schema from source files.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} [project_directory]
|
|
66
|
+
* @returns {Promise<FieldDiscoveryResult>}
|
|
67
|
+
*/
|
|
68
|
+
export async function discoverFields(project_directory = process.cwd()) {
|
|
69
|
+
const source_file_paths = await listSourceFiles(
|
|
70
|
+
DEFAULT_INCLUDE_PATTERNS,
|
|
71
|
+
project_directory,
|
|
72
|
+
);
|
|
73
|
+
const parse_results = await Promise.all(
|
|
74
|
+
source_file_paths.map(async (source_file_path) => {
|
|
75
|
+
const source_text = await readFile(
|
|
76
|
+
resolve(project_directory, source_file_path),
|
|
77
|
+
'utf8',
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
claims: parseSourceFile({
|
|
82
|
+
path: source_file_path,
|
|
83
|
+
source: source_text,
|
|
84
|
+
}).claims,
|
|
85
|
+
path: source_file_path,
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
/** @type {FieldObservation[]} */
|
|
90
|
+
const field_observations = parse_results.flatMap((parse_result) => {
|
|
91
|
+
/** @type {Set<string>} */
|
|
92
|
+
const document_classes = new Set();
|
|
93
|
+
|
|
94
|
+
for (const claim of parse_result.claims) {
|
|
95
|
+
if (
|
|
96
|
+
claim.type === 'directive' &&
|
|
97
|
+
claim.name === 'kind' &&
|
|
98
|
+
typeof claim.value === 'string' &&
|
|
99
|
+
claim.value.length > 0
|
|
100
|
+
) {
|
|
101
|
+
document_classes.add(claim.value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return parse_result.claims.flatMap((claim) => {
|
|
106
|
+
if (
|
|
107
|
+
claim.type !== 'directive' ||
|
|
108
|
+
!claim.name ||
|
|
109
|
+
claim.name.startsWith('$') ||
|
|
110
|
+
typeof claim.value !== 'string' ||
|
|
111
|
+
claim.value.length === 0
|
|
112
|
+
) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
class_names: new Set(document_classes),
|
|
119
|
+
document_id: claim.document_id,
|
|
120
|
+
name: claim.name,
|
|
121
|
+
origin: claim.origin,
|
|
122
|
+
value: claim.value,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
/** @type {Map<string, FieldBucket>} */
|
|
128
|
+
const field_buckets = field_observations.reduce(
|
|
129
|
+
(buckets, field_observation) => {
|
|
130
|
+
const bucket = buckets.get(field_observation.name) ?? {
|
|
131
|
+
name: field_observation.name,
|
|
132
|
+
observations: [],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
bucket.observations.push(field_observation);
|
|
136
|
+
buckets.set(field_observation.name, bucket);
|
|
137
|
+
return buckets;
|
|
138
|
+
},
|
|
139
|
+
new Map(),
|
|
140
|
+
);
|
|
141
|
+
const fields = [...field_buckets.values()]
|
|
142
|
+
.map(buildFieldSuggestion)
|
|
143
|
+
.sort((left_suggestion, right_suggestion) =>
|
|
144
|
+
left_suggestion.confidence !== right_suggestion.confidence
|
|
145
|
+
? right_suggestion.confidence - left_suggestion.confidence
|
|
146
|
+
: left_suggestion.name.localeCompare(right_suggestion.name, 'en'),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
fields,
|
|
151
|
+
summary: {
|
|
152
|
+
claim_count: parse_results.reduce(
|
|
153
|
+
(sum, parse_result) => sum + parse_result.claims.length,
|
|
154
|
+
0,
|
|
155
|
+
),
|
|
156
|
+
count: fields.length,
|
|
157
|
+
source_file_count: source_file_paths.length,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {FieldBucket} field_bucket
|
|
164
|
+
* @returns {FieldDiscoverySuggestion}
|
|
165
|
+
*/
|
|
166
|
+
function buildFieldSuggestion(field_bucket) {
|
|
167
|
+
const type_result = inferFieldType(field_bucket.observations);
|
|
168
|
+
const multiplicity_result = inferFieldMultiplicity(field_bucket.observations);
|
|
169
|
+
const class_usage_result = inferFieldClassUsage(field_bucket.observations);
|
|
170
|
+
const evidence_references = buildEvidenceReferences(
|
|
171
|
+
field_bucket.observations,
|
|
172
|
+
);
|
|
173
|
+
const conflicting_evidence = buildEvidenceReferences(
|
|
174
|
+
field_bucket.observations.filter(
|
|
175
|
+
(field_observation) =>
|
|
176
|
+
scoreFieldValue(field_observation.value, type_result.name) === 0,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
confidence:
|
|
182
|
+
Math.round(
|
|
183
|
+
((type_result.confidence +
|
|
184
|
+
multiplicity_result.confidence +
|
|
185
|
+
class_usage_result.confidence) /
|
|
186
|
+
3) *
|
|
187
|
+
100,
|
|
188
|
+
) / 100,
|
|
189
|
+
conflicting_evidence,
|
|
190
|
+
evidence_references,
|
|
191
|
+
likely_class_usage: {
|
|
192
|
+
classes: class_usage_result.classes,
|
|
193
|
+
},
|
|
194
|
+
likely_multiplicity: multiplicity_result,
|
|
195
|
+
likely_type: type_result,
|
|
196
|
+
name: field_bucket.name,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {FieldObservation[]} observations
|
|
202
|
+
* @returns {FieldDiscoveryEvidenceReference[]}
|
|
203
|
+
*/
|
|
204
|
+
function buildEvidenceReferences(observations) {
|
|
205
|
+
return observations
|
|
206
|
+
.map((observation) => ({
|
|
207
|
+
column: observation.origin.column,
|
|
208
|
+
line: observation.origin.line,
|
|
209
|
+
path: observation.origin.path,
|
|
210
|
+
value: observation.value,
|
|
211
|
+
}))
|
|
212
|
+
.sort(compareEvidenceReferences);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {FieldObservation[]} observations
|
|
217
|
+
* @returns {FieldDiscoveryMultiplicitySuggestion}
|
|
218
|
+
*/
|
|
219
|
+
function inferFieldMultiplicity(observations) {
|
|
220
|
+
/** @type {Map<string, Set<string>>} */
|
|
221
|
+
const values_by_document = observations.reduce((values, observation) => {
|
|
222
|
+
const current_values = values.get(observation.document_id);
|
|
223
|
+
|
|
224
|
+
if (current_values) {
|
|
225
|
+
current_values.add(observation.value);
|
|
226
|
+
} else {
|
|
227
|
+
values.set(observation.document_id, new Set([observation.value]));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return values;
|
|
231
|
+
}, new Map());
|
|
232
|
+
const repeated_identical_documents = [...values_by_document.values()].reduce(
|
|
233
|
+
(count, values) => {
|
|
234
|
+
if (values.size > 1) {
|
|
235
|
+
return Infinity;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return values.size === 1 ? count + 1 : count;
|
|
239
|
+
},
|
|
240
|
+
0,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (repeated_identical_documents === Infinity) {
|
|
244
|
+
return {
|
|
245
|
+
confidence: 1,
|
|
246
|
+
name: 'multiple',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
confidence:
|
|
252
|
+
Math.round(
|
|
253
|
+
(values_by_document.size > 1 && repeated_identical_documents > 0
|
|
254
|
+
? 0.9
|
|
255
|
+
: 0.8) * 100,
|
|
256
|
+
) / 100,
|
|
257
|
+
name: 'single',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {FieldObservation[]} observations
|
|
263
|
+
* @returns {InferredFieldClassUsage}
|
|
264
|
+
*/
|
|
265
|
+
function inferFieldClassUsage(observations) {
|
|
266
|
+
/** @type {Map<string, number>} */
|
|
267
|
+
const class_counts = new Map();
|
|
268
|
+
let documented_observation_count = 0;
|
|
269
|
+
|
|
270
|
+
for (const observation of observations) {
|
|
271
|
+
if (observation.class_names.size === 0) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
documented_observation_count += 1;
|
|
276
|
+
|
|
277
|
+
for (const class_name of observation.class_names) {
|
|
278
|
+
class_counts.set(class_name, (class_counts.get(class_name) ?? 0) + 1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (class_counts.size === 0) {
|
|
283
|
+
return {
|
|
284
|
+
confidence: 0.2,
|
|
285
|
+
classes: ['document'],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
confidence:
|
|
291
|
+
Math.round(
|
|
292
|
+
(documented_observation_count / Math.max(observations.length, 1)) * 100,
|
|
293
|
+
) / 100,
|
|
294
|
+
classes: [...class_counts.keys()].sort((left_class, right_class) =>
|
|
295
|
+
left_class.localeCompare(right_class, 'en'),
|
|
296
|
+
),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param {FieldObservation[]} observations
|
|
302
|
+
* @returns {FieldDiscoveryTypeSuggestion}
|
|
303
|
+
*/
|
|
304
|
+
function inferFieldType(observations) {
|
|
305
|
+
/** @type {FieldDiscoveryTypeSuggestion[]} */
|
|
306
|
+
const type_candidates = TYPE_NAME_ORDER.map((type_name) => ({
|
|
307
|
+
confidence: scoreFieldType(observations, type_name),
|
|
308
|
+
name: type_name,
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
type_candidates.sort((left_candidate, right_candidate) => {
|
|
312
|
+
if (left_candidate.confidence !== right_candidate.confidence) {
|
|
313
|
+
return right_candidate.confidence - left_candidate.confidence;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
TYPE_NAME_ORDER.indexOf(left_candidate.name) -
|
|
318
|
+
TYPE_NAME_ORDER.indexOf(right_candidate.name)
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return type_candidates[0];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {FieldDiscoveryEvidenceReference} left_reference
|
|
327
|
+
* @param {FieldDiscoveryEvidenceReference} right_reference
|
|
328
|
+
* @returns {number}
|
|
329
|
+
*/
|
|
330
|
+
function compareEvidenceReferences(left_reference, right_reference) {
|
|
331
|
+
const path_compare = left_reference.path.localeCompare(
|
|
332
|
+
right_reference.path,
|
|
333
|
+
'en',
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (path_compare !== 0) {
|
|
337
|
+
return path_compare;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (left_reference.line !== right_reference.line) {
|
|
341
|
+
return left_reference.line - right_reference.line;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (left_reference.column !== right_reference.column) {
|
|
345
|
+
return left_reference.column - right_reference.column;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return left_reference.value.localeCompare(right_reference.value, 'en');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {FieldObservation[]} observations
|
|
353
|
+
* @param {DiscoveredFieldTypeName} field_type_name
|
|
354
|
+
* @returns {number}
|
|
355
|
+
*/
|
|
356
|
+
function scoreFieldType(observations, field_type_name) {
|
|
357
|
+
if (observations.length === 0) {
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const total_score = observations.reduce(
|
|
362
|
+
(sum, observation) =>
|
|
363
|
+
sum + scoreFieldValue(observation.value, field_type_name),
|
|
364
|
+
0,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return Math.round((total_score / observations.length) * 100) / 100;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* @param {string} value
|
|
372
|
+
* @param {DiscoveredFieldTypeName} field_type_name
|
|
373
|
+
* @returns {number}
|
|
374
|
+
*/
|
|
375
|
+
function scoreFieldValue(value, field_type_name) {
|
|
376
|
+
const scorer = FIELD_TYPE_SCORERS[field_type_name];
|
|
377
|
+
return scorer ? scorer(value) : 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** @type {Record<DiscoveredFieldTypeName, FieldTypeScorer>} */
|
|
381
|
+
const FIELD_TYPE_SCORERS = {
|
|
382
|
+
date: (value) => (DATE_PATTERN.test(value) ? 1 : 0),
|
|
383
|
+
date_time: (value) => (DATE_TIME_PATTERN.test(value) ? 1 : 0),
|
|
384
|
+
enum: (value) =>
|
|
385
|
+
ENUM_PATTERN.test(value) && value.includes(' ') === false ? 1 : 0,
|
|
386
|
+
glob: (value) =>
|
|
387
|
+
value.includes('*') ||
|
|
388
|
+
value.includes('?') ||
|
|
389
|
+
value.includes('[') ||
|
|
390
|
+
value.includes(']')
|
|
391
|
+
? 1
|
|
392
|
+
: 0,
|
|
393
|
+
integer: (value) => (INTEGER_PATTERN.test(value) ? 1 : 0),
|
|
394
|
+
path: (value) =>
|
|
395
|
+
!(
|
|
396
|
+
value.includes('/') ||
|
|
397
|
+
PATH_PATTERN.test(value) ||
|
|
398
|
+
value.startsWith('docs/') ||
|
|
399
|
+
value.startsWith('lib/') ||
|
|
400
|
+
value.startsWith('test/')
|
|
401
|
+
)
|
|
402
|
+
? 0
|
|
403
|
+
: value.includes('*') ||
|
|
404
|
+
value.includes('?') ||
|
|
405
|
+
value.includes('[') ||
|
|
406
|
+
value.includes(']')
|
|
407
|
+
? 0.8
|
|
408
|
+
: 1,
|
|
409
|
+
string: () => 0.5,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @typedef {{
|
|
414
|
+
* class_names: Set<string>,
|
|
415
|
+
* document_id: string,
|
|
416
|
+
* name: string,
|
|
417
|
+
* origin: ClaimOrigin,
|
|
418
|
+
* value: string,
|
|
419
|
+
* }} FieldObservation
|
|
420
|
+
*/
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @typedef {{
|
|
424
|
+
* name: string,
|
|
425
|
+
* observations: FieldObservation[],
|
|
426
|
+
* }} FieldBucket
|
|
427
|
+
*/
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type DiscoveredFieldTypeName =
|
|
2
|
+
| 'date'
|
|
3
|
+
| 'date_time'
|
|
4
|
+
| 'enum'
|
|
5
|
+
| 'glob'
|
|
6
|
+
| 'integer'
|
|
7
|
+
| 'path'
|
|
8
|
+
| 'string';
|
|
9
|
+
|
|
10
|
+
export type DiscoveredFieldMultiplicity = 'multiple' | 'single';
|
|
11
|
+
|
|
12
|
+
export interface FieldDiscoveryEvidenceReference {
|
|
13
|
+
column: number;
|
|
14
|
+
line: number;
|
|
15
|
+
path: string;
|
|
16
|
+
value: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FieldDiscoveryClassUsage {
|
|
20
|
+
classes: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FieldDiscoveryTypeSuggestion {
|
|
24
|
+
confidence: number;
|
|
25
|
+
name: DiscoveredFieldTypeName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FieldDiscoveryMultiplicitySuggestion {
|
|
29
|
+
confidence: number;
|
|
30
|
+
name: DiscoveredFieldMultiplicity;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FieldDiscoverySuggestion {
|
|
34
|
+
confidence: number;
|
|
35
|
+
conflicting_evidence: FieldDiscoveryEvidenceReference[];
|
|
36
|
+
evidence_references: FieldDiscoveryEvidenceReference[];
|
|
37
|
+
likely_class_usage: FieldDiscoveryClassUsage;
|
|
38
|
+
likely_multiplicity: FieldDiscoveryMultiplicitySuggestion;
|
|
39
|
+
likely_type: FieldDiscoveryTypeSuggestion;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FieldDiscoverySummary {
|
|
44
|
+
claim_count: number;
|
|
45
|
+
count: number;
|
|
46
|
+
source_file_count: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FieldDiscoveryResult {
|
|
50
|
+
fields: FieldDiscoverySuggestion[];
|
|
51
|
+
summary: FieldDiscoverySummary;
|
|
52
|
+
}
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
* @returns {string}
|
|
4
4
|
*/
|
|
5
5
|
export function formatNodeHeader(output_item) {
|
|
6
|
-
if (
|
|
7
|
-
return
|
|
6
|
+
if (output_item.path) {
|
|
7
|
+
return `${output_item.node_kind} ${output_item.path}`;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
return `${output_item.node_kind} ${output_item.id}`;
|
|
10
|
+
return `${output_item.node_kind} ${getOutputNodeKey(output_item.id)}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @param {
|
|
15
|
-
* @returns {
|
|
14
|
+
* @param {string} node_id
|
|
15
|
+
* @returns {string}
|
|
16
16
|
*/
|
|
17
|
-
|
|
18
|
-
return
|
|
17
|
+
function getOutputNodeKey(node_id) {
|
|
18
|
+
return node_id.includes(':')
|
|
19
|
+
? node_id.split(':').slice(1).join(':')
|
|
20
|
+
: node_id;
|
|
19
21
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { formatDerivedSummaryRow } from './format-derived-summary-row.js';
|
|
2
|
-
import { isDocumentNode } from './format-node-header.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* @param {import('./output-view.types.ts').OutputNodeItem} output_item
|
|
@@ -8,18 +7,8 @@ import { isDocumentNode } from './format-node-header.js';
|
|
|
8
7
|
export function formatOutputNodeMetadataRows(output_item) {
|
|
9
8
|
/** @type {string[]} */
|
|
10
9
|
const metadata_rows = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (isDocumentNode(output_item)) {
|
|
15
|
-
stored_metadata_fields.push(`kind: ${output_item.node_kind}`);
|
|
16
|
-
} else {
|
|
17
|
-
stored_metadata_fields.push(`path: ${output_item.path}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (output_item.status) {
|
|
21
|
-
stored_metadata_fields.push(`status: ${output_item.status}`);
|
|
22
|
-
}
|
|
10
|
+
const stored_metadata_fields =
|
|
11
|
+
output_item.visible_fields.map(formatMetadataField);
|
|
23
12
|
|
|
24
13
|
if (stored_metadata_fields.length > 0) {
|
|
25
14
|
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
@@ -39,16 +28,7 @@ export function formatOutputNodeMetadataRows(output_item) {
|
|
|
39
28
|
export function formatResolvedLinkMetadataRows(target) {
|
|
40
29
|
/** @type {string[]} */
|
|
41
30
|
const metadata_rows = [];
|
|
42
|
-
|
|
43
|
-
const stored_metadata_fields = [];
|
|
44
|
-
|
|
45
|
-
if (target.kind) {
|
|
46
|
-
stored_metadata_fields.push(`kind: ${target.kind}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (target.status) {
|
|
50
|
-
stored_metadata_fields.push(`status: ${target.status}`);
|
|
51
|
-
}
|
|
31
|
+
const stored_metadata_fields = target.visible_fields.map(formatMetadataField);
|
|
52
32
|
|
|
53
33
|
if (stored_metadata_fields.length > 0) {
|
|
54
34
|
metadata_rows.push(stored_metadata_fields.join(' '));
|
|
@@ -60,3 +40,15 @@ export function formatResolvedLinkMetadataRows(target) {
|
|
|
60
40
|
|
|
61
41
|
return metadata_rows;
|
|
62
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {import('./output-view.types.ts').OutputMetadataField} output_field
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function formatMetadataField(output_field) {
|
|
49
|
+
const value = Array.isArray(output_field.value)
|
|
50
|
+
? output_field.value.join(', ')
|
|
51
|
+
: output_field.value;
|
|
52
|
+
|
|
53
|
+
return `${output_field.name}: ${value}`;
|
|
54
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable max-lines */
|
|
2
1
|
/**
|
|
3
2
|
* @import { OutputStoredQueryItem } from './output-view.types.ts';
|
|
4
3
|
*/
|
|
@@ -98,22 +97,7 @@ function createStoredQueryPhrases(where_clause) {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
/**
|
|
101
|
-
* @param {
|
|
102
|
-
* is_negated: boolean,
|
|
103
|
-
* term:
|
|
104
|
-
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
|
|
105
|
-
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
|
|
106
|
-
* | { kind: 'relation', relation_name: string }
|
|
107
|
-
* | { kind: 'relation_target', relation_name: string, target_id: string }
|
|
108
|
-
* | {
|
|
109
|
-
* aggregate_name: 'any' | 'count' | 'none',
|
|
110
|
-
* clauses: unknown[],
|
|
111
|
-
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
112
|
-
* kind: 'aggregate',
|
|
113
|
-
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
114
|
-
* value?: number,
|
|
115
|
-
* },
|
|
116
|
-
* }} clause
|
|
100
|
+
* @param {import('./parse-where-clause.types.ts').ParsedClause} clause
|
|
117
101
|
* @param {boolean} should_prefix_and
|
|
118
102
|
* @returns {StoredQuerySegment[]}
|
|
119
103
|
*/
|
|
@@ -137,20 +121,7 @@ function createClausePhrase(clause, should_prefix_and) {
|
|
|
137
121
|
}
|
|
138
122
|
|
|
139
123
|
/**
|
|
140
|
-
* @param {
|
|
141
|
-
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
|
|
142
|
-
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
|
|
143
|
-
* | { kind: 'relation', relation_name: string }
|
|
144
|
-
* | { kind: 'relation_target', relation_name: string, target_id: string }
|
|
145
|
-
* | {
|
|
146
|
-
* aggregate_name: 'any' | 'count' | 'none',
|
|
147
|
-
* clauses: { is_negated: boolean, term: unknown }[],
|
|
148
|
-
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
149
|
-
* kind: 'aggregate',
|
|
150
|
-
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
151
|
-
* value?: number,
|
|
152
|
-
* }
|
|
153
|
-
* } term
|
|
124
|
+
* @param {import('./parse-where-clause.types.ts').ParsedTerm} term
|
|
154
125
|
* @returns {StoredQuerySegment[]}
|
|
155
126
|
*/
|
|
156
127
|
function createTermSegments(term) {
|
|
@@ -185,7 +156,7 @@ function createTermSegments(term) {
|
|
|
185
156
|
}
|
|
186
157
|
|
|
187
158
|
/**
|
|
188
|
-
* @param {
|
|
159
|
+
* @param {import('./parse-where-clause.types.ts').ParsedFieldSetTerm} term
|
|
189
160
|
* @returns {StoredQuerySegment[]}
|
|
190
161
|
*/
|
|
191
162
|
function createFieldSetSegments(term) {
|
|
@@ -201,14 +172,7 @@ function createFieldSetSegments(term) {
|
|
|
201
172
|
}
|
|
202
173
|
|
|
203
174
|
/**
|
|
204
|
-
* @param {
|
|
205
|
-
* aggregate_name: 'any' | 'count' | 'none',
|
|
206
|
-
* clauses: { is_negated: boolean, term: unknown }[],
|
|
207
|
-
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
208
|
-
* kind: 'aggregate',
|
|
209
|
-
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
210
|
-
* value?: number,
|
|
211
|
-
* }} term
|
|
175
|
+
* @param {import('./parse-where-clause.types.ts').ParsedAggregateTerm} term
|
|
212
176
|
* @returns {StoredQuerySegment[]}
|
|
213
177
|
*/
|
|
214
178
|
function createAggregateSegments(term) {
|
|
@@ -245,30 +209,12 @@ function createTraversalSegments(traversal) {
|
|
|
245
209
|
}
|
|
246
210
|
|
|
247
211
|
/**
|
|
248
|
-
* @param {
|
|
212
|
+
* @param {import('./parse-where-clause.types.ts').ParsedClause[]} clauses
|
|
249
213
|
* @returns {StoredQuerySegment[]}
|
|
250
214
|
*/
|
|
251
215
|
function createNestedClauseSegments(clauses) {
|
|
252
216
|
return clauses.flatMap((clause, clause_index) => {
|
|
253
|
-
const clause_phrase = createClausePhrase(
|
|
254
|
-
/** @type {{
|
|
255
|
-
* is_negated: boolean,
|
|
256
|
-
* term:
|
|
257
|
-
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field', operator: '=' | '^=' | '~', value: string }
|
|
258
|
-
* | { field_name: 'id' | 'kind' | 'path' | 'status' | 'title', kind: 'field_set', operator: 'in' | 'not in', values: string[] }
|
|
259
|
-
* | { kind: 'relation', relation_name: string }
|
|
260
|
-
* | { kind: 'relation_target', relation_name: string, target_id: string }
|
|
261
|
-
* | {
|
|
262
|
-
* aggregate_name: 'any' | 'count' | 'none',
|
|
263
|
-
* clauses: { is_negated: boolean, term: unknown }[],
|
|
264
|
-
* comparison?: '!=' | '<' | '<=' | '=' | '>' | '>=',
|
|
265
|
-
* kind: 'aggregate',
|
|
266
|
-
* traversal: { direction: 'in' | 'out', relation_name: string },
|
|
267
|
-
* value?: number,
|
|
268
|
-
* },
|
|
269
|
-
* }} */ (clause),
|
|
270
|
-
clause_index > 0,
|
|
271
|
-
);
|
|
217
|
+
const clause_phrase = createClausePhrase(clause, clause_index > 0);
|
|
272
218
|
|
|
273
219
|
if (clause_index === 0) {
|
|
274
220
|
return clause_phrase;
|