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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { DirectiveTypeConfig, MetadataDirectiveRuleConfig, PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
|
|
3
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
4
|
+
* @import { MappingDefinition } from './patram-config.types.ts';
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolveTargetReference } from './build-graph-identity.js';
|
|
8
|
+
import { createOriginDiagnostic } from './directive-diagnostics.js';
|
|
9
|
+
import {
|
|
10
|
+
formatQuotedList,
|
|
11
|
+
getInvalidTypeMessage,
|
|
12
|
+
isDirectiveValueValid,
|
|
13
|
+
} from './directive-type-rules.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check one directive claim value against typed validation rules.
|
|
17
|
+
*
|
|
18
|
+
* @param {PatramClaim} claim
|
|
19
|
+
* @param {string} directive_name
|
|
20
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
21
|
+
* @param {PatramRepoConfig} repo_config
|
|
22
|
+
* @param {MetadataDirectiveRuleConfig | undefined} _directive_rule
|
|
23
|
+
* @param {Map<string, string>} document_entity_keys
|
|
24
|
+
* @param {Set<string>} document_paths
|
|
25
|
+
* @returns {PatramDiagnostic[]}
|
|
26
|
+
*/
|
|
27
|
+
export function checkDirectiveValue(
|
|
28
|
+
claim,
|
|
29
|
+
directive_name,
|
|
30
|
+
mappings,
|
|
31
|
+
repo_config,
|
|
32
|
+
_directive_rule,
|
|
33
|
+
document_entity_keys,
|
|
34
|
+
document_paths,
|
|
35
|
+
) {
|
|
36
|
+
const mapping_definition = resolveDirectiveMapping(mappings, claim);
|
|
37
|
+
const validation_field_name = getDirectiveValidationFieldName(
|
|
38
|
+
directive_name,
|
|
39
|
+
mapping_definition,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (!validation_field_name || typeof claim.value !== 'string') {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (validation_field_name === '$class') {
|
|
47
|
+
return checkClassValue(claim, directive_name, repo_config);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isStructuralDirectiveField(validation_field_name)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const type_definition = repo_config.fields?.[validation_field_name];
|
|
55
|
+
|
|
56
|
+
if (!type_definition) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (type_definition.type === 'enum') {
|
|
61
|
+
return checkEnumValue(claim, directive_name, type_definition.values);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const type_diagnostic = createInvalidTypeDiagnostic(
|
|
65
|
+
claim,
|
|
66
|
+
directive_name,
|
|
67
|
+
type_definition,
|
|
68
|
+
claim.value,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (type_diagnostic) {
|
|
72
|
+
return [type_diagnostic];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return createPathClassDiagnostics(
|
|
76
|
+
claim,
|
|
77
|
+
directive_name,
|
|
78
|
+
mappings,
|
|
79
|
+
repo_config,
|
|
80
|
+
type_definition,
|
|
81
|
+
document_entity_keys,
|
|
82
|
+
document_paths,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {PatramClaim} claim
|
|
88
|
+
* @param {string} directive_name
|
|
89
|
+
* @param {PatramRepoConfig} repo_config
|
|
90
|
+
* @returns {PatramDiagnostic[]}
|
|
91
|
+
*/
|
|
92
|
+
function checkClassValue(claim, directive_name, repo_config) {
|
|
93
|
+
if (
|
|
94
|
+
typeof claim.value !== 'string' ||
|
|
95
|
+
repo_config.classes?.[claim.value] !== undefined
|
|
96
|
+
) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [
|
|
101
|
+
createOriginDiagnostic(
|
|
102
|
+
claim,
|
|
103
|
+
'directive.invalid_enum',
|
|
104
|
+
`Directive "${directive_name}" must reference a configured class.`,
|
|
105
|
+
),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {PatramClaim} claim
|
|
111
|
+
* @param {string} directive_name
|
|
112
|
+
* @param {string[]} allowed_values
|
|
113
|
+
* @returns {PatramDiagnostic[]}
|
|
114
|
+
*/
|
|
115
|
+
function checkEnumValue(claim, directive_name, allowed_values) {
|
|
116
|
+
if (typeof claim.value !== 'string' || allowed_values.includes(claim.value)) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [
|
|
121
|
+
createOriginDiagnostic(
|
|
122
|
+
claim,
|
|
123
|
+
'directive.invalid_enum',
|
|
124
|
+
`Directive "${directive_name}" must be one of ${formatQuotedList(allowed_values)}.`,
|
|
125
|
+
),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {PatramClaim} claim
|
|
131
|
+
* @param {string} directive_name
|
|
132
|
+
* @param {Exclude<DirectiveTypeConfig, { type: 'enum' }>} type_definition
|
|
133
|
+
* @param {string} directive_value
|
|
134
|
+
* @returns {PatramDiagnostic | null}
|
|
135
|
+
*/
|
|
136
|
+
function createInvalidTypeDiagnostic(
|
|
137
|
+
claim,
|
|
138
|
+
directive_name,
|
|
139
|
+
type_definition,
|
|
140
|
+
directive_value,
|
|
141
|
+
) {
|
|
142
|
+
if (isDirectiveValueValid(type_definition, directive_value)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return createOriginDiagnostic(
|
|
147
|
+
claim,
|
|
148
|
+
'directive.invalid_type',
|
|
149
|
+
getInvalidTypeMessage(directive_name, type_definition.type),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {PatramClaim} claim
|
|
155
|
+
* @param {string} directive_name
|
|
156
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
157
|
+
* @param {PatramRepoConfig} repo_config
|
|
158
|
+
* @param {Exclude<DirectiveTypeConfig, { type: 'enum' }>} type_definition
|
|
159
|
+
* @param {Map<string, string>} document_entity_keys
|
|
160
|
+
* @param {Set<string>} document_paths
|
|
161
|
+
* @returns {PatramDiagnostic[]}
|
|
162
|
+
*/
|
|
163
|
+
function createPathClassDiagnostics(
|
|
164
|
+
claim,
|
|
165
|
+
directive_name,
|
|
166
|
+
mappings,
|
|
167
|
+
repo_config,
|
|
168
|
+
type_definition,
|
|
169
|
+
document_entity_keys,
|
|
170
|
+
document_paths,
|
|
171
|
+
) {
|
|
172
|
+
if (
|
|
173
|
+
type_definition.type !== 'path' ||
|
|
174
|
+
type_definition.path_class === undefined ||
|
|
175
|
+
isDirectivePathInClass(
|
|
176
|
+
mappings,
|
|
177
|
+
claim,
|
|
178
|
+
type_definition.path_class,
|
|
179
|
+
document_entity_keys,
|
|
180
|
+
document_paths,
|
|
181
|
+
repo_config,
|
|
182
|
+
)
|
|
183
|
+
) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return [
|
|
188
|
+
createOriginDiagnostic(
|
|
189
|
+
claim,
|
|
190
|
+
'directive.invalid_path_class',
|
|
191
|
+
`Directive "${directive_name}" must point to path class "${type_definition.path_class}".`,
|
|
192
|
+
),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
198
|
+
* @param {PatramClaim} claim
|
|
199
|
+
* @param {string} path_class_name
|
|
200
|
+
* @param {Map<string, string>} document_entity_keys
|
|
201
|
+
* @param {Set<string>} document_paths
|
|
202
|
+
* @param {PatramRepoConfig} repo_config
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
function isDirectivePathInClass(
|
|
206
|
+
mappings,
|
|
207
|
+
claim,
|
|
208
|
+
path_class_name,
|
|
209
|
+
document_entity_keys,
|
|
210
|
+
document_paths,
|
|
211
|
+
repo_config,
|
|
212
|
+
) {
|
|
213
|
+
const path_class_definition = repo_config.path_classes?.[path_class_name];
|
|
214
|
+
|
|
215
|
+
if (!path_class_definition) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const mapping_definition = resolveDirectiveMapping(mappings, claim);
|
|
220
|
+
const target_kind = mapping_definition?.emit?.target_class ?? 'document';
|
|
221
|
+
const resolved_target = resolveTargetReference(
|
|
222
|
+
target_kind,
|
|
223
|
+
'path',
|
|
224
|
+
claim,
|
|
225
|
+
document_entity_keys,
|
|
226
|
+
document_paths,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (!resolved_target.path) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return path_class_definition.prefixes.some((prefix) =>
|
|
234
|
+
resolved_target.path?.startsWith(prefix),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {Record<string, MappingDefinition>} mappings
|
|
240
|
+
* @param {PatramClaim} claim
|
|
241
|
+
* @returns {MappingDefinition | null}
|
|
242
|
+
*/
|
|
243
|
+
function resolveDirectiveMapping(mappings, claim) {
|
|
244
|
+
if (!claim.name || !claim.parser) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return mappings[`${claim.parser}.directive.${claim.name}`] ?? null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {string} directive_name
|
|
253
|
+
* @param {MappingDefinition | null} mapping_definition
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
function getDirectiveValidationFieldName(directive_name, mapping_definition) {
|
|
257
|
+
if (mapping_definition?.node?.field) {
|
|
258
|
+
return mapping_definition.node.field;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return directive_name;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} field_name
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
function isStructuralDirectiveField(field_name) {
|
|
269
|
+
return (
|
|
270
|
+
field_name === '$class' ||
|
|
271
|
+
field_name === '$id' ||
|
|
272
|
+
field_name === '$path' ||
|
|
273
|
+
field_name === 'title'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {PatramClaim} claim
|
|
279
|
+
* @param {string} code
|
|
280
|
+
* @param {string} message
|
|
281
|
+
* @returns {PatramDiagnostic}
|
|
282
|
+
*/
|
package/lib/check-graph.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { BuildGraphResult, GraphEdge, GraphNode } from './build-graph.types.ts';
|
|
3
|
-
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
3
|
+
* @import { PatramDiagnostic, PatramRepoConfig } from './load-patram-config.types.ts';
|
|
4
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
4
5
|
*/
|
|
5
6
|
|
|
7
|
+
import { checkDirectiveMetadata } from './check-directive-metadata.js';
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Graph validation.
|
|
8
11
|
*
|
|
@@ -24,9 +27,11 @@
|
|
|
24
27
|
*
|
|
25
28
|
* @param {BuildGraphResult} graph
|
|
26
29
|
* @param {string[]} existing_file_paths
|
|
30
|
+
* @param {PatramRepoConfig} [repo_config]
|
|
31
|
+
* @param {PatramClaim[]} [claims]
|
|
27
32
|
* @returns {PatramDiagnostic[]}
|
|
28
33
|
*/
|
|
29
|
-
export function checkGraph(graph, existing_file_paths) {
|
|
34
|
+
export function checkGraph(graph, existing_file_paths, repo_config, claims) {
|
|
30
35
|
/** @type {PatramDiagnostic[]} */
|
|
31
36
|
const diagnostics = [];
|
|
32
37
|
const existing_file_path_set = new Set(existing_file_paths);
|
|
@@ -54,6 +59,17 @@ export function checkGraph(graph, existing_file_paths) {
|
|
|
54
59
|
);
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
if (repo_config && claims) {
|
|
63
|
+
diagnostics.push(
|
|
64
|
+
...checkDirectiveMetadata(
|
|
65
|
+
graph,
|
|
66
|
+
repo_config,
|
|
67
|
+
claims,
|
|
68
|
+
existing_file_paths,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
return diagnostics;
|
|
58
74
|
}
|
|
59
75
|
|
|
@@ -102,15 +118,18 @@ function collectBrokenLinkDiagnostics(
|
|
|
102
118
|
target_node,
|
|
103
119
|
existing_file_path_set,
|
|
104
120
|
) {
|
|
121
|
+
const target_class = target_node.$class ?? target_node.kind;
|
|
122
|
+
const target_path = target_node.$path ?? target_node.path;
|
|
123
|
+
|
|
105
124
|
if (graph_edge.relation !== 'links_to') {
|
|
106
125
|
return;
|
|
107
126
|
}
|
|
108
127
|
|
|
109
|
-
if (
|
|
128
|
+
if (target_class !== 'document' || !target_path) {
|
|
110
129
|
return;
|
|
111
130
|
}
|
|
112
131
|
|
|
113
|
-
if (existing_file_path_set.has(
|
|
132
|
+
if (existing_file_path_set.has(target_path)) {
|
|
114
133
|
return;
|
|
115
134
|
}
|
|
116
135
|
|
|
@@ -118,7 +137,7 @@ function collectBrokenLinkDiagnostics(
|
|
|
118
137
|
createDiagnostic(
|
|
119
138
|
graph_edge,
|
|
120
139
|
'graph.link_broken',
|
|
121
|
-
`Document link target "${
|
|
140
|
+
`Document link target "${target_path}" was not found.`,
|
|
122
141
|
),
|
|
123
142
|
);
|
|
124
143
|
}
|
package/lib/cli-help-metadata.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
export const COMMAND_NAMES = /** @type {const} */ ([
|
|
49
49
|
'check',
|
|
50
|
+
'fields',
|
|
50
51
|
'query',
|
|
51
52
|
'queries',
|
|
52
53
|
'show',
|
|
@@ -108,6 +109,33 @@ const COMMAND_DEFINITIONS = {
|
|
|
108
109
|
'Validate a project, directory, or file and report graph diagnostics.',
|
|
109
110
|
usage_lines: ['patram check [path] [options]'],
|
|
110
111
|
},
|
|
112
|
+
fields: {
|
|
113
|
+
allowed_option_names: new Set(),
|
|
114
|
+
examples: ['patram fields', 'patram fields --json'],
|
|
115
|
+
extra_positionals_message: 'Fields does not accept positional arguments.',
|
|
116
|
+
help_topics: [],
|
|
117
|
+
max_positionals: 0,
|
|
118
|
+
min_positionals: 0,
|
|
119
|
+
missing_argument_examples: [],
|
|
120
|
+
missing_argument_label: null,
|
|
121
|
+
missing_usage_lines: ['patram fields'],
|
|
122
|
+
option_column_width: 10,
|
|
123
|
+
options: [
|
|
124
|
+
{
|
|
125
|
+
description: 'Print plain text output',
|
|
126
|
+
label: '--plain',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
description: 'Print JSON output',
|
|
130
|
+
label: '--json',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
related: ['query', 'check'],
|
|
134
|
+
root_summary: 'Discover likely field schema from source claims',
|
|
135
|
+
summary:
|
|
136
|
+
'Discover likely metadata fields, multiplicity, and class usage from source claims.',
|
|
137
|
+
usage_lines: ['patram fields [options]'],
|
|
138
|
+
},
|
|
111
139
|
query: {
|
|
112
140
|
allowed_option_names: new Set([
|
|
113
141
|
'explain',
|
|
@@ -120,10 +148,10 @@ const COMMAND_DEFINITIONS = {
|
|
|
120
148
|
'patram query active-plans',
|
|
121
149
|
'patram query --where "tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md"',
|
|
122
150
|
'patram query --where "status not in [done, dropped, superseded]"',
|
|
123
|
-
'patram query --where "
|
|
124
|
-
'patram query --where "count(in:decided_by,
|
|
151
|
+
'patram query --where "$class=plan and none(in:tracked_in, $class=decision)"',
|
|
152
|
+
'patram query --where "count(in:decided_by, $class=task) = 0"',
|
|
125
153
|
'patram query ready-tasks --explain',
|
|
126
|
-
'patram query --where "
|
|
154
|
+
'patram query --where "$class=decision and status=accepted and count(in:decided_by, $class=task) = 0" --lint',
|
|
127
155
|
'patram query active-plans --limit 10 --offset 20',
|
|
128
156
|
],
|
|
129
157
|
extra_positionals_message:
|
|
@@ -176,8 +204,8 @@ const COMMAND_DEFINITIONS = {
|
|
|
176
204
|
summary:
|
|
177
205
|
'Run a stored query or an ad hoc where clause against graph nodes.',
|
|
178
206
|
syntax_lines: [
|
|
179
|
-
'id=<value> |
|
|
180
|
-
'id^=<prefix> | path^=<prefix> | title~<text>',
|
|
207
|
+
'$id=<value> | $class=<value> | $path=<value> | status=<value>',
|
|
208
|
+
'$id^=<prefix> | $path^=<prefix> | title~<text>',
|
|
181
209
|
'<field> in [<value>, ...] | <field> not in [<value>, ...]',
|
|
182
210
|
'<relation>:* | <relation>=<target-id>',
|
|
183
211
|
'any(<traversal>, <term> and <term>)',
|
|
@@ -251,15 +279,15 @@ const COMMAND_DEFINITIONS = {
|
|
|
251
279
|
const HELP_TOPIC_DEFINITIONS = {
|
|
252
280
|
'query-language': {
|
|
253
281
|
examples: [
|
|
254
|
-
'
|
|
255
|
-
'path^=docs/plans/',
|
|
282
|
+
'$class=decision and status=accepted',
|
|
283
|
+
'$path^=docs/plans/',
|
|
256
284
|
'title~query',
|
|
257
285
|
'tracked_in=doc:docs/plans/v0/worktracking-agent-guidance.md',
|
|
258
286
|
'implements_command=command:query',
|
|
259
287
|
'status not in [done, dropped, superseded]',
|
|
260
|
-
'any(in:tracked_in,
|
|
261
|
-
'none(in:tracked_in,
|
|
262
|
-
'count(in:decided_by,
|
|
288
|
+
'any(in:tracked_in, $class=task and status in [pending, ready, in_progress, blocked])',
|
|
289
|
+
'none(in:tracked_in, $class=decision)',
|
|
290
|
+
'count(in:decided_by, $class=task) = 0',
|
|
263
291
|
'not uses_term=term:graph',
|
|
264
292
|
],
|
|
265
293
|
lead: 'Query language filters graph nodes with field, relation, traversal, and aggregate terms.',
|
|
@@ -269,7 +297,7 @@ const HELP_TOPIC_DEFINITIONS = {
|
|
|
269
297
|
label: '=',
|
|
270
298
|
},
|
|
271
299
|
{
|
|
272
|
-
description: 'Prefix match for id and path',
|
|
300
|
+
description: 'Prefix match for structural id and path',
|
|
273
301
|
label: '^=',
|
|
274
302
|
},
|
|
275
303
|
{
|
|
@@ -316,15 +344,15 @@ const HELP_TOPIC_DEFINITIONS = {
|
|
|
316
344
|
},
|
|
317
345
|
],
|
|
318
346
|
terms: [
|
|
319
|
-
'Exact match: id,
|
|
320
|
-
'Prefix match: id, path',
|
|
347
|
+
'Exact match: $id, $class, $path, status',
|
|
348
|
+
'Prefix match: $id, $path',
|
|
321
349
|
'Contains text: title',
|
|
322
|
-
'Set membership: id,
|
|
350
|
+
'Set membership: $id, $class, $path, status, title',
|
|
323
351
|
],
|
|
324
352
|
usage_lines: [
|
|
325
353
|
'<field>=<value>',
|
|
326
|
-
'id^=<prefix>',
|
|
327
|
-
'path^=<prefix>',
|
|
354
|
+
'$id^=<prefix>',
|
|
355
|
+
'$path^=<prefix>',
|
|
328
356
|
'title~<text>',
|
|
329
357
|
'<field> in [<value>, ...]',
|
|
330
358
|
'<field> not in [<value>, ...]',
|
package/lib/derived-summary.js
CHANGED
|
@@ -34,13 +34,15 @@ import { queryGraph } from './query-graph.js';
|
|
|
34
34
|
* @returns {DerivedSummaryEvaluator}
|
|
35
35
|
*/
|
|
36
36
|
export function createDerivedSummaryEvaluator(repo_config, graph) {
|
|
37
|
-
const
|
|
37
|
+
const summary_by_class = createSummaryByClass(repo_config.derived_summaries);
|
|
38
38
|
/** @type {Map<string, Set<string>>} */
|
|
39
39
|
const matching_node_id_cache = new Map();
|
|
40
40
|
|
|
41
41
|
return {
|
|
42
42
|
evaluate(graph_node) {
|
|
43
|
-
const configured_summary =
|
|
43
|
+
const configured_summary = summary_by_class.get(
|
|
44
|
+
graph_node.$class ?? 'document',
|
|
45
|
+
);
|
|
44
46
|
|
|
45
47
|
if (!configured_summary) {
|
|
46
48
|
return null;
|
|
@@ -255,24 +257,24 @@ function parseTraversal(traversal_text) {
|
|
|
255
257
|
* @param {PatramRepoConfig['derived_summaries']} derived_summaries
|
|
256
258
|
* @returns {Map<string, { definition: DerivedSummaryConfig, name: string }>}
|
|
257
259
|
*/
|
|
258
|
-
function
|
|
260
|
+
function createSummaryByClass(derived_summaries) {
|
|
259
261
|
/** @type {Map<string, { definition: DerivedSummaryConfig, name: string }>} */
|
|
260
|
-
const
|
|
262
|
+
const summary_by_class = new Map();
|
|
261
263
|
|
|
262
264
|
if (!derived_summaries) {
|
|
263
|
-
return
|
|
265
|
+
return summary_by_class;
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
for (const [summary_name, summary_definition] of Object.entries(
|
|
267
269
|
derived_summaries,
|
|
268
270
|
)) {
|
|
269
|
-
for (const
|
|
270
|
-
|
|
271
|
+
for (const class_name of summary_definition.classes) {
|
|
272
|
+
summary_by_class.set(class_name, {
|
|
271
273
|
definition: summary_definition,
|
|
272
274
|
name: summary_name,
|
|
273
275
|
});
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
|
|
277
|
-
return
|
|
279
|
+
return summary_by_class;
|
|
278
280
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
3
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {PatramClaim} claim
|
|
8
|
+
* @param {string} code
|
|
9
|
+
* @param {string} message
|
|
10
|
+
* @returns {PatramDiagnostic}
|
|
11
|
+
*/
|
|
12
|
+
export function createOriginDiagnostic(claim, code, message) {
|
|
13
|
+
return {
|
|
14
|
+
code,
|
|
15
|
+
column: claim.origin.column,
|
|
16
|
+
level: 'error',
|
|
17
|
+
line: claim.origin.line,
|
|
18
|
+
message,
|
|
19
|
+
path: claim.origin.path,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} document_path
|
|
25
|
+
* @param {string} code
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @returns {PatramDiagnostic}
|
|
28
|
+
*/
|
|
29
|
+
export function createDocumentDiagnostic(document_path, code, message) {
|
|
30
|
+
return {
|
|
31
|
+
code,
|
|
32
|
+
column: 1,
|
|
33
|
+
level: 'error',
|
|
34
|
+
line: 1,
|
|
35
|
+
message,
|
|
36
|
+
path: document_path,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { MetadataFieldConfig } from './load-patram-config.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isPathLikeTarget } from './claim-helpers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {MetadataFieldConfig} type_definition
|
|
9
|
+
* @param {string} directive_value
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isDirectiveValueValid(type_definition, directive_value) {
|
|
13
|
+
if (directive_value.length === 0) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
switch (type_definition.type) {
|
|
18
|
+
case 'string':
|
|
19
|
+
return true;
|
|
20
|
+
case 'integer':
|
|
21
|
+
return /^-?\d+$/du.test(directive_value);
|
|
22
|
+
case 'path':
|
|
23
|
+
return isPathLikeTarget(directive_value);
|
|
24
|
+
case 'glob':
|
|
25
|
+
return true;
|
|
26
|
+
case 'date':
|
|
27
|
+
return isValidDateValue(directive_value);
|
|
28
|
+
case 'date_time':
|
|
29
|
+
return isValidDateTimeValue(directive_value);
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unsupported directive type "${type_definition.type}".`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} directive_name
|
|
37
|
+
* @param {Exclude<MetadataFieldConfig['type'], 'enum'>} type_name
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function getInvalidTypeMessage(directive_name, type_name) {
|
|
41
|
+
switch (type_name) {
|
|
42
|
+
case 'string':
|
|
43
|
+
return `Directive "${directive_name}" must be a non-empty string.`;
|
|
44
|
+
case 'integer':
|
|
45
|
+
return `Directive "${directive_name}" must be a base-10 integer.`;
|
|
46
|
+
case 'path':
|
|
47
|
+
return `Directive "${directive_name}" must be a path-like string.`;
|
|
48
|
+
case 'glob':
|
|
49
|
+
return `Directive "${directive_name}" must be a non-empty glob string.`;
|
|
50
|
+
case 'date':
|
|
51
|
+
return `Directive "${directive_name}" must use YYYY-MM-DD.`;
|
|
52
|
+
case 'date_time':
|
|
53
|
+
return `Directive "${directive_name}" must use YYYY-MM-DD HH:MM.`;
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unsupported directive type "${type_name}".`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string[]} values
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function formatQuotedList(values) {
|
|
64
|
+
return values.map((value) => `"${value}"`).join(', ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} directive_value
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
function isValidDateValue(directive_value) {
|
|
72
|
+
const date_match = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/du.exec(
|
|
73
|
+
directive_value,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!date_match?.groups) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return isRealCalendarDate(
|
|
81
|
+
Number(date_match.groups.year),
|
|
82
|
+
Number(date_match.groups.month),
|
|
83
|
+
Number(date_match.groups.day),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} directive_value
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function isValidDateTimeValue(directive_value) {
|
|
92
|
+
const date_time_match =
|
|
93
|
+
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}) (?<hour>\d{2}):(?<minute>\d{2})$/du.exec(
|
|
94
|
+
directive_value,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!date_time_match?.groups) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hour = Number(date_time_match.groups.hour);
|
|
102
|
+
const minute = Number(date_time_match.groups.minute);
|
|
103
|
+
|
|
104
|
+
if (hour > 23 || minute > 59) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return isRealCalendarDate(
|
|
109
|
+
Number(date_time_match.groups.year),
|
|
110
|
+
Number(date_time_match.groups.month),
|
|
111
|
+
Number(date_time_match.groups.day),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {number} year
|
|
117
|
+
* @param {number} month
|
|
118
|
+
* @param {number} day
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
function isRealCalendarDate(year, month, day) {
|
|
122
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const candidate_date = new Date(Date.UTC(year, month - 1, day));
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
candidate_date.getUTCFullYear() === year &&
|
|
130
|
+
candidate_date.getUTCMonth() === month - 1 &&
|
|
131
|
+
candidate_date.getUTCDate() === day
|
|
132
|
+
);
|
|
133
|
+
}
|