From 416bbcbbae47ae36e2adc09478f920b0d0cfd457 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 8 Jun 2026 19:43:10 +0000 Subject: [PATCH] =?UTF-8?q?feat(cli):=20U5=20=E2=80=94=20`--pdg`=20opt-in?= =?UTF-8?q?=20plumbing=20(CLI=20+=20.gitnexusrc=20=E2=86=92=20both=20sinks?= =?UTF-8?q?)=20(#2081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the CFG/PDG substrate as an opt-in and thread it from CLI/.gitnexusrc to the single source of truth (PipelineOptions.pdg), which fans out to BOTH sinks already wired in U3/U4: the worker build gate (workerData.pdg) and the scope-resolution emit gate. Off by default (R7). - cli/index.ts: `--pdg` commander flag. - cli/analyze.ts: AnalyzeOptions.pdg + pass `pdg` into runFullAnalysis options. - cli/analyze-config.ts: KEY_SPECS `pdg` (boolean) so `.gitnexusrc { "pdg": true }` normalizes and a non-boolean value fails closed with GitNexusRcError. - core/run-analyze.ts: AnalyzeOptions.pdg → runPipelineFromRepo({ pdg }). (The internal PipelineOptions/WorkerPoolOptions/workerData fields + the parse-cache key fold landed in U3/U4; this unit adds the user-facing surface. The budget knobs stay at internal defaults for M1.) Tests: analyze-config pdg normalization + non-boolean rejection; opt-in.test.ts covers the CLI/file merge precedence and that pdg perturbs the chunk-dispatch key. The full worker-build + main-emit round-trip is the U7 integration test. Co-Authored-By: Claude Opus 4.8 (1M context) --- gitnexus/src/cli/analyze-config.ts | 1 + gitnexus/src/cli/analyze.ts | 8 +++++ gitnexus/src/cli/index.ts | 5 +++ gitnexus/src/core/run-analyze.ts | 9 ++++++ gitnexus/test/unit/analyze-config.test.ts | 11 +++++++ gitnexus/test/unit/cfg/opt-in.test.ts | 39 +++++++++++++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 gitnexus/test/unit/cfg/opt-in.test.ts diff --git a/gitnexus/src/cli/analyze-config.ts b/gitnexus/src/cli/analyze-config.ts index 8fbe40feb..09ca4682b 100644 --- a/gitnexus/src/cli/analyze-config.ts +++ b/gitnexus/src/cli/analyze-config.ts @@ -84,6 +84,7 @@ const KEY_SPECS: Record = { skipContextFiles: { target: 'skipAgentsMd', kind: 'boolean' }, skipAiContext: { target: 'skipAgentsMd', kind: 'boolean' }, skipSkills: { target: 'skipSkills', kind: 'boolean' }, + pdg: { target: 'pdg', kind: 'boolean' }, indexOnly: { target: 'indexOnly', kind: 'boolean' }, stats: { target: 'stats', kind: 'boolean' }, noStats: { target: 'stats', kind: 'boolean-negate' }, diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 3f196b11d..08f69f64a 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -599,6 +599,12 @@ export interface AnalyzeOptions { verbose?: boolean; /** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */ skipAgentsMd?: boolean; + /** + * Build the control-flow-graph / PDG substrate (#2081 M1). Opt-in; off by + * default. Threaded to both the worker (CFG build) and scope-resolution + * (BasicBlock/CFG emit). + */ + pdg?: boolean; /** * Stats inclusion in AGENTS.md and CLAUDE.md. * @@ -1089,6 +1095,8 @@ const analyzeCommandImpl = async ( skipGit: options.skipGit, skipAgentsMd, skipSkills, + // CFG/PDG substrate opt-in (#2081 M1) — threaded to both sinks downstream. + pdg: options.pdg === true, // Resolved default branch (CLI > .gitnexusrc > auto-detect > "main") // threaded into the generated regression-compare example (#243). defaultBranch: resolvedDefaultBranch, diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index a2a88bd83..d519496f2 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -44,6 +44,11 @@ program '(no-op when --index-only is also set).', ) .option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md') + .option( + '--pdg', + 'Build the control-flow-graph / PDG substrate (BasicBlock nodes + CFG edges) ' + + 'for TypeScript/JavaScript. Opt-in; off by default. (#2081 M1)', + ) .option( '--default-branch ', 'Default branch used in the generated regression-compare example (base_ref). ' + diff --git a/gitnexus/src/core/run-analyze.ts b/gitnexus/src/core/run-analyze.ts index fd7fb1d3f..35702b67f 100644 --- a/gitnexus/src/core/run-analyze.ts +++ b/gitnexus/src/core/run-analyze.ts @@ -115,6 +115,12 @@ export interface AnalyzeOptions { noStats?: boolean; /** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */ skipSkills?: boolean; + /** + * Build the CFG/PDG substrate (#2081 M1). Forwarded to `PipelineOptions.pdg`, + * which threads to BOTH the worker (CFG build, via workerData) AND + * scope-resolution (BasicBlock/CFG emit gate). Off by default. + */ + pdg?: boolean; /** * Default branch threaded into generated AGENTS.md / CLAUDE.md so the * regression-compare example uses the configured branch instead of a @@ -506,6 +512,9 @@ export async function runFullAnalysis( { parseCache, workerPoolSize: options.workerPoolSize, + // CFG/PDG opt-in (#2081 M1). PipelineOptions.pdg fans out to the worker + // build gate (workerData.pdg) and the scope-resolution emit gate. + pdg: options.pdg === true, }, ); diff --git a/gitnexus/test/unit/analyze-config.test.ts b/gitnexus/test/unit/analyze-config.test.ts index 07bbdbea1..0dab5e178 100644 --- a/gitnexus/test/unit/analyze-config.test.ts +++ b/gitnexus/test/unit/analyze-config.test.ts @@ -75,6 +75,17 @@ describe('analyze-config (.gitnexusrc support, #243)', () => { }); }); + it('normalizes the pdg opt-in (#2081) and rejects a non-boolean value', async () => { + await writeRc(JSON.stringify({ pdg: true })); + expect(loadAnalyzeConfig(dir)).toEqual({ pdg: true }); + + await writeRc(JSON.stringify({ pdg: false })); + expect(loadAnalyzeConfig(dir)).toEqual({ pdg: false }); + + await writeRc(JSON.stringify({ pdg: 'yes' })); + expect(() => loadAnalyzeConfig(dir)).toThrow(/must be a boolean/); + }); + it('parses the nested analyze form', async () => { await writeRc(JSON.stringify({ analyze: { defaultBranch: 'master', skipSkills: true } })); expect(loadAnalyzeConfig(dir)).toEqual({ defaultBranch: 'master', skipSkills: true }); diff --git a/gitnexus/test/unit/cfg/opt-in.test.ts b/gitnexus/test/unit/cfg/opt-in.test.ts new file mode 100644 index 000000000..11a0d45fa --- /dev/null +++ b/gitnexus/test/unit/cfg/opt-in.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { mergeAnalyzeOptions } from '../../../src/cli/analyze-config.js'; +import { computeChunkHash } from '../../../src/storage/parse-cache.js'; + +// U5 — the `--pdg` opt-in plumbing (R7). The flag has TWO sinks downstream of +// PipelineOptions.pdg: the parse worker (CFG build, gated on workerData.pdg) +// and scope-resolution (BasicBlock/CFG emit, gated on the run input). These +// tests pin the observable plumbing: the CLI/.gitnexusrc merge, and that the +// flag perturbs the parse-cache/worker-dispatch key so a pdg run never reuses +// a pdg-off shard. The full worker-build + main-emit round-trip is exercised +// end-to-end by the U7 runPipelineFromRepo({ pdg: true }) integration test. + +describe('U5 — --pdg merges from CLI and .gitnexusrc', () => { + it('a CLI --pdg flag flows into the merged options', () => { + expect(mergeAnalyzeOptions({ pdg: true }, undefined).pdg).toBe(true); + }); + + it('a .gitnexusrc pdg value flows through when the CLI omits it', () => { + expect(mergeAnalyzeOptions({}, { pdg: true }).pdg).toBe(true); + }); + + it('the CLI flag wins over the file config', () => { + expect(mergeAnalyzeOptions({ pdg: true }, { pdg: false }).pdg).toBe(true); + }); + + it('absent everywhere ⇒ pdg is undefined (default off)', () => { + expect(mergeAnalyzeOptions({}, undefined).pdg).toBeUndefined(); + }); +}); + +describe('U5 — pdg perturbs the parse-cache / worker-dispatch key', () => { + // The chunk hash is what decides whether a chunk is re-dispatched to the + // workers (and thus whether the worker builds a CFG). Folding pdg in is the + // mechanism that makes the worker sink honor the flag across warm caches. + it('a pdg run computes a different chunk key than a pdg-off run', () => { + const entries = [{ filePath: 'a.ts', contentHash: 'h1' }]; + expect(computeChunkHash(entries, true)).not.toBe(computeChunkHash(entries, false)); + }); +});