patram 0.0.2 → 0.2.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.
Files changed (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. package/lib/patram-config.test.js +0 -147
@@ -4,7 +4,34 @@
4
4
  * @import { MappingDefinition, PatramConfig } from './patram-config.types.ts';
5
5
  */
6
6
 
7
- import { posix } from 'node:path';
7
+ import {
8
+ collectDocumentEntityKeys,
9
+ normalizeRepoRelativePath,
10
+ resolveNodeKey,
11
+ resolveTargetReference,
12
+ setNonDocumentPath,
13
+ } from './build-graph-identity.js';
14
+
15
+ /**
16
+ * Claim-to-graph materialization.
17
+ *
18
+ * Maps parsed claims into document nodes and relations using the resolved
19
+ * Patram graph config.
20
+ *
21
+ * Kind: graph
22
+ * Status: active
23
+ * Uses Term: ../docs/reference/terms/claim.md
24
+ * Uses Term: ../docs/reference/terms/document.md
25
+ * Uses Term: ../docs/reference/terms/graph.md
26
+ * Uses Term: ../docs/reference/terms/mapping.md
27
+ * Uses Term: ../docs/reference/terms/relation.md
28
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
29
+ * Decided by: ../docs/decisions/graph-materialization.md
30
+ * Implements: ../docs/tasks/v0/materialize-graph.md
31
+ * @patram
32
+ * @see {@link ./load-project-graph.js}
33
+ * @see {@link ../docs/decisions/graph-materialization.md}
34
+ */
8
35
 
9
36
  /**
10
37
  * Build a Patram graph from semantic config and parsed claims.
@@ -16,49 +43,29 @@ import { posix } from 'node:path';
16
43
  export function buildGraph(patram_config, claims) {
17
44
  /** @type {Map<string, GraphNode>} */
18
45
  const graph_nodes = new Map();
19
- /** @type {GraphEdge[]} */
20
- const graph_edges = [];
21
- let edge_number = 0;
22
-
23
- for (const claim of claims) {
24
- const source_document_node = upsertNode(
25
- graph_nodes,
26
- 'document',
27
- normalizeRepoRelativePath(claim.origin.path),
28
- );
29
- const mapping_definition = resolveMappingDefinition(
30
- patram_config.mappings,
31
- claim,
32
- );
33
-
34
- if (!mapping_definition) {
35
- continue;
36
- }
37
-
38
- if (mapping_definition.node) {
39
- applyNodeMapping(graph_nodes, mapping_definition.node, claim);
40
- }
41
-
42
- if (!mapping_definition.emit) {
43
- continue;
44
- }
45
-
46
- const target_key = resolveTargetKey(mapping_definition.emit.target, claim);
47
- const target_node = upsertNode(
48
- graph_nodes,
49
- mapping_definition.emit.target_kind,
50
- target_key,
51
- );
46
+ const document_entity_keys = collectDocumentEntityKeys(
47
+ patram_config.mappings,
48
+ claims,
49
+ );
50
+ /** @type {Set<string>} */
51
+ const document_paths = new Set(
52
+ claims.map((claim) => normalizeRepoRelativePath(claim.origin.path)),
53
+ );
52
54
 
53
- edge_number += 1;
54
- graph_edges.push({
55
- from: source_document_node.id,
56
- id: `edge:${edge_number}`,
57
- origin: claim.origin,
58
- relation: mapping_definition.emit.relation,
59
- to: target_node.id,
60
- });
61
- }
55
+ createDocumentNodes(graph_nodes, claims);
56
+ applyNodeMappings(
57
+ graph_nodes,
58
+ patram_config.mappings,
59
+ claims,
60
+ document_entity_keys,
61
+ );
62
+ const graph_edges = createGraphEdges(
63
+ graph_nodes,
64
+ patram_config.mappings,
65
+ claims,
66
+ document_entity_keys,
67
+ document_paths,
68
+ );
62
69
 
63
70
  return {
64
71
  edges: graph_edges,
@@ -96,56 +103,124 @@ function resolveDirectiveMapping(mappings, claim) {
96
103
 
97
104
  /**
98
105
  * @param {Map<string, GraphNode>} graph_nodes
99
- * @param {{ field: string, kind: string }} node_mapping
100
- * @param {PatramClaim} claim
106
+ * @param {PatramClaim[]} claims
101
107
  */
102
- function applyNodeMapping(graph_nodes, node_mapping, claim) {
103
- const source_key = normalizeRepoRelativePath(claim.origin.path);
104
- const graph_node = upsertNode(graph_nodes, node_mapping.kind, source_key);
105
-
106
- graph_node[node_mapping.field] = getStringClaimValue(claim);
108
+ function createDocumentNodes(graph_nodes, claims) {
109
+ for (const claim of claims) {
110
+ upsertNode(
111
+ graph_nodes,
112
+ 'document',
113
+ normalizeRepoRelativePath(claim.origin.path),
114
+ );
115
+ }
107
116
  }
108
117
 
109
118
  /**
110
- * @param {'path'} target_type
111
- * @param {PatramClaim} claim
112
- * @returns {string}
119
+ * @param {Map<string, GraphNode>} graph_nodes
120
+ * @param {Record<string, MappingDefinition>} mappings
121
+ * @param {PatramClaim[]} claims
122
+ * @param {Map<string, string>} document_entity_keys
113
123
  */
114
- function resolveTargetKey(target_type, claim) {
115
- if (target_type !== 'path') {
116
- throw new Error(`Unsupported target type "${target_type}".`);
117
- }
124
+ function applyNodeMappings(
125
+ graph_nodes,
126
+ mappings,
127
+ claims,
128
+ document_entity_keys,
129
+ ) {
130
+ for (const claim of claims) {
131
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
118
132
 
119
- const source_directory = posix.dirname(
120
- normalizeRepoRelativePath(claim.origin.path),
121
- );
122
- const raw_target = getPathTargetValue(claim);
133
+ if (!mapping_definition?.node) {
134
+ continue;
135
+ }
123
136
 
124
- return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
137
+ applyNodeMapping(
138
+ graph_nodes,
139
+ mapping_definition.node,
140
+ claim,
141
+ document_entity_keys,
142
+ );
143
+ }
125
144
  }
126
145
 
127
146
  /**
128
- * @param {PatramClaim} claim
129
- * @returns {string}
147
+ * @param {Map<string, GraphNode>} graph_nodes
148
+ * @param {Record<string, MappingDefinition>} mappings
149
+ * @param {PatramClaim[]} claims
150
+ * @param {Map<string, string>} document_entity_keys
151
+ * @param {Set<string>} document_paths
152
+ * @returns {GraphEdge[]}
130
153
  */
131
- function getPathTargetValue(claim) {
132
- if (typeof claim.value === 'string') {
133
- return claim.value;
154
+ function createGraphEdges(
155
+ graph_nodes,
156
+ mappings,
157
+ claims,
158
+ document_entity_keys,
159
+ document_paths,
160
+ ) {
161
+ /** @type {GraphEdge[]} */
162
+ const graph_edges = [];
163
+ let edge_number = 0;
164
+
165
+ for (const claim of claims) {
166
+ const mapping_definition = resolveMappingDefinition(mappings, claim);
167
+
168
+ if (!mapping_definition?.emit) {
169
+ continue;
170
+ }
171
+
172
+ const source_document_node = upsertNode(
173
+ graph_nodes,
174
+ 'document',
175
+ normalizeRepoRelativePath(claim.origin.path),
176
+ );
177
+ const target_reference = resolveTargetReference(
178
+ mapping_definition.emit.target_kind,
179
+ mapping_definition.emit.target,
180
+ claim,
181
+ document_entity_keys,
182
+ document_paths,
183
+ );
184
+ const target_node = upsertNode(
185
+ graph_nodes,
186
+ mapping_definition.emit.target_kind,
187
+ target_reference.key,
188
+ );
189
+
190
+ setNonDocumentPath(target_node, target_reference.path);
191
+
192
+ edge_number += 1;
193
+ graph_edges.push({
194
+ from: source_document_node.id,
195
+ id: `edge:${edge_number}`,
196
+ origin: claim.origin,
197
+ relation: mapping_definition.emit.relation,
198
+ to: target_node.id,
199
+ });
134
200
  }
135
201
 
136
- return claim.value.target;
202
+ return graph_edges;
137
203
  }
138
204
 
139
205
  /**
206
+ * @param {Map<string, GraphNode>} graph_nodes
207
+ * @param {{ field: string, key?: 'path' | 'value', kind: string }} node_mapping
140
208
  * @param {PatramClaim} claim
141
- * @returns {string}
209
+ * @param {Map<string, string>} document_entity_keys
142
210
  */
143
- function getStringClaimValue(claim) {
144
- if (typeof claim.value === 'string') {
145
- return claim.value;
146
- }
211
+ function applyNodeMapping(
212
+ graph_nodes,
213
+ node_mapping,
214
+ claim,
215
+ document_entity_keys,
216
+ ) {
217
+ const source_key = normalizeRepoRelativePath(claim.origin.path);
218
+ const node_key = resolveNodeKey(node_mapping, claim, document_entity_keys);
219
+ const graph_node = upsertNode(graph_nodes, node_mapping.kind, node_key);
147
220
 
148
- throw new Error(`Claim "${claim.id}" does not carry a string value.`);
221
+ setNonDocumentPath(graph_node, source_key);
222
+
223
+ graph_node[node_mapping.field] = getNodeFieldValue(claim);
149
224
  }
150
225
 
151
226
  /**
@@ -205,11 +280,15 @@ function getNodeId(kind_name, node_key) {
205
280
  }
206
281
 
207
282
  /**
208
- * @param {string} source_path
283
+ * @param {PatramClaim} claim
209
284
  * @returns {string}
210
285
  */
211
- function normalizeRepoRelativePath(source_path) {
212
- return posix.normalize(source_path.replaceAll('\\', '/'));
286
+ function getNodeFieldValue(claim) {
287
+ if (typeof claim.value === 'string') {
288
+ return claim.value;
289
+ }
290
+
291
+ throw new Error(`Claim "${claim.id}" does not carry a string value.`);
213
292
  }
214
293
 
215
294
  /**
@@ -3,17 +3,33 @@
3
3
  * @import { PatramDiagnostic } from './load-patram-config.types.ts';
4
4
  */
5
5
 
6
+ /**
7
+ * Graph validation.
8
+ *
9
+ * Reports broken document links and missing edge nodes after graph
10
+ * materialization.
11
+ *
12
+ * Kind: graph
13
+ * Status: active
14
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
15
+ * Decided by: ../docs/decisions/check-link-target-existence.md
16
+ * Implements: ../docs/tasks/v0/check-command.md
17
+ * @patram
18
+ * @see {@link ./build-graph.js}
19
+ * @see {@link ../docs/decisions/check-link-target-existence.md}
20
+ */
21
+
6
22
  /**
7
23
  * Check a materialized graph for broken document links and missing edge nodes.
8
24
  *
9
25
  * @param {BuildGraphResult} graph
10
- * @param {string[]} source_file_paths
26
+ * @param {string[]} existing_file_paths
11
27
  * @returns {PatramDiagnostic[]}
12
28
  */
13
- export function checkGraph(graph, source_file_paths) {
29
+ export function checkGraph(graph, existing_file_paths) {
14
30
  /** @type {PatramDiagnostic[]} */
15
31
  const diagnostics = [];
16
- const source_file_path_set = new Set(source_file_paths);
32
+ const existing_file_path_set = new Set(existing_file_paths);
17
33
 
18
34
  for (const graph_edge of graph.edges) {
19
35
  const source_node = graph.nodes[graph_edge.from];
@@ -34,7 +50,7 @@ export function checkGraph(graph, source_file_paths) {
34
50
  diagnostics,
35
51
  graph_edge,
36
52
  target_node,
37
- source_file_path_set,
53
+ existing_file_path_set,
38
54
  );
39
55
  }
40
56
 
@@ -78,13 +94,13 @@ function collectMissingNodeDiagnostics(
78
94
  * @param {PatramDiagnostic[]} diagnostics
79
95
  * @param {GraphEdge} graph_edge
80
96
  * @param {GraphNode} target_node
81
- * @param {Set<string>} source_file_path_set
97
+ * @param {Set<string>} existing_file_path_set
82
98
  */
83
99
  function collectBrokenLinkDiagnostics(
84
100
  diagnostics,
85
101
  graph_edge,
86
102
  target_node,
87
- source_file_path_set,
103
+ existing_file_path_set,
88
104
  ) {
89
105
  if (graph_edge.relation !== 'links_to') {
90
106
  return;
@@ -94,7 +110,7 @@ function collectBrokenLinkDiagnostics(
94
110
  return;
95
111
  }
96
112
 
97
- if (source_file_path_set.has(target_node.path)) {
113
+ if (existing_file_path_set.has(target_node.path)) {
98
114
  return;
99
115
  }
100
116
 
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @import { PatramClaim, PatramClaimFields } from './parse-claims.types.ts';
3
+ */
4
+
5
+ const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/du;
6
+
7
+ /**
8
+ * @param {string} target_value
9
+ * @returns {boolean}
10
+ */
11
+ export function isPathLikeTarget(target_value) {
12
+ if (target_value.startsWith('#')) {
13
+ return false;
14
+ }
15
+
16
+ return !URI_SCHEME_PATTERN.test(target_value);
17
+ }
18
+
19
+ /**
20
+ * @param {string} file_path
21
+ * @param {number} claim_number
22
+ * @param {string} claim_type
23
+ * @param {PatramClaimFields} claim_fields
24
+ * @returns {PatramClaim}
25
+ */
26
+ export function createClaim(file_path, claim_number, claim_type, claim_fields) {
27
+ const document_id = `doc:${file_path}`;
28
+ const origin = claim_fields.origin ?? {
29
+ column: 1,
30
+ line: 1,
31
+ path: file_path,
32
+ };
33
+
34
+ return {
35
+ ...claim_fields,
36
+ document_id,
37
+ id: `claim:${document_id}:${claim_number}`,
38
+ origin,
39
+ type: claim_type,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * @param {string} file_path
45
+ * @returns {string}
46
+ */
47
+ export function getFileExtension(file_path) {
48
+ const last_dot_index = file_path.lastIndexOf('.');
49
+
50
+ if (last_dot_index < 0) {
51
+ return '';
52
+ }
53
+
54
+ return file_path.slice(last_dot_index);
55
+ }