patram 0.0.2 → 0.1.1

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 (51) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +238 -0
  3. package/lib/build-graph.js +143 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/command-output.js +83 -0
  7. package/lib/layout-stored-queries.js +213 -0
  8. package/lib/list-queries.js +18 -0
  9. package/lib/list-source-files.js +50 -15
  10. package/lib/load-patram-config.js +106 -18
  11. package/lib/load-patram-config.types.ts +9 -0
  12. package/lib/load-project-graph.js +124 -0
  13. package/lib/output-view.types.ts +73 -0
  14. package/lib/parse-claims.js +38 -158
  15. package/lib/parse-claims.types.ts +7 -0
  16. package/lib/parse-cli-arguments-helpers.js +273 -0
  17. package/lib/parse-cli-arguments.js +114 -0
  18. package/lib/parse-cli-arguments.types.ts +24 -0
  19. package/lib/parse-cli-color-options.js +44 -0
  20. package/lib/parse-cli-query-pagination.js +49 -0
  21. package/lib/parse-jsdoc-blocks.js +184 -0
  22. package/lib/parse-jsdoc-claims.js +280 -0
  23. package/lib/parse-jsdoc-prose.js +111 -0
  24. package/lib/parse-markdown-claims.js +242 -0
  25. package/lib/parse-markdown-directives.js +136 -0
  26. package/lib/parse-where-clause.js +312 -0
  27. package/lib/patram-cli.js +337 -0
  28. package/lib/patram-config.js +3 -1
  29. package/lib/patram-config.types.ts +2 -1
  30. package/lib/query-graph.js +256 -0
  31. package/lib/render-check-output.js +315 -0
  32. package/lib/render-json-output.js +108 -0
  33. package/lib/render-output-view.js +193 -0
  34. package/lib/render-plain-output.js +237 -0
  35. package/lib/render-rich-output.js +293 -0
  36. package/lib/render-rich-source.js +1333 -0
  37. package/lib/resolve-check-target.js +190 -0
  38. package/lib/resolve-output-mode.js +60 -0
  39. package/lib/resolve-patram-graph-config.js +88 -0
  40. package/lib/resolve-where-clause.js +51 -0
  41. package/lib/show-document.js +311 -0
  42. package/lib/source-file-defaults.js +28 -0
  43. package/lib/write-paged-output.js +87 -0
  44. package/package.json +21 -10
  45. package/bin/patram.test.js +0 -184
  46. package/lib/build-graph.test.js +0 -141
  47. package/lib/check-graph.test.js +0 -103
  48. package/lib/list-source-files.test.js +0 -101
  49. package/lib/load-patram-config.test.js +0 -211
  50. package/lib/parse-claims.test.js +0 -113
  51. package/lib/patram-config.test.js +0 -147
@@ -1,101 +0,0 @@
1
- import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
-
5
- import { afterEach, expect, it } from 'vitest';
6
-
7
- import { listSourceFiles } from './list-source-files.js';
8
-
9
- const test_context = createTestContext();
10
-
11
- afterEach(async () => {
12
- await cleanupTestContext(test_context);
13
- });
14
-
15
- it('lists matching source files as sorted repo-relative paths', async () => {
16
- test_context.project_directory = await createTempProjectDirectory();
17
-
18
- await writeProjectFile(test_context.project_directory, 'docs/zeta.md');
19
- await writeProjectFile(test_context.project_directory, 'docs/alpha.md');
20
- await writeProjectFile(
21
- test_context.project_directory,
22
- 'docs/nested/overview.md',
23
- );
24
- await writeProjectFile(test_context.project_directory, 'docs/notes.txt');
25
- await writeProjectFile(test_context.project_directory, 'notes/todo.md');
26
-
27
- const source_files = await listSourceFiles(
28
- ['docs/**/*.md'],
29
- test_context.project_directory,
30
- );
31
-
32
- expect(source_files).toEqual([
33
- 'docs/alpha.md',
34
- 'docs/nested/overview.md',
35
- 'docs/zeta.md',
36
- ]);
37
- });
38
-
39
- it('deduplicates paths matched by multiple include globs', async () => {
40
- test_context.project_directory = await createTempProjectDirectory();
41
-
42
- await writeProjectFile(test_context.project_directory, 'docs/patram.md');
43
-
44
- const source_files = await listSourceFiles(
45
- ['docs/**/*.md', 'docs/patram.md'],
46
- test_context.project_directory,
47
- );
48
-
49
- expect(source_files).toEqual(['docs/patram.md']);
50
- });
51
-
52
- /**
53
- * Create a temporary project directory.
54
- *
55
- * @returns {Promise<string>}
56
- */
57
- async function createTempProjectDirectory() {
58
- return mkdtemp(join(tmpdir(), 'patram-list-source-files-'));
59
- }
60
-
61
- /**
62
- * Write a file under the temporary project directory.
63
- *
64
- * @param {string} project_directory
65
- * @param {string} relative_path
66
- */
67
- async function writeProjectFile(project_directory, relative_path) {
68
- const file_path = join(project_directory, relative_path);
69
- const parent_directory = file_path.slice(0, file_path.lastIndexOf('/'));
70
-
71
- await mkdir(parent_directory, { recursive: true });
72
- await writeFile(file_path, `# ${relative_path}\n`);
73
- }
74
-
75
- /**
76
- * Remove a temporary directory tree.
77
- *
78
- * @param {string} project_directory
79
- */
80
- async function removeDirectory(project_directory) {
81
- await rm(project_directory, { force: true, recursive: true });
82
- }
83
-
84
- /**
85
- * @returns {{ project_directory: string | null }}
86
- */
87
- function createTestContext() {
88
- return {
89
- project_directory: null,
90
- };
91
- }
92
-
93
- /**
94
- * @param {{ project_directory: string | null }} test_context
95
- */
96
- async function cleanupTestContext(test_context) {
97
- if (test_context.project_directory) {
98
- await removeDirectory(test_context.project_directory);
99
- test_context.project_directory = null;
100
- }
101
- }
@@ -1,211 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
-
5
- import { afterEach, expect, it } from 'vitest';
6
-
7
- import { loadPatramConfig } from './load-patram-config.js';
8
-
9
- const test_context = createTestContext();
10
-
11
- afterEach(async () => {
12
- await cleanupTestContext(test_context);
13
- });
14
-
15
- it('loads and validates .patram.json from a project directory', async () => {
16
- test_context.project_directory = await createTempProjectDirectory();
17
- await writeProjectConfig(
18
- test_context.project_directory,
19
- createValidConfigSource(),
20
- );
21
-
22
- const load_result = await loadPatramConfig(test_context.project_directory);
23
-
24
- expect(load_result).toEqual({
25
- config: {
26
- include: ['docs/**/*.md'],
27
- queries: {
28
- pending: {
29
- where: 'kind=task and status=pending',
30
- },
31
- },
32
- },
33
- config_path: '.patram.json',
34
- diagnostics: [],
35
- });
36
- });
37
-
38
- it('defaults to the current working directory', async () => {
39
- test_context.project_directory = await createTempProjectDirectory();
40
- await writeProjectConfig(
41
- test_context.project_directory,
42
- createValidConfigSource(),
43
- );
44
- process.chdir(test_context.project_directory);
45
-
46
- const load_result = await loadPatramConfig();
47
-
48
- expect(load_result.config?.queries.pending.where).toBe(
49
- 'kind=task and status=pending',
50
- );
51
- expect(load_result.diagnostics).toEqual([]);
52
- });
53
-
54
- it('reports a missing config file', async () => {
55
- test_context.project_directory = await createTempProjectDirectory();
56
- const load_result = await loadPatramConfig(test_context.project_directory);
57
-
58
- expect(load_result.config).toBeNull();
59
- expect(load_result.diagnostics).toEqual([
60
- {
61
- code: 'config.not_found',
62
- column: 1,
63
- level: 'error',
64
- line: 1,
65
- message: 'Config file ".patram.json" was not found.',
66
- path: '.patram.json',
67
- },
68
- ]);
69
- });
70
-
71
- it('reports invalid JSON syntax', async () => {
72
- test_context.project_directory = await createTempProjectDirectory();
73
- await writeProjectConfig(test_context.project_directory, createBrokenJson());
74
-
75
- const load_result = await loadPatramConfig(test_context.project_directory);
76
-
77
- expect(load_result.config).toBeNull();
78
- expect(load_result.diagnostics).toEqual([
79
- {
80
- code: 'config.invalid_json',
81
- column: 1,
82
- level: 'error',
83
- line: 3,
84
- message: 'Invalid JSON syntax.',
85
- path: '.patram.json',
86
- },
87
- ]);
88
- });
89
-
90
- it('reports config validation diagnostics', async () => {
91
- test_context.project_directory = await createTempProjectDirectory();
92
- await writeProjectConfig(
93
- test_context.project_directory,
94
- createInvalidConfigSource(),
95
- );
96
-
97
- const load_result = await loadPatramConfig(test_context.project_directory);
98
-
99
- expect(load_result.config).toBeNull();
100
- expect(load_result.diagnostics).toEqual([
101
- {
102
- code: 'config.invalid',
103
- column: 1,
104
- level: 'error',
105
- line: 1,
106
- message:
107
- 'Invalid config at "include": Include must contain at least one glob.',
108
- path: '.patram.json',
109
- },
110
- {
111
- code: 'config.invalid',
112
- column: 1,
113
- level: 'error',
114
- line: 1,
115
- message:
116
- 'Invalid config at "queries.pending.where": Stored query "where" must not be empty.',
117
- path: '.patram.json',
118
- },
119
- ]);
120
- });
121
-
122
- /**
123
- * Create a temporary project directory.
124
- *
125
- * @returns {Promise<string>}
126
- */
127
- async function createTempProjectDirectory() {
128
- return mkdtemp(join(tmpdir(), 'patram-load-config-'));
129
- }
130
-
131
- /**
132
- * Write a project config file.
133
- *
134
- * @param {string} project_directory
135
- * @param {string} config_source
136
- */
137
- async function writeProjectConfig(project_directory, config_source) {
138
- await writeFile(join(project_directory, '.patram.json'), config_source);
139
- }
140
-
141
- /**
142
- * Remove a temporary directory tree.
143
- *
144
- * @param {string} project_directory
145
- */
146
- async function removeDirectory(project_directory) {
147
- await rm(project_directory, { force: true, recursive: true });
148
- }
149
-
150
- /**
151
- * Create invalid JSON for parser tests.
152
- *
153
- * @returns {string}
154
- */
155
- function createBrokenJson() {
156
- return ['{', ' "include": [', '}'].join('\n');
157
- }
158
-
159
- /**
160
- * Create invalid config JSON.
161
- *
162
- * @returns {string}
163
- */
164
- function createInvalidConfigSource() {
165
- return JSON.stringify({
166
- include: [],
167
- queries: {
168
- pending: {
169
- where: '',
170
- },
171
- },
172
- });
173
- }
174
-
175
- /**
176
- * Create valid config JSON.
177
- *
178
- * @returns {string}
179
- */
180
- function createValidConfigSource() {
181
- return JSON.stringify({
182
- include: ['docs/**/*.md'],
183
- queries: {
184
- pending: {
185
- where: 'kind=task and status=pending',
186
- },
187
- },
188
- });
189
- }
190
-
191
- /**
192
- * @returns {{ original_working_directory: string, project_directory: string | null }}
193
- */
194
- function createTestContext() {
195
- return {
196
- original_working_directory: process.cwd(),
197
- project_directory: null,
198
- };
199
- }
200
-
201
- /**
202
- * @param {{ original_working_directory: string, project_directory: string | null }} test_context
203
- */
204
- async function cleanupTestContext(test_context) {
205
- process.chdir(test_context.original_working_directory);
206
-
207
- if (test_context.project_directory) {
208
- await removeDirectory(test_context.project_directory);
209
- test_context.project_directory = null;
210
- }
211
- }
@@ -1,113 +0,0 @@
1
- import { expect, it } from 'vitest';
2
-
3
- import { parseClaims } from './parse-claims.js';
4
-
5
- it('extracts markdown title, links and directives as neutral claims', () => {
6
- const claims = parseClaims({
7
- path: 'docs/patram.md',
8
- source: createMarkdownSource(),
9
- });
10
-
11
- expect(claims).toEqual(createExpectedMarkdownClaims());
12
- });
13
-
14
- it('uses the first line as a plain markdown title', () => {
15
- const claims = parseClaims({
16
- path: 'docs/plain-title.md',
17
- source: ['Patram Plain Title', '', 'Body text.'].join('\n'),
18
- });
19
-
20
- expect(claims).toEqual([
21
- {
22
- document_id: 'doc:docs/plain-title.md',
23
- id: 'claim:doc:docs/plain-title.md:1',
24
- origin: {
25
- column: 1,
26
- line: 1,
27
- path: 'docs/plain-title.md',
28
- },
29
- type: 'document.title',
30
- value: 'Patram Plain Title',
31
- },
32
- ]);
33
- });
34
-
35
- it('returns no claims for unsupported file types', () => {
36
- const claims = parseClaims({
37
- path: 'bin/patram.js',
38
- source: 'console.log("Patram");',
39
- });
40
-
41
- expect(claims).toEqual([]);
42
- });
43
-
44
- it('returns no claims for extensionless files', () => {
45
- const claims = parseClaims({
46
- path: 'README',
47
- source: '# Patram',
48
- });
49
-
50
- expect(claims).toEqual([]);
51
- });
52
-
53
- /**
54
- * Create markdown input for parser tests.
55
- *
56
- * @returns {string}
57
- */
58
- function createMarkdownSource() {
59
- return [
60
- '# Patram',
61
- '',
62
- 'Read the [graph design](./graph-v0.md).',
63
- 'Defined by: terms/patram.md',
64
- ].join('\n');
65
- }
66
-
67
- /**
68
- * Create the expected markdown claims.
69
- *
70
- * @returns {object[]}
71
- */
72
- function createExpectedMarkdownClaims() {
73
- return [
74
- {
75
- document_id: 'doc:docs/patram.md',
76
- id: 'claim:doc:docs/patram.md:1',
77
- origin: {
78
- column: 1,
79
- line: 1,
80
- path: 'docs/patram.md',
81
- },
82
- type: 'document.title',
83
- value: 'Patram',
84
- },
85
- {
86
- document_id: 'doc:docs/patram.md',
87
- id: 'claim:doc:docs/patram.md:2',
88
- origin: {
89
- column: 10,
90
- line: 3,
91
- path: 'docs/patram.md',
92
- },
93
- type: 'markdown.link',
94
- value: {
95
- target: './graph-v0.md',
96
- text: 'graph design',
97
- },
98
- },
99
- {
100
- document_id: 'doc:docs/patram.md',
101
- id: 'claim:doc:docs/patram.md:3',
102
- name: 'defined_by',
103
- origin: {
104
- column: 1,
105
- line: 4,
106
- path: 'docs/patram.md',
107
- },
108
- parser: 'markdown',
109
- type: 'directive',
110
- value: 'terms/patram.md',
111
- },
112
- ];
113
- }
@@ -1,147 +0,0 @@
1
- import { expect, it } from 'vitest';
2
-
3
- import graph_v0_config from '../docs/graph-v0.config.json' with { type: 'json' };
4
- import { parsePatramConfig } from './patram-config.js';
5
-
6
- it('parses the documented v0 config example', () => {
7
- const config_json = parsePatramConfig(graph_v0_config);
8
-
9
- expect(config_json.kinds.term.label).toBe('Term');
10
- expect(config_json.mappings['markdown.directive.defined_by'].emit).toEqual({
11
- relation: 'defines',
12
- target: 'path',
13
- target_kind: 'term',
14
- });
15
- });
16
-
17
- it('rejects mappings that reference unknown relations', () => {
18
- const issues = getIssues(createMissingRelationConfig());
19
-
20
- expect(issues).toContainEqual(
21
- expect.objectContaining({
22
- message: 'Unknown relation "missing_relation".',
23
- path: ['mappings', 'markdown.link', 'emit', 'relation'],
24
- }),
25
- );
26
- });
27
-
28
- it('rejects mappings that reference unknown kinds', () => {
29
- const issues = getIssues(createUnknownKindConfig());
30
-
31
- expect(issues).toContainEqual(
32
- expect.objectContaining({
33
- message: 'Unknown kind "term".',
34
- path: [
35
- 'mappings',
36
- 'markdown.directive.defined_by',
37
- 'emit',
38
- 'target_kind',
39
- ],
40
- }),
41
- );
42
- });
43
-
44
- it('rejects mappings without semantic output', () => {
45
- const issues = getIssues(createEmptyMappingConfig());
46
-
47
- expect(issues).toContainEqual(
48
- expect.objectContaining({
49
- message: 'Mapping must define at least one of "emit" or "node".',
50
- path: ['mappings', 'document.title'],
51
- }),
52
- );
53
- });
54
-
55
- /**
56
- * Create a config with an unknown relation reference.
57
- *
58
- * @returns {object}
59
- */
60
- function createMissingRelationConfig() {
61
- return {
62
- kinds: {
63
- document: {
64
- builtin: true,
65
- },
66
- },
67
- mappings: {
68
- 'markdown.link': {
69
- emit: {
70
- relation: 'missing_relation',
71
- target: 'path',
72
- target_kind: 'document',
73
- },
74
- },
75
- },
76
- relations: {},
77
- };
78
- }
79
-
80
- /**
81
- * Create a config with an unknown kind reference.
82
- *
83
- * @returns {object}
84
- */
85
- function createUnknownKindConfig() {
86
- return {
87
- kinds: {
88
- document: {
89
- builtin: true,
90
- },
91
- },
92
- mappings: {
93
- 'markdown.directive.defined_by': {
94
- emit: {
95
- relation: 'links_to',
96
- target: 'path',
97
- target_kind: 'term',
98
- },
99
- },
100
- },
101
- relations: {
102
- links_to: {
103
- from: ['document'],
104
- to: ['document'],
105
- },
106
- },
107
- };
108
- }
109
-
110
- /**
111
- * Create a config with an empty mapping definition.
112
- *
113
- * @returns {object}
114
- */
115
- function createEmptyMappingConfig() {
116
- return {
117
- kinds: {
118
- document: {
119
- builtin: true,
120
- },
121
- },
122
- mappings: {
123
- 'document.title': {},
124
- },
125
- relations: {},
126
- };
127
- }
128
-
129
- /**
130
- * Parse config and return validation issues.
131
- *
132
- * @param {object} config_json
133
- * @returns {unknown[]}
134
- */
135
- function getIssues(config_json) {
136
- try {
137
- parsePatramConfig(config_json);
138
- } catch (error) {
139
- if (error instanceof Error && 'issues' in error) {
140
- return /** @type {{ issues: unknown[] }} */ (error).issues;
141
- }
142
-
143
- throw error;
144
- }
145
-
146
- throw new Error('Expected configuration parsing to fail.');
147
- }