@opensip-cli/graph-adapter-common 0.1.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 (52) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +31 -0
  4. package/dist/__tests__/discover-config.test.d.ts +16 -0
  5. package/dist/__tests__/discover-config.test.d.ts.map +1 -0
  6. package/dist/__tests__/discover-config.test.js +67 -0
  7. package/dist/__tests__/discover-config.test.js.map +1 -0
  8. package/dist/__tests__/helpers.test.d.ts +2 -0
  9. package/dist/__tests__/helpers.test.d.ts.map +1 -0
  10. package/dist/__tests__/helpers.test.js +258 -0
  11. package/dist/__tests__/helpers.test.js.map +1 -0
  12. package/dist/__tests__/parse-from-adapter.test.d.ts +14 -0
  13. package/dist/__tests__/parse-from-adapter.test.d.ts.map +1 -0
  14. package/dist/__tests__/parse-from-adapter.test.js +93 -0
  15. package/dist/__tests__/parse-from-adapter.test.js.map +1 -0
  16. package/dist/__tests__/run-walk.test.d.ts +12 -0
  17. package/dist/__tests__/run-walk.test.d.ts.map +1 -0
  18. package/dist/__tests__/run-walk.test.js +183 -0
  19. package/dist/__tests__/run-walk.test.js.map +1 -0
  20. package/dist/body-digest.d.ts +25 -0
  21. package/dist/body-digest.d.ts.map +1 -0
  22. package/dist/body-digest.js +39 -0
  23. package/dist/body-digest.js.map +1 -0
  24. package/dist/cache-key.d.ts +34 -0
  25. package/dist/cache-key.d.ts.map +1 -0
  26. package/dist/cache-key.js +54 -0
  27. package/dist/cache-key.js.map +1 -0
  28. package/dist/discover.d.ts +35 -0
  29. package/dist/discover.d.ts.map +1 -0
  30. package/dist/discover.js +114 -0
  31. package/dist/discover.js.map +1 -0
  32. package/dist/index.d.ts +26 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +25 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/parse-from-adapter.d.ts +24 -0
  37. package/dist/parse-from-adapter.d.ts.map +1 -0
  38. package/dist/parse-from-adapter.js +81 -0
  39. package/dist/parse-from-adapter.js.map +1 -0
  40. package/dist/parse.d.ts +24 -0
  41. package/dist/parse.d.ts.map +1 -0
  42. package/dist/parse.js +14 -0
  43. package/dist/parse.js.map +1 -0
  44. package/dist/return-discarded.d.ts +22 -0
  45. package/dist/return-discarded.d.ts.map +1 -0
  46. package/dist/return-discarded.js +31 -0
  47. package/dist/return-discarded.js.map +1 -0
  48. package/dist/walk.d.ts +108 -0
  49. package/dist/walk.d.ts.map +1 -0
  50. package/dist/walk.js +141 -0
  51. package/dist/walk.js.map +1 -0
  52. package/package.json +49 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Behavior tests for `createParseProjectFromAdapter`.
3
+ *
4
+ * The driver is deliberately decoupled from any one grammar: it accepts a
5
+ * `LanguageAdapter<ParsedFile>` and only ever touches `adapter.id`,
6
+ * `adapter.parse(source, path)`, and the returned `parsed.tree.rootNode.hasError`
7
+ * flag. These tests drive it with a tiny fake adapter (the same `as never`
8
+ * fake-node harness the sibling tests use) plus real temp source files, and
9
+ * assert the real I-7 contract: every file either lands in `project.files`
10
+ * with its source threaded through, or surfaces in `parseErrors` — and a
11
+ * `hasError` tree is kept as a partial parse AND recorded as an error.
12
+ */
13
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+ import { createParseProjectFromAdapter } from '../parse-from-adapter.js';
18
+ // A fake parsed tree whose only observed field is `rootNode.hasError`. The
19
+ // driver also threads `source` straight through, so we keep it on the object.
20
+ const mkParsed = (source, hasError) => ({ source, tree: { rootNode: { hasError } } });
21
+ /**
22
+ * A minimal `LanguageAdapter<ParsedFile>` that records every `parse` call and
23
+ * flags any file whose source contains `BROKEN` as `hasError`. This is enough
24
+ * to exercise both sides of the `hasError` branch with real on-disk files.
25
+ */
26
+ const makeFakeAdapter = () => {
27
+ const calls = [];
28
+ const adapter = {
29
+ id: 'fake',
30
+ parse(source, path) {
31
+ calls.push({ source, path });
32
+ return mkParsed(source, source.includes('BROKEN'));
33
+ },
34
+ };
35
+ return { adapter, calls };
36
+ };
37
+ describe('createParseProjectFromAdapter', () => {
38
+ let dir;
39
+ beforeEach(() => {
40
+ dir = mkdtempSync(join(tmpdir(), 'gac-pfa-'));
41
+ });
42
+ afterEach(() => {
43
+ rmSync(dir, { recursive: true, force: true });
44
+ });
45
+ it('parses each file via the adapter, threading source through, no errors for clean files', () => {
46
+ const a = join(dir, 'a.txt');
47
+ const b = join(dir, 'b.txt');
48
+ writeFileSync(a, 'alpha-source', 'utf8');
49
+ writeFileSync(b, 'beta-source', 'utf8');
50
+ const { adapter, calls } = makeFakeAdapter();
51
+ const parseProject = createParseProjectFromAdapter(adapter);
52
+ const out = parseProject({ projectDirAbs: dir, files: [a, b], resolutionMode: 'exact' });
53
+ expect(out.parseErrors).toEqual([]);
54
+ expect(out.project.files.size).toBe(2);
55
+ // The adapter was asked to parse each file's real on-disk content.
56
+ expect(calls).toEqual([
57
+ { source: 'alpha-source', path: a },
58
+ { source: 'beta-source', path: b },
59
+ ]);
60
+ // The parsed file holds the original source text for later body slicing.
61
+ expect(out.project.files.get(a)?.source).toBe('alpha-source');
62
+ expect(out.project.files.get(b)?.source).toBe('beta-source');
63
+ });
64
+ it('records a parseError (project-relative path) when the tree reports hasError, keeping the partial tree', () => {
65
+ const good = join(dir, 'good.txt');
66
+ const bad = join(dir, 'bad.txt');
67
+ writeFileSync(good, 'fine', 'utf8');
68
+ writeFileSync(bad, 'BROKEN tree', 'utf8');
69
+ const { adapter } = makeFakeAdapter();
70
+ const parseProject = createParseProjectFromAdapter(adapter);
71
+ const out = parseProject({
72
+ projectDirAbs: dir,
73
+ files: [good, bad],
74
+ resolutionMode: 'exact',
75
+ });
76
+ // hasError is recorded as an error...
77
+ expect(out.parseErrors).toHaveLength(1);
78
+ expect(out.parseErrors[0]?.filePath).toBe('bad.txt');
79
+ expect(out.parseErrors[0]?.message).toContain('partial tree retained');
80
+ // ...but the partial tree is still retained in the project.
81
+ expect(out.project.files.has(bad)).toBe(true);
82
+ expect(out.project.files.has(good)).toBe(true);
83
+ expect(out.project.files.size).toBe(2);
84
+ });
85
+ it('returns an empty project for an empty file list', () => {
86
+ const { adapter } = makeFakeAdapter();
87
+ const parseProject = createParseProjectFromAdapter(adapter);
88
+ const out = parseProject({ projectDirAbs: dir, files: [], resolutionMode: 'exact' });
89
+ expect(out.project.files.size).toBe(0);
90
+ expect(out.parseErrors).toEqual([]);
91
+ });
92
+ });
93
+ //# sourceMappingURL=parse-from-adapter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-from-adapter.test.js","sourceRoot":"","sources":["../../src/__tests__/parse-from-adapter.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Behavior tests for the shared `runWalk` driver.
3
+ *
4
+ * `runWalk` owns the `walkProject` skeleton: it allocates the three output
5
+ * sinks, filters `input.files` to those actually present in the parsed
6
+ * project, sorts them for I-1 determinism, runs the adapter's per-file
7
+ * `walkFile` inside a try/catch, and folds any throw into a project-relative
8
+ * `ParseError`. These tests assert each of those contracts with a fake
9
+ * `walkFile` that records what it sees and pushes into the supplied sinks.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=run-walk.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-walk.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/run-walk.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Behavior tests for the shared `runWalk` driver.
3
+ *
4
+ * `runWalk` owns the `walkProject` skeleton: it allocates the three output
5
+ * sinks, filters `input.files` to those actually present in the parsed
6
+ * project, sorts them for I-1 determinism, runs the adapter's per-file
7
+ * `walkFile` inside a try/catch, and folds any throw into a project-relative
8
+ * `ParseError`. These tests assert each of those contracts with a fake
9
+ * `walkFile` that records what it sees and pushes into the supplied sinks.
10
+ */
11
+ import { describe, expect, it } from 'vitest';
12
+ import { buildNameIndex, record, runWalk } from '../walk.js';
13
+ // A parsed file is opaque to runWalk (only walkFile reads it), so a tagged
14
+ // stub suffices.
15
+ const mkFile = (tag) => ({ tag });
16
+ const occ = (name, hash) => ({
17
+ bodyHash: hash,
18
+ bodySize: 1,
19
+ simpleName: name,
20
+ qualifiedName: name,
21
+ filePath: 'x',
22
+ line: 1,
23
+ column: 0,
24
+ endLine: 1,
25
+ kind: 'function-declaration',
26
+ params: [],
27
+ returnType: null,
28
+ enclosingClass: null,
29
+ decorators: [],
30
+ visibility: 'private',
31
+ inTestFile: false,
32
+ definedInGenerated: false,
33
+ calls: [],
34
+ });
35
+ const callSite = (ownerHash) => ({
36
+ nodeRef: {},
37
+ sourceFileRef: {},
38
+ ownerHash,
39
+ kind: 'call',
40
+ });
41
+ const depSite = (specifier, ownerHash) => ({
42
+ nodeRef: {},
43
+ sourceFileRef: {},
44
+ ownerHash,
45
+ specifier,
46
+ line: 1,
47
+ column: 0,
48
+ });
49
+ describe('runWalk', () => {
50
+ it('visits parsed files in sorted order, threading sinks + projectDir into walkFile', () => {
51
+ const files = new Map([
52
+ ['/proj/b.go', mkFile('b')],
53
+ ['/proj/a.go', mkFile('a')],
54
+ ]);
55
+ const input = {
56
+ project: { files },
57
+ projectDirAbs: '/proj',
58
+ // Deliberately out of order — runWalk must sort for determinism.
59
+ files: ['/proj/b.go', '/proj/a.go'],
60
+ };
61
+ const seen = [];
62
+ const out = runWalk({
63
+ input,
64
+ walkFile: (absPath, file, projectDirAbs, sinks) => {
65
+ seen.push({ path: absPath, projectDirAbs });
66
+ // Push one of each record kind so we can assert the sinks flow through.
67
+ record(sinks.occurrences, occ(`fn-${file.tag}`, `h-${absPath}`));
68
+ sinks.callSites.push(callSite(`h-${absPath}`));
69
+ sinks.dependencySites.push(depSite('./dep', `h-${absPath}`));
70
+ },
71
+ });
72
+ // Sorted: a.go before b.go.
73
+ expect(seen.map((s) => s.path)).toEqual(['/proj/a.go', '/proj/b.go']);
74
+ expect(seen.every((s) => s.projectDirAbs === '/proj')).toBe(true);
75
+ // Sinks flowed back out into WalkOutput.
76
+ expect(out.occurrences['fn-a']?.[0]?.bodyHash).toBe('h-/proj/a.go');
77
+ expect(out.occurrences['fn-b']?.[0]?.bodyHash).toBe('h-/proj/b.go');
78
+ expect(out.callSites).toHaveLength(2);
79
+ expect(out.dependencySites).toHaveLength(2);
80
+ expect(out.parseErrors).toEqual([]);
81
+ });
82
+ it('filters out requested files that are not in the parsed project', () => {
83
+ const files = new Map([['/proj/a.go', mkFile('a')]]);
84
+ const input = {
85
+ project: { files },
86
+ projectDirAbs: '/proj',
87
+ files: ['/proj/a.go', '/proj/missing.go'],
88
+ };
89
+ const visited = [];
90
+ runWalk({
91
+ input,
92
+ walkFile: (absPath) => {
93
+ visited.push(absPath);
94
+ },
95
+ });
96
+ expect(visited).toEqual(['/proj/a.go']);
97
+ });
98
+ it('records a project-relative ParseError when walkFile throws an Error', () => {
99
+ const files = new Map([['/proj/pkg/bad.go', mkFile('bad')]]);
100
+ const input = {
101
+ project: { files },
102
+ projectDirAbs: '/proj',
103
+ files: ['/proj/pkg/bad.go'],
104
+ };
105
+ const out = runWalk({
106
+ input,
107
+ walkFile: () => {
108
+ throw new Error('boom');
109
+ },
110
+ });
111
+ expect(out.parseErrors).toHaveLength(1);
112
+ expect(out.parseErrors[0]?.filePath).toBe('pkg/bad.go');
113
+ expect(out.parseErrors[0]?.message).toBe('boom');
114
+ // The walk still returns the (empty) sinks rather than aborting.
115
+ expect(out.callSites).toEqual([]);
116
+ });
117
+ it('stringifies a non-Error throw value in the ParseError message', () => {
118
+ const files = new Map([['/proj/a.go', mkFile('a')]]);
119
+ const input = {
120
+ project: { files },
121
+ projectDirAbs: '/proj',
122
+ files: ['/proj/a.go'],
123
+ };
124
+ const out = runWalk({
125
+ input,
126
+ walkFile: () => {
127
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- exercising the non-Error branch
128
+ throw 'plain-string-failure';
129
+ },
130
+ });
131
+ expect(out.parseErrors[0]?.message).toBe('plain-string-failure');
132
+ });
133
+ it('skips an entry whose project map claims `has` but returns no value from `get` (defensive guard)', () => {
134
+ // A Map-like whose has/get disagree for one key — the exact inconsistency
135
+ // the `if (!file) continue;` guard defends against (e.g. a concurrently
136
+ // mutated map). The driver must skip it without throwing.
137
+ const real = new Map([['/proj/a.go', mkFile('a')]]);
138
+ const inconsistent = {
139
+ has: (k) => k === '/proj/a.go' || k === '/proj/ghost.go',
140
+ get: (k) => real.get(k),
141
+ };
142
+ const input = {
143
+ project: { files: inconsistent },
144
+ projectDirAbs: '/proj',
145
+ files: ['/proj/a.go', '/proj/ghost.go'],
146
+ };
147
+ const visited = [];
148
+ const out = runWalk({
149
+ input,
150
+ walkFile: (absPath) => {
151
+ visited.push(absPath);
152
+ },
153
+ });
154
+ // ghost.go passed the `has` filter but `get` returned undefined → skipped.
155
+ expect(visited).toEqual(['/proj/a.go']);
156
+ expect(out.parseErrors).toEqual([]);
157
+ });
158
+ });
159
+ describe('buildNameIndex — empty / sparse occurrence slots', () => {
160
+ it('skips a name whose occurrence list is empty (no entry created)', () => {
161
+ // A real name mapping to an empty array must NOT produce a map entry,
162
+ // since there are no bodyHashes to resolve against.
163
+ const functions = {
164
+ foo: [occ('foo', 'h1')],
165
+ bar: [], // empty → length 0 → not indexed
166
+ };
167
+ const idx = buildNameIndex(functions);
168
+ expect(idx.get('foo')).toEqual(['h1']);
169
+ expect(idx.has('bar')).toBe(false);
170
+ });
171
+ it('skips a falsy occurrence slot (defensive `!occs` guard)', () => {
172
+ // A sparse/undefined slot (e.g. from a holey record) must be skipped
173
+ // rather than throwing on iteration.
174
+ const functions = {
175
+ foo: [occ('foo', 'h1')],
176
+ missing: undefined,
177
+ };
178
+ const idx = buildNameIndex(functions);
179
+ expect(idx.get('foo')).toEqual(['h1']);
180
+ expect(idx.has('missing')).toBe(false);
181
+ });
182
+ });
183
+ //# sourceMappingURL=run-walk.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-walk.test.js","sourceRoot":"","sources":["../../src/__tests__/run-walk.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared body-digest leaf primitives.
3
+ *
4
+ * The per-language comment strippers (`stripGoComments`,
5
+ * `stripRustComments` with nested block comments + char-vs-lifetime
6
+ * heuristic, `stripPythonComments` + docstring strip, Java's stripper)
7
+ * stay in each adapter's `body-digest.ts` — they are genuinely
8
+ * language-specific. Only the byte-identical `skipToEndOfLine` primitive
9
+ * (scan from `start` to the next newline) is shared here; each adapter's
10
+ * `digestXBody = hashBody(normalizeWhitespace(stripXComments(text)))`
11
+ * wiring and its `digestSyntheticBody` alias remain adapter-owned.
12
+ */
13
+ /** Advance from `start` to the index of the next `\n` (or end of text). */
14
+ export declare function skipToEndOfLine(text: string, start: number): number;
15
+ /**
16
+ * Skip a NON-nesting C-style block comment: from `start` (just past the
17
+ * opening `/*`), scan to and past the first `*\/`. Returns the index
18
+ * immediately after the closing delimiter, or end-of-text if unterminated.
19
+ *
20
+ * This is the Go / Java / C-style form, where block comments do NOT nest.
21
+ * Languages whose block comments DO nest (e.g. Rust's `/* /* *\/ *\/`)
22
+ * need their own depth-tracking variant and must not use this one.
23
+ */
24
+ export declare function skipBlockComment(text: string, start: number): number;
25
+ //# sourceMappingURL=body-digest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-digest.d.ts","sourceRoot":"","sources":["../src/body-digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,2EAA2E;AAC3E,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAInE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpE"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared body-digest leaf primitives.
3
+ *
4
+ * The per-language comment strippers (`stripGoComments`,
5
+ * `stripRustComments` with nested block comments + char-vs-lifetime
6
+ * heuristic, `stripPythonComments` + docstring strip, Java's stripper)
7
+ * stay in each adapter's `body-digest.ts` — they are genuinely
8
+ * language-specific. Only the byte-identical `skipToEndOfLine` primitive
9
+ * (scan from `start` to the next newline) is shared here; each adapter's
10
+ * `digestXBody = hashBody(normalizeWhitespace(stripXComments(text)))`
11
+ * wiring and its `digestSyntheticBody` alias remain adapter-owned.
12
+ */
13
+ /** Advance from `start` to the index of the next `\n` (or end of text). */
14
+ export function skipToEndOfLine(text, start) {
15
+ let i = start;
16
+ while (i < text.length && text[i] !== '\n')
17
+ i++;
18
+ return i;
19
+ }
20
+ /**
21
+ * Skip a NON-nesting C-style block comment: from `start` (just past the
22
+ * opening `/*`), scan to and past the first `*\/`. Returns the index
23
+ * immediately after the closing delimiter, or end-of-text if unterminated.
24
+ *
25
+ * This is the Go / Java / C-style form, where block comments do NOT nest.
26
+ * Languages whose block comments DO nest (e.g. Rust's `/* /* *\/ *\/`)
27
+ * need their own depth-tracking variant and must not use this one.
28
+ */
29
+ export function skipBlockComment(text, start) {
30
+ let i = start;
31
+ while (i < text.length) {
32
+ if (text.slice(i, i + 2) === '*/')
33
+ return i + 2;
34
+ i++;
35
+ }
36
+ /* v8 ignore next */
37
+ return i;
38
+ }
39
+ //# sourceMappingURL=body-digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-digest.js","sourceRoot":"","sources":["../src/body-digest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa;IACzD,IAAI,CAAC,GAAG,KAAK,CAAC;IACd,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,CAAC,EAAE,CAAC;IAChD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,CAAC,GAAG,KAAK,CAAC;IACd,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI;YAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAChD,CAAC,EAAE,CAAC;IACN,CAAC;IACD,oBAAoB;IACpB,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared config-fingerprint helpers for the tree-sitter adapters'
3
+ * `cacheKey`.
4
+ *
5
+ * `hashConfig(configPathAbs)` is byte-identical in graph-go / graph-java /
6
+ * graph-rust: it returns the literal `no-config` when no anchor exists,
7
+ * `missing:<path>` / `unreadable:<path>` sentinels on fs failure, and the
8
+ * first 16 hex chars of the sha256 of the manifest content otherwise. Per
9
+ * contract invariant I-6 it is a pure function of the config content.
10
+ *
11
+ * `makeConfigCacheKey({ prefix })` returns the trivial
12
+ * `cacheKey(input) => `${prefix}-${hashConfig(...)}`` used by go/java/rust
13
+ * (with prefixes `go-` / `java-` / `rs-`). Python keeps its own
14
+ * `cache-key.ts` but imports `hashConfig` from here and layers its
15
+ * `requires-python` extraction on top (DEC-4).
16
+ */
17
+ import type { CacheKeyInput } from '@opensip-cli/graph';
18
+ /**
19
+ * Fingerprint a language config file's content.
20
+ *
21
+ * - `undefined` / empty path → `'no-config'`
22
+ * - path does not exist → `'missing:<path>'`
23
+ * - read fails → `'unreadable:<path>'`
24
+ * - otherwise → first 16 hex of sha256(content)
25
+ */
26
+ export declare function hashConfig(configPathAbs: string | undefined): string;
27
+ /**
28
+ * Builds a `cacheKey` that emits `${prefix}-${hashConfig(configPathAbs)}`.
29
+ * Per I-8 the prefix must be distinct per adapter (`go-`, `java-`, `rs-`).
30
+ */
31
+ export declare function makeConfigCacheKey(options: {
32
+ readonly prefix: string;
33
+ }): (input: CacheKeyInput) => string;
34
+ //# sourceMappingURL=cache-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-key.d.ts","sourceRoot":"","sources":["../src/cache-key.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAcpE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAKnC"}
@@ -0,0 +1,54 @@
1
+ // @fitness-ignore-file unbounded-memory -- reads a single language manifest (go.mod / Cargo.toml / pom.xml / pyproject.toml); bounded by standard project metadata
2
+ /**
3
+ * Shared config-fingerprint helpers for the tree-sitter adapters'
4
+ * `cacheKey`.
5
+ *
6
+ * `hashConfig(configPathAbs)` is byte-identical in graph-go / graph-java /
7
+ * graph-rust: it returns the literal `no-config` when no anchor exists,
8
+ * `missing:<path>` / `unreadable:<path>` sentinels on fs failure, and the
9
+ * first 16 hex chars of the sha256 of the manifest content otherwise. Per
10
+ * contract invariant I-6 it is a pure function of the config content.
11
+ *
12
+ * `makeConfigCacheKey({ prefix })` returns the trivial
13
+ * `cacheKey(input) => `${prefix}-${hashConfig(...)}`` used by go/java/rust
14
+ * (with prefixes `go-` / `java-` / `rs-`). Python keeps its own
15
+ * `cache-key.ts` but imports `hashConfig` from here and layers its
16
+ * `requires-python` extraction on top (DEC-4).
17
+ */
18
+ import { createHash } from 'node:crypto';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
+ /**
21
+ * Fingerprint a language config file's content.
22
+ *
23
+ * - `undefined` / empty path → `'no-config'`
24
+ * - path does not exist → `'missing:<path>'`
25
+ * - read fails → `'unreadable:<path>'`
26
+ * - otherwise → first 16 hex of sha256(content)
27
+ */
28
+ export function hashConfig(configPathAbs) {
29
+ if (configPathAbs === undefined || configPathAbs.length === 0) {
30
+ return 'no-config';
31
+ }
32
+ if (!existsSync(configPathAbs)) {
33
+ return `missing:${configPathAbs}`;
34
+ }
35
+ try {
36
+ const content = readFileSync(configPathAbs, 'utf8');
37
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
38
+ }
39
+ catch {
40
+ /* v8 ignore next */
41
+ return `unreadable:${configPathAbs}`;
42
+ }
43
+ }
44
+ /**
45
+ * Builds a `cacheKey` that emits `${prefix}-${hashConfig(configPathAbs)}`.
46
+ * Per I-8 the prefix must be distinct per adapter (`go-`, `java-`, `rs-`).
47
+ */
48
+ export function makeConfigCacheKey(options) {
49
+ const { prefix } = options;
50
+ return function cacheKey(input) {
51
+ return `${prefix}-${hashConfig(input.configPathAbs)}`;
52
+ };
53
+ }
54
+ //# sourceMappingURL=cache-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-key.js","sourceRoot":"","sources":["../src/cache-key.ts"],"names":[],"mappings":"AAAA,mKAAmK;AACnK;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAInD;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,aAAiC;IAC1D,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,OAAO,WAAW,aAAa,EAAE,CAAC;IACpC,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QACpD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB;QACpB,OAAO,cAAc,aAAa,EAAE,CAAC;IACvC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAElC;IACC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,OAAO,SAAS,QAAQ,CAAC,KAAoB;QAC3C,OAAO,GAAG,MAAM,IAAI,UAAU,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;IACxD,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared file-discovery scaffolding for the tree-sitter graph adapters.
3
+ *
4
+ * The discover template is byte-identical across graph-go / graph-java /
5
+ * graph-python / graph-rust save four data inputs:
6
+ *
7
+ * - `extension` — the source-file extension to glob (`.go`, …).
8
+ * - `excludedDirGlobs` — vendored / build-output / VCS dirs to skip.
9
+ * - `configCandidates` — the ordered config-file precedence list
10
+ * (resolved-deps first, e.g. `['go.sum','go.mod']`).
11
+ * - `languageId` — the `graph:discover:<id>` log tag.
12
+ *
13
+ * `createDiscover` closes over those and returns the adapter's
14
+ * `discoverFiles(input): DiscoverOutput`. The collect loop, the symlink
15
+ * realpath/dedup/sort normalization (so I-9 referential transparency
16
+ * holds), and the `DiscoverOutput` assembly are shared verbatim.
17
+ */
18
+ import type { DiscoverInput, DiscoverOutput } from '@opensip-cli/graph';
19
+ /** Per-language inputs to the shared discover template. */
20
+ export interface TreeSitterDiscoverConfig {
21
+ /** Source-file extension WITHOUT the leading dot, e.g. `'go'`, `'py'`. */
22
+ readonly extension: string;
23
+ /** Directory globs to exclude from the recursive source-file walk. */
24
+ readonly excludedDirGlobs: readonly string[];
25
+ /**
26
+ * Ordered config-file precedence list (resolved-deps first). The first
27
+ * candidate that exists at the project root becomes the cacheKey anchor.
28
+ */
29
+ readonly configCandidates: readonly string[];
30
+ /** Log-tag suffix for `graph:discover:<languageId>`. */
31
+ readonly languageId: string;
32
+ }
33
+ /** Builds the adapter's `discoverFiles` from per-language config. */
34
+ export declare function createDiscover(config: TreeSitterDiscoverConfig): (input: DiscoverInput) => DiscoverOutput;
35
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExE,2DAA2D;AAC3D,MAAM,WAAW,wBAAwB;IACvC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,wDAAwD;IACxD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAC5B,MAAM,EAAE,wBAAwB,GAC/B,CAAC,KAAK,EAAE,aAAa,KAAK,cAAc,CAkC1C"}
@@ -0,0 +1,114 @@
1
+ // @fitness-ignore-file error-handling-quality -- realpathSync probe for symlink dedup; exception → fall through with the original path (file might be in a symlinked dir or have been unlinked), already marked v8-ignore as effectively unreachable on real input.
2
+ // @fitness-ignore-file batch-operation-limits -- iterates bounded collection (source directories within a single project root)
3
+ /**
4
+ * Shared file-discovery scaffolding for the tree-sitter graph adapters.
5
+ *
6
+ * The discover template is byte-identical across graph-go / graph-java /
7
+ * graph-python / graph-rust save four data inputs:
8
+ *
9
+ * - `extension` — the source-file extension to glob (`.go`, …).
10
+ * - `excludedDirGlobs` — vendored / build-output / VCS dirs to skip.
11
+ * - `configCandidates` — the ordered config-file precedence list
12
+ * (resolved-deps first, e.g. `['go.sum','go.mod']`).
13
+ * - `languageId` — the `graph:discover:<id>` log tag.
14
+ *
15
+ * `createDiscover` closes over those and returns the adapter's
16
+ * `discoverFiles(input): DiscoverOutput`. The collect loop, the symlink
17
+ * realpath/dedup/sort normalization (so I-9 referential transparency
18
+ * holds), and the `DiscoverOutput` assembly are shared verbatim.
19
+ */
20
+ import { existsSync, realpathSync } from 'node:fs';
21
+ import { resolve, sep } from 'node:path';
22
+ import { logger } from '@opensip-cli/core';
23
+ import { glob } from 'glob';
24
+ /** Builds the adapter's `discoverFiles` from per-language config. */
25
+ export function createDiscover(config) {
26
+ const { extension, excludedDirGlobs, configCandidates, languageId } = config;
27
+ const module = `graph:discover:${languageId}`;
28
+ const pattern = `**/*.${extension}`;
29
+ return function discoverFiles(input) {
30
+ logger.info({
31
+ evt: 'graph.discover.start',
32
+ module,
33
+ projectDir: input.cwd,
34
+ });
35
+ const projectDirAbs = normalizeProjectDir(input.cwd);
36
+ const configPathAbs = resolveConfigPath(projectDirAbs, input.configPathOverride, configCandidates);
37
+ const files = collectFiles(projectDirAbs, pattern, excludedDirGlobs);
38
+ logger.info({
39
+ evt: 'graph.discover.complete',
40
+ module,
41
+ projectDir: projectDirAbs,
42
+ configPath: configPathAbs ?? '(none)',
43
+ fileCount: files.length,
44
+ });
45
+ const out = configPathAbs === undefined
46
+ ? { projectDirAbs, files }
47
+ : { projectDirAbs, files, configPathAbs };
48
+ return out;
49
+ };
50
+ }
51
+ /* v8 ignore start */
52
+ function normalizeProjectDir(projectDir) {
53
+ const abs = resolve(projectDir);
54
+ try {
55
+ return realpathSync(abs);
56
+ }
57
+ catch {
58
+ return abs;
59
+ }
60
+ }
61
+ /* v8 ignore stop */
62
+ function resolveConfigPath(projectDirAbs, override, configCandidates) {
63
+ if (override !== undefined && override.length > 0) {
64
+ const abs = resolve(projectDirAbs, override);
65
+ return existsSync(abs) ? realpathOrPath(abs) : abs;
66
+ }
67
+ for (const candidate of configCandidates) {
68
+ const path = resolve(projectDirAbs, candidate);
69
+ if (existsSync(path))
70
+ return realpathOrPath(path);
71
+ }
72
+ return undefined;
73
+ }
74
+ /* v8 ignore start */
75
+ function realpathOrPath(p) {
76
+ try {
77
+ return realpathSync(p);
78
+ }
79
+ catch {
80
+ return p;
81
+ }
82
+ }
83
+ /* v8 ignore stop */
84
+ function collectFiles(projectDirAbs, pattern, excludedDirGlobs) {
85
+ const matches = glob.sync(pattern, {
86
+ cwd: projectDirAbs,
87
+ absolute: true,
88
+ ignore: [...excludedDirGlobs],
89
+ nodir: true,
90
+ follow: false,
91
+ dot: false,
92
+ });
93
+ const seen = new Set();
94
+ const out = [];
95
+ for (const m of matches) {
96
+ let real = m;
97
+ /* v8 ignore start */
98
+ try {
99
+ real = realpathSync(m);
100
+ }
101
+ catch {
102
+ // fall through with original
103
+ }
104
+ /* v8 ignore stop */
105
+ const key = real.split(sep).join('/');
106
+ if (seen.has(key))
107
+ continue;
108
+ seen.add(key);
109
+ out.push(real);
110
+ }
111
+ out.sort();
112
+ return out;
113
+ }
114
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"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"}