codesynapt 0.0.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.
- package/CHANGELOG.md +17 -0
- package/LICENSE +686 -0
- package/LICENSES.md +141 -0
- package/README.md +331 -0
- package/electron/main.cjs +2849 -0
- package/electron/plugin-loader.cjs +184 -0
- package/electron/preload.cjs +108 -0
- package/package.json +216 -0
- package/packages/core/bin/codesynapt-mcp.cjs +611 -0
- package/packages/core/bin/codesynapt.cjs +1933 -0
- package/packages/core/legacy.js +300 -0
- package/packages/core/lib/control-server.cjs +1539 -0
- package/packages/core/lib/embedding.cjs +89 -0
- package/packages/core/lib/logger.cjs +63 -0
- package/packages/core/lib/search-cache.cjs +140 -0
- package/packages/core/lib/search-worker.cjs +255 -0
- package/packages/core/lib/search.cjs +211 -0
- package/packages/core/lib/symbol-graph.cjs +402 -0
- package/packages/core/lib/symbol-parser-js.cjs +542 -0
- package/packages/core/lib/symbol-parser-misc.cjs +394 -0
- package/packages/core/lib/symbol-parser-py.cjs +215 -0
- package/packages/core/lib/symbol-parser-treesitter.cjs +658 -0
- package/packages/core/lib/symbol-parser-tsc.cjs +332 -0
- package/packages/core/monorepo.js +310 -0
- package/packages/core/parser.js +2234 -0
- package/packages/core/scanner.js +623 -0
- package/plugin-api/LICENSE +21 -0
- package/plugin-api/README.md +114 -0
- package/plugin-api/docs/01-getting-started.md +197 -0
- package/plugin-api/docs/02-concepts.md +269 -0
- package/plugin-api/docs/api-reference.md +463 -0
- package/plugin-api/docs/troubleshooting.md +332 -0
- package/plugin-api/docs/types/exporter.md +377 -0
- package/plugin-api/docs/types/theme.md +312 -0
- package/plugin-api/examples/hello-world-plugin/README.md +70 -0
- package/plugin-api/examples/hello-world-plugin/main.js +36 -0
- package/plugin-api/examples/hello-world-plugin/manifest.json +12 -0
- package/plugin-api/examples/mermaid-exporter/README.md +125 -0
- package/plugin-api/examples/mermaid-exporter/main.js +58 -0
- package/plugin-api/examples/mermaid-exporter/manifest.json +12 -0
- package/plugin-api/examples/rust-parser/README.md +71 -0
- package/plugin-api/examples/rust-parser/main.js +123 -0
- package/plugin-api/examples/rust-parser/manifest.json +12 -0
- package/plugin-api/examples/sunset-theme/README.md +95 -0
- package/plugin-api/examples/sunset-theme/manifest.json +12 -0
- package/plugin-api/examples/sunset-theme/theme.css +31 -0
- package/plugin-api/package.json +20 -0
- package/plugin-api/types.d.ts +395 -0
- package/public/app.js +6837 -0
- package/public/backend.js +285 -0
- package/public/index.html +647 -0
- package/public/plugin-host.js +321 -0
- package/public/style.css +4359 -0
- package/public/vendor/three.module.js +53044 -0
- package/scripts/competitor-watch.mjs +144 -0
- package/scripts/copy-vendor.js +21 -0
- package/scripts/download-bundled-node.cjs +53 -0
- package/scripts/fuses-after-pack.cjs +34 -0
- package/scripts/license-check.js +119 -0
- package/scripts/perf-test.js +200 -0
- package/server.js +132 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Competitor monitoring — fetch the latest releases and issues from
|
|
3
|
+
// every repo listed in docs/COMPETITOR-WATCH.md and write a digest
|
|
4
|
+
// to docs/competitor-log.md. Run weekly.
|
|
5
|
+
//
|
|
6
|
+
// Uses the public GitHub API (no token required for low-rate usage).
|
|
7
|
+
// If GITHUB_TOKEN is set, it's used to lift the 60/h anon rate limit.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node scripts/competitor-watch.mjs # default cadence
|
|
11
|
+
// node scripts/competitor-watch.mjs --since 14d # last 14 days only
|
|
12
|
+
// node scripts/competitor-watch.mjs --repo codegraph # just one repo
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs/promises'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
|
|
17
|
+
const REPOS = [
|
|
18
|
+
{ owner: 'colbymchenry', name: 'codegraph', cadence: 'weekly', why: 'direct competitor' },
|
|
19
|
+
{ owner: 'sourcegraph', name: 'sourcegraph', cadence: 'biweekly', why: 'SCIP/LSIF gold standard' },
|
|
20
|
+
{ owner: 'sourcegraph', name: 'scip', cadence: 'monthly', why: 'symbol-index format' },
|
|
21
|
+
{ owner: 'continuedev', name: 'continue', cadence: 'monthly', why: 'open-source AI agent' },
|
|
22
|
+
{ owner: 'Aider-AI', name: 'aider', cadence: 'monthly', why: 'repo-map analogue' },
|
|
23
|
+
{ owner: 'modelcontextprotocol', name: 'servers', cadence: 'monthly', why: 'MCP spec drift' },
|
|
24
|
+
{ owner: 'anthropics', name: 'claude-code', cadence: 'weekly', why: 'primary client' },
|
|
25
|
+
{ owner: 'tree-sitter', name: 'tree-sitter', cadence: 'biweekly', why: 'parser ABI' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2)
|
|
29
|
+
const getArg = (k) => { const i = args.indexOf(k); return i >= 0 ? args[i + 1] : null }
|
|
30
|
+
const sinceArg = getArg('--since') || '7d'
|
|
31
|
+
const repoFilter = getArg('--repo')
|
|
32
|
+
|
|
33
|
+
const sinceMs = (() => {
|
|
34
|
+
const m = sinceArg.match(/^(\d+)([dhw])$/)
|
|
35
|
+
if (!m) return 7 * 86400_000
|
|
36
|
+
const n = parseInt(m[1], 10)
|
|
37
|
+
return { d: 86400_000, h: 3600_000, w: 7 * 86400_000 }[m[2]] * n
|
|
38
|
+
})()
|
|
39
|
+
const sinceDate = new Date(Date.now() - sinceMs)
|
|
40
|
+
const sinceIso = sinceDate.toISOString()
|
|
41
|
+
|
|
42
|
+
const TOKEN = process.env.GITHUB_TOKEN
|
|
43
|
+
const HEADERS = {
|
|
44
|
+
'Accept': 'application/vnd.github+json',
|
|
45
|
+
'User-Agent': 'codesynapt-competitor-watch',
|
|
46
|
+
...(TOKEN ? { 'Authorization': `Bearer ${TOKEN}` } : {}),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function gh(pathPart) {
|
|
50
|
+
const url = `https://api.github.com${pathPart}`
|
|
51
|
+
const res = await fetch(url, { headers: HEADERS })
|
|
52
|
+
if (res.status === 403) {
|
|
53
|
+
const remaining = res.headers.get('x-ratelimit-remaining')
|
|
54
|
+
if (remaining === '0') {
|
|
55
|
+
console.error(`[rate-limit] hit; set GITHUB_TOKEN to lift to 5000/h`)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
console.error(`[gh] ${pathPart} → ${res.status}`)
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
return res.json()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fetchRepo(repo) {
|
|
67
|
+
const base = `/repos/${repo.owner}/${repo.name}`
|
|
68
|
+
const [releases, issues, commits] = await Promise.all([
|
|
69
|
+
gh(`${base}/releases?per_page=5`),
|
|
70
|
+
gh(`${base}/issues?state=all&sort=updated&since=${sinceIso}&per_page=10`),
|
|
71
|
+
gh(`${base}/commits?since=${sinceIso}&per_page=10`),
|
|
72
|
+
])
|
|
73
|
+
return {
|
|
74
|
+
repo,
|
|
75
|
+
releases: (releases || []).filter((r) => new Date(r.published_at) > sinceDate),
|
|
76
|
+
issues: (issues || []).filter((i) => !i.pull_request),
|
|
77
|
+
prs: (issues || []).filter((i) => i.pull_request),
|
|
78
|
+
commits: (commits || []).slice(0, 5),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fmtDate(s) { return s ? s.split('T')[0] : '' }
|
|
83
|
+
|
|
84
|
+
function repoSection(r) {
|
|
85
|
+
const lines = []
|
|
86
|
+
lines.push(`### [${r.repo.owner}/${r.repo.name}](https://github.com/${r.repo.owner}/${r.repo.name}) — _${r.repo.why}_`)
|
|
87
|
+
if (r.releases.length) {
|
|
88
|
+
lines.push(`\n**Releases (${r.releases.length})**`)
|
|
89
|
+
for (const rel of r.releases) {
|
|
90
|
+
lines.push(`- [\`${rel.tag_name}\`](${rel.html_url}) — ${fmtDate(rel.published_at)} · ${(rel.name || '').slice(0, 80)}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (r.prs.length) {
|
|
94
|
+
lines.push(`\n**PRs (${r.prs.length} updated since ${sinceArg})**`)
|
|
95
|
+
for (const pr of r.prs.slice(0, 5)) {
|
|
96
|
+
lines.push(`- [#${pr.number}](${pr.html_url}) ${pr.state} · ${pr.title.slice(0, 100)}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (r.issues.length) {
|
|
100
|
+
lines.push(`\n**Issues (${r.issues.length} updated since ${sinceArg})**`)
|
|
101
|
+
for (const iss of r.issues.slice(0, 5)) {
|
|
102
|
+
lines.push(`- [#${iss.number}](${iss.html_url}) ${iss.state} · ${iss.title.slice(0, 100)}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!r.releases.length && !r.issues.length && !r.prs.length) {
|
|
106
|
+
lines.push(`\n_no activity since ${sinceArg}_`)
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
const targets = repoFilter
|
|
113
|
+
? REPOS.filter((r) => r.name === repoFilter)
|
|
114
|
+
: REPOS
|
|
115
|
+
if (!targets.length) {
|
|
116
|
+
console.error(`No repo matches "${repoFilter}". Available: ${REPOS.map((r) => r.name).join(', ')}`)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
console.log(`[watch] fetching ${targets.length} repos, since ${sinceIso}`)
|
|
120
|
+
const results = []
|
|
121
|
+
for (const r of targets) {
|
|
122
|
+
process.stdout.write(` ${r.owner}/${r.name}… `)
|
|
123
|
+
const out = await fetchRepo(r)
|
|
124
|
+
results.push(out)
|
|
125
|
+
console.log(`${out.releases.length}r ${out.prs.length}p ${out.issues.length}i`)
|
|
126
|
+
}
|
|
127
|
+
const today = new Date().toISOString().split('T')[0]
|
|
128
|
+
const md = [
|
|
129
|
+
`## ${today} — last ${sinceArg}`,
|
|
130
|
+
'',
|
|
131
|
+
...results.map(repoSection).map((s) => s + '\n'),
|
|
132
|
+
'\n---\n',
|
|
133
|
+
].join('\n')
|
|
134
|
+
|
|
135
|
+
// Prepend to docs/competitor-log.md so the newest entry is on top.
|
|
136
|
+
const logPath = path.resolve('docs', 'competitor-log.md')
|
|
137
|
+
let existing = ''
|
|
138
|
+
try { existing = await fs.readFile(logPath, 'utf8') } catch {}
|
|
139
|
+
const header = existing.startsWith('# ') ? '' : '# Competitor activity log\n\n'
|
|
140
|
+
await fs.writeFile(logPath, header + md + existing.replace(/^# Competitor activity log\n\n/, ''))
|
|
141
|
+
console.log(`\nWrote ${logPath}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copy three.module.js from node_modules into public/vendor/
|
|
2
|
+
// so it can be loaded via a relative path in both browser and Electron modes.
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const ROOT = path.resolve(__dirname, '..')
|
|
9
|
+
|
|
10
|
+
const src = path.join(ROOT, 'node_modules/three/build/three.module.js')
|
|
11
|
+
const dst = path.join(ROOT, 'public/vendor/three.module.js')
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(src)) {
|
|
14
|
+
console.warn('[copy-vendor] three not installed yet — skipping')
|
|
15
|
+
process.exit(0)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true })
|
|
19
|
+
fs.copyFileSync(src, dst)
|
|
20
|
+
const size = (fs.statSync(dst).size / 1024).toFixed(0)
|
|
21
|
+
console.log(`[copy-vendor] three.module.js → public/vendor/ (${size} KB)`)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Downloads Node 22 LTS Windows x64 binary into build/bundled-node/
|
|
2
|
+
// so electron-builder can bundle it into the NSIS installer.
|
|
3
|
+
//
|
|
4
|
+
// Run before `npm run dist:win` if you want the offline installer
|
|
5
|
+
// option to include a bundled Node. Skipped otherwise — the installer
|
|
6
|
+
// just relies on the user's system Node.
|
|
7
|
+
//
|
|
8
|
+
// Usage: node scripts/download-bundled-node.cjs
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const https = require('https')
|
|
12
|
+
|
|
13
|
+
const NODE_VERSION = 'v22.11.0'
|
|
14
|
+
const URL = `https://nodejs.org/dist/${NODE_VERSION}/win-x64/node.exe`
|
|
15
|
+
const OUT_DIR = path.resolve(__dirname, '..', 'build', 'bundled-node')
|
|
16
|
+
const OUT_FILE = path.join(OUT_DIR, 'node.exe')
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(OUT_FILE)) {
|
|
19
|
+
const size = fs.statSync(OUT_FILE).size
|
|
20
|
+
if (size > 50 * 1024 * 1024) {
|
|
21
|
+
console.log(`[bundled-node] already present (${(size/1024/1024).toFixed(1)} MB) — skipping`)
|
|
22
|
+
process.exit(0)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(OUT_DIR, { recursive: true })
|
|
27
|
+
|
|
28
|
+
console.log(`[bundled-node] downloading ${URL}`)
|
|
29
|
+
const file = fs.createWriteStream(OUT_FILE)
|
|
30
|
+
function fetch(url) {
|
|
31
|
+
https.get(url, (res) => {
|
|
32
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
33
|
+
file.close()
|
|
34
|
+
fs.unlinkSync(OUT_FILE)
|
|
35
|
+
return fetch(res.headers.location)
|
|
36
|
+
}
|
|
37
|
+
if (res.statusCode !== 200) {
|
|
38
|
+
console.error(`[bundled-node] HTTP ${res.statusCode}`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
res.pipe(file)
|
|
42
|
+
file.on('finish', () => {
|
|
43
|
+
file.close()
|
|
44
|
+
const size = fs.statSync(OUT_FILE).size
|
|
45
|
+
console.log(`[bundled-node] downloaded ${(size/1024/1024).toFixed(1)} MB → ${OUT_FILE}`)
|
|
46
|
+
})
|
|
47
|
+
}).on('error', (e) => {
|
|
48
|
+
console.error(`[bundled-node] error: ${e.message}`)
|
|
49
|
+
fs.unlinkSync(OUT_FILE)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
fetch(URL)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Electron Fuses — flip security-relevant flags off in the packaged binary.
|
|
2
|
+
// Called via package.json `build.afterPack` hook by electron-builder.
|
|
3
|
+
//
|
|
4
|
+
// Why: a signed Electron binary by default lets `ELECTRON_RUN_AS_NODE=1`
|
|
5
|
+
// run arbitrary Node code. Closing that fuse removes that RCE vector.
|
|
6
|
+
//
|
|
7
|
+
// docs: https://www.electronjs.org/docs/latest/tutorial/fuses
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const fs = require('fs')
|
|
10
|
+
|
|
11
|
+
exports.default = async function fusesAfterPack(context) {
|
|
12
|
+
// @electron/fuses is ESM-only, so dynamic import from this CJS hook.
|
|
13
|
+
const { flipFuses, FuseVersion, FuseV1Options } = await import('@electron/fuses')
|
|
14
|
+
const exeName = context.packager.platform.nodeName === 'darwin'
|
|
15
|
+
? `${context.packager.appInfo.productFilename}.app`
|
|
16
|
+
: `${context.packager.appInfo.productFilename}.exe`
|
|
17
|
+
const exePath = path.join(context.appOutDir, exeName)
|
|
18
|
+
if (!fs.existsSync(exePath)) {
|
|
19
|
+
console.warn(`[fuses] target not found: ${exePath} — skipping`)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
await flipFuses(exePath, {
|
|
23
|
+
version: FuseVersion.V1,
|
|
24
|
+
[FuseV1Options.RunAsNode]: false,
|
|
25
|
+
[FuseV1Options.EnableCookieEncryption]: true,
|
|
26
|
+
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
|
27
|
+
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
|
28
|
+
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: false,
|
|
29
|
+
[FuseV1Options.OnlyLoadAppFromAsar]: false,
|
|
30
|
+
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
|
|
31
|
+
[FuseV1Options.GrantFileProtocolExtraPrivileges]: false,
|
|
32
|
+
})
|
|
33
|
+
console.log(`[fuses] applied to ${exePath}`)
|
|
34
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// License compliance check.
|
|
3
|
+
// Walks node_modules, reads each package's license field, and fails
|
|
4
|
+
// if any dependency uses a license outside the allowed list.
|
|
5
|
+
//
|
|
6
|
+
// Run via `npm run license-check`.
|
|
7
|
+
//
|
|
8
|
+
// Allowed licenses are the standard permissive set. GPL, AGPL, LGPL,
|
|
9
|
+
// CC-BY-NC, and other copyleft / restrictive licenses are blocked
|
|
10
|
+
// to avoid surprises in a project intended for MIT distribution.
|
|
11
|
+
|
|
12
|
+
import fs from 'fs'
|
|
13
|
+
import path from 'path'
|
|
14
|
+
import { fileURLToPath } from 'url'
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const ROOT = path.resolve(__dirname, '..')
|
|
18
|
+
const MODULES = path.join(ROOT, 'node_modules')
|
|
19
|
+
|
|
20
|
+
const ALLOWED = new Set([
|
|
21
|
+
'MIT',
|
|
22
|
+
'Apache-2.0',
|
|
23
|
+
'BSD',
|
|
24
|
+
'BSD-2-Clause',
|
|
25
|
+
'BSD-3-Clause',
|
|
26
|
+
'BSD-3-Clause-Clear',
|
|
27
|
+
'ISC',
|
|
28
|
+
'CC0-1.0',
|
|
29
|
+
'0BSD',
|
|
30
|
+
'Unlicense',
|
|
31
|
+
'WTFPL',
|
|
32
|
+
'Python-2.0', // some Python-related tools
|
|
33
|
+
'BlueOak-1.0.0',
|
|
34
|
+
'Zlib',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
// Some packages bundle both — we accept if any constituent is allowed.
|
|
38
|
+
function isAllowedLicense(raw) {
|
|
39
|
+
if (!raw) return false
|
|
40
|
+
if (typeof raw === 'object') raw = raw.type || raw.license || ''
|
|
41
|
+
if (Array.isArray(raw)) return raw.some(isAllowedLicense)
|
|
42
|
+
const norm = String(raw).trim()
|
|
43
|
+
if (ALLOWED.has(norm)) return true
|
|
44
|
+
// Handle SPDX expressions like "(MIT OR Apache-2.0)"
|
|
45
|
+
const stripped = norm.replace(/^\(|\)$/g, '')
|
|
46
|
+
if (stripped.includes(' OR ')) {
|
|
47
|
+
return stripped.split(' OR ').some((p) => ALLOWED.has(p.trim()))
|
|
48
|
+
}
|
|
49
|
+
if (stripped.includes(' AND ')) {
|
|
50
|
+
return stripped.split(' AND ').every((p) => ALLOWED.has(p.trim()))
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function walkModules(dir, results = []) {
|
|
56
|
+
if (!fs.existsSync(dir)) return results
|
|
57
|
+
for (const name of fs.readdirSync(dir)) {
|
|
58
|
+
if (name === '.bin' || name === '.cache') continue
|
|
59
|
+
const full = path.join(dir, name)
|
|
60
|
+
let stat
|
|
61
|
+
try { stat = fs.statSync(full) } catch { continue }
|
|
62
|
+
if (!stat.isDirectory()) continue
|
|
63
|
+
if (name.startsWith('@')) {
|
|
64
|
+
// scoped: descend one level
|
|
65
|
+
walkModules(full, results)
|
|
66
|
+
} else {
|
|
67
|
+
const pkgPath = path.join(full, 'package.json')
|
|
68
|
+
if (fs.existsSync(pkgPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
71
|
+
results.push({
|
|
72
|
+
name: pkg.name,
|
|
73
|
+
version: pkg.version,
|
|
74
|
+
license: pkg.license || pkg.licenses || null,
|
|
75
|
+
path: full,
|
|
76
|
+
})
|
|
77
|
+
} catch { /* skip malformed */ }
|
|
78
|
+
}
|
|
79
|
+
// Recurse into nested node_modules
|
|
80
|
+
const nested = path.join(full, 'node_modules')
|
|
81
|
+
if (fs.existsSync(nested)) walkModules(nested, results)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return results
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const all = walkModules(MODULES)
|
|
88
|
+
if (all.length === 0) {
|
|
89
|
+
console.log('No node_modules found. Run `npm install` first.')
|
|
90
|
+
process.exit(0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const seen = new Set()
|
|
94
|
+
const violations = []
|
|
95
|
+
const unknown = []
|
|
96
|
+
for (const m of all) {
|
|
97
|
+
const key = `${m.name}@${m.version}`
|
|
98
|
+
if (seen.has(key)) continue
|
|
99
|
+
seen.add(key)
|
|
100
|
+
if (!m.license) { unknown.push(m); continue }
|
|
101
|
+
if (!isAllowedLicense(m.license)) {
|
|
102
|
+
violations.push(m)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(`Checked ${seen.size} unique packages.`)
|
|
107
|
+
if (unknown.length > 0) {
|
|
108
|
+
console.log(`\n⚠️ ${unknown.length} packages have no license declared:`)
|
|
109
|
+
for (const m of unknown) console.log(` ${m.name}@${m.version}`)
|
|
110
|
+
}
|
|
111
|
+
if (violations.length > 0) {
|
|
112
|
+
console.log(`\n❌ ${violations.length} packages with disallowed licenses:`)
|
|
113
|
+
for (const m of violations) {
|
|
114
|
+
const lic = typeof m.license === 'string' ? m.license : JSON.stringify(m.license)
|
|
115
|
+
console.log(` ${m.name}@${m.version} → ${lic}`)
|
|
116
|
+
}
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
console.log('\n✅ All dependencies use permissive licenses.')
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Performance test for the linked-list grid + RR springs version.
|
|
2
|
+
// Mirrors public/app.js step() exactly.
|
|
3
|
+
|
|
4
|
+
const MAX_NODES = 300_000
|
|
5
|
+
const SIZES = [1_000, 10_000, 30_000, 100_000, 300_000]
|
|
6
|
+
const FRAMES = 30
|
|
7
|
+
|
|
8
|
+
function makeSyntheticGraph(n) {
|
|
9
|
+
const nodes = []
|
|
10
|
+
const spread = Math.max(20, Math.sqrt(n) * 1.6)
|
|
11
|
+
for (let i = 0; i < n; i++) {
|
|
12
|
+
const r = 4 + Math.random() * spread
|
|
13
|
+
const theta = Math.random() * Math.PI * 2
|
|
14
|
+
nodes.push({
|
|
15
|
+
id: 'f' + i, idx: i,
|
|
16
|
+
p: { x: r * Math.cos(theta), y: (Math.random() - 0.5) * 8, z: r * Math.sin(theta) },
|
|
17
|
+
v: { x: 0, y: 0, z: 0 },
|
|
18
|
+
mass: 1 + Math.sqrt(Math.floor(Math.random() * 12)) * 1.6,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
const edges = []
|
|
22
|
+
for (let i = 0; i < n; i++) {
|
|
23
|
+
for (let j = 0; j < 3; j++) {
|
|
24
|
+
const t = Math.floor(Math.random() * n)
|
|
25
|
+
if (t !== i) edges.push({ s: nodes[i].id, t: nodes[t].id, k: 'import' })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { nodes, edges }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const GRID_CELL = 11
|
|
32
|
+
const GRID_INV = 1 / GRID_CELL
|
|
33
|
+
const GRID_SIZE_BITS = 6
|
|
34
|
+
const GRID_SIZE = 1 << GRID_SIZE_BITS
|
|
35
|
+
const GRID_MASK = GRID_SIZE - 1
|
|
36
|
+
const GRID_TOTAL = GRID_SIZE ** 3
|
|
37
|
+
|
|
38
|
+
const cellHead = new Int32Array(GRID_TOTAL)
|
|
39
|
+
const nextInCell = new Int32Array(MAX_NODES)
|
|
40
|
+
|
|
41
|
+
function buildGrid(arr) {
|
|
42
|
+
cellHead.fill(-1)
|
|
43
|
+
const n = arr.length
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
const p = arr[i].p
|
|
46
|
+
const cx = ((p.x * GRID_INV) | 0) & GRID_MASK
|
|
47
|
+
const cy = ((p.y * GRID_INV) | 0) & GRID_MASK
|
|
48
|
+
const cz = ((p.z * GRID_INV) | 0) & GRID_MASK
|
|
49
|
+
const cell = (cx << (GRID_SIZE_BITS * 2)) | (cy << GRID_SIZE_BITS) | cz
|
|
50
|
+
nextInCell[i] = cellHead[cell]
|
|
51
|
+
cellHead[cell] = i
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let rrCursor = 0, springCursor = 0
|
|
56
|
+
|
|
57
|
+
function step(arr, edges, nodeMap, alpha = 0.3, dt = 0.016) {
|
|
58
|
+
if (alpha === 0) return
|
|
59
|
+
const n = arr.length
|
|
60
|
+
const REPEL = 240, SPRING = 0.05, REST = 9
|
|
61
|
+
const CENTER = 0.0018, DISK = 0.012
|
|
62
|
+
const CUT2 = GRID_CELL * GRID_CELL * 2.25
|
|
63
|
+
const VEL_KEEP = 0.62
|
|
64
|
+
|
|
65
|
+
buildGrid(arr)
|
|
66
|
+
|
|
67
|
+
const RR_CHUNK = Math.min(n, Math.max(4000, Math.floor(120_000 / Math.max(1, Math.log2(n)))))
|
|
68
|
+
const PAIR_CAP_LOCAL = 32
|
|
69
|
+
|
|
70
|
+
for (let k = 0; k < RR_CHUNK; k++) {
|
|
71
|
+
const i = (rrCursor + k) % n
|
|
72
|
+
const ni = arr[i]
|
|
73
|
+
const aix = ni.p.x, aiy = ni.p.y, aiz = ni.p.z
|
|
74
|
+
const aim = ni.mass, invMi = 1 / aim
|
|
75
|
+
const cx = ((aix * GRID_INV) | 0) & GRID_MASK
|
|
76
|
+
const cy = ((aiy * GRID_INV) | 0) & GRID_MASK
|
|
77
|
+
const cz = ((aiz * GRID_INV) | 0) & GRID_MASK
|
|
78
|
+
let count = 0
|
|
79
|
+
|
|
80
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
81
|
+
const ncx = (cx + dx) & GRID_MASK
|
|
82
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
83
|
+
const ncy = (cy + dy) & GRID_MASK
|
|
84
|
+
for (let dz = -1; dz <= 1; dz++) {
|
|
85
|
+
const ncz = (cz + dz) & GRID_MASK
|
|
86
|
+
const cell = (ncx << (GRID_SIZE_BITS * 2)) | (ncy << GRID_SIZE_BITS) | ncz
|
|
87
|
+
let j = cellHead[cell]
|
|
88
|
+
while (j !== -1) {
|
|
89
|
+
if (j === i) { j = nextInCell[j]; continue }
|
|
90
|
+
const nj = arr[j]
|
|
91
|
+
const ex = aix - nj.p.x, ey = aiy - nj.p.y, ez = aiz - nj.p.z
|
|
92
|
+
const d2 = ex*ex + ey*ey + ez*ez
|
|
93
|
+
if (d2 > CUT2 || d2 < 0.0001) { j = nextInCell[j]; continue }
|
|
94
|
+
const safeD2 = d2 < 0.5 ? 0.5 : d2
|
|
95
|
+
const invD = 1 / Math.sqrt(d2)
|
|
96
|
+
const f = REPEL * aim * nj.mass / safeD2 * alpha * 0.5
|
|
97
|
+
const fx = ex * invD * f, fy = ey * invD * f, fz = ez * invD * f
|
|
98
|
+
ni.v.x += fx * invMi; ni.v.y += fy * invMi; ni.v.z += fz * invMi
|
|
99
|
+
const invMj = 1 / nj.mass
|
|
100
|
+
nj.v.x -= fx * invMj; nj.v.y -= fy * invMj; nj.v.z -= fz * invMj
|
|
101
|
+
if (++count > PAIR_CAP_LOCAL) { dx = 2; dy = 2; dz = 2; break }
|
|
102
|
+
j = nextInCell[j]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
rrCursor = (rrCursor + RR_CHUNK) % n
|
|
109
|
+
|
|
110
|
+
const eLen = edges.length
|
|
111
|
+
if (eLen > 0) {
|
|
112
|
+
const SPRING_CHUNK = Math.min(eLen, Math.max(8000, Math.floor(200_000 / Math.max(1, Math.log2(eLen)))))
|
|
113
|
+
const springAlpha = alpha * Math.min(1, eLen / SPRING_CHUNK)
|
|
114
|
+
for (let k = 0; k < SPRING_CHUNK; k++) {
|
|
115
|
+
const idx = (springCursor + k) % eLen
|
|
116
|
+
const e = edges[idx]
|
|
117
|
+
const na = nodeMap.get(e.s), nb = nodeMap.get(e.t)
|
|
118
|
+
if (!na || !nb) continue
|
|
119
|
+
const ex = nb.p.x - na.p.x, ey = nb.p.y - na.p.y, ez = nb.p.z - na.p.z
|
|
120
|
+
const d2 = ex*ex + ey*ey + ez*ez
|
|
121
|
+
if (d2 < 1e-6) continue
|
|
122
|
+
const d = Math.sqrt(d2)
|
|
123
|
+
const f = SPRING * (d - REST) * springAlpha / d
|
|
124
|
+
const fx = ex * f, fy = ey * f, fz = ez * f
|
|
125
|
+
const inv1 = 1 / na.mass, inv2 = 1 / nb.mass
|
|
126
|
+
na.v.x += fx * inv1; na.v.y += fy * inv1; na.v.z += fz * inv1
|
|
127
|
+
nb.v.x -= fx * inv2; nb.v.y -= fy * inv2; nb.v.z -= fz * inv2
|
|
128
|
+
}
|
|
129
|
+
springCursor = (springCursor + SPRING_CHUNK) % eLen
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const CA = CENTER * alpha, DA = DISK * alpha, DT6 = dt * 6
|
|
133
|
+
for (let i = 0; i < n; i++) {
|
|
134
|
+
const node = arr[i]
|
|
135
|
+
const p = node.p, v = node.v
|
|
136
|
+
v.x += -p.x * CA
|
|
137
|
+
v.y += -p.y * CA - p.y * DA
|
|
138
|
+
v.z += -p.z * CA
|
|
139
|
+
v.x *= VEL_KEEP; v.y *= VEL_KEEP; v.z *= VEL_KEEP
|
|
140
|
+
p.x += v.x * DT6; p.y += v.y * DT6; p.z += v.z * DT6
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function writeBuffers(arr) {
|
|
145
|
+
const n = arr.length
|
|
146
|
+
const positions = new Float32Array(n * 3)
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
positions[i*3] = arr[i].p.x
|
|
149
|
+
positions[i*3+1] = arr[i].p.y
|
|
150
|
+
positions[i*3+2] = arr[i].p.z
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log('Performance test — flat grid + RR repulsion + RR springs + inlined math')
|
|
155
|
+
console.log('Targets: <16ms=60fps, <33ms=30fps, <100ms=10fps\n')
|
|
156
|
+
console.log(' SIMULATING (during first ~5s of layout) SETTLED (steady state)')
|
|
157
|
+
console.log(' ───────────────────────────────────── ─────────────────────')
|
|
158
|
+
|
|
159
|
+
function stepSettled(arr, dt = 0.016) {
|
|
160
|
+
// Mirror app.js settled-state fast path (alpha == 0)
|
|
161
|
+
const n = arr.length
|
|
162
|
+
for (let i = 0; i < n; i++) {
|
|
163
|
+
const v = arr[i].v
|
|
164
|
+
if (v.x * v.x + v.y * v.y + v.z * v.z < 0.0002) { v.x = 0; v.y = 0; v.z = 0; continue }
|
|
165
|
+
v.x *= 0.62; v.y *= 0.62; v.z *= 0.62
|
|
166
|
+
const p = arr[i].p
|
|
167
|
+
p.x += v.x * dt * 6; p.y += v.y * dt * 6; p.z += v.z * dt * 6
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const N of SIZES) {
|
|
172
|
+
const { nodes, edges } = makeSyntheticGraph(N)
|
|
173
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
|
174
|
+
rrCursor = 0; springCursor = 0
|
|
175
|
+
|
|
176
|
+
// Warm-up
|
|
177
|
+
step(nodes, edges, nodeMap, 0.3)
|
|
178
|
+
step(nodes, edges, nodeMap, 0.3)
|
|
179
|
+
|
|
180
|
+
// Active simulation
|
|
181
|
+
const t0 = performance.now()
|
|
182
|
+
for (let f = 0; f < FRAMES; f++) {
|
|
183
|
+
step(nodes, edges, nodeMap, 0.3)
|
|
184
|
+
writeBuffers(nodes)
|
|
185
|
+
}
|
|
186
|
+
const msSim = (performance.now() - t0) / FRAMES
|
|
187
|
+
|
|
188
|
+
// Settled — zero velocity, just buffer write
|
|
189
|
+
for (const node of nodes) { node.v.x = 0; node.v.y = 0; node.v.z = 0 }
|
|
190
|
+
const t1 = performance.now()
|
|
191
|
+
for (let f = 0; f < FRAMES; f++) {
|
|
192
|
+
stepSettled(nodes)
|
|
193
|
+
writeBuffers(nodes)
|
|
194
|
+
}
|
|
195
|
+
const msSettled = (performance.now() - t1) / FRAMES
|
|
196
|
+
|
|
197
|
+
const v1 = msSim < 18 ? '🟢' : msSim < 34 ? '🟡' : msSim < 105 ? '🟠' : '🔴'
|
|
198
|
+
const v2 = msSettled < 18 ? '🟢' : msSettled < 34 ? '🟡' : msSettled < 105 ? '🟠' : '🔴'
|
|
199
|
+
console.log(`${N.toString().padStart(7)} nodes ${msSim.toFixed(1).padStart(6)} ms (${(1000/msSim).toFixed(0).padStart(3)} fps) ${v1} ${msSettled.toFixed(1).padStart(6)} ms (${(1000/msSettled).toFixed(0).padStart(3)} fps) ${v2}`)
|
|
200
|
+
}
|