weavatrix 0.2.13 → 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.
- package/README.md +83 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.run.js +113 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +100 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp-server.mjs +35 -146
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -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
|
+
}
|