@dailephd/my-dev-kit-lab 0.7.0 → 0.7.1

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +3 -2
  3. package/benchmarks/projects/README.md +3 -1
  4. package/dist/src/cli/help.js +8 -2
  5. package/dist/src/commands/runExperimentDescribeCommand.js +6 -0
  6. package/dist/src/commands/runExperimentRunCommand.js +57 -6
  7. package/dist/src/evaluation/syntheticRepository/config.js +304 -0
  8. package/dist/src/evaluation/syntheticRepository/evaluationCase.js +43 -0
  9. package/dist/src/evaluation/syntheticRepository/identity.js +41 -0
  10. package/dist/src/evaluation/syntheticRepository/index.js +16 -0
  11. package/dist/src/evaluation/syntheticRepository/inspection.js +92 -0
  12. package/dist/src/evaluation/syntheticRepository/layout.js +50 -0
  13. package/dist/src/evaluation/syntheticRepository/manifest.js +281 -0
  14. package/dist/src/evaluation/syntheticRepository/manifestVerification.js +112 -0
  15. package/dist/src/evaluation/syntheticRepository/materialize.js +170 -0
  16. package/dist/src/evaluation/syntheticRepository/planning.js +479 -0
  17. package/dist/src/evaluation/syntheticRepository/prng.js +47 -0
  18. package/dist/src/evaluation/syntheticRepository/renderPython.js +68 -0
  19. package/dist/src/evaluation/syntheticRepository/renderRepository.js +39 -0
  20. package/dist/src/evaluation/syntheticRepository/renderShared.js +93 -0
  21. package/dist/src/evaluation/syntheticRepository/renderTypeScript.js +82 -0
  22. package/dist/src/evaluation/syntheticRepository/topology.js +96 -0
  23. package/dist/src/evaluation/syntheticRepository/types.js +26 -0
  24. package/dist/src/experiments/plugins/contextWindowScaling/index.js +1 -0
  25. package/dist/src/experiments/plugins/contextWindowScaling/metadata.js +1 -1
  26. package/dist/src/experiments/plugins/contextWindowScaling/syntheticInputs.js +56 -0
  27. package/docs/METRICS.md +3 -1
  28. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.1] - 2026-10-02
8
+
9
+ ### Synthetic large-repository generator
10
+
11
+ - Added deterministic synthetic TypeScript and Python repository generation under `src/evaluation/syntheticRepository/`: a strict `SyntheticRepositoryConfigV1`, seed- and identity-based pure planning, a versioned generator-owned PRNG, exact source/test/symbol/import/depth/repeated-pattern dimensions, and localized, cross-module, and broad-change task answer keys.
12
+ - Added contained, idempotent materialization with a per-repository generation manifest (deterministic file hashes and metrics) and a read-only manifest verifier, plus an in-memory `EvaluationCase` adapter that reuses the existing raw baseline and scoring.
13
+ - Added `--synthetic-config <path>` to `experiment run --experiment context-window-scaling` so the experiment can run over generated repositories placed beneath the run output; it is a command input selector (mutually exclusive with `--case`), not a plugin config field. Without it, the bundled v0.7.0 fixed corpus is used unchanged.
14
+ - Extended help, `experiment list`/`describe` text, and installed-package acceptance to cover the synthetic input for both languages. The `context-window-scaling` treatments, budget semantics, correctness/success semantics, execution artifact, reports, and plots are unchanged.
15
+
7
16
  ## [0.7.0] - 2026-10-01
8
17
 
9
18
  ### Context-window scaling
package/README.md CHANGED
@@ -6,7 +6,7 @@ my-dev-kit provides local repository indexing and graph-guided retrieval. my-dev
6
6
 
7
7
  Whole-ecosystem workflows are centralized in [my-dev-kit/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md). Its [command-surface compatibility map](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md#915-command-surface-compatibility-map) documents which Lab outputs can safely seed my-dev-kit investigation, Observer reference work, or Orchestrator evidence, and which look-alike artifacts are not direct inputs.
8
8
 
9
- The latest release is v0.7.0 (context-window scaling), and the published package and installed CLI are version 0.7.0. The previous release is v0.6.3 (four-treatment partial-refresh experiments). The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for current state.
9
+ The latest release is v0.7.1 (synthetic large-repository generator), and the published package and installed CLI are version 0.7.1. The previous release is v0.7.0 (context-window scaling), and v0.7.2 (real/local external repository experiments) is planned after the current release. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for current state.
10
10
 
11
11
  v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--version`, `security validate`, `audit`, the `experiment` family, `report render`, `plots generate`, `gallery build`, `demo final`, and the historical direct final-demo invocation form), a writable lab workspace model kept separate from the installed package and the inspected target, and a permanent packed-tarball installation/execution acceptance gate (`npm run verify:packed-package`). See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for status detail and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime path model. The "Installed CLI" section below documents the shipped command surface; the source-checkout `npm run` workflow in Quickstart remains available for contributors.
12
12
 
@@ -18,6 +18,7 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
18
18
  - **Record index freshness evidence (v0.6.0):** a warm-index run captures an index snapshot after its one index build — the exact indexed file set from the my-dev-kit index contract, each with SHA-256 content identity, size, and modified-time metadata, plus the my-dev-kit version and index-command evidence — and assesses freshness immediately before each task's warm retrieval as `fresh`, `stale`, `partially-stale`, or `unknown`. The evidence appears in `warm-index-execution.json` and in the existing `report.json`/`report.txt`/`report.html` warm-index reports. It is observational only: it covers only files the snapshot lists (not the whole repository), never changes retrieval, execution or provider status, and adds no CLI flag, metric, plot, gallery item, or reindex recommendation in v0.6.0. See [docs/METRICS.md](docs/METRICS.md#index-freshness-evidence-v060) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment).
19
19
  - **Compare no-refresh, partial-refresh, and full-refresh retrieval after a controlled change (v0.6.3, previous release):** the `incremental-change-staleness` experiment plugin runs six frozen controlled-change scenario families (unrelated-file, local-implementation, exported-symbol, public-API, import-graph, and test-only changes) against four disposable target copies — `stale-index` (no refresh), `changed-files-refresh` and `affected-neighborhood-refresh` (real partial refreshes performed by my-dev-kit 1.12.5 through `index --incremental --refresh-scope`, selected internally with no Lab-level flag), and `full-refresh` (a complete new post-change index, used as a comparison reference rather than a preferred treatment). Each partial treatment truthfully records whether upstream applied the requested partial scope or fell back to a full rebuild (`APPLIED_PARTIAL` or `FALLBACK_FULL`); a fallback is not counted as partial-refresh evidence. Three comparisons against `full-refresh` are persisted with retrieval, deterministic fake-agent correctness, and required-file evidence in `incremental-change-staleness-execution.json` (schema `my-dev-kit-lab-incremental-change-staleness-execution-v2`) and the existing `report.json`/`report.txt`/`report.html`; historical v0.6.2 (V1) artifacts stay readable. There is no winner, ranking, or safety claim, and the plugin is self-only (`--target` is not accepted). `--kit-command` also accepts `incremental-change-staleness` and now defaults to my-dev-kit 1.12.5. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) and [docs/METRICS.md](docs/METRICS.md).
20
20
  - **Measure context-window scaling (v0.7.0, released):** the self-only `context-window-scaling` plugin compares `raw-full-file` and `my-dev-kit-guided` over 8k, 16k, 32k, 64k, or custom positive-integer estimated-token budgets. It records fit, utilization, deterministic fake-agent correctness/success, and omitted expected relevant files, with JSON/text/HTML reports and three SVG plots. `plots generate` reads `context-window-scaling-execution.json`. Token counts are a character-based estimate, not provider-tokenizer or billing data; fake-agent correctness is context-independent in the current harness. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md), [docs/METRICS.md](docs/METRICS.md), and [docs/COMMANDS.md](docs/COMMANDS.md).
21
+ - **Run context-window scaling over synthetic repositories (v0.7.1, current release):** `context-window-scaling` uses the bundled fixed corpus by default, or `--synthetic-config <path>` (a `SyntheticRepositoryConfigV1` JSON file) to generate disposable deterministic TypeScript/Python repositories beneath the run output. The option is a command input selector, not plugin scientific config; it is mutually exclusive with `--case`, and the scientific semantics, execution artifact, reports, and plots are unchanged. See [docs/COMMANDS.md](docs/COMMANDS.md) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md).
21
22
  - **Assess affected-neighborhood evidence (v0.6.1, released; retained in the current package):** using the baseline my-dev-kit graph of the same prepared warm index (loaded once per session), each task's confirmed modified or missing indexed files, and the baseline symbols those files contain, are mapped to graph nodes. The affected neighborhood is those seed nodes plus their direct one-hop neighbors (edges of any kind, either direction). The task's `expectedFiles` and file-scoped `expectedSymbols` are mapped to graph nodes and compared with that neighborhood, which yields six numeric warm-side metrics (`changedFileCount`, `changedSymbolCount`, `affectedNodeCount`, `affectedEdgeCount`, `taskOverlapCount`, `taskOverlapPercent`) plus categorical evidence: a `related`, `unrelated`, or `unknown` relationship and a `recommended`, `not-indicated`, or `unknown` `reindexRecommendation`. Incomplete evidence is reported as `unknown`, never as unrelated. The assessment is recorded additively in `warm-index-execution.json` and shown in the existing `report.json`/`report.txt`/`report.html` warm-index reports. It is observational only: retrieval, execution status, and provider status are unchanged, nothing is reindexed automatically, and no CLI flag, experiment plugin, plot, or gallery item was added. See [docs/METRICS.md](docs/METRICS.md#affected-neighborhood-metrics-v061) and [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment).
22
23
  - **Audit repository health:** run conservative code-rot detectors for TypeScript/JavaScript, Python, Java, and Kotlin, or adapt security findings into the common audit report.
23
24
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
@@ -237,7 +238,7 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
237
238
  - Claude and Codex token totals are used only when the CLI/run exposes them; missing provider token evidence stays explicitly unavailable (never zero, and never substituted with an estimated context-token count)
238
239
  - Codex and Claude campaign runs can produce timeouts, invalid-output, agent-unavailable, or agent-limit-reached outcomes; these are structured provider states, not warm-index infrastructure failures
239
240
  - Small projects may make raw-full-file cheaper than my-dev-kit-guided; larger localized tasks are where my-dev-kit is expected to become more useful
240
- - The generic experiment-plugin framework has four registered plugins: `context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, and `context-window-scaling`. The v0.7.0 context-window-scaling plugin is released; retrieval precision/recall and agent-success plugins remain planned.
241
+ - The generic experiment-plugin framework has four registered plugins: `context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, and `context-window-scaling`. The v0.7.0 context-window-scaling plugin is released; v0.7.1 lets it also run over caller-supplied synthetic repositories; external/local repository experiments (v0.7.2), retrieval precision/recall, and agent-success plugins remain planned.
241
242
  - `warm-index-reuse` without `--campaign` still uses the deterministic fake agent only. With `--campaign`, exactly one real provider (Codex or Claude) runs per campaign preset — there is no multi-agent matrix, retry, provider switching, or scheduler. The v0.5.1 expanded corpus is deterministic fake-agent benchmark evidence when run without `--campaign`. No warm-index mode calculates a token-savings percentage, break-even task, winner, or ranking, and component-duration sums are not wall-clock latency
242
243
  - The current release does not guarantee token savings; it produces auditable evidence for specific cases, targets, agents, and strategies
243
244
  - Context-window scaling uses estimated tokens (`ceil(characters / 4)`) and experiment budgets, not actual provider tokenizer or model-window limits. Its deterministic fake-agent correctness check does not measure semantic sensitivity to the supplied context.
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory contains deterministic benchmark projects used by my-dev-kit-lab.
4
4
 
5
- The current suite mixes baseline Todo fixtures with more complex workflow and analytics projects:
5
+ The shared-profile suite mixes baseline Todo fixtures with more complex workflow and analytics projects:
6
6
 
7
7
  - `todo-ts`
8
8
  - `todo-js`
@@ -11,6 +11,8 @@ The current suite mixes baseline Todo fixtures with more complex workflow and an
11
11
  - `task-workflow-medium-ts`
12
12
  - `task-analytics-large-mixed`
13
13
 
14
+ The v0.7.0 release also includes `context-window-scaling-fixed-ts` as an experiment-specific, static scaling corpus. It is intentionally not part of the shared `benchmark-project-profiles.json` contract: the `context-window-scaling` plugin derives its project profile in memory. The fixed corpus is frozen v0.7.0 evidence and is not a generator.
15
+
14
16
  The Todo fixtures stay intentionally small so validation and smoke experiments remain cheap. The newer medium and large projects are intentionally more connected so raw full-file context and guided retrieval diverge more meaningfully.
15
17
 
16
18
  Benchmark project profiles live in `benchmarks/contracts/benchmark-project-profiles.json`.
@@ -167,8 +167,14 @@ export function renderExperimentRunHelp() {
167
167
  " 64k (8192/16384/32768/65536), or positive integers such as",
168
168
  " 12000 (default: 8k,16k,32k,64k). Duplicates after",
169
169
  " normalization are rejected. Only --out, --case,",
170
- " --context-budgets, and --kit-command are accepted; --case",
171
- " selects from the bundled four-case catalog. Self target only.",
170
+ " --synthetic-config, --context-budgets, and --kit-command are",
171
+ " accepted; --case selects from the bundled four-case catalog.",
172
+ " Self target only.",
173
+ " --synthetic-config <path> Path to a SyntheticRepositoryConfigV1 JSON file; relative paths",
174
+ " resolve against the invocation directory. Generated",
175
+ " repositories are written beneath the selected experiment",
176
+ " output directory (synthetic-repositories/). Mutually exclusive",
177
+ " with --case. Without it, the bundled four-case catalog is used.",
172
178
  " Generate plots with \"plots generate --experiment <run dir>\".",
173
179
  "",
174
180
  "warm-index-reuse only:",
@@ -133,6 +133,9 @@ function describeTargetBehavior(plugin) {
133
133
  if (supportsExternal) {
134
134
  return "Runs against the current lab repository when --target is omitted, or against an explicit local target project when --target <path> is provided. Outputs stay under lab-controlled output directories by default.";
135
135
  }
136
+ if (plugin.metadata.id === "context-window-scaling") {
137
+ return "Self-targeted. Uses the bundled fixed scaling corpus by default, or caller-supplied deterministic synthetic repositories when --synthetic-config <path> (a SyntheticRepositoryConfigV1 JSON file, mutually exclusive with --case) is provided; generated repositories are written beneath the experiment output directory.";
138
+ }
136
139
  return "Runs against the current lab repository.";
137
140
  }
138
141
  function describeExpectedReports(plugin) {
@@ -157,6 +160,9 @@ function describeExamples(plugin) {
157
160
  if (hasConfigField(plugin, "campaignPreset")) {
158
161
  examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --campaign codex-full --include-real-agents --out <dir>`, `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --campaign claude-full --include-real-agents --case warm-medium-complete-idempotent --out <dir>`);
159
162
  }
163
+ if (plugin.metadata.id === "context-window-scaling") {
164
+ examples.push(`my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --synthetic-config <path-to-config.json> --out <run-dir>`);
165
+ }
160
166
  return examples;
161
167
  }
162
168
  function hasConfigField(plugin, name) {
@@ -1,8 +1,10 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import { parseAgentCommandTemplate } from "../agents/index.js";
3
4
  import { parseAgentId } from "../agents/agentRegistry.js";
4
5
  import { readBenchmarkProjectProfiles, readEvaluationCases } from "../evaluation/index.js";
5
6
  import { contextStrategyComparisonPlugin, contextWindowScalingPlugin, createDefaultExperimentPluginRegistry, getWarmIndexCampaignPreset, incrementalChangeStalenessPlugin, parseContextBudgetsCliValue, parseWarmIndexCampaignPresetId, resolveExperimentTarget, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
7
+ import { prepareSyntheticContextWindowScalingInputs } from "../experiments/plugins/contextWindowScaling/index.js";
6
8
  import { buildDefaultExperimentOutputRoot } from "../experiments/outputPaths.js";
7
9
  import { parsePromptComplexityLevel, parsePromptStrategy } from "../prompts/index.js";
8
10
  import { writePluginExperimentReports } from "../report/index.js";
@@ -36,22 +38,32 @@ const DEFAULT_PROJECT_PROFILES_RESOURCE = "benchmarks/contracts/benchmark-projec
36
38
  // catalog's package-relative targetRoot, so both resolve from the package root, never the cwd.
37
39
  const CONTEXT_WINDOW_SCALING_CASES_RESOURCE = "benchmarks/contracts/context-window-scaling-cases.json";
38
40
  // Options the context-window-scaling plugin accepts; every other flag is rejected, not ignored.
39
- const CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS = ["--experiment", "--out", "--case", "--context-budgets", "--kit-command"];
41
+ const CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS = ["--experiment", "--out", "--case", "--synthetic-config", "--context-budgets", "--kit-command"];
40
42
  export async function runExperimentRunCommandFromArgs(argv, options = {}) {
41
43
  try {
42
44
  const args = parseRunExperimentArgs(argv);
43
45
  const context = options.context ?? createLabExecutionContext();
44
46
  const toolRoot = context.packageRoot;
45
47
  const registry = createDefaultExperimentPluginRegistry();
46
- const inputs = await loadPluginInputs(args, toolRoot, context);
47
48
  const runId = generateExperimentRunId(args.experimentId);
48
- const outputRoot = resolveOutputRoot(args, {
49
+ let outputRoot = resolveOutputRoot(args, {
49
50
  toolRoot,
50
51
  invocationCwd: context.invocationCwd,
51
52
  installedDefaultOutputRoot: options.installedDefaultOutputRoot,
52
53
  experimentId: args.experimentId,
53
54
  runId
54
55
  });
56
+ if (args.syntheticConfigPath !== undefined && outputRoot === undefined) {
57
+ // Synthetic generation needs the concrete root before the runner runs; use the exact default the
58
+ // runner itself would compute for this self-targeted invocation.
59
+ outputRoot = buildDefaultExperimentOutputRoot({
60
+ toolRoot,
61
+ pluginId: args.experimentId,
62
+ target: resolveExperimentTarget(args.targetPath, toolRoot),
63
+ runId
64
+ });
65
+ }
66
+ const inputs = await loadPluginInputs(args, toolRoot, context, outputRoot);
55
67
  const result = await runExperiment({
56
68
  pluginId: args.experimentId,
57
69
  registry,
@@ -148,6 +160,7 @@ export function parseRunExperimentArgs(argv) {
148
160
  let kitCommand;
149
161
  let campaignPreset;
150
162
  let contextBudgets;
163
+ let syntheticConfigPath;
151
164
  const seenFlags = [];
152
165
  const commandTemplates = {};
153
166
  for (let index = 0; index < argv.length; index += 1) {
@@ -169,6 +182,12 @@ export function parseRunExperimentArgs(argv) {
169
182
  else if (arg === "--project-profiles") {
170
183
  projectProfilesPath = readRequiredValue(argv, ++index, "--project-profiles");
171
184
  }
185
+ else if (arg === "--synthetic-config") {
186
+ if (syntheticConfigPath !== undefined) {
187
+ throw new Error("--synthetic-config may be supplied only once.");
188
+ }
189
+ syntheticConfigPath = readRequiredValue(argv, ++index, "--synthetic-config");
190
+ }
172
191
  else if (arg === "--case") {
173
192
  caseIds.push(...splitList(readRequiredValue(argv, ++index, "--case")));
174
193
  }
@@ -236,6 +255,9 @@ export function parseRunExperimentArgs(argv) {
236
255
  if (contextBudgets !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
237
256
  throw new Error(`--context-budgets is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
238
257
  }
258
+ if (syntheticConfigPath !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
259
+ throw new Error(`--synthetic-config is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
260
+ }
239
261
  if (experimentId === contextWindowScalingPlugin.metadata.id) {
240
262
  const unsupported = [...new Set(seenFlags.filter((flag) => !CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS.includes(flag)))];
241
263
  if (unsupported.length > 0) {
@@ -244,6 +266,9 @@ export function parseRunExperimentArgs(argv) {
244
266
  if (seenFlags.includes("--case") && caseIds.length === 0) {
245
267
  throw new Error("--case must list at least one case id.");
246
268
  }
269
+ if (syntheticConfigPath !== undefined && seenFlags.includes("--case")) {
270
+ throw new Error("--case and --synthetic-config are mutually exclusive; the synthetic config owns the generated case set.");
271
+ }
247
272
  }
248
273
  if (kitCommand !== undefined && !KIT_COMMAND_PLUGIN_IDS.includes(experimentId)) {
249
274
  throw new Error(`--kit-command is only supported for --experiment ${KIT_COMMAND_PLUGIN_IDS.join(" or ")}.`);
@@ -266,6 +291,7 @@ export function parseRunExperimentArgs(argv) {
266
291
  experimentId,
267
292
  targetPath,
268
293
  outDir,
294
+ syntheticConfigPath,
269
295
  config: withoutUndefined({
270
296
  casesPath,
271
297
  projectProfilesPath,
@@ -308,7 +334,7 @@ function generateExperimentRunId(pluginId) {
308
334
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
309
335
  return `${pluginId}-${timestamp}`;
310
336
  }
311
- async function loadPluginInputs(args, toolRoot, context) {
337
+ async function loadPluginInputs(args, toolRoot, context, outputRoot) {
312
338
  if (args.experimentId === contextStrategyComparisonPlugin.metadata.id) {
313
339
  const validation = contextStrategyComparisonPlugin.validateConfig(args.config);
314
340
  if (!validation.valid || !validation.config) {
@@ -327,7 +353,7 @@ async function loadPluginInputs(args, toolRoot, context) {
327
353
  return loadCasesAndProjectProfiles(args, toolRoot, context);
328
354
  }
329
355
  if (args.experimentId === contextWindowScalingPlugin.metadata.id) {
330
- return loadContextWindowScalingInputs(args, toolRoot, context);
356
+ return loadContextWindowScalingInputs(args, toolRoot, context, outputRoot);
331
357
  }
332
358
  return undefined;
333
359
  }
@@ -339,11 +365,20 @@ function pluginConfigFor(args) {
339
365
  const { caseIds: _caseIds, ...config } = args.config;
340
366
  return config;
341
367
  }
342
- async function loadContextWindowScalingInputs(args, toolRoot, context) {
368
+ async function loadContextWindowScalingInputs(args, toolRoot, context, outputRoot) {
343
369
  const validation = contextWindowScalingPlugin.validateConfig(pluginConfigFor(args));
344
370
  if (!validation.valid || !validation.config) {
345
371
  throw new Error(`Invalid context window scaling config: ${validation.errors.join("; ")}`);
346
372
  }
373
+ if (args.syntheticConfigPath !== undefined) {
374
+ if (outputRoot === undefined) {
375
+ throw new Error("Internal error: synthetic generation requires a resolved experiment output root.");
376
+ }
377
+ const configPath = path.resolve(context.invocationCwd, args.syntheticConfigPath);
378
+ const syntheticRepositoryConfig = await readSyntheticConfigJson(configPath, args.syntheticConfigPath);
379
+ const prepared = prepareSyntheticContextWindowScalingInputs({ syntheticRepositoryConfig, outputRoot });
380
+ return { cases: prepared.cases, projectProfiles: prepared.projectProfiles, env: process.env };
381
+ }
347
382
  const catalog = await readEvaluationCases(resolvePackageResource(context, CONTEXT_WINDOW_SCALING_CASES_RESOURCE), toolRoot);
348
383
  const requested = args.config.caseIds;
349
384
  if (requested === undefined)
@@ -359,6 +394,22 @@ async function loadContextWindowScalingInputs(args, toolRoot, context) {
359
394
  }
360
395
  return { cases: catalog.filter((evaluationCase) => requested.includes(evaluationCase.id)), env: process.env };
361
396
  }
397
+ // The command only reads and parses the caller-supplied file; schema validation belongs to the synthetic planner.
398
+ async function readSyntheticConfigJson(resolvedPath, displayPath) {
399
+ let text;
400
+ try {
401
+ text = await readFile(resolvedPath, "utf8");
402
+ }
403
+ catch (error) {
404
+ throw new Error(`Cannot read --synthetic-config ${displayPath}: ${error instanceof Error ? error.message : String(error)}`);
405
+ }
406
+ try {
407
+ return JSON.parse(text);
408
+ }
409
+ catch (error) {
410
+ throw new Error(`--synthetic-config ${displayPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
411
+ }
412
+ }
362
413
  // Shared by both registered plugins: explicit paths resolve against toolRoot, defaults come from
363
414
  // bundled package resources so they do not depend on the invocation cwd.
364
415
  async function loadCasesAndProjectProfiles(args, toolRoot, context) {
@@ -0,0 +1,304 @@
1
+ import { TASK_LOCALITIES } from "../types.js";
2
+ import { SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION, SYNTHETIC_REPOSITORY_LANGUAGES, SyntheticRepositoryConfigError, } from "./types.js";
3
+ export const SYNTHETIC_REPOSITORY_LIMITS = {
4
+ maxCases: 64,
5
+ caseIdMaxLength: 64,
6
+ seedMaxLength: 256,
7
+ sourceFileCount: { min: 1, max: 10000 },
8
+ moduleDepth: { min: 1, max: 64 },
9
+ internalImportCount: { min: 0, max: 100000 },
10
+ symbolCount: { min: 1, max: 100000 },
11
+ testFileCount: { min: 0, max: 10000 },
12
+ repeatedPatternCount: { min: 0, max: 100000 },
13
+ broadChangeMinSourceFiles: 4,
14
+ crossModuleMinSourceFiles: 2,
15
+ };
16
+ /** Fixed field order of one case; also the canonical serialization order. */
17
+ export const SYNTHETIC_REPOSITORY_CASE_FIELDS = [
18
+ "id",
19
+ "language",
20
+ "seed",
21
+ "sourceFileCount",
22
+ "moduleDepth",
23
+ "internalImportCount",
24
+ "symbolCount",
25
+ "testFileCount",
26
+ "taskLocality",
27
+ "repeatedPatternCount",
28
+ ];
29
+ const COUNT_FIELDS = [
30
+ "sourceFileCount",
31
+ "moduleDepth",
32
+ "internalImportCount",
33
+ "symbolCount",
34
+ "testFileCount",
35
+ "repeatedPatternCount",
36
+ ];
37
+ const CASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
38
+ const WINDOWS_DEVICE_NAME_PATTERN = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
39
+ /** Locale-independent ordering by UTF-16 code unit. */
40
+ export function compareCodeUnits(left, right) {
41
+ if (left < right)
42
+ return -1;
43
+ if (left > right)
44
+ return 1;
45
+ return 0;
46
+ }
47
+ /**
48
+ * Maximum number of unique directed acyclic import edges among `sourceFileCount` modules whose longest
49
+ * dependency chain has exactly `moduleDepth` modules: the edge count of the balanced complete
50
+ * multipartite arrangement, (N^2 - sum of squared layer sizes) / 2.
51
+ */
52
+ export function maxImportEdges(sourceFileCount, moduleDepth) {
53
+ const base = Math.floor(sourceFileCount / moduleDepth);
54
+ const larger = sourceFileCount % moduleDepth;
55
+ const sumOfSquares = larger * (base + 1) * (base + 1) + (moduleDepth - larger) * base * base;
56
+ return (sourceFileCount * sourceFileCount - sumOfSquares) / 2;
57
+ }
58
+ /** Returns the case in canonical field order. Assumes the spec is valid. */
59
+ export function orderCaseSpec(spec) {
60
+ return {
61
+ id: spec.id,
62
+ language: spec.language,
63
+ seed: spec.seed,
64
+ sourceFileCount: spec.sourceFileCount,
65
+ moduleDepth: spec.moduleDepth,
66
+ internalImportCount: spec.internalImportCount,
67
+ symbolCount: spec.symbolCount,
68
+ testFileCount: spec.testFileCount,
69
+ taskLocality: spec.taskLocality,
70
+ repeatedPatternCount: spec.repeatedPatternCount,
71
+ };
72
+ }
73
+ function isPlainObject(value) {
74
+ if (typeof value !== "object" || value === null || Array.isArray(value))
75
+ return false;
76
+ const prototype = Object.getPrototypeOf(value);
77
+ return prototype === Object.prototype || prototype === null;
78
+ }
79
+ function hasOwn(value, key) {
80
+ return Object.prototype.hasOwnProperty.call(value, key);
81
+ }
82
+ function describeValue(value) {
83
+ if (typeof value === "string") {
84
+ return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value);
85
+ }
86
+ if (typeof value === "number" || typeof value === "boolean")
87
+ return String(value);
88
+ if (value === null)
89
+ return "null";
90
+ if (Array.isArray(value))
91
+ return "array";
92
+ return typeof value;
93
+ }
94
+ function hasLoneSurrogate(value) {
95
+ for (let index = 0; index < value.length; index += 1) {
96
+ const unit = value.charCodeAt(index);
97
+ if (unit >= 0xd800 && unit <= 0xdbff) {
98
+ const next = value.charCodeAt(index + 1);
99
+ if (next >= 0xdc00 && next <= 0xdfff) {
100
+ index += 1;
101
+ continue;
102
+ }
103
+ return true;
104
+ }
105
+ if (unit >= 0xdc00 && unit <= 0xdfff)
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ function validateCaseId(value, label, errors) {
111
+ if (typeof value !== "string") {
112
+ errors.push(`${label}.id: must be a string (received ${describeValue(value)}).`);
113
+ return false;
114
+ }
115
+ const before = errors.length;
116
+ if (value.length < 1 || value.length > SYNTHETIC_REPOSITORY_LIMITS.caseIdMaxLength) {
117
+ errors.push(`${label}.id: length must be between 1 and ${SYNTHETIC_REPOSITORY_LIMITS.caseIdMaxLength}.`);
118
+ }
119
+ else if (!CASE_ID_PATTERN.test(value)) {
120
+ errors.push(`${label}.id: must begin with an ASCII letter or digit and contain only ASCII letters, digits, ".", "_" and "-" (received ${describeValue(value)}).`);
121
+ }
122
+ else {
123
+ if (value.endsWith(".")) {
124
+ errors.push(`${label}.id: must not end with ".".`);
125
+ }
126
+ if (WINDOWS_DEVICE_NAME_PATTERN.test(value.split(".")[0])) {
127
+ errors.push(`${label}.id: must not use a reserved device name (received ${describeValue(value)}).`);
128
+ }
129
+ }
130
+ return errors.length === before;
131
+ }
132
+ function validateSeed(value, label, errors) {
133
+ if (typeof value !== "string") {
134
+ errors.push(`${label}.seed: must be a string (received ${describeValue(value)}).`);
135
+ return false;
136
+ }
137
+ const before = errors.length;
138
+ if (!/\S/.test(value)) {
139
+ errors.push(`${label}.seed: must contain at least one non-whitespace character.`);
140
+ }
141
+ if (value.length > SYNTHETIC_REPOSITORY_LIMITS.seedMaxLength) {
142
+ errors.push(`${label}.seed: must be at most ${SYNTHETIC_REPOSITORY_LIMITS.seedMaxLength} UTF-16 code units.`);
143
+ }
144
+ if (hasLoneSurrogate(value)) {
145
+ errors.push(`${label}.seed: must be well-formed Unicode (lone surrogate found).`);
146
+ }
147
+ return errors.length === before;
148
+ }
149
+ function validateCount(value, field, label, errors) {
150
+ const { min, max } = SYNTHETIC_REPOSITORY_LIMITS[field];
151
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
152
+ errors.push(`${label}.${field}: must be an integer between ${min} and ${max} (received ${describeValue(value)}).`);
153
+ return false;
154
+ }
155
+ return true;
156
+ }
157
+ function validateCase(value, label, errors) {
158
+ if (!isPlainObject(value)) {
159
+ errors.push(`${label}: must be an object (received ${describeValue(value)}).`);
160
+ return undefined;
161
+ }
162
+ const before = errors.length;
163
+ const allowed = new Set(SYNTHETIC_REPOSITORY_CASE_FIELDS);
164
+ for (const key of Object.keys(value).sort(compareCodeUnits)) {
165
+ if (!allowed.has(key))
166
+ errors.push(`${label}: unknown field ${JSON.stringify(key)}.`);
167
+ }
168
+ for (const field of SYNTHETIC_REPOSITORY_CASE_FIELDS) {
169
+ if (!hasOwn(value, field))
170
+ errors.push(`${label}: missing required field ${field}.`);
171
+ }
172
+ const idOk = hasOwn(value, "id") && validateCaseId(value.id, label, errors);
173
+ const languageOk = hasOwn(value, "language") &&
174
+ SYNTHETIC_REPOSITORY_LANGUAGES.includes(value.language);
175
+ if (hasOwn(value, "language") && !languageOk) {
176
+ errors.push(`${label}.language: must be one of ${SYNTHETIC_REPOSITORY_LANGUAGES.join(", ")} (received ${describeValue(value.language)}).`);
177
+ }
178
+ const seedOk = hasOwn(value, "seed") && validateSeed(value.seed, label, errors);
179
+ const countOk = new Map();
180
+ for (const field of COUNT_FIELDS) {
181
+ countOk.set(field, hasOwn(value, field) && validateCount(value[field], field, label, errors));
182
+ }
183
+ const localityOk = hasOwn(value, "taskLocality") && TASK_LOCALITIES.includes(value.taskLocality);
184
+ if (hasOwn(value, "taskLocality") && !localityOk) {
185
+ errors.push(`${label}.taskLocality: must be one of ${TASK_LOCALITIES.join(", ")} (received ${describeValue(value.taskLocality)}).`);
186
+ }
187
+ const sourceFileCount = value.sourceFileCount;
188
+ const moduleDepth = value.moduleDepth;
189
+ const internalImportCount = value.internalImportCount;
190
+ const symbolCount = value.symbolCount;
191
+ const taskLocality = value.taskLocality;
192
+ const sourceOk = countOk.get("sourceFileCount") === true;
193
+ const depthOk = countOk.get("moduleDepth") === true;
194
+ const importsOk = countOk.get("internalImportCount") === true;
195
+ const symbolsOk = countOk.get("symbolCount") === true;
196
+ if (sourceOk && depthOk) {
197
+ if (moduleDepth > sourceFileCount) {
198
+ errors.push(`${label}.moduleDepth: must not exceed sourceFileCount (${moduleDepth} > ${sourceFileCount}).`);
199
+ }
200
+ else if (importsOk) {
201
+ if (moduleDepth > 1 && internalImportCount < moduleDepth - 1) {
202
+ errors.push(`${label}.internalImportCount: must be at least moduleDepth - 1 (${moduleDepth - 1}) to realize moduleDepth ${moduleDepth}.`);
203
+ }
204
+ const capacity = maxImportEdges(sourceFileCount, moduleDepth);
205
+ if (internalImportCount > capacity) {
206
+ errors.push(`${label}.internalImportCount: exceeds the ${capacity} unique acyclic edges possible for sourceFileCount ${sourceFileCount} with exact moduleDepth ${moduleDepth}.`);
207
+ }
208
+ }
209
+ }
210
+ if (sourceOk && symbolsOk && symbolCount < sourceFileCount) {
211
+ errors.push(`${label}.symbolCount: must be at least sourceFileCount (${symbolCount} < ${sourceFileCount}).`);
212
+ }
213
+ if (sourceOk && localityOk) {
214
+ if (taskLocality === "cross-module") {
215
+ if (sourceFileCount < SYNTHETIC_REPOSITORY_LIMITS.crossModuleMinSourceFiles) {
216
+ errors.push(`${label}.taskLocality: cross-module requires sourceFileCount >= 2.`);
217
+ }
218
+ if (importsOk && internalImportCount < 1) {
219
+ errors.push(`${label}.taskLocality: cross-module requires internalImportCount >= 1.`);
220
+ }
221
+ }
222
+ else if (taskLocality === "broad-change" && sourceFileCount < SYNTHETIC_REPOSITORY_LIMITS.broadChangeMinSourceFiles) {
223
+ errors.push(`${label}.taskLocality: broad-change requires sourceFileCount >= 4.`);
224
+ }
225
+ }
226
+ if (errors.length !== before || !idOk || !languageOk || !seedOk || !localityOk)
227
+ return undefined;
228
+ return orderCaseSpec(value);
229
+ }
230
+ /** Validates one case spec and returns it in canonical field order. Never throws, never repairs. */
231
+ export function validateSyntheticRepositoryCaseSpec(input) {
232
+ const errors = [];
233
+ const spec = validateCase(input, "case", errors);
234
+ if (errors.length > 0 || spec === undefined)
235
+ return { ok: false, errors };
236
+ return { ok: true, errors: [], spec };
237
+ }
238
+ /**
239
+ * Strictly validates an unknown value as SyntheticRepositoryConfigV1. On success the returned config is
240
+ * canonical: cases sorted ascending by id (code-unit order), fixed field order, values untouched.
241
+ */
242
+ export function validateSyntheticRepositoryConfig(input) {
243
+ const errors = [];
244
+ if (!isPlainObject(input)) {
245
+ return { ok: false, errors: [`config: must be an object (received ${describeValue(input)}).`] };
246
+ }
247
+ for (const key of Object.keys(input).sort(compareCodeUnits)) {
248
+ if (key !== "schemaVersion" && key !== "cases")
249
+ errors.push(`config: unknown field ${JSON.stringify(key)}.`);
250
+ }
251
+ if (!hasOwn(input, "schemaVersion")) {
252
+ errors.push("config: missing required field schemaVersion.");
253
+ }
254
+ else if (input.schemaVersion !== SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION) {
255
+ errors.push(`config.schemaVersion: must be ${JSON.stringify(SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION)} (received ${describeValue(input.schemaVersion)}).`);
256
+ }
257
+ if (!hasOwn(input, "cases")) {
258
+ errors.push("config: missing required field cases.");
259
+ return { ok: false, errors };
260
+ }
261
+ const cases = input.cases;
262
+ if (!Array.isArray(cases)) {
263
+ errors.push(`config.cases: must be an array (received ${describeValue(cases)}).`);
264
+ return { ok: false, errors };
265
+ }
266
+ if (cases.length < 1 || cases.length > SYNTHETIC_REPOSITORY_LIMITS.maxCases) {
267
+ errors.push(`config.cases: must contain between 1 and ${SYNTHETIC_REPOSITORY_LIMITS.maxCases} cases (received ${cases.length}).`);
268
+ return { ok: false, errors };
269
+ }
270
+ const valid = [];
271
+ const firstIndexById = new Map();
272
+ cases.forEach((candidate, index) => {
273
+ const label = `cases[${index}]`;
274
+ const spec = validateCase(candidate, label, errors);
275
+ if (spec)
276
+ valid.push(spec);
277
+ const id = isPlainObject(candidate) ? candidate.id : undefined;
278
+ if (typeof id === "string") {
279
+ const key = id.toLowerCase();
280
+ const first = firstIndexById.get(key);
281
+ if (first === undefined) {
282
+ firstIndexById.set(key, index);
283
+ }
284
+ else {
285
+ errors.push(`${label}.id: duplicates cases[${first}].id (ids are compared without regard to letter case).`);
286
+ }
287
+ }
288
+ });
289
+ if (errors.length > 0)
290
+ return { ok: false, errors };
291
+ const sorted = [...valid].sort((left, right) => compareCodeUnits(left.id, right.id));
292
+ return {
293
+ ok: true,
294
+ errors: [],
295
+ config: { schemaVersion: SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION, cases: sorted },
296
+ };
297
+ }
298
+ /** Validates then returns the canonical configuration; throws SyntheticRepositoryConfigError when invalid. */
299
+ export function normalizeSyntheticRepositoryConfig(input) {
300
+ const result = validateSyntheticRepositoryConfig(input);
301
+ if (!result.ok)
302
+ throw new SyntheticRepositoryConfigError(result.errors);
303
+ return result.config;
304
+ }
@@ -0,0 +1,43 @@
1
+ import { describeSyntheticTask } from "./layout.js";
2
+ /** The plan's answer key expressed in the existing benchmark answer-key vocabulary (no second correctness contract). */
3
+ export function buildSyntheticAnswerKey(plan) {
4
+ const { factTextById } = describeSyntheticTask(plan);
5
+ return {
6
+ expectedFiles: [...plan.answerKey.expectedFiles],
7
+ expectedSymbols: [...plan.answerKey.expectedSymbols],
8
+ expectedFacts: plan.answerKey.facts.map((fact) => ({
9
+ id: fact.factId,
10
+ text: factTextById.get(fact.factId),
11
+ weight: fact.weight,
12
+ required: fact.required,
13
+ })),
14
+ expectedContextTargets: plan.answerKey.expectedContextTargets.map((target) => ({
15
+ file: target.file,
16
+ symbols: [...target.symbols],
17
+ required: target.required,
18
+ })),
19
+ minimumCorrectFacts: plan.answerKey.minimumCorrectFacts,
20
+ };
21
+ }
22
+ /**
23
+ * Builds the existing EvaluationCase directly in memory (never through readEvaluationCases, which owns
24
+ * repository-contained JSON cases). `targetRoot` is the stable logical root from the manifest; only
25
+ * `absoluteTargetRoot` carries the physical generated repository directory.
26
+ */
27
+ export function buildSyntheticEvaluationCase(plan, manifest, repositoryRoot) {
28
+ const text = describeSyntheticTask(plan);
29
+ return {
30
+ id: plan.task.taskId,
31
+ title: text.title,
32
+ benchmarkProject: plan.logicalProjectId,
33
+ targetRoot: manifest.logicalTargetRoot,
34
+ sourceRoots: [...manifest.sourceRoots],
35
+ query: text.query,
36
+ expectedFiles: [...plan.answerKey.expectedFiles],
37
+ expectedSymbols: [...plan.answerKey.expectedSymbols],
38
+ rawIncludeGlobs: [...manifest.rawIncludeGlobs],
39
+ answerKey: buildSyntheticAnswerKey(plan),
40
+ taskLocality: plan.task.locality,
41
+ absoluteTargetRoot: repositoryRoot,
42
+ };
43
+ }