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.
@@ -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
+ */
@@ -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 (target_node.kind !== 'document' || !target_node.path) {
128
+ if (target_class !== 'document' || !target_path) {
110
129
  return;
111
130
  }
112
131
 
113
- if (existing_file_path_set.has(target_node.path)) {
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 "${target_node.path}" was not found.`,
140
+ `Document link target "${target_path}" was not found.`,
122
141
  ),
123
142
  );
124
143
  }
@@ -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 "kind=plan and none(in:tracked_in, kind=decision)"',
124
- 'patram query --where "count(in:decided_by, kind=task) = 0"',
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 "kind=decision and status=accepted and count(in:decided_by, kind=task) = 0" --lint',
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> | kind=<value> | path=<value> | status=<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
- 'kind=decision and status=accepted',
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, kind=task and status in [pending, ready, in_progress, blocked])',
261
- 'none(in:tracked_in, kind=decision)',
262
- 'count(in:decided_by, kind=task) = 0',
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, kind, path, status',
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, kind, path, status, title',
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>, ...]',
@@ -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 summary_by_kind = createSummaryByKind(repo_config.derived_summaries);
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 = summary_by_kind.get(graph_node.kind);
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 createSummaryByKind(derived_summaries) {
260
+ function createSummaryByClass(derived_summaries) {
259
261
  /** @type {Map<string, { definition: DerivedSummaryConfig, name: string }>} */
260
- const summary_by_kind = new Map();
262
+ const summary_by_class = new Map();
261
263
 
262
264
  if (!derived_summaries) {
263
- return summary_by_kind;
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 kind_name of summary_definition.kinds) {
270
- summary_by_kind.set(kind_name, {
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 summary_by_kind;
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
+ }