weavatrix 0.2.12 → 0.2.14

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 (132) hide show
  1. package/README.md +90 -21
  2. package/SECURITY.md +2 -2
  3. package/docs/releases/v0.2.14.md +93 -0
  4. package/package.json +2 -2
  5. package/skill/SKILL.md +108 -43
  6. package/src/analysis/allowed-test-runner.js +20 -9
  7. package/src/analysis/architecture/contract-graph.js +119 -0
  8. package/src/analysis/architecture/contract-schema.js +110 -0
  9. package/src/analysis/architecture/contract-storage.js +35 -0
  10. package/src/analysis/architecture/contract-verification.js +168 -0
  11. package/src/analysis/architecture-contract.js +16 -343
  12. package/src/analysis/change-classification/diff-parser.js +103 -0
  13. package/src/analysis/change-classification/options.js +40 -0
  14. package/src/analysis/change-classification/symbol-classifier.js +177 -0
  15. package/src/analysis/change-classification.js +120 -519
  16. package/src/analysis/dead-code-review/policy.js +23 -0
  17. package/src/analysis/dead-code-review.js +9 -19
  18. package/src/analysis/dep-check.js +10 -57
  19. package/src/analysis/dep-rules.js +10 -303
  20. package/src/analysis/dependency/scoped-dependencies.js +40 -0
  21. package/src/analysis/endpoints/common.js +62 -0
  22. package/src/analysis/endpoints/extract.js +107 -0
  23. package/src/analysis/endpoints/inventory.js +84 -0
  24. package/src/analysis/endpoints/mounts.js +129 -0
  25. package/src/analysis/endpoints-java.js +80 -3
  26. package/src/analysis/endpoints.js +3 -448
  27. package/src/analysis/git-history/analytics.js +177 -0
  28. package/src/analysis/git-history/collector.js +109 -0
  29. package/src/analysis/git-history/options.js +68 -0
  30. package/src/analysis/git-history.js +128 -577
  31. package/src/analysis/health-capabilities.js +82 -0
  32. package/src/analysis/http-contracts/analysis.js +169 -0
  33. package/src/analysis/http-contracts/client-call-detection.js +81 -0
  34. package/src/analysis/http-contracts/client-call-parser.js +255 -0
  35. package/src/analysis/http-contracts/graph-context.js +143 -0
  36. package/src/analysis/http-contracts/matching.js +61 -0
  37. package/src/analysis/http-contracts/shared.js +86 -0
  38. package/src/analysis/http-contracts.js +6 -822
  39. package/src/analysis/internal-audit/dependency-health.js +111 -0
  40. package/src/analysis/internal-audit/python-manifests.js +65 -0
  41. package/src/analysis/internal-audit/repo-files.js +55 -0
  42. package/src/analysis/internal-audit/supply-chain.js +132 -0
  43. package/src/analysis/internal-audit.collect.js +5 -106
  44. package/src/analysis/internal-audit.run.js +113 -200
  45. package/src/analysis/jvm-dependency-evidence.js +69 -0
  46. package/src/analysis/source-correctness/retry-patterns.js +93 -0
  47. package/src/analysis/source-correctness.js +225 -0
  48. package/src/analysis/structure/dependency-graph.js +156 -0
  49. package/src/analysis/structure/findings.js +142 -0
  50. package/src/graph/builder/go-receiver-resolution.js +112 -0
  51. package/src/graph/builder/js/queries.js +48 -0
  52. package/src/graph/builder/lang-go.js +89 -4
  53. package/src/graph/builder/lang-js.js +4 -44
  54. package/src/graph/builder/pass2-resolution.js +68 -0
  55. package/src/graph/freshness-probe.js +2 -1
  56. package/src/graph/incremental-refresh.js +3 -2
  57. package/src/graph/internal-builder.build.js +22 -183
  58. package/src/graph/internal-builder.pass2.js +161 -0
  59. package/src/graph/internal-builder.resolvers.js +2 -105
  60. package/src/graph/resolvers/rust.js +117 -0
  61. package/src/mcp/actions/advisories.mjs +18 -0
  62. package/src/mcp/actions/graph-lifecycle.mjs +148 -0
  63. package/src/mcp/actions/graph-sync.mjs +195 -0
  64. package/src/mcp/actions/hosted-architecture.mjs +57 -0
  65. package/src/mcp/architecture-bootstrap.mjs +168 -0
  66. package/src/mcp/architecture-starter.mjs +234 -0
  67. package/src/mcp/catalog.mjs +22 -6
  68. package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
  69. package/src/mcp/evidence/package-graph-common.mjs +115 -0
  70. package/src/mcp/evidence/package-lock-graph.mjs +92 -0
  71. package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
  72. package/src/mcp/graph/context-core.mjs +111 -0
  73. package/src/mcp/graph/context-seeds.mjs +208 -0
  74. package/src/mcp/graph/context-state.mjs +86 -0
  75. package/src/mcp/graph/tools-core.mjs +143 -0
  76. package/src/mcp/graph/tools-query.mjs +223 -0
  77. package/src/mcp/graph-context.mjs +4 -496
  78. package/src/mcp/health/audit-format.mjs +172 -0
  79. package/src/mcp/health/audit.mjs +171 -0
  80. package/src/mcp/health/dead-code.mjs +110 -0
  81. package/src/mcp/health/duplicates.mjs +83 -0
  82. package/src/mcp/health/endpoints.mjs +42 -0
  83. package/src/mcp/health/structure.mjs +219 -0
  84. package/src/mcp/runtime-version.mjs +32 -0
  85. package/src/mcp/server/auto-refresh.mjs +66 -0
  86. package/src/mcp/server/runtime-config.mjs +43 -0
  87. package/src/mcp/server/shutdown.mjs +40 -0
  88. package/src/mcp/sync/evidence-architecture.mjs +54 -0
  89. package/src/mcp/sync/evidence-common.mjs +88 -0
  90. package/src/mcp/sync/evidence-duplicates.mjs +100 -0
  91. package/src/mcp/sync/evidence-health.mjs +56 -0
  92. package/src/mcp/sync/evidence-packages.mjs +95 -0
  93. package/src/mcp/sync/payload-common.mjs +97 -0
  94. package/src/mcp/sync/payload-v2.mjs +53 -0
  95. package/src/mcp/sync/payload-v3.mjs +79 -0
  96. package/src/mcp/sync-evidence.mjs +7 -402
  97. package/src/mcp/sync-payload.mjs +10 -244
  98. package/src/mcp/tools-actions.mjs +18 -414
  99. package/src/mcp/tools-architecture.mjs +18 -70
  100. package/src/mcp/tools-context.mjs +56 -15
  101. package/src/mcp/tools-endpoints.mjs +4 -1
  102. package/src/mcp/tools-graph.mjs +17 -330
  103. package/src/mcp/tools-health.mjs +24 -705
  104. package/src/mcp/tools-verified-change.mjs +12 -4
  105. package/src/mcp-server.mjs +49 -146
  106. package/src/precision/lsp-client/constants.js +12 -0
  107. package/src/precision/lsp-client/environment.js +20 -0
  108. package/src/precision/lsp-client/errors.js +15 -0
  109. package/src/precision/lsp-client/lifecycle.js +127 -0
  110. package/src/precision/lsp-client/message-parser.js +81 -0
  111. package/src/precision/lsp-client/protocol.js +212 -0
  112. package/src/precision/lsp-client/registry.js +58 -0
  113. package/src/precision/lsp-client/repo-uri.js +60 -0
  114. package/src/precision/lsp-client/stdio-client.js +151 -0
  115. package/src/precision/lsp-client.js +10 -682
  116. package/src/precision/lsp-overlay/build.js +238 -0
  117. package/src/precision/lsp-overlay/contract.js +61 -0
  118. package/src/precision/lsp-overlay/reference-results.js +108 -0
  119. package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
  120. package/src/precision/lsp-overlay/source-session.js +134 -0
  121. package/src/precision/lsp-overlay/store.js +150 -0
  122. package/src/precision/lsp-overlay/target-index.js +147 -0
  123. package/src/precision/lsp-overlay/target-query.js +55 -0
  124. package/src/precision/lsp-overlay.js +11 -868
  125. package/src/precision/typescript-lsp-provider.js +12 -682
  126. package/src/precision/typescript-provider/client.js +69 -0
  127. package/src/precision/typescript-provider/discovery.js +124 -0
  128. package/src/precision/typescript-provider/project-host.js +249 -0
  129. package/src/precision/typescript-provider/project-safety.js +161 -0
  130. package/src/precision/typescript-provider/reference-usage.js +53 -0
  131. package/src/version.js +7 -0
  132. package/docs/releases/v0.2.12.md +0 -45
@@ -0,0 +1,69 @@
1
+ import {resolve} from 'node:path'
2
+ import {startStdioLspClient} from '../lsp-client.js'
3
+ import {WEAVATRIX_VERSION} from '../../version.js'
4
+ import {
5
+ discoverTypeScriptProvider,
6
+ typeScriptLanguageId,
7
+ typeScriptLspContract,
8
+ } from './discovery.js'
9
+
10
+ /** Starts Weavatrix's bundled TypeScript server without executing repository configuration. */
11
+ export async function createTypeScriptLspClient({repoRoot, timeoutMs = 10_000} = {}) {
12
+ const discovered = discoverTypeScriptProvider()
13
+ const absoluteRepoRoot = resolve(repoRoot)
14
+ let client
15
+ let reportedTypeScript = null
16
+ try {
17
+ client = await startStdioLspClient({
18
+ repoRoot: absoluteRepoRoot,
19
+ executablePath: process.execPath,
20
+ args: [discovered.cliPath, '--stdio'],
21
+ requestTimeoutMs: timeoutMs,
22
+ onNotification(method, params) {
23
+ if (method === '$/typescriptVersion' && params && typeof params === 'object') {
24
+ reportedTypeScript = {
25
+ version: typeof params.version === 'string' ? params.version : null,
26
+ source: typeof params.source === 'string' ? params.source : null,
27
+ }
28
+ }
29
+ },
30
+ })
31
+ await client.initialize({
32
+ clientInfo: {name: 'weavatrix', version: WEAVATRIX_VERSION},
33
+ capabilities: {
34
+ workspace: {configuration: true, workspaceFolders: true},
35
+ textDocument: {
36
+ definition: {linkSupport: true},
37
+ references: {},
38
+ publishDiagnostics: {relatedInformation: false},
39
+ },
40
+ },
41
+ initializationOptions: {
42
+ hostInfo: 'weavatrix',
43
+ disableAutomaticTypingAcquisition: true,
44
+ tsserver: {path: discovered.tsserverPath},
45
+ },
46
+ })
47
+ } catch (error) {
48
+ client?.kill(error)
49
+ throw error
50
+ }
51
+ return Object.freeze({
52
+ provider: discovered.provider,
53
+ version: discovered.version,
54
+ providerContract: typeScriptLspContract(),
55
+ get typescriptVersion() { return reportedTypeScript?.version || discovered.typescriptVersion },
56
+ get typescriptSource() { return reportedTypeScript?.source || 'configured-bundled-path' },
57
+ async openDocument(relPath, text, languageId = typeScriptLanguageId(relPath)) {
58
+ if (!languageId) throw new TypeError(`Unsupported TypeScript LSP document extension: ${relPath}`)
59
+ return client.openDocument({filePath: relPath, text, languageId})
60
+ },
61
+ references(relPath, position, includeDeclaration = true, referenceTimeoutMs = timeoutMs) {
62
+ return client.references({filePath: relPath, position, includeDeclaration, timeoutMs: referenceTimeoutMs})
63
+ },
64
+ definition(relPath, position) { return client.definition({filePath: relPath, position}) },
65
+ closeDocument(relPath) { return client.closeDocument(relPath) },
66
+ close(shutdownTimeoutMs = timeoutMs) { return client.shutdown({timeoutMs: shutdownTimeoutMs}) },
67
+ kill() { client.kill() },
68
+ })
69
+ }
@@ -0,0 +1,124 @@
1
+ import {existsSync, readFileSync, realpathSync} from 'node:fs'
2
+ import {dirname, extname, isAbsolute, join} from 'node:path'
3
+ import {createRequire} from 'node:module'
4
+
5
+ const requireFromWeavatrix = createRequire(import.meta.url)
6
+ export const PROVIDER = 'typescript-language-server'
7
+ export const TYPESCRIPT_LSP_CAPABILITY_CONTRACT = 'typescript-references-v3'
8
+ let discoveredProvider = null
9
+
10
+ function resolveOwn(specifier) {
11
+ const resolved = requireFromWeavatrix.resolve(specifier)
12
+ if (!isAbsolute(resolved)) throw new Error(`Resolved dependency path is not absolute: ${specifier}`)
13
+ return realpathSync.native(resolved)
14
+ }
15
+
16
+ function packageInfoFrom(startPath, expectedName) {
17
+ let directory = dirname(startPath)
18
+ for (let depth = 0; depth < 10; depth++) {
19
+ const packagePath = join(directory, 'package.json')
20
+ if (existsSync(packagePath)) {
21
+ try {
22
+ const manifest = JSON.parse(readFileSync(packagePath, 'utf8'))
23
+ if (manifest.name === expectedName) {
24
+ return {directory, manifest, packagePath: realpathSync.native(packagePath)}
25
+ }
26
+ } catch { /* continue upward */ }
27
+ }
28
+ const parent = dirname(directory)
29
+ if (parent === directory) break
30
+ directory = parent
31
+ }
32
+ throw new Error(`Could not locate ${expectedName} package metadata`)
33
+ }
34
+
35
+ function resolveServerCli() {
36
+ const candidates = [
37
+ 'typescript-language-server',
38
+ 'typescript-language-server/lib/cli.mjs',
39
+ 'typescript-language-server/lib/cli.js',
40
+ ]
41
+ let lastError
42
+ for (const candidate of candidates) {
43
+ try {
44
+ const path = resolveOwn(candidate)
45
+ if (existsSync(path)) return path
46
+ } catch (error) { lastError = error }
47
+ }
48
+ throw lastError || new Error('typescript-language-server CLI was not found')
49
+ }
50
+
51
+ function resolveTypeScriptServer() {
52
+ const candidates = ['typescript/lib/tsserver.js', 'typescript/lib/_tsserver.js']
53
+ let lastError
54
+ for (const candidate of candidates) {
55
+ try {
56
+ const path = resolveOwn(candidate)
57
+ if (existsSync(path)) return path
58
+ } catch (error) { lastError = error }
59
+ }
60
+ try {
61
+ const typescriptEntry = resolveOwn('typescript')
62
+ for (const name of ['tsserver.js', '_tsserver.js']) {
63
+ const candidate = join(dirname(typescriptEntry), name)
64
+ if (existsSync(candidate)) return realpathSync.native(candidate)
65
+ }
66
+ } catch (error) { lastError = error }
67
+ throw lastError || new Error('Bundled TypeScript tsserver was not found')
68
+ }
69
+
70
+ export function discoverTypeScriptProvider() {
71
+ if (discoveredProvider) return discoveredProvider
72
+ const cliPath = resolveServerCli()
73
+ const tsserverPath = resolveTypeScriptServer()
74
+ const serverPackage = packageInfoFrom(cliPath, PROVIDER)
75
+ const typescriptPackage = packageInfoFrom(tsserverPath, 'typescript')
76
+ discoveredProvider = Object.freeze({
77
+ available: true,
78
+ provider: PROVIDER,
79
+ version: String(serverPackage.manifest.version || 'unknown'),
80
+ typescriptVersion: String(typescriptPackage.manifest.version || 'unknown'),
81
+ cliPath,
82
+ tsserverPath,
83
+ })
84
+ return discoveredProvider
85
+ }
86
+
87
+ export function typeScriptLspAvailability() {
88
+ try {
89
+ const result = discoverTypeScriptProvider()
90
+ return {
91
+ available: true,
92
+ provider: result.provider,
93
+ version: result.version,
94
+ typescriptVersion: result.typescriptVersion,
95
+ }
96
+ } catch (error) {
97
+ return {
98
+ available: false,
99
+ provider: PROVIDER,
100
+ version: null,
101
+ typescriptVersion: null,
102
+ reason: error?.code === 'MODULE_NOT_FOUND' ? 'DEPENDENCY_NOT_INSTALLED' : 'DISCOVERY_FAILED',
103
+ }
104
+ }
105
+ }
106
+
107
+ export function typeScriptLspContract() {
108
+ const availability = typeScriptLspAvailability()
109
+ return [
110
+ TYPESCRIPT_LSP_CAPABILITY_CONTRACT,
111
+ `${PROVIDER}@${availability.version || 'unavailable'}`,
112
+ `typescript@${availability.typescriptVersion || 'unavailable'}`,
113
+ `runtime@${process.platform}-${process.arch}-node${String(process.versions.node || '0').split('.')[0]}`,
114
+ ].join('|')
115
+ }
116
+
117
+ export function typeScriptLanguageId(filePath) {
118
+ const extension = extname(String(filePath)).toLowerCase()
119
+ if (extension === '.ts' || extension === '.mts' || extension === '.cts') return 'typescript'
120
+ if (extension === '.tsx') return 'typescriptreact'
121
+ if (extension === '.js' || extension === '.mjs' || extension === '.cjs') return 'javascript'
122
+ if (extension === '.jsx') return 'javascriptreact'
123
+ return null
124
+ }
@@ -0,0 +1,249 @@
1
+ import {createHash} from 'node:crypto'
2
+ import {existsSync, opendirSync, readFileSync, realpathSync, statSync} from 'node:fs'
3
+ import {dirname, isAbsolute, join, relative, resolve} from 'node:path'
4
+ import {createRequire} from 'node:module'
5
+ import {isPathInside} from '../../repo-path.js'
6
+
7
+ const requireFromWeavatrix = createRequire(import.meta.url)
8
+ const MAX_PROJECT_FILES = 8_192
9
+ const MAX_CONFIG_FILES = 128
10
+ const MAX_CONFIG_BYTES = 8 * 1024 * 1024
11
+ const MAX_DIRECTORY_ENTRIES = 32_768
12
+ const MAX_DIRECTORIES = 4_096
13
+ const DEFAULT_SAFETY_TIMEOUT_MS = 5_000
14
+ export const norm = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
15
+
16
+ export function canonicalKey(path, caseSensitive) {
17
+ let canonical
18
+ try { canonical = realpathSync.native(path) } catch { return null }
19
+ return caseSensitive ? canonical : canonical.toLowerCase()
20
+ }
21
+
22
+ export function typeScriptRepoContext(repoRoot) {
23
+ let root
24
+ let ts
25
+ try {
26
+ root = realpathSync.native(repoRoot)
27
+ ts = requireFromWeavatrix('typescript')
28
+ } catch { return null }
29
+ return {root, ts, caseSensitive: Boolean(ts.sys.useCaseSensitiveFileNames)}
30
+ }
31
+
32
+ export function guardedExistingPath(root, candidate, state = null) {
33
+ const input = String(candidate || '')
34
+ const lexical = isAbsolute(input) ? resolve(input) : resolve(root, input)
35
+ if (!isPathInside(root, lexical)) {
36
+ if (state) state.outsideAccess = true
37
+ return null
38
+ }
39
+ try {
40
+ const canonical = realpathSync.native(lexical)
41
+ if (!isPathInside(root, canonical)) {
42
+ if (state) state.outsideAccess = true
43
+ return null
44
+ }
45
+ return canonical
46
+ } catch { return null }
47
+ }
48
+
49
+ export function nearestTypeScriptConfig(context, absoluteFile) {
50
+ const {root} = context
51
+ let directory = dirname(absoluteFile)
52
+ while (isPathInside(root, directory)) {
53
+ for (const name of ['tsconfig.json', 'jsconfig.json']) {
54
+ const candidate = join(directory, name)
55
+ if (!existsSync(candidate)) continue
56
+ const configPath = guardedExistingPath(root, candidate)
57
+ return configPath ? {ok: true, configPath} : {ok: false, reason: 'CONFIG_OUTSIDE_REPOSITORY'}
58
+ }
59
+ if (directory === root) break
60
+ const parent = dirname(directory)
61
+ if (parent === directory) break
62
+ directory = parent
63
+ }
64
+ return {ok: true, configPath: null}
65
+ }
66
+
67
+ export function safetyBudget(options = {}) {
68
+ const requestedDeadline = Number(options.deadline)
69
+ const requestedTimeout = Number(options.timeoutMs)
70
+ const timeout = Number.isFinite(requestedTimeout)
71
+ ? Math.max(100, Math.min(60_000, Math.floor(requestedTimeout)))
72
+ : DEFAULT_SAFETY_TIMEOUT_MS
73
+ return {
74
+ deadline: Number.isFinite(requestedDeadline) ? requestedDeadline : Date.now() + timeout,
75
+ maxEntries: Math.max(1, Math.min(MAX_DIRECTORY_ENTRIES,
76
+ Number.isFinite(Number(options.maxDirectoryEntries))
77
+ ? Math.floor(Number(options.maxDirectoryEntries)) : MAX_DIRECTORY_ENTRIES)),
78
+ maxDirectories: Math.max(1, Math.min(MAX_DIRECTORIES,
79
+ Number.isFinite(Number(options.maxDirectories))
80
+ ? Math.floor(Number(options.maxDirectories)) : MAX_DIRECTORIES)),
81
+ entries: 0,
82
+ directories: 0,
83
+ configBytes: 0,
84
+ configFiles: new Set(),
85
+ reason: null,
86
+ }
87
+ }
88
+
89
+ function safetyLimitReached(budget, reason = 'PROJECT_INPUT_LIMIT') {
90
+ if (!budget.reason && Date.now() >= budget.deadline) budget.reason = 'SAFETY_DEADLINE'
91
+ if (!budget.reason && reason) budget.reason = reason
92
+ return Boolean(budget.reason)
93
+ }
94
+
95
+ function boundedReadDirectory(context, state, budget, candidate, extensions, excludes, includes, depth) {
96
+ const {root, ts, caseSensitive} = context
97
+ const path = guardedExistingPath(root, candidate, state)
98
+ if (!path || safetyLimitReached(budget, null)) return []
99
+ const getFileSystemEntries = (directoryPath) => {
100
+ if (safetyLimitReached(budget, null)) return {files: [], directories: []}
101
+ const directory = guardedExistingPath(root, directoryPath, state)
102
+ if (!directory) return {files: [], directories: []}
103
+ budget.directories++
104
+ if (budget.directories > budget.maxDirectories) {
105
+ safetyLimitReached(budget)
106
+ return {files: [], directories: []}
107
+ }
108
+ const files = []
109
+ const directories = []
110
+ let handle
111
+ try {
112
+ handle = opendirSync(directory)
113
+ while (!safetyLimitReached(budget, null)) {
114
+ const entry = handle.readSync()
115
+ if (!entry) break
116
+ budget.entries++
117
+ if (budget.entries > budget.maxEntries) {
118
+ safetyLimitReached(budget)
119
+ break
120
+ }
121
+ if (entry.isFile()) files.push(entry.name)
122
+ else if (entry.isDirectory()) directories.push(entry.name)
123
+ else if (entry.isSymbolicLink()) {
124
+ const linked = guardedExistingPath(root, join(directory, entry.name), state)
125
+ if (!linked) continue
126
+ let stats
127
+ try { stats = statSync(linked) } catch { continue }
128
+ if (stats.isFile()) files.push(entry.name)
129
+ else if (stats.isDirectory()) directories.push(entry.name)
130
+ }
131
+ }
132
+ } catch {
133
+ return {files: [], directories: []}
134
+ } finally {
135
+ try { handle?.closeSync() } catch {}
136
+ }
137
+ return {files, directories}
138
+ }
139
+ const guardedRealpath = (entry) => guardedExistingPath(root, entry, state)
140
+ || resolve(root, '.weavatrix-invalid-path')
141
+ try {
142
+ return ts.matchFiles(
143
+ path, extensions, excludes, includes, caseSensitive, root, depth,
144
+ getFileSystemEntries, guardedRealpath,
145
+ )
146
+ } catch {
147
+ state.limitExceeded = true
148
+ return []
149
+ }
150
+ }
151
+
152
+ export function parseRepoConfig(context, configPath, budget) {
153
+ const {root, ts, caseSensitive} = context
154
+ const state = {
155
+ outsideAccess: false,
156
+ limitExceeded: false,
157
+ configuredPlugins: false,
158
+ bytes: 0,
159
+ records: new Map(),
160
+ }
161
+ const diagnostics = []
162
+ const host = {
163
+ useCaseSensitiveFileNames: caseSensitive,
164
+ getCurrentDirectory: () => root,
165
+ fileExists(candidate) {
166
+ const path = guardedExistingPath(root, candidate, state)
167
+ return Boolean(path && ts.sys.fileExists(path))
168
+ },
169
+ readFile(candidate) {
170
+ if (safetyLimitReached(budget, null)) return undefined
171
+ const path = guardedExistingPath(root, candidate, state)
172
+ if (!path) return undefined
173
+ let body
174
+ try {
175
+ const size = statSync(path).size
176
+ if (size > MAX_CONFIG_BYTES || state.bytes + size > MAX_CONFIG_BYTES) {
177
+ state.limitExceeded = true
178
+ return undefined
179
+ }
180
+ body = readFileSync(path)
181
+ } catch { return undefined }
182
+ const rel = norm(relative(root, path))
183
+ if (!state.records.has(rel)) {
184
+ if (!budget.configFiles.has(rel)
185
+ && (budget.configFiles.size >= MAX_CONFIG_FILES
186
+ || budget.configBytes + body.byteLength > MAX_CONFIG_BYTES)) {
187
+ state.limitExceeded = true
188
+ safetyLimitReached(budget, 'CONFIG_INPUT_LIMIT')
189
+ return undefined
190
+ }
191
+ if (!budget.configFiles.has(rel)) {
192
+ budget.configFiles.add(rel)
193
+ budget.configBytes += body.byteLength
194
+ }
195
+ state.records.set(rel, createHash('sha256').update(body).digest('hex'))
196
+ state.bytes += body.byteLength
197
+ }
198
+ try {
199
+ const raw = ts.parseConfigFileTextToJson(path, body.toString('utf8')).config
200
+ if (Array.isArray(raw?.compilerOptions?.plugins) && raw.compilerOptions.plugins.length) {
201
+ state.configuredPlugins = true
202
+ }
203
+ } catch { /* parser diagnostics below decide safety */ }
204
+ return body.toString('utf8')
205
+ },
206
+ readDirectory(candidate, extensions, excludes, includes, depth) {
207
+ return boundedReadDirectory(context, state, budget, candidate, extensions, excludes, includes, depth)
208
+ },
209
+ onUnRecoverableConfigFileDiagnostic(diagnostic) { diagnostics.push(diagnostic) },
210
+ trace() {},
211
+ }
212
+ let parsed
213
+ try { parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host) }
214
+ catch { return {complete: false, reason: 'CONFIG_PARSE_FAILED'} }
215
+ const errors = [...diagnostics, ...(parsed?.errors || [])]
216
+ if (state.configuredPlugins || (Array.isArray(parsed?.options?.plugins) && parsed.options.plugins.length)) {
217
+ return {complete: false, reason: 'CONFIGURED_TSSERVER_PLUGINS'}
218
+ }
219
+ if (budget.reason) return {complete: false, reason: budget.reason}
220
+ if (!parsed || state.outsideAccess || state.limitExceeded
221
+ || errors.some((diagnostic) => diagnostic?.category === ts.DiagnosticCategory.Error)) {
222
+ return {
223
+ complete: false,
224
+ reason: state.outsideAccess
225
+ ? 'CONFIG_OUTSIDE_REPOSITORY'
226
+ : state.limitExceeded ? 'CONFIG_INPUT_LIMIT' : 'CONFIG_PARSE_FAILED',
227
+ }
228
+ }
229
+ const projectFiles = []
230
+ const projectKeys = new Set()
231
+ for (const file of parsed.fileNames) {
232
+ const canonical = guardedExistingPath(root, file, state)
233
+ if (!canonical) return {complete: false, reason: 'PROJECT_INPUT_OUTSIDE_REPOSITORY'}
234
+ const key = caseSensitive ? canonical : canonical.toLowerCase()
235
+ if (projectKeys.has(key)) continue
236
+ projectKeys.add(key)
237
+ projectFiles.push(norm(relative(root, canonical)))
238
+ if (projectFiles.length > MAX_PROJECT_FILES) return {complete: false, reason: 'PROJECT_INPUT_LIMIT'}
239
+ }
240
+ projectFiles.sort()
241
+ return {
242
+ complete: true,
243
+ parsed,
244
+ projectFiles,
245
+ projectKeys,
246
+ configRecords: state.records,
247
+ plugins: [],
248
+ }
249
+ }
@@ -0,0 +1,161 @@
1
+ import {createHash} from 'node:crypto'
2
+ import {readFileSync, statSync} from 'node:fs'
3
+ import {relative} from 'node:path'
4
+ import {
5
+ canonicalKey,
6
+ guardedExistingPath,
7
+ nearestTypeScriptConfig,
8
+ norm,
9
+ parseRepoConfig,
10
+ safetyBudget,
11
+ typeScriptRepoContext,
12
+ } from './project-host.js'
13
+
14
+ const MAX_PROJECT_FILES = 8_192
15
+ const MAX_CONFIG_FILES = 128
16
+ const MAX_EXTRA_INPUT_BYTES = 64 * 1024 * 1024
17
+ const MAX_PROJECT_INPUT_BYTES = 128 * 1024 * 1024
18
+ const MAX_SINGLE_INPUT_BYTES = 4 * 1024 * 1024
19
+
20
+ function referenceConfigPath(context, reference) {
21
+ const {root, ts} = context
22
+ let candidate
23
+ try {
24
+ candidate = typeof ts.resolveProjectReferencePath === 'function'
25
+ ? ts.resolveProjectReferencePath(reference)
26
+ : reference?.path
27
+ } catch { return null }
28
+ if (!candidate) return null
29
+ return guardedExistingPath(root, candidate)
30
+ }
31
+
32
+ export function typeScriptProjectSafety(repoRoot, relFiles = [], options = {}) {
33
+ const context = typeScriptRepoContext(repoRoot)
34
+ if (!context) return {safe: false, reason: 'TYPESCRIPT_UNAVAILABLE', fingerprint: null}
35
+ const {root} = context
36
+ const budget = safetyBudget(options)
37
+ const files = [...new Set((relFiles || []).map(norm).filter(Boolean))].sort()
38
+ if (files.length > MAX_PROJECT_FILES) return {safe: false, reason: 'PROJECT_INPUT_LIMIT', fingerprint: null}
39
+ const graphFiles = new Set(files)
40
+ const mappings = []
41
+ const fileConfigs = {}
42
+ const queue = []
43
+ const queued = new Set()
44
+ for (const file of files) {
45
+ const absolute = guardedExistingPath(root, file)
46
+ if (!absolute) return {safe: false, reason: 'UNREADABLE_PROJECT_INPUT', fingerprint: null}
47
+ const nearest = nearestTypeScriptConfig(context, absolute)
48
+ if (!nearest.ok) return {safe: false, reason: nearest.reason, fingerprint: null}
49
+ const configRel = nearest.configPath ? norm(relative(root, nearest.configPath)) : '<inferred>'
50
+ mappings.push(`${file}=>${configRel}`)
51
+ fileConfigs[file] = nearest.configPath ? configRel : null
52
+ if (nearest.configPath && !queued.has(nearest.configPath)) {
53
+ queued.add(nearest.configPath)
54
+ queue.push(nearest.configPath)
55
+ }
56
+ }
57
+
58
+ const configRecords = new Map()
59
+ const projectFiles = new Set()
60
+ const projects = {}
61
+ for (let cursor = 0; cursor < queue.length; cursor++) {
62
+ if (Date.now() >= budget.deadline) return {safe: false, reason: 'SAFETY_DEADLINE', fingerprint: null}
63
+ if (queue.length > MAX_CONFIG_FILES) return {safe: false, reason: 'CONFIG_INPUT_LIMIT', fingerprint: null}
64
+ const configPath = queue[cursor]
65
+ const parsed = parseRepoConfig(context, configPath, budget)
66
+ if (!parsed.complete) return {safe: false, reason: parsed.reason, fingerprint: null}
67
+ const configRel = norm(relative(root, configPath))
68
+ projects[configRel] = {
69
+ projectFiles: parsed.projectFiles,
70
+ configFiles: [...parsed.configRecords.keys()].sort(),
71
+ }
72
+ for (const [file, digest] of parsed.configRecords) {
73
+ configRecords.set(file, digest)
74
+ if (configRecords.size > MAX_CONFIG_FILES) {
75
+ return {safe: false, reason: 'CONFIG_INPUT_LIMIT', fingerprint: null}
76
+ }
77
+ }
78
+ for (const file of parsed.projectFiles) {
79
+ projectFiles.add(file)
80
+ if (projectFiles.size > MAX_PROJECT_FILES) {
81
+ return {safe: false, reason: 'PROJECT_INPUT_LIMIT', fingerprint: null}
82
+ }
83
+ }
84
+ for (const reference of parsed.parsed.projectReferences || []) {
85
+ const referenced = referenceConfigPath(context, reference)
86
+ if (!referenced) return {safe: false, reason: 'PROJECT_REFERENCE_UNRESOLVED', fingerprint: null}
87
+ if (!queued.has(referenced)) {
88
+ queued.add(referenced)
89
+ queue.push(referenced)
90
+ }
91
+ }
92
+ }
93
+
94
+ const extraRecords = []
95
+ let projectBytes = 0
96
+ let extraBytes = 0
97
+ for (const file of [...projectFiles].sort()) {
98
+ if (Date.now() >= budget.deadline) return {safe: false, reason: 'SAFETY_DEADLINE', fingerprint: null}
99
+ const absolute = guardedExistingPath(root, file)
100
+ if (!absolute) return {safe: false, reason: 'UNREADABLE_PROJECT_INPUT', fingerprint: null}
101
+ let size
102
+ try {
103
+ const stats = statSync(absolute)
104
+ if (!stats.isFile()) return {safe: false, reason: 'UNREADABLE_PROJECT_INPUT', fingerprint: null}
105
+ size = stats.size
106
+ if (size > MAX_SINGLE_INPUT_BYTES || projectBytes + size > MAX_PROJECT_INPUT_BYTES) {
107
+ return {safe: false, reason: 'PROJECT_INPUT_LIMIT', fingerprint: null}
108
+ }
109
+ } catch { return {safe: false, reason: 'UNREADABLE_PROJECT_INPUT', fingerprint: null} }
110
+ projectBytes += size
111
+ if (graphFiles.has(file)) continue
112
+ if (extraBytes + size > MAX_EXTRA_INPUT_BYTES) {
113
+ return {safe: false, reason: 'PROJECT_INPUT_LIMIT', fingerprint: null}
114
+ }
115
+ let body
116
+ try { body = readFileSync(absolute) }
117
+ catch { return {safe: false, reason: 'UNREADABLE_PROJECT_INPUT', fingerprint: null} }
118
+ extraBytes += body.byteLength
119
+ extraRecords.push(`${file}:${createHash('sha256').update(body).digest('hex')}`)
120
+ }
121
+ const fingerprint = createHash('sha256').update([
122
+ ...mappings.map((item) => `map:${item}`),
123
+ ...[...configRecords.entries()].sort(([a], [b]) => a.localeCompare(b))
124
+ .map(([file, digest]) => `config:${file}:${digest}`),
125
+ ...[...projectFiles].sort().map((file) => `project:${file}`),
126
+ ...extraRecords.map((item) => `extra:${item}`),
127
+ ].join('\n')).digest('hex')
128
+ return {
129
+ safe: true,
130
+ reason: null,
131
+ fingerprint,
132
+ configFiles: [...configRecords.keys()].sort(),
133
+ projectFiles: [...projectFiles].sort(),
134
+ fileConfigs,
135
+ projects,
136
+ }
137
+ }
138
+
139
+ export function typeScriptConfiguredProjectMembership(repoRoot, relFile) {
140
+ const context = typeScriptRepoContext(repoRoot)
141
+ if (!context) return {complete: false, member: false, reason: 'TYPESCRIPT_UNAVAILABLE'}
142
+ const {root, caseSensitive} = context
143
+ const target = guardedExistingPath(root, relFile)
144
+ if (!target) return {complete: false, member: false, reason: 'UNREADABLE_PATH'}
145
+ const nearest = nearestTypeScriptConfig(context, target)
146
+ if (!nearest.ok) return {complete: false, member: false, reason: nearest.reason}
147
+ const configPath = nearest.configPath
148
+ if (!configPath) return {complete: false, member: false, reason: 'NO_CONFIGURED_PROJECT'}
149
+ const parsed = parseRepoConfig(context, configPath, safetyBudget())
150
+ if (!parsed.complete) return {complete: false, member: false, reason: parsed.reason}
151
+ const targetKey = canonicalKey(target, caseSensitive)
152
+ const member = parsed.projectKeys.has(targetKey)
153
+ return {
154
+ complete: true,
155
+ member,
156
+ projectFiles: parsed.projectFiles,
157
+ configFiles: [...parsed.configRecords.keys()].sort(),
158
+ configFile: norm(relative(root, configPath)),
159
+ reason: member ? null : 'NOT_IN_CONFIGURED_PROJECT',
160
+ }
161
+ }
@@ -0,0 +1,53 @@
1
+ import {extname} from 'node:path'
2
+ import {createRequire} from 'node:module'
3
+
4
+ const requireFromWeavatrix = createRequire(import.meta.url)
5
+
6
+ function typeScriptScriptKind(ts, filePath) {
7
+ const extension = extname(String(filePath)).toLowerCase()
8
+ if (extension === '.tsx') return ts.ScriptKind.TSX
9
+ if (extension === '.jsx') return ts.ScriptKind.JSX
10
+ if (extension === '.js' || extension === '.mjs' || extension === '.cjs') return ts.ScriptKind.JS
11
+ return ts.ScriptKind.TS
12
+ }
13
+
14
+ // Unknown is deliberately fail-closed: callers must not promote it to runtime.
15
+ export function classifyTypeScriptReferenceUsage(filePath, text, position) {
16
+ if (!Number.isInteger(position?.line) || !Number.isInteger(position?.character)) return 'unknown'
17
+ let ts
18
+ try { ts = requireFromWeavatrix('typescript') } catch { return 'unknown' }
19
+ let sourceFile
20
+ let offset
21
+ try {
22
+ sourceFile = ts.createSourceFile(
23
+ String(filePath || 'source.ts'),
24
+ String(text || ''),
25
+ ts.ScriptTarget.Latest,
26
+ true,
27
+ typeScriptScriptKind(ts, filePath),
28
+ )
29
+ if (sourceFile.parseDiagnostics?.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)) {
30
+ return 'unknown'
31
+ }
32
+ offset = sourceFile.getPositionOfLineAndCharacter(position.line, position.character)
33
+ } catch { return 'unknown' }
34
+ let token
35
+ try { token = ts.getTokenAtPosition(sourceFile, offset) } catch { return 'unknown' }
36
+ if (!token || (!ts.isIdentifier(token) && !ts.isPrivateIdentifier(token))) return 'unknown'
37
+ if (offset < token.getStart(sourceFile) || offset >= token.getEnd()) return 'unknown'
38
+ for (let current = token; current && current !== sourceFile; current = current.parent) {
39
+ if ((ts.isImportSpecifier(current) || ts.isImportClause(current)
40
+ || ts.isExportSpecifier(current) || ts.isExportDeclaration(current))
41
+ && current.isTypeOnly === true) return 'type'
42
+ if (ts.isExpressionWithTypeArguments(current) && ts.isHeritageClause(current.parent)) {
43
+ const heritage = current.parent
44
+ if (heritage.token === ts.SyntaxKind.ExtendsKeyword && ts.isClassLike(heritage.parent)) {
45
+ const expression = current.expression
46
+ if (offset >= expression.getStart(sourceFile) && offset < expression.getEnd()) return 'value'
47
+ }
48
+ return 'type'
49
+ }
50
+ if (ts.isTypeNode(current)) return 'type'
51
+ }
52
+ return 'value'
53
+ }
package/src/version.js ADDED
@@ -0,0 +1,7 @@
1
+ import {createRequire} from 'node:module'
2
+
3
+ const requireFromWeavatrix = createRequire(import.meta.url)
4
+
5
+ export const WEAVATRIX_VERSION = String(
6
+ requireFromWeavatrix('../package.json').version || 'unknown',
7
+ )