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,9 +1,11 @@
1
1
  /** @import * as $k$$l$output$j$view$k$types$k$ts from './output-view.types.ts'; */
2
+ /* eslint-disable max-lines */
2
3
  /**
3
4
  * @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
4
5
  * @import { DerivedSummaryEvaluator } from './derived-summary.js';
6
+ * @import { PatramRepoConfig } from './load-patram-config.types.ts';
5
7
  * @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
6
- * @import { OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
8
+ * @import { OutputMetadataField, OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
7
9
  */
8
10
 
9
11
  import { renderJsonOutput } from './render-json-output.js';
@@ -30,7 +32,7 @@ import { renderRichOutput } from './render-rich-output.js';
30
32
  *
31
33
  * @param {'query' | 'queries'} command_name
32
34
  * @param {GraphNode[] | { name: string, where: string }[]} command_items
33
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
35
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
34
36
  * @returns {OutputView}
35
37
  */
36
38
  export function createOutputView(command_name, command_items, command_options) {
@@ -54,7 +56,7 @@ export function createOutputView(command_name, command_items, command_options) {
54
56
  * Create a shared output view for the show command.
55
57
  *
56
58
  * @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
57
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, graph_nodes?: BuildGraphResult['nodes'] }=} command_options
59
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }=} command_options
58
60
  * @returns {ShowOutputView}
59
61
  */
60
62
  export function createShowOutputView(show_output, command_options = {}) {
@@ -69,6 +71,7 @@ export function createShowOutputView(show_output, command_options = {}) {
69
71
  command_options.derived_summary_evaluator?.evaluate(
70
72
  shown_document_node,
71
73
  ) ?? null,
74
+ command_options.repo_config?.fields ?? {},
72
75
  )
73
76
  : undefined,
74
77
  hints: [],
@@ -78,6 +81,7 @@ export function createShowOutputView(show_output, command_options = {}) {
78
81
  reference: resolved_link.reference,
79
82
  target: createResolvedLinkTarget(
80
83
  resolved_link.target,
84
+ command_options.repo_config?.fields ?? {},
81
85
  command_options.graph_nodes?.[`doc:${resolved_link.target.path}`]
82
86
  ? (command_options.derived_summary_evaluator?.evaluate(
83
87
  command_options.graph_nodes[`doc:${resolved_link.target.path}`],
@@ -124,7 +128,7 @@ export async function renderOutputView(
124
128
 
125
129
  /**
126
130
  * @param {GraphNode[]} graph_nodes
127
- * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
131
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
128
132
  * @returns {OutputView}
129
133
  */
130
134
  function createQueryOutputView(graph_nodes, command_options = {}) {
@@ -134,11 +138,12 @@ function createQueryOutputView(graph_nodes, command_options = {}) {
134
138
  command: 'query',
135
139
  hints:
136
140
  command_options.hints ??
137
- (total_count === 0 ? ['Try: patram query --where "kind=task"'] : []),
141
+ (total_count === 0 ? ['Try: patram query --where "$class=task"'] : []),
138
142
  items: graph_nodes.map((graph_node) =>
139
143
  createOutputNodeItem(
140
144
  graph_node,
141
145
  command_options.derived_summary_evaluator?.evaluate(graph_node) ?? null,
146
+ command_options.repo_config?.fields ?? {},
142
147
  ),
143
148
  ),
144
149
  summary: {
@@ -174,13 +179,17 @@ function createStoredQueriesOutputView(stored_queries) {
174
179
  /**
175
180
  * @param {GraphNode} graph_node
176
181
  * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
182
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
177
183
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputNodeItem}
178
184
  */
179
- function createOutputNodeItem(graph_node, derived_summary) {
180
- const title =
181
- graph_node.title ?? graph_node.label ?? graph_node.path ?? graph_node.key;
185
+ function createOutputNodeItem(graph_node, derived_summary, field_definitions) {
186
+ const title = getOutputNodeTitle(graph_node);
187
+ const path = getOutputNodePath(graph_node);
188
+ const node_class = getOutputNodeClass(graph_node);
189
+ const fields = collectOutputFields(graph_node, field_definitions);
190
+ const visible_fields = createVisibleOutputFields(fields, field_definitions);
182
191
 
183
- if (!title || !graph_node.path) {
192
+ if (!title || !node_class) {
184
193
  throw new Error(
185
194
  `Expected graph node "${graph_node.id}" to have a title and path.`,
186
195
  );
@@ -188,35 +197,247 @@ function createOutputNodeItem(graph_node, derived_summary) {
188
197
 
189
198
  return {
190
199
  derived_summary: derived_summary ?? undefined,
191
- id: graph_node.id,
200
+ fields,
201
+ id: getOutputNodeId(graph_node),
192
202
  kind: 'node',
193
- node_kind: graph_node.kind,
194
- path: graph_node.path,
195
- status: graph_node.status,
203
+ node_kind: node_class,
204
+ path,
196
205
  title,
206
+ visible_fields,
197
207
  };
198
208
  }
199
209
 
200
210
  /**
201
211
  * @param {{ kind?: string, path: string, status?: string, title: string }} target
212
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
202
213
  * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
203
214
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
204
215
  */
205
- function createResolvedLinkTarget(target, derived_summary) {
216
+ function createResolvedLinkTarget(target, field_definitions, derived_summary) {
217
+ /** @type {Record<string, string | string[]>} */
218
+ const fields = {};
219
+
220
+ if (target.status) {
221
+ fields.status = target.status;
222
+ }
223
+
206
224
  /** @type {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget} */
207
225
  const resolved_target = {
208
226
  derived_summary: derived_summary ?? undefined,
227
+ fields,
228
+ id: `doc:${target.path}`,
229
+ kind: target.kind ?? 'document',
209
230
  path: target.path,
210
231
  title: target.title,
232
+ visible_fields: createVisibleOutputFields(fields, field_definitions),
211
233
  };
212
234
 
213
- if (target.kind && target.kind !== 'document') {
214
- resolved_target.kind = target.kind;
235
+ return resolved_target;
236
+ }
237
+
238
+ /**
239
+ * @param {string | string[] | undefined} field_value
240
+ * @returns {string | undefined}
241
+ */
242
+ function getScalarGraphNodeField(field_value) {
243
+ if (Array.isArray(field_value)) {
244
+ return field_value[0];
215
245
  }
216
246
 
217
- if (target.status) {
218
- resolved_target.status = target.status;
247
+ return field_value;
248
+ }
249
+
250
+ /**
251
+ * @param {GraphNode} graph_node
252
+ * @returns {string | undefined}
253
+ */
254
+ function getOutputNodeTitle(graph_node) {
255
+ return (
256
+ getScalarGraphNodeField(graph_node.title) ??
257
+ getScalarGraphNodeField(graph_node.label) ??
258
+ getOutputNodePath(graph_node) ??
259
+ getScalarGraphNodeField(graph_node.key)
260
+ );
261
+ }
262
+
263
+ /**
264
+ * @param {GraphNode} graph_node
265
+ * @returns {string | undefined}
266
+ */
267
+ function getOutputNodePath(graph_node) {
268
+ return getScalarGraphNodeField(graph_node.$path ?? graph_node.path);
269
+ }
270
+
271
+ /**
272
+ * @param {GraphNode} graph_node
273
+ * @returns {string | undefined}
274
+ */
275
+ function getOutputNodeClass(graph_node) {
276
+ return getScalarGraphNodeField(graph_node.$class ?? graph_node.kind);
277
+ }
278
+
279
+ /**
280
+ * @param {GraphNode} graph_node
281
+ * @returns {string}
282
+ */
283
+ function getOutputNodeId(graph_node) {
284
+ return (
285
+ getScalarGraphNodeField(graph_node.$id ?? graph_node.id) ?? graph_node.id
286
+ );
287
+ }
288
+
289
+ /**
290
+ * @param {GraphNode} graph_node
291
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
292
+ * @returns {Record<string, string | string[]>}
293
+ */
294
+ function collectOutputFields(graph_node, field_definitions) {
295
+ /** @type {Record<string, string | string[]>} */
296
+ const fields = {};
297
+
298
+ for (const [field_name, field_value] of Object.entries(graph_node)) {
299
+ const normalized_value = getCollectedOutputFieldValue(
300
+ graph_node,
301
+ field_name,
302
+ field_value,
303
+ );
304
+
305
+ if (normalized_value === undefined) {
306
+ continue;
307
+ }
308
+
309
+ fields[field_name] = normalized_value;
219
310
  }
220
311
 
221
- return resolved_target;
312
+ for (const field_name of Object.keys(field_definitions)) {
313
+ if (fields[field_name] !== undefined) {
314
+ continue;
315
+ }
316
+
317
+ const field_value = normalizeOutputFieldValue(graph_node[field_name]);
318
+
319
+ if (field_value !== undefined) {
320
+ fields[field_name] = field_value;
321
+ }
322
+ }
323
+
324
+ return fields;
325
+ }
326
+
327
+ /**
328
+ * @param {GraphNode} graph_node
329
+ * @param {string} field_name
330
+ * @param {unknown} field_value
331
+ * @returns {string | string[] | undefined}
332
+ */
333
+ function getCollectedOutputFieldValue(graph_node, field_name, field_value) {
334
+ if (isInternalOutputField(field_name)) {
335
+ return undefined;
336
+ }
337
+
338
+ const normalized_value = normalizeOutputFieldValue(field_value);
339
+
340
+ if (normalized_value === undefined) {
341
+ return undefined;
342
+ }
343
+
344
+ if (isLegacyMirrorOutputField(graph_node, field_name, normalized_value)) {
345
+ return undefined;
346
+ }
347
+
348
+ return normalized_value;
349
+ }
350
+
351
+ /**
352
+ * @param {GraphNode} graph_node
353
+ * @param {string} field_name
354
+ * @param {string | string[]} normalized_value
355
+ * @returns {boolean}
356
+ */
357
+ function isLegacyMirrorOutputField(graph_node, field_name, normalized_value) {
358
+ if (Array.isArray(normalized_value)) {
359
+ return false;
360
+ }
361
+
362
+ if (field_name === 'kind') {
363
+ return normalized_value === graph_node.$class;
364
+ }
365
+
366
+ if (field_name === 'path') {
367
+ return normalized_value === graph_node.$path;
368
+ }
369
+
370
+ if (field_name === 'id') {
371
+ return normalized_value === graph_node.$id;
372
+ }
373
+
374
+ return false;
375
+ }
376
+
377
+ /**
378
+ * @param {Record<string, string | string[]>} fields
379
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
380
+ * @returns {OutputMetadataField[]}
381
+ */
382
+ function createVisibleOutputFields(fields, field_definitions) {
383
+ return Object.entries(fields)
384
+ .filter(
385
+ ([field_name]) => field_definitions[field_name]?.display?.hidden !== true,
386
+ )
387
+ .sort(([left_name], [right_name]) =>
388
+ compareOutputFieldNames(left_name, right_name, field_definitions),
389
+ )
390
+ .map(([name, value]) => ({ name, value }));
391
+ }
392
+
393
+ /**
394
+ * @param {string} field_name
395
+ * @returns {boolean}
396
+ */
397
+ function isInternalOutputField(field_name) {
398
+ return (
399
+ field_name === '$class' ||
400
+ field_name === '$id' ||
401
+ field_name === '$path' ||
402
+ field_name === 'id' ||
403
+ field_name === 'key' ||
404
+ field_name === 'label' ||
405
+ field_name === 'path' ||
406
+ field_name === 'title'
407
+ );
408
+ }
409
+
410
+ /**
411
+ * @param {string} left_name
412
+ * @param {string} right_name
413
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
414
+ * @returns {number}
415
+ */
416
+ function compareOutputFieldNames(left_name, right_name, field_definitions) {
417
+ const left_order =
418
+ field_definitions[left_name]?.display?.order ?? Number.MAX_SAFE_INTEGER;
419
+ const right_order =
420
+ field_definitions[right_name]?.display?.order ?? Number.MAX_SAFE_INTEGER;
421
+
422
+ if (left_order !== right_order) {
423
+ return left_order - right_order;
424
+ }
425
+
426
+ return left_name.localeCompare(right_name, 'en');
427
+ }
428
+
429
+ /**
430
+ * @param {unknown} field_value
431
+ * @returns {string | string[] | undefined}
432
+ */
433
+ function normalizeOutputFieldValue(field_value) {
434
+ if (Array.isArray(field_value)) {
435
+ const string_values = field_value.flatMap((value) =>
436
+ typeof value === 'string' ? [value] : [],
437
+ );
438
+
439
+ return string_values.length > 0 ? string_values : undefined;
440
+ }
441
+
442
+ return typeof field_value === 'string' ? field_value : undefined;
222
443
  }
@@ -128,7 +128,7 @@ function formatPlainStoredQueryLine(line_segments) {
128
128
  */
129
129
  function formatPlainResolvedLinkItem(output_item) {
130
130
  return formatOutputItemBlock({
131
- header: `[${output_item.reference}] document ${output_item.target.path}`,
131
+ header: `[${output_item.reference}] ${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`,
132
132
  metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
133
133
  metadata_indent: ' ',
134
134
  title: output_item.target.title,
@@ -155,7 +155,7 @@ function formatRichStoredQueryLine(line_segments, ansi) {
155
155
  */
156
156
  function formatRichResolvedLinkItem(output_item, ansi) {
157
157
  return formatOutputItemBlock({
158
- header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`document ${output_item.target.path}`)}`,
158
+ header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`)}`,
159
159
  metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
160
160
  metadata_indent: ' ',
161
161
  title: output_item.target.title,
@@ -21,7 +21,7 @@ import { parsePatramConfig } from './patram-config.js';
21
21
  */
22
22
 
23
23
  const BUILT_IN_PATRAM_CONFIG = {
24
- kinds: {
24
+ classes: {
25
25
  document: {
26
26
  builtin: true,
27
27
  label: 'Document',
@@ -30,28 +30,28 @@ const BUILT_IN_PATRAM_CONFIG = {
30
30
  mappings: {
31
31
  'document.title': {
32
32
  node: {
33
+ class: 'document',
33
34
  field: 'title',
34
- kind: 'document',
35
35
  },
36
36
  },
37
37
  'document.description': {
38
38
  node: {
39
+ class: 'document',
39
40
  field: 'description',
40
- kind: 'document',
41
41
  },
42
42
  },
43
43
  'jsdoc.link': {
44
44
  emit: {
45
45
  relation: 'links_to',
46
46
  target: 'path',
47
- target_kind: 'document',
47
+ target_class: 'document',
48
48
  },
49
49
  },
50
50
  'markdown.link': {
51
51
  emit: {
52
52
  relation: 'links_to',
53
53
  target: 'path',
54
- target_kind: 'document',
54
+ target_class: 'document',
55
55
  },
56
56
  },
57
57
  },
@@ -71,10 +71,10 @@ const BUILT_IN_PATRAM_CONFIG = {
71
71
  * @returns {PatramConfig}
72
72
  */
73
73
  export function resolvePatramGraphConfig(repo_config) {
74
- return parsePatramConfig({
75
- kinds: {
76
- ...BUILT_IN_PATRAM_CONFIG.kinds,
77
- ...repo_config.kinds,
74
+ const graph_config = parsePatramConfig({
75
+ classes: {
76
+ ...BUILT_IN_PATRAM_CONFIG.classes,
77
+ ...repo_config.classes,
78
78
  },
79
79
  mappings: {
80
80
  ...BUILT_IN_PATRAM_CONFIG.mappings,
@@ -85,4 +85,10 @@ export function resolvePatramGraphConfig(repo_config) {
85
85
  ...repo_config.relations,
86
86
  },
87
87
  });
88
+
89
+ return {
90
+ ...graph_config,
91
+ class_schemas: repo_config.class_schemas,
92
+ fields: repo_config.fields,
93
+ };
88
94
  }
@@ -212,17 +212,15 @@ function createResolvedLinkSummary(
212
212
  claim_value.target,
213
213
  );
214
214
  const target_node = graph_nodes[`doc:${target_path}`];
215
- const target_title = target_node?.title ?? claim_value.text;
216
215
 
217
216
  return {
218
217
  label: claim_value.text,
219
218
  reference,
220
- target: {
221
- kind: target_node?.kind,
222
- path: target_node?.path ?? target_path,
223
- status: target_node?.status,
224
- title: target_title,
225
- },
219
+ target: createResolvedLinkTarget(
220
+ target_node,
221
+ target_path,
222
+ claim_value.text,
223
+ ),
226
224
  };
227
225
  }
228
226
 
@@ -239,6 +237,52 @@ function resolveShowTargetPath(source_file_path, raw_target) {
239
237
  return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
240
238
  }
241
239
 
240
+ /**
241
+ * @param {string | string[] | undefined} field_value
242
+ * @returns {string | undefined}
243
+ */
244
+ function getScalarGraphField(field_value) {
245
+ if (Array.isArray(field_value)) {
246
+ return field_value[0];
247
+ }
248
+
249
+ return field_value;
250
+ }
251
+
252
+ /**
253
+ * @param {GraphNode | undefined} target_node
254
+ * @param {string} target_path
255
+ * @param {string} fallback_title
256
+ * @returns {{ kind?: string, path: string, status?: string, title: string }}
257
+ */
258
+ function createResolvedLinkTarget(target_node, target_path, fallback_title) {
259
+ return {
260
+ kind: getResolvedLinkTargetKind(target_node),
261
+ path: getResolvedLinkTargetPath(target_node, target_path),
262
+ status: getScalarGraphField(target_node?.status),
263
+ title: getScalarGraphField(target_node?.title) ?? fallback_title,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * @param {GraphNode | undefined} target_node
269
+ * @returns {string | undefined}
270
+ */
271
+ function getResolvedLinkTargetKind(target_node) {
272
+ return getScalarGraphField(target_node?.$class ?? target_node?.kind);
273
+ }
274
+
275
+ /**
276
+ * @param {GraphNode | undefined} target_node
277
+ * @param {string} target_path
278
+ * @returns {string}
279
+ */
280
+ function getResolvedLinkTargetPath(target_node, target_path) {
281
+ return (
282
+ getScalarGraphField(target_node?.$path ?? target_node?.path) ?? target_path
283
+ );
284
+ }
285
+
242
286
  /**
243
287
  * @param {PatramClaim} claim
244
288
  * @returns {claim is PatramClaim & { type: 'markdown.link', value: { target: string, text: string } }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patram",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./lib/patram.js",
6
6
  "exports": {
@@ -65,8 +65,8 @@
65
65
  "devDependencies": {
66
66
  "@eslint/js": "^10.0.1",
67
67
  "@types/node": "^24.12.0",
68
- "@vitest/coverage-v8": "^4.1.0",
69
- "eslint": "^10.0.3",
68
+ "@vitest/coverage-v8": "^4.1.1",
69
+ "eslint": "^10.1.0",
70
70
  "eslint-plugin-jsdoc": "^62.8.0",
71
71
  "globals": "^17.4.0",
72
72
  "husky": "^9.1.7",
@@ -74,7 +74,7 @@
74
74
  "lint-staged": "^16.2.6",
75
75
  "prettier": "^3.5.3",
76
76
  "slice-ansi": "^8.0.0",
77
- "typescript": "^5.8.2",
78
- "vitest": "^4.1.0"
77
+ "typescript": "^6.0.2",
78
+ "vitest": "^4.1.1"
79
79
  }
80
80
  }