patram 0.10.0 → 0.12.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 (117) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/arguments.types.d.ts +1 -0
  3. package/lib/cli/commands/check.js +27 -15
  4. package/lib/cli/commands/fields.js +0 -4
  5. package/lib/cli/commands/queries.js +179 -1
  6. package/lib/cli/commands/query.js +1 -8
  7. package/lib/cli/commands/refs.js +3 -10
  8. package/lib/cli/commands/show.js +1 -8
  9. package/lib/cli/help-metadata.js +106 -111
  10. package/lib/cli/main.js +10 -10
  11. package/lib/cli/parse-arguments-helpers.js +416 -66
  12. package/lib/cli/parse-arguments.js +4 -4
  13. package/lib/cli/render-help.js +10 -4
  14. package/lib/config/defaults.js +33 -25
  15. package/lib/config/load-patram-config.d.ts +19 -33
  16. package/lib/config/load-patram-config.js +18 -121
  17. package/lib/config/load-patram-config.types.d.ts +3 -40
  18. package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
  19. package/lib/config/manage-stored-queries-helpers.js +320 -0
  20. package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
  21. package/lib/config/manage-stored-queries-jsonc.js +95 -0
  22. package/lib/config/manage-stored-queries.d.ts +77 -0
  23. package/lib/config/manage-stored-queries.js +300 -0
  24. package/lib/config/patram-config.d.ts +34 -34
  25. package/lib/config/patram-config.js +3 -3
  26. package/lib/config/patram-config.types.d.ts +5 -11
  27. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  28. package/lib/config/resolve-patram-graph-config.js +3 -119
  29. package/lib/config/schema.d.ts +158 -269
  30. package/lib/config/schema.js +72 -210
  31. package/lib/config/validate-patram-config-value.d.ts +13 -0
  32. package/lib/config/validate-patram-config-value.js +94 -0
  33. package/lib/config/validation.d.ts +2 -12
  34. package/lib/config/validation.js +125 -483
  35. package/lib/find-close-match.d.ts +4 -1
  36. package/lib/graph/build-graph-identity.d.ts +1 -32
  37. package/lib/graph/build-graph-identity.js +5 -269
  38. package/lib/graph/build-graph.d.ts +13 -4
  39. package/lib/graph/build-graph.js +347 -488
  40. package/lib/graph/build-graph.types.d.ts +8 -9
  41. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  42. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  43. package/lib/graph/check-directive-metadata.d.ts +8 -9
  44. package/lib/graph/check-directive-metadata.js +70 -561
  45. package/lib/graph/check-directive-path-target.d.ts +6 -13
  46. package/lib/graph/check-directive-path-target.js +26 -57
  47. package/lib/graph/check-directive-value.d.ts +1 -5
  48. package/lib/graph/check-directive-value.js +40 -180
  49. package/lib/graph/check-graph.d.ts +5 -5
  50. package/lib/graph/check-graph.js +8 -6
  51. package/lib/graph/document-node-identity.d.ts +23 -7
  52. package/lib/graph/document-node-identity.js +417 -160
  53. package/lib/graph/graph-node.d.ts +42 -0
  54. package/lib/graph/graph-node.js +83 -0
  55. package/lib/graph/inspect-reverse-references.js +16 -11
  56. package/lib/graph/load-project-graph.d.ts +7 -7
  57. package/lib/graph/load-project-graph.js +7 -7
  58. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  59. package/lib/graph/query/cypher-reader.d.ts +59 -0
  60. package/lib/graph/query/cypher-reader.js +151 -0
  61. package/lib/graph/query/cypher-support.d.ts +79 -0
  62. package/lib/graph/query/cypher-support.js +213 -0
  63. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  64. package/lib/graph/query/cypher-tokenize.js +225 -0
  65. package/lib/graph/query/cypher.types.d.ts +43 -0
  66. package/lib/graph/query/execute.d.ts +7 -7
  67. package/lib/graph/query/execute.js +71 -33
  68. package/lib/graph/query/inspect.js +58 -24
  69. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  70. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  71. package/lib/graph/query/parse-cypher.d.ts +7 -0
  72. package/lib/graph/query/parse-cypher.js +580 -0
  73. package/lib/graph/query/parse-query.d.ts +13 -0
  74. package/lib/graph/query/parse-query.js +97 -0
  75. package/lib/graph/query/resolve.d.ts +6 -0
  76. package/lib/graph/query/resolve.js +81 -24
  77. package/lib/output/command-output.js +12 -5
  78. package/lib/output/compact-layout.js +221 -0
  79. package/lib/output/format-output-item-block.js +31 -1
  80. package/lib/output/format-output-metadata.js +16 -29
  81. package/lib/output/format-stored-query-block.js +95 -0
  82. package/lib/output/layout-incoming-references.js +101 -19
  83. package/lib/output/layout-stored-queries.js +23 -330
  84. package/lib/output/list-queries.js +1 -1
  85. package/lib/output/render-field-discovery.js +11 -2
  86. package/lib/output/render-output-view.js +9 -5
  87. package/lib/output/renderers/json.js +5 -26
  88. package/lib/output/renderers/plain.js +155 -35
  89. package/lib/output/renderers/rich.js +250 -36
  90. package/lib/output/resolve-check-target.js +120 -11
  91. package/lib/output/resolved-link-layout.js +43 -0
  92. package/lib/output/rich-source/render.js +193 -35
  93. package/lib/output/show-document.js +25 -18
  94. package/lib/output/view-model/index.js +124 -103
  95. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  96. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  97. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  98. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  99. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  100. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  101. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  102. package/lib/parse/parse-claims.d.ts +2 -6
  103. package/lib/parse/parse-claims.js +11 -53
  104. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  105. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  106. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  107. package/lib/patram.d.ts +9 -3
  108. package/lib/patram.js +1 -1
  109. package/lib/scan/discover-fields.js +194 -55
  110. package/lib/scan/list-source-files.d.ts +4 -4
  111. package/lib/scan/list-source-files.js +4 -4
  112. package/package.json +2 -1
  113. package/lib/directive-validation-test-helpers.js +0 -87
  114. package/lib/graph/query/parse.d.ts +0 -75
  115. package/lib/graph/query/parse.js +0 -1064
  116. package/lib/output/derived-summary.js +0 -280
  117. package/lib/output/format-derived-summary-row.js +0 -9
@@ -11,16 +11,12 @@ export function parseSourceFile(parse_input: ParseClaimsInput, parse_options?: {
11
11
  /**
12
12
  * Build parser options from repo config.
13
13
  *
14
- * @param {{ fields?: Record<string, { multiple?: boolean }>, mappings?: Record<string, { emit?: unknown, node?: unknown }> } | undefined} repo_config
14
+ * @param {{ fields?: Record<string, { many?: boolean }> } | undefined} repo_config
15
15
  * @returns {{ multi_value_directive_names: Set<string> }}
16
16
  */
17
17
  export function createParseOptions(repo_config: {
18
18
  fields?: Record<string, {
19
- multiple?: boolean;
20
- }>;
21
- mappings?: Record<string, {
22
- emit?: unknown;
23
- node?: unknown;
19
+ many?: boolean;
24
20
  }>;
25
21
  } | undefined): {
26
22
  multi_value_directive_names: Set<string>;
@@ -20,13 +20,13 @@ import {
20
20
  * Routes each source file to markdown or JSDoc claim parsing and keeps claim
21
21
  * extraction on one entrypoint.
22
22
  *
23
- * Kind: parse
24
- * Status: active
25
- * Uses Term: ../../docs/reference/terms/claim.md
26
- * Uses Term: ../../docs/reference/terms/document.md
27
- * Tracked in: ../../docs/plans/v0/source-anchor-dogfooding.md
28
- * Decided by: ../../docs/decisions/jsdoc-metadata-directive-syntax.md
29
- * Implements: ../../docs/tasks/v0/parse-claims.md
23
+ * kind: parse
24
+ * status: active
25
+ * uses_term: ../../docs/reference/terms/claim.md
26
+ * uses_term: ../../docs/reference/terms/document.md
27
+ * tracked_in: ../../docs/plans/v0/source-anchor-dogfooding.md
28
+ * decided_by: ../../docs/decisions/jsdoc-metadata-directive-syntax.md
29
+ * implements: ../../docs/tasks/v0/parse-claims.md
30
30
  * @patram
31
31
  * @see {@link ./markdown/parse-markdown-claims.js}
32
32
  * @see {@link ./jsdoc/parse-jsdoc-claims.js}
@@ -59,7 +59,7 @@ export function parseSourceFile(parse_input, parse_options) {
59
59
  /**
60
60
  * Build parser options from repo config.
61
61
  *
62
- * @param {{ fields?: Record<string, { multiple?: boolean }>, mappings?: Record<string, { emit?: unknown, node?: unknown }> } | undefined} repo_config
62
+ * @param {{ fields?: Record<string, { many?: boolean }> } | undefined} repo_config
63
63
  * @returns {{ multi_value_directive_names: Set<string> }}
64
64
  */
65
65
  export function createParseOptions(repo_config) {
@@ -69,7 +69,7 @@ export function createParseOptions(repo_config) {
69
69
  }
70
70
 
71
71
  /**
72
- * @param {{ fields?: Record<string, { multiple?: boolean }>, mappings?: Record<string, { emit?: unknown, node?: unknown }> } | undefined} repo_config
72
+ * @param {{ fields?: Record<string, { many?: boolean }> } | undefined} repo_config
73
73
  * @returns {Set<string>}
74
74
  */
75
75
  function collectMultiValueDirectiveNames(repo_config) {
@@ -77,60 +77,18 @@ function collectMultiValueDirectiveNames(repo_config) {
77
77
  const multi_value_directive_names = new Set();
78
78
 
79
79
  collectMultipleFieldNames(repo_config?.fields, multi_value_directive_names);
80
- collectEmitOnlyDirectiveNames(
81
- repo_config?.mappings,
82
- multi_value_directive_names,
83
- );
84
80
 
85
81
  return multi_value_directive_names;
86
82
  }
87
83
 
88
84
  /**
89
- * @param {Record<string, { multiple?: boolean }> | undefined} fields
85
+ * @param {Record<string, { many?: boolean }> | undefined} fields
90
86
  * @param {Set<string>} multi_value_directive_names
91
87
  */
92
88
  function collectMultipleFieldNames(fields, multi_value_directive_names) {
93
89
  for (const [field_name, field_definition] of Object.entries(fields ?? {})) {
94
- if (field_definition.multiple === true) {
90
+ if (field_definition.many === true) {
95
91
  multi_value_directive_names.add(field_name);
96
92
  }
97
93
  }
98
94
  }
99
-
100
- /**
101
- * @param {Record<string, { emit?: unknown, node?: unknown }> | undefined} mappings
102
- * @param {Set<string>} multi_value_directive_names
103
- */
104
- function collectEmitOnlyDirectiveNames(mappings, multi_value_directive_names) {
105
- for (const [mapping_name, mapping_definition] of Object.entries(
106
- mappings ?? {},
107
- )) {
108
- const directive_name = resolveEmitOnlyDirectiveName(
109
- mapping_name,
110
- mapping_definition,
111
- );
112
-
113
- if (directive_name) {
114
- multi_value_directive_names.add(directive_name);
115
- }
116
- }
117
- }
118
-
119
- /**
120
- * @param {string} mapping_name
121
- * @param {{ emit?: unknown, node?: unknown }} mapping_definition
122
- * @returns {string | null}
123
- */
124
- function resolveEmitOnlyDirectiveName(mapping_name, mapping_definition) {
125
- const directive_match = mapping_name.match(/^[^.]+\.directive\.(.+)$/du);
126
-
127
- if (
128
- !directive_match ||
129
- mapping_definition.emit === undefined ||
130
- mapping_definition.node !== undefined
131
- ) {
132
- return null;
133
- }
134
-
135
- return directive_match[1];
136
- }
@@ -4,10 +4,10 @@
4
4
  * Loads or extracts one markdown file worth of tagged fenced blocks and
5
5
  * provides exact-match selection helpers.
6
6
  *
7
- * Kind: parse
8
- * Status: active
9
- * Tracked in: ../../../docs/plans/v0/tagged-fenced-block-extraction.md
10
- * Decided by: ../../../docs/decisions/tagged-fenced-block-extraction.md
7
+ * kind: parse
8
+ * status: active
9
+ * tracked_in: ../../../docs/plans/v0/tagged-fenced-block-extraction.md
10
+ * decided_by: ../../../docs/decisions/tagged-fenced-block-extraction.md
11
11
  * @patram
12
12
  * @see {@link ../../../docs/decisions/tagged-fenced-block-extraction.md}
13
13
  */
@@ -18,10 +18,10 @@ import { extractTaggedFencedBlocksFromSource } from './tagged-fenced-block-parse
18
18
  * Loads or extracts one markdown file worth of tagged fenced blocks and
19
19
  * provides exact-match selection helpers.
20
20
  *
21
- * Kind: parse
22
- * Status: active
23
- * Tracked in: ../../../docs/plans/v0/tagged-fenced-block-extraction.md
24
- * Decided by: ../../../docs/decisions/tagged-fenced-block-extraction.md
21
+ * kind: parse
22
+ * status: active
23
+ * tracked_in: ../../../docs/plans/v0/tagged-fenced-block-extraction.md
24
+ * decided_by: ../../../docs/decisions/tagged-fenced-block-extraction.md
25
25
  * @patram
26
26
  * @see {@link ../../../docs/decisions/tagged-fenced-block-extraction.md}
27
27
  */
@@ -16,10 +16,10 @@ import { YAML_SOURCE_FILE_EXTENSIONS } from '../../config/source-file-defaults.j
16
16
  * Parses standalone YAML metadata files and front matter with one projection
17
17
  * model for top-level scalar directives.
18
18
  *
19
- * Kind: parse
20
- * Status: active
21
- * Tracked in: ../../../docs/plans/v0/yaml-source-and-front-matter.md
22
- * Decided by: ../../../docs/decisions/yaml-source-and-front-matter.md
19
+ * kind: parse
20
+ * status: active
21
+ * tracked_in: ../../../docs/plans/v0/yaml-source-and-front-matter.md
22
+ * decided_by: ../../../docs/decisions/yaml-source-and-front-matter.md
23
23
  * @patram
24
24
  * @see {@link ../parse-claims.js}
25
25
  * @see {@link ../markdown/parse-markdown-directives.js}
package/lib/patram.d.ts CHANGED
@@ -5,7 +5,7 @@ export {
5
5
  selectTaggedBlocks,
6
6
  } from './parse/tagged-fenced/tagged-fenced-blocks.js';
7
7
 
8
- export { parseWhereClause } from './graph/query/parse.js';
8
+ export { parseQueryExpression } from './graph/query/parse-query.js';
9
9
  export { getQuerySemanticDiagnostics } from './graph/query/inspect.js';
10
10
  export { loadProjectGraph } from './graph/load-project-graph.js';
11
11
  export { queryGraph } from './graph/query/execute.js';
@@ -48,8 +48,8 @@ export type PatramParsedTerm =
48
48
  import('./graph/parse-where-clause.types.d.ts').ParsedTerm;
49
49
  export type PatramParsedExpression =
50
50
  import('./graph/parse-where-clause.types.d.ts').ParsedExpression;
51
- export type PatramParseWhereClauseResult =
52
- import('./graph/parse-where-clause.types.d.ts').ParseWhereClauseResult;
51
+ export type PatramParseQueryResult =
52
+ import('./graph/parse-where-clause.types.d.ts').ParseQueryResult;
53
53
  export type PatramQuerySource =
54
54
  | {
55
55
  kind: 'ad_hoc';
@@ -59,6 +59,12 @@ export type PatramQuerySource =
59
59
  name: string;
60
60
  };
61
61
 
62
+ export interface PatramQueryGraphOptions {
63
+ bindings?: Record<string, string>;
64
+ limit?: number;
65
+ offset?: number;
66
+ }
67
+
62
68
  export interface PatramProjectGraphResult {
63
69
  claims: import('./parse/parse-claims.types.d.ts').PatramClaim[];
64
70
  config: import('./config/load-patram-config.types.d.ts').PatramRepoConfig;
package/lib/patram.js CHANGED
@@ -5,7 +5,7 @@ export {
5
5
  selectTaggedBlocks,
6
6
  } from './parse/tagged-fenced/tagged-fenced-blocks.js';
7
7
 
8
- export { parseWhereClause } from './graph/query/parse.js';
8
+ export { parseQueryExpression } from './graph/query/parse-query.js';
9
9
  export { getQuerySemanticDiagnostics } from './graph/query/inspect.js';
10
10
  export { loadProjectGraph } from './graph/load-project-graph.js';
11
11
  export { queryGraph } from './graph/query/execute.js';
@@ -2,20 +2,20 @@
2
2
  /**
3
3
  * @import { ClaimOrigin, PatramClaim } from '../parse/parse-claims.types.ts';
4
4
  * @import {
5
- * DiscoveredFieldMultiplicity,
6
5
  * DiscoveredFieldTypeName,
7
- * FieldDiscoveryClassUsage,
8
6
  * FieldDiscoveryEvidenceReference,
9
7
  * FieldDiscoveryMultiplicitySuggestion,
8
+ * FieldDiscoveryOnUsage,
10
9
  * FieldDiscoveryResult,
11
10
  * FieldDiscoverySuggestion,
11
+ * FieldDiscoveryTargetSuggestion,
12
12
  * FieldDiscoveryTypeSuggestion,
13
13
  * } from './discover-fields.types.ts';
14
14
  */
15
15
 
16
16
  import { readFile } from 'node:fs/promises';
17
17
  import process from 'node:process';
18
- import { resolve } from 'node:path';
18
+ import { posix, resolve } from 'node:path';
19
19
 
20
20
  import { DEFAULT_INCLUDE_PATTERNS } from '../config/source-file-defaults.js';
21
21
  import { listSourceFiles } from './list-source-files.js';
@@ -26,21 +26,8 @@ import {
26
26
  } from '../parse/markdown/parse-markdown-directives.js';
27
27
  import { isPathLikeTarget } from '../parse/claim-helpers.js';
28
28
 
29
- /**
30
- * Field discovery from source claims.
31
- *
32
- * Scans the repository source files directly, infers likely metadata fields,
33
- * and reports advisory suggestions without requiring repo config to load.
34
- *
35
- * Kind: discovery
36
- * Status: active
37
- * Tracked in: ../../docs/plans/v1/field-model-redesign.md
38
- * Decided by: ../../docs/decisions/field-discovery-workflow.md
39
- * @patram
40
- * @see {@link ../output/render-field-discovery.js}
41
- */
42
-
43
29
  const TYPE_NAME_ORDER = /** @type {const} */ ([
30
+ 'ref',
44
31
  'integer',
45
32
  'date_time',
46
33
  'date',
@@ -57,7 +44,11 @@ const ENUM_PATTERN = /^[a-z0-9_][a-z0-9_-]*$/du;
57
44
  const PATH_PATTERN = /^[a-z0-9_.-]+\.[a-z0-9]+$/du;
58
45
 
59
46
  /**
60
- * @typedef {FieldDiscoveryClassUsage & { confidence: number }} InferredFieldClassUsage
47
+ * @typedef {FieldDiscoveryOnUsage & { confidence: number }} InferredFieldOnUsage
48
+ */
49
+
50
+ /**
51
+ * @typedef {FieldDiscoveryTargetSuggestion & { confidence: number }} InferredFieldTargetSuggestion
61
52
  */
62
53
 
63
54
  /**
@@ -98,20 +89,10 @@ export async function discoverFields(
98
89
  );
99
90
  /** @type {FieldObservation[]} */
100
91
  const field_observations = parse_results.flatMap((parse_result) => {
101
- /** @type {Set<string>} */
102
- const document_classes = new Set();
103
-
104
- for (const claim of parse_result.claims) {
105
- if (
106
- claim.type === 'directive' &&
107
- claim.name === 'kind' &&
108
- typeof claim.value === 'string' &&
109
- claim.value.length > 0
110
- ) {
111
- document_classes.add(claim.value);
112
- }
113
- }
114
-
92
+ const document_types = inferDocumentTypes(
93
+ parse_result.path,
94
+ parse_result.claims,
95
+ );
115
96
  const allowed_markdown_lines = collectAllowedMarkdownDirectiveLines(
116
97
  parse_result.path,
117
98
  parse_result.source_text,
@@ -132,11 +113,11 @@ export async function discoverFields(
132
113
 
133
114
  return [
134
115
  {
135
- class_names: new Set(document_classes),
136
116
  document_id: claim.document_id,
137
117
  name: claim.name,
138
118
  normalized_value: normalizeDiscoveryValue(claim.value),
139
119
  origin: claim.origin,
120
+ type_names: new Set(document_types),
140
121
  value: claim.value,
141
122
  },
142
123
  ];
@@ -188,20 +169,28 @@ export async function discoverFields(
188
169
  * @returns {FieldDiscoverySuggestion}
189
170
  */
190
171
  function buildFieldSuggestion(field_bucket) {
191
- const type_result = inferFieldType(field_bucket.observations);
172
+ const target_result = inferFieldTarget(field_bucket.observations);
173
+ const type_result = inferFieldType(field_bucket.observations, target_result);
192
174
  const multiplicity_result = inferFieldMultiplicity(field_bucket.observations);
193
- const class_usage_result = inferFieldClassUsage(field_bucket.observations);
175
+ const on_result = inferFieldOn(field_bucket.observations);
194
176
  const evidence_references = buildEvidenceReferences(
195
177
  field_bucket.observations,
196
178
  );
197
179
  const conflicting_evidence = buildEvidenceReferences(
198
- field_bucket.observations.filter(
199
- (field_observation) =>
180
+ field_bucket.observations.filter((field_observation) => {
181
+ if (type_result.name === 'ref') {
182
+ return (
183
+ scoreFieldValue(field_observation.normalized_value, 'path') === 0
184
+ );
185
+ }
186
+
187
+ return (
200
188
  scoreFieldValue(
201
189
  field_observation.normalized_value,
202
190
  type_result.name,
203
- ) === 0,
204
- ),
191
+ ) === 0
192
+ );
193
+ }),
205
194
  );
206
195
 
207
196
  return {
@@ -209,16 +198,17 @@ function buildFieldSuggestion(field_bucket) {
209
198
  Math.round(
210
199
  ((type_result.confidence +
211
200
  multiplicity_result.confidence +
212
- class_usage_result.confidence) /
201
+ on_result.confidence) /
213
202
  3) *
214
203
  100,
215
204
  ) / 100,
216
205
  conflicting_evidence,
217
206
  evidence_references,
218
- likely_class_usage: {
219
- classes: class_usage_result.classes,
220
- },
221
207
  likely_multiplicity: multiplicity_result,
208
+ likely_on: {
209
+ types: on_result.types,
210
+ },
211
+ likely_to: type_result.name === 'ref' ? target_result : undefined,
222
212
  likely_type: type_result,
223
213
  name: field_bucket.name,
224
214
  };
@@ -288,29 +278,29 @@ function inferFieldMultiplicity(observations) {
288
278
 
289
279
  /**
290
280
  * @param {FieldObservation[]} observations
291
- * @returns {InferredFieldClassUsage}
281
+ * @returns {InferredFieldOnUsage}
292
282
  */
293
- function inferFieldClassUsage(observations) {
283
+ function inferFieldOn(observations) {
294
284
  /** @type {Map<string, number>} */
295
- const class_counts = new Map();
285
+ const type_counts = new Map();
296
286
  let documented_observation_count = 0;
297
287
 
298
288
  for (const observation of observations) {
299
- if (observation.class_names.size === 0) {
289
+ if (observation.type_names.size === 0) {
300
290
  continue;
301
291
  }
302
292
 
303
293
  documented_observation_count += 1;
304
294
 
305
- for (const class_name of observation.class_names) {
306
- class_counts.set(class_name, (class_counts.get(class_name) ?? 0) + 1);
295
+ for (const type_name of observation.type_names) {
296
+ type_counts.set(type_name, (type_counts.get(type_name) ?? 0) + 1);
307
297
  }
308
298
  }
309
299
 
310
- if (class_counts.size === 0) {
300
+ if (type_counts.size === 0) {
311
301
  return {
312
302
  confidence: 0.2,
313
- classes: ['document'],
303
+ types: ['document'],
314
304
  };
315
305
  }
316
306
 
@@ -319,17 +309,25 @@ function inferFieldClassUsage(observations) {
319
309
  Math.round(
320
310
  (documented_observation_count / Math.max(observations.length, 1)) * 100,
321
311
  ) / 100,
322
- classes: [...class_counts.keys()].sort((left_class, right_class) =>
323
- left_class.localeCompare(right_class, 'en'),
312
+ types: [...type_counts.keys()].sort((left_type, right_type) =>
313
+ left_type.localeCompare(right_type, 'en'),
324
314
  ),
325
315
  };
326
316
  }
327
317
 
328
318
  /**
329
319
  * @param {FieldObservation[]} observations
320
+ * @param {InferredFieldTargetSuggestion | undefined} target_result
330
321
  * @returns {FieldDiscoveryTypeSuggestion}
331
322
  */
332
- function inferFieldType(observations) {
323
+ function inferFieldType(observations, target_result) {
324
+ if (target_result && scoreFieldType(observations, 'path') >= 0.9) {
325
+ return {
326
+ confidence: target_result.confidence,
327
+ name: 'ref',
328
+ };
329
+ }
330
+
333
331
  /** @type {FieldDiscoveryTypeSuggestion[]} */
334
332
  const type_candidates = TYPE_NAME_ORDER.map((type_name) => ({
335
333
  confidence: scoreFieldType(observations, type_name),
@@ -350,6 +348,38 @@ function inferFieldType(observations) {
350
348
  return type_candidates[0];
351
349
  }
352
350
 
351
+ /**
352
+ * @param {FieldObservation[]} observations
353
+ * @returns {InferredFieldTargetSuggestion | undefined}
354
+ */
355
+ function inferFieldTarget(observations) {
356
+ /** @type {Map<string, number>} */
357
+ const target_counts = new Map();
358
+ let path_observation_count = 0;
359
+
360
+ for (const observation of observations) {
361
+ const target_type = inferTargetTypeFromObservation(observation);
362
+
363
+ if (!target_type) {
364
+ continue;
365
+ }
366
+
367
+ path_observation_count += 1;
368
+ target_counts.set(target_type, (target_counts.get(target_type) ?? 0) + 1);
369
+ }
370
+
371
+ if (path_observation_count === 0 || target_counts.size !== 1) {
372
+ return undefined;
373
+ }
374
+
375
+ const [type_name, count] = [...target_counts.entries()][0];
376
+
377
+ return {
378
+ confidence: Math.round((count / path_observation_count) * 100) / 100,
379
+ type: type_name,
380
+ };
381
+ }
382
+
353
383
  /**
354
384
  * @param {FieldDiscoveryEvidenceReference} left_reference
355
385
  * @param {FieldDiscoveryEvidenceReference} right_reference
@@ -434,16 +464,17 @@ const FIELD_TYPE_SCORERS = {
434
464
  value.includes(']')
435
465
  ? 0.8
436
466
  : 1,
467
+ ref: () => 0,
437
468
  string: () => 0.5,
438
469
  };
439
470
 
440
471
  /**
441
472
  * @typedef {{
442
- * class_names: Set<string>,
443
473
  * document_id: string,
444
474
  * name: string,
445
475
  * normalized_value: string,
446
476
  * origin: ClaimOrigin,
477
+ * type_names: Set<string>,
447
478
  * value: string,
448
479
  * }} FieldObservation
449
480
  */
@@ -551,6 +582,114 @@ function normalizeDiscoveryValue(value) {
551
582
  return trimmed_value;
552
583
  }
553
584
 
585
+ /**
586
+ * @param {string} source_path
587
+ * @param {PatramClaim[]} claims
588
+ * @returns {string[]}
589
+ */
590
+ function inferDocumentTypes(source_path, claims) {
591
+ /** @type {Set<string>} */
592
+ const document_types = new Set();
593
+ const path_type = inferPathBackedType(source_path);
594
+
595
+ if (path_type) {
596
+ document_types.add(path_type);
597
+ }
598
+
599
+ for (const claim of claims) {
600
+ if (
601
+ claim.type === 'directive' &&
602
+ (claim.name === 'command' || claim.name === 'term') &&
603
+ typeof claim.value === 'string' &&
604
+ claim.value.length > 0
605
+ ) {
606
+ document_types.add(claim.name);
607
+ }
608
+ }
609
+
610
+ return [...document_types].sort((left_type, right_type) =>
611
+ left_type.localeCompare(right_type, 'en'),
612
+ );
613
+ }
614
+
615
+ /**
616
+ * @param {string} source_path
617
+ * @returns {string | null}
618
+ */
619
+ function inferPathBackedType(source_path) {
620
+ if (source_path.startsWith('docs/conventions/')) {
621
+ return 'convention';
622
+ }
623
+
624
+ if (source_path.startsWith('docs/decisions/')) {
625
+ return 'decision';
626
+ }
627
+
628
+ if (source_path.startsWith('docs/plans/')) {
629
+ return 'plan';
630
+ }
631
+
632
+ if (source_path.startsWith('docs/research/')) {
633
+ return 'idea';
634
+ }
635
+
636
+ if (source_path.startsWith('docs/roadmap/')) {
637
+ return 'roadmap';
638
+ }
639
+
640
+ if (source_path.startsWith('docs/tasks/')) {
641
+ return 'task';
642
+ }
643
+
644
+ return null;
645
+ }
646
+
647
+ /**
648
+ * @param {FieldObservation} observation
649
+ * @returns {string | null}
650
+ */
651
+ function inferTargetTypeFromObservation(observation) {
652
+ const value = resolveDiscoveryTargetPath(
653
+ observation.origin.path,
654
+ observation.normalized_value,
655
+ );
656
+
657
+ if (
658
+ !value.includes('/') &&
659
+ !PATH_PATTERN.test(value) &&
660
+ !value.startsWith('docs/') &&
661
+ !value.startsWith('lib/') &&
662
+ !value.startsWith('test/')
663
+ ) {
664
+ return null;
665
+ }
666
+
667
+ if (value.startsWith('docs/reference/commands/')) {
668
+ return 'command';
669
+ }
670
+
671
+ if (value.startsWith('docs/reference/terms/')) {
672
+ return 'term';
673
+ }
674
+
675
+ return inferPathBackedType(value) ?? 'document';
676
+ }
677
+
678
+ /**
679
+ * @param {string} source_path
680
+ * @param {string} target_value
681
+ * @returns {string}
682
+ */
683
+ function resolveDiscoveryTargetPath(source_path, target_value) {
684
+ if (target_value.startsWith('./') || target_value.startsWith('../')) {
685
+ const parent_directory = posix.dirname(source_path);
686
+
687
+ return posix.normalize(posix.join(parent_directory, target_value));
688
+ }
689
+
690
+ return target_value;
691
+ }
692
+
554
693
  /**
555
694
  * @param {string} field_name
556
695
  * @returns {boolean}
@@ -4,10 +4,10 @@
4
4
  * Expands include globs into stable repo-relative file lists for indexing and
5
5
  * broken-link validation.
6
6
  *
7
- * Kind: scan
8
- * Status: active
9
- * Tracked in: ../../docs/plans/v0/source-anchor-dogfooding.md
10
- * Decided by: ../../docs/decisions/source-scan.md
7
+ * kind: scan
8
+ * status: active
9
+ * tracked_in: ../../docs/plans/v0/source-anchor-dogfooding.md
10
+ * decided_by: ../../docs/decisions/source-scan.md
11
11
  * @patram
12
12
  * @see {@link ../graph/load-project-graph.js}
13
13
  * @see {@link ../../docs/decisions/source-scan.md}
@@ -7,10 +7,10 @@ import { listMatchingFiles } from './list-repo-files.js';
7
7
  * Expands include globs into stable repo-relative file lists for indexing and
8
8
  * broken-link validation.
9
9
  *
10
- * Kind: scan
11
- * Status: active
12
- * Tracked in: ../../docs/plans/v0/source-anchor-dogfooding.md
13
- * Decided by: ../../docs/decisions/source-scan.md
10
+ * kind: scan
11
+ * status: active
12
+ * tracked_in: ../../docs/plans/v0/source-anchor-dogfooding.md
13
+ * decided_by: ../../docs/decisions/source-scan.md
14
14
  * @patram
15
15
  * @see {@link ../graph/load-project-graph.js}
16
16
  * @see {@link ../../docs/decisions/source-scan.md}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patram",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "main": "./lib/patram.js",
6
6
  "types": "./lib/patram.d.ts",
@@ -67,6 +67,7 @@
67
67
  "ansis": "^4.2.0",
68
68
  "beautiful-mermaid": "^1.1.3",
69
69
  "globby": "^16.1.1",
70
+ "jsonc-parser": "^3.3.1",
70
71
  "md4x": "^0.0.25",
71
72
  "shiki": "^4.0.2",
72
73
  "string-width": "^8.2.0",