Merge branch 'main' into feat/pdg-impact-mode

This commit is contained in:
Gergő Magyar 2026-06-18 05:59:30 +01:00 • committed by GitHub
commit 246ff5d7be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 182 additions and 76 deletions

View file

@ -39,6 +39,13 @@ _spec.loader.exec_module(readiness) # type: ignore[union-attr]
# format change that would silently break change-detection fails here. Group 2 is
# ONLY the Status cell ([^|]+? before the final `|$`).
_ROW_DIFF_RE = re.compile(r"\| `(tree-sitter-[^`]+)` \|.*\| ([^|]+?) \|$", re.M)
# Mirrors the scheduled issue-update summary extraction in
# tree-sitter-upgrade-readiness.yml. If the report prose changes again, the issue
# comment should not silently degrade to "?/? ready. ? blocker(s)".
_ISSUE_READY_RE = re.compile(
r"- (\d+)/(\d+) npm-installed grammars already accept tree-sitter@"
)
_ISSUE_BLOCKER_RE = re.compile(r"\*\*Blocked\*\* — (\d+) grammars? ")
def _physical_vendor_grammars() -> set[str]:
@ -291,6 +298,14 @@ class ReportRendering(TestCase):
for status in self.rows.values():
self.assertNotIn("|", status)
def test_issue_update_summary_regex_matches_current_report(self):
ready = _ISSUE_READY_RE.search(self.report)
blockers = _ISSUE_BLOCKER_RE.search(self.report)
self.assertIsNotNone(ready)
self.assertIsNotNone(blockers)
self.assertEqual(ready.groups(), ("9", "10"))
self.assertEqual(blockers.group(1), "2")
def _matrix_row(self, name: str) -> str:
for line in self.report.splitlines():
if line.startswith(f"| `{name}` |"):

View file

@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
queries: security-and-quality
@ -73,6 +73,6 @@ jobs:
- '**/test/**/fixtures/**'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: '/language:${{ matrix.language }}'

View file

@ -53,7 +53,7 @@ jobs:
# No GITLEAKS_LICENSE secret is required for OSS / public-repo usage.
# If this repo becomes private, the action will require a license key.
- name: Gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: true

View file

@ -53,6 +53,6 @@ jobs:
retention-days: 5
- name: Upload to Security tab
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: results.sarif

View file

@ -133,8 +133,8 @@ jobs:
const existing = open.find(i => i.title === title);
if (existing) {
// Extract ready/total count for the changelog comment.
const readyMatch = report.match(/\*\*(\d+)\/(\d+)\*\* grammars ready/);
const blockerMatch = report.match(/\*\*(\d+) blocker/);
const readyMatch = report.match(/- (\d+)\/(\d+) npm-installed grammars already accept tree-sitter@/);
const blockerMatch = report.match(/\*\*Blocked\*\* — (\d+) grammars? /);
const ready = readyMatch ? readyMatch[1] : '?';
const total = readyMatch ? readyMatch[2] : '?';
const blockers = blockerMatch ? blockerMatch[1] : '?';
@ -165,7 +165,7 @@ jobs:
}
const today = new Date().toISOString().slice(0, 10);
let comment = `**${today}:** ${ready}/${total} ready. ${blockers} blocker(s) remaining.`;
let comment = `**${today}:** ${ready}/${total} npm-installed ready. ${blockers} blocker(s) remaining.`;
if (changes.length > 0) {
comment += '\n\nChanges:\n' + changes.map(c => `- ${c}`).join('\n');
} else {

View file

@ -76,7 +76,7 @@ jobs:
exit-code: '0'
- name: Upload to Security tab
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: trivy-${{ matrix.image.name }}.sarif
category: trivy-${{ matrix.image.name }}

View file

@ -76,7 +76,7 @@ jobs:
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: zizmor.sarif
category: zizmor

View file

@ -12,7 +12,7 @@
"@langchain/core": "^1.1.44",
"@langchain/google-genai": "^2.1.30",
"@langchain/langgraph": "^1.3.2",
"@langchain/ollama": "^1.2.6",
"@langchain/ollama": "^1.2.7",
"@langchain/openai": "^1.4.5",
"@sigma/edge-curve": "^3.1.0",
"@tailwindcss/vite": "^4.3.0",
@ -35,7 +35,7 @@
"mnemonist": "^0.39.0",
"pandemonium": "^2.4.0",
"react": "^19.2.5",
"react-dom": "^19.2.6",
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.1",
@ -1538,13 +1538,12 @@
}
},
"node_modules/@langchain/ollama": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.6.tgz",
"integrity": "sha512-wEfjRjyB20SMduqjriIBEalXZf1twbfaNTxxLIjKCVrufHPtKJKGy1a0tQHqa+27HwektNNXlcMre7MTuaS5Rw==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.7.tgz",
"integrity": "sha512-7Gu17q1dn4nKGB3ZYJyaaL8H/2k7LHvcL6V25S8cVDniTTSvd0fWzI5MzPJvbf9WPxBhvmf+rDDLAhOWljHEtQ==",
"license": "MIT",
"dependencies": {
"ollama": "^0.6.3",
"uuid": "^10.0.0"
"ollama": "^0.6.3"
},
"engines": {
"node": ">=20"
@ -1553,19 +1552,6 @@
"@langchain/core": "^1.0.0"
}
},
"node_modules/@langchain/ollama/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@langchain/openai": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.4.5.tgz",
@ -7753,24 +7739,24 @@
"license": "MIT"
},
"node_modules/react": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.6"
"react": "^19.2.7"
}
},
"node_modules/react-i18next": {

View file

@ -22,7 +22,7 @@
"@langchain/core": "^1.1.44",
"@langchain/google-genai": "^2.1.30",
"@langchain/langgraph": "^1.3.2",
"@langchain/ollama": "^1.2.6",
"@langchain/ollama": "^1.2.7",
"@langchain/openai": "^1.4.5",
"@sigma/edge-curve": "^3.1.0",
"@tailwindcss/vite": "^4.3.0",
@ -45,7 +45,7 @@
"mnemonist": "^0.39.0",
"pandemonium": "^2.4.0",
"react": "^19.2.5",
"react-dom": "^19.2.6",
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.1",

View file

@ -3269,9 +3269,9 @@
"license": "MIT"
},
"node_modules/hono": {
"version": "4.12.23",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
"integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
"version": "4.12.26",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz",
"integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@ -5628,40 +5628,6 @@
"peerDependencies": {
"zod": "^3.25.28 || ^4"
}
},
"vendor/tree-sitter-dart": {
"version": "1.0.0",
"extraneous": true,
"license": "ISC",
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"vendor/tree-sitter-proto": {
"version": "0.4.1",
"extraneous": true,
"license": "MIT",
"peerDependencies": {
"tree-sitter": ">=0.21.0"
}
},
"vendor/tree-sitter-swift": {
"version": "0.7.1",
"extraneous": true,
"license": "MIT",
"peerDependencies": {
"tree-sitter": "^0.21.1 || ^0.22.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
}
}
}

View file

@ -18,6 +18,7 @@ import {
initWikiDb,
closeWikiDb,
touchWikiDb,
pinWikiDb,
getFilesWithExports,
getAllFiles,
getIntraModuleCallEdges,
@ -291,6 +292,7 @@ export class WikiGenerator {
// Init graph
this.onProgress('init', 2, 'Connecting to knowledge graph...');
const releaseWikiDbPin = pinWikiDb();
await initWikiDb(this.lbugPath);
let result: WikiRunResult;
@ -310,6 +312,7 @@ export class WikiGenerator {
result = await this.fullGeneration(currentCommit);
}
} finally {
releaseWikiDbPin();
await closeWikiDb();
}

View file

@ -5,7 +5,7 @@
* Uses the MCP-style pooled lbug-adapter for connection management.
*/
import { initLbug, executeQuery, closeLbug, touchRepo } from '../lbug/pool-adapter.js';
import { initLbug, executeQuery, closeLbug, touchRepo, pinRepo } from '../lbug/pool-adapter.js';
const REPO_ID = '__wiki__';
@ -16,6 +16,15 @@ export function touchWikiDb(): void {
touchRepo(REPO_ID);
}
/**
* Keep the wiki DB resident for a full generation run. Wiki generation can spend
* minutes inside LLM calls, and the pooled DB must survive both idle cleanup and
* unrelated LRU pressure until the run reaches its final graph queries.
*/
export function pinWikiDb(): () => void {
return pinRepo(REPO_ID);
}
export interface FileWithExports {
filePath: string;
symbols: Array<{ name: string; type: string }>;

View file

@ -62,6 +62,7 @@ vi.mock('../../src/core/lbug/sidecar-recovery.js', () => ({
const { initLbug, closeLbug, isLbugReady, pinRepo, unpinRepo } =
await import('../../src/core/lbug/pool-adapter.js');
const { initWikiDb, closeWikiDb, pinWikiDb } = await import('../../src/core/wiki/graph-queries.js');
describe('pool-adapter repo pinning (issue #2189)', () => {
let tmpDir: string;
@ -151,6 +152,23 @@ describe('pool-adapter repo pinning (issue #2189)', () => {
expect(isLbugReady('unpinned-idle')).toBe(false);
});
it('wiki DB pin wrapper keeps __wiki__ resident past idle cleanup', async () => {
vi.useFakeTimers();
const releaseWikiPin = pinWikiDb();
await initWikiDb(dbPathFor('wiki-wrapper'));
expect(isLbugReady('__wiki__')).toBe(true);
await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 60 * 1000);
expect(isLbugReady('__wiki__')).toBe(true);
releaseWikiPin();
await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 60 * 1000);
expect(isLbugReady('__wiki__')).toBe(false);
await closeWikiDb();
});
it('unpinRepo re-enables eviction for that repo', async () => {
// Pin five repos and fill the pool; a sixth init evicts nothing (all pinned).
for (let i = 1; i <= 5; i++) {

View file

@ -0,0 +1,104 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
const {
closeWikiDbMock,
getAllFilesMock,
getFilesWithExportsMock,
initWikiDbMock,
pinWikiDbMock,
releaseWikiDbPinMock,
} = vi.hoisted(() => ({
closeWikiDbMock: vi.fn(),
getAllFilesMock: vi.fn(),
getFilesWithExportsMock: vi.fn(),
initWikiDbMock: vi.fn(),
pinWikiDbMock: vi.fn(),
releaseWikiDbPinMock: vi.fn(),
}));
vi.mock('../../src/core/wiki/graph-queries.js', () => ({
initWikiDb: initWikiDbMock,
closeWikiDb: closeWikiDbMock,
touchWikiDb: vi.fn(),
pinWikiDb: pinWikiDbMock,
getFilesWithExports: getFilesWithExportsMock,
getAllFiles: getAllFilesMock,
getIntraModuleCallEdges: vi.fn(),
getInterModuleCallEdges: vi.fn(),
getProcessesForFiles: vi.fn(),
getAllProcesses: vi.fn(),
getInterModuleEdgesForOverview: vi.fn(),
}));
vi.mock('../../src/core/wiki/html-viewer.js', () => ({
generateHTMLViewer: vi.fn().mockResolvedValue(''),
}));
describe('WikiGenerator DB pinning', () => {
let tmpDir: string;
let repoPath: string;
let storagePath: string;
let lbugPath: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-wiki-pin-'));
repoPath = path.join(tmpDir, 'repo');
storagePath = path.join(repoPath, '.gitnexus');
lbugPath = path.join(storagePath, 'lbug');
await fs.mkdir(path.join(repoPath, 'src'), { recursive: true });
await fs.mkdir(path.join(storagePath, 'wiki'), { recursive: true });
await fs.writeFile(path.join(repoPath, 'src', 'index.ts'), 'export const value = 1;\n');
await fs.writeFile(lbugPath, '');
await fs.writeFile(
path.join(storagePath, 'wiki', 'module_tree.json'),
JSON.stringify([{ name: 'Core', slug: 'core', files: ['src/index.ts'] }]),
);
initWikiDbMock.mockResolvedValue(undefined);
closeWikiDbMock.mockResolvedValue(undefined);
releaseWikiDbPinMock.mockReset();
pinWikiDbMock.mockReturnValue(releaseWikiDbPinMock);
getFilesWithExportsMock.mockResolvedValue([
{ filePath: 'src/index.ts', symbols: [{ name: 'value', type: 'Variable' }] },
]);
getAllFilesMock.mockResolvedValue(['src/index.ts']);
});
afterEach(async () => {
vi.restoreAllMocks();
await fs.rm(tmpDir, { recursive: true, force: true });
});
it('pins the wiki DB for the run and releases the lease before closing', async () => {
const { WikiGenerator } = await import('../../src/core/wiki/generator.js');
const generator = new WikiGenerator(
repoPath,
storagePath,
lbugPath,
{
provider: 'openai',
model: 'test-model',
baseUrl: 'http://127.0.0.1:1/v1',
apiKey: 'test',
},
{ force: true, reviewOnly: true },
);
await expect(generator.run()).resolves.toMatchObject({
mode: 'full',
pagesGenerated: 0,
});
expect(initWikiDbMock).toHaveBeenCalledWith(lbugPath);
expect(pinWikiDbMock).toHaveBeenCalledTimes(1);
expect(releaseWikiDbPinMock).toHaveBeenCalledTimes(1);
expect(closeWikiDbMock).toHaveBeenCalledTimes(1);
expect(releaseWikiDbPinMock.mock.invocationCallOrder[0]).toBeLessThan(
closeWikiDbMock.mock.invocationCallOrder[0],
);
});
});

View file

@ -323,6 +323,7 @@ describe('WikiGenerator --review mode', () => {
initWikiDb: vi.fn().mockResolvedValue(undefined),
closeWikiDb: vi.fn().mockResolvedValue(undefined),
touchWikiDb: vi.fn(),
pinWikiDb: vi.fn(() => vi.fn()),
getFilesWithExports: vi
.fn()
.mockResolvedValue(fakeFiles.map((f) => ({ filePath: f, symbols: [] }))),
@ -1762,6 +1763,7 @@ describe('WikiGenerator grouping prompt isolation', () => {
initWikiDb: vi.fn().mockResolvedValue(undefined),
closeWikiDb: vi.fn().mockResolvedValue(undefined),
touchWikiDb: vi.fn(),
pinWikiDb: vi.fn(() => vi.fn()),
getFilesWithExports: vi.fn().mockResolvedValue([{ filePath: 'src/auth.ts', symbols: [] }]),
getAllFiles: vi.fn().mockResolvedValue(['src/auth.ts']),
getIntraModuleCallEdges: vi.fn().mockResolvedValue([]),

View file

@ -351,6 +351,7 @@ describe('buildModuleTree batched grouping', () => {
initWikiDb: vi.fn().mockResolvedValue(undefined),
closeWikiDb: vi.fn().mockResolvedValue(undefined),
touchWikiDb: vi.fn(),
pinWikiDb: vi.fn(() => vi.fn()),
getFilesWithExports: vi.fn().mockResolvedValue(fakeFiles),
getAllFiles: vi.fn().mockResolvedValue(fakeFiles.map((f) => f.filePath)),
getIntraModuleCallEdges: vi.fn().mockResolvedValue([]),
@ -426,6 +427,7 @@ describe('buildModuleTree batched grouping', () => {
initWikiDb: vi.fn().mockResolvedValue(undefined),
closeWikiDb: vi.fn().mockResolvedValue(undefined),
touchWikiDb: vi.fn(),
pinWikiDb: vi.fn(() => vi.fn()),
getFilesWithExports: vi.fn().mockResolvedValue(fakeFiles),
getAllFiles: vi.fn().mockResolvedValue(fakeFiles.map((f) => f.filePath)),
getIntraModuleCallEdges: vi.fn().mockResolvedValue([]),
@ -513,6 +515,7 @@ describe('buildModuleTree batched grouping', () => {
initWikiDb: vi.fn().mockResolvedValue(undefined),
closeWikiDb: vi.fn().mockResolvedValue(undefined),
touchWikiDb: vi.fn(),
pinWikiDb: vi.fn(() => vi.fn()),
getFilesWithExports: vi.fn().mockResolvedValue(fakeFiles),
getAllFiles: vi.fn().mockResolvedValue(fakeFiles.map((f) => f.filePath)),
getIntraModuleCallEdges: vi.fn().mockResolvedValue([]),