feat(cli): U5 — --pdg opt-in plumbing (CLI + .gitnexusrc → both sinks) (#2081)

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) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-08 19:43:10 +00:00
parent d8b14cfa7f
commit 416bbcbbae
6 changed files with 73 additions and 0 deletions

View file

@ -84,6 +84,7 @@ const KEY_SPECS: Record<string, KeySpec> = {
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' },

View file

@ -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,

View file

@ -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 <branch>',
'Default branch used in the generated regression-compare example (base_ref). ' +

View file

@ -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,
},
);

View file

@ -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 });

View file

@ -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));
});
});