Skip to content

feat(cli): --program shard selector, --no-repo-sections, and shard IR - #149

Open
rahlk wants to merge 2 commits into
mainfrom
feat/issue-146-program-selector
Open

feat(cli): --program shard selector, --no-repo-sections, and shard IR#149
rahlk wants to merge 2 commits into
mainfrom
feat/issue-146-program-selector

Conversation

@rahlk

@rahlk rahlk commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Closes #146. Units 1-2 of the sharded two-wave L4 design.

What this adds

  • --program <scope...> — restrict a run to named programs; --list-programs enumerates them
  • --no-repo-sections — skip the repo-scoped artifact layer
  • --emit-ir — persist the shard's graph IR as NDJSON for a later cross-shard stitch

Measured on vscode (-a 4, all graph selectors)

run peak RSS output result
whole repo, no sharding 28.0–31.7 GB killed (exit 133)
--program src 39.16 GB 1.57 GB completes
--program src --no-repo-sections 33.85 GB 1.49 GB completes
standalone --input vscode/src (baseline) 28.75 GB 1.49 GB completes

The shard's code analysis is byte-equivalent to the baseline: 6,758 modules and 1,053,108 call-graph edges, exact match.

Two findings that change the design

Repo-scoped sections were being recomputed per shard. The artifact layer derives from --input, not from the selected programs, so the src shard inventoried all of vscode — 4,961 artifacts and 4,107 dependency records, against 1,759 and 5 for the same code standalone. That is 5.3 GB per shard for an identical result, and a 92-shard run would union 92 duplicate copies. Hence --no-repo-sections: one run computes them, the rest skip. This needs to land in the spec as a design element, not a flag.

The ceiling was never a fixed RSS number. Whole-repo L4 dies at 28–31 GB, but this shard reached 33.85 GB and completed. Those deaths are JSC heap exhaustion (exit 133), not RSS pressure — which is why per-shard analysis clears the wall despite a higher peak. Worth correcting wherever the earlier framing implied an RSS limit.

Honest gap

The shard still sits 5.1 GB above the standalone baseline (33.85 vs 28.75 GB) and runs ~30% slower. I ruled out the analysis cache (identical 0.54 GB in both layouts) and have not identified the cause. It does not block the design — every shard completes, which is the property that matters — but it is unexplained, not explained-and-accepted.

Correctness

The load-bearing rule is that selection must not change file→program assignment. ownerProgram falls back to the root program, so filtering the spec list before assignment would hand a selected ancestor every file its deeper unselected descendants own, compiling them under the wrong tsconfig. Ownership is computed globally and filtered after; the test for this is mutation-checked — reordering the two steps fails it (and three sibling tests).

An unmatched --program is a hard error rather than an empty shard, so an orchestrator typo cannot produce something that unions cleanly into a graph missing a third of the repository.

252 tests pass (7 new), typecheck clean.

Whole-repository analysis holds every program's ts-morph Project at once. On vscode
that is 92 programs, and it is why -a 4 is killed there (exit 133, JSC heap). This
adds the shard selector the two-wave L4 design (#112 step 4) is built on.

`--program <scope...>` restricts the run; `--list-programs` enumerates the shards so
an orchestrator can find them. Programs are named by SCOPE dir, not tsconfig path: a
nested tsconfig.json that only `references` others resolves to its LEAF config, so
`web/tsconfig.json` becomes a program named `web` whose configPath is
`web/src/tsconfig.app.json` -- and two specs can share one leaf config under
different scopes, which makes the config path unusable as an identity.

Ownership is computed against ALL discovered programs and filtered afterwards.
Filtering first would be silently wrong: ownerProgram falls back to the root program,
so a selected ancestor would absorb every file its deeper unselected descendants own
and compile them under the wrong tsconfig. The test for this is mutation-checked --
reordering the two steps fails it.

An unmatched --program is a hard error, not an empty shard: an orchestrator typo must
not produce a shard that unions cleanly into a graph missing a third of the repo.

Verified on vscode: a `--program src` shard reproduces the standalone run's 6,758
modules and 1,053,108 call-graph edges exactly, and completes -- where the whole-repo
run at the same level does not.
…epository

The repository-artifact layer is scoped to --input, not to --program, so a shard
recomputed the WHOLE repository's inventory: measured on vscode, a `--program src`
shard emitted 4,961 artifacts and 4,107 dependency records for a code analysis
covering 6,758 modules, against 1,759 and 5 for the same code analysed standalone.

That is 5.3GB per shard (39.16GB -> 33.85GB measured) for a result identical in every
shard, and a full 92-shard run would also union 92 duplicate copies of it. Repo-scoped
sections belong to the repository, not to a shard: one run computes them, every other
run passes --no-repo-sections.

Also adds `--emit-ir` and the NDJSON shard-IR reader/writer (unit 2 of the design):
what a later cross-shard stitch needs, and nothing tsc owns. NDJSON because
JSON.stringify on the whole IR would build one multi-hundred-megabyte string -- the
emit-time wall #112 lists as ceiling 3. The header pins ir_version, k_limit, --input
and --app-name, because a divergence there does not produce a partial union but a
silently wrong one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): --program selector, so one program can be analysed at a time

1 participant