@dailephd/my-dev-kit-lab 0.7.1 → 0.8.0
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.
- package/CHANGELOG.md +28 -0
- package/README.md +10 -5
- package/benchmarks/contracts/warm-index-benchmark-cases.json +330 -0
- package/dist/src/cli/help.js +27 -4
- package/dist/src/commands/runExperimentDescribeCommand.js +13 -2
- package/dist/src/commands/runExperimentRunCommand.js +210 -20
- package/dist/src/core/fileGlobs.js +44 -0
- package/dist/src/evaluation/benchmarkMetadata.js +13 -0
- package/dist/src/evaluation/localRepositorySubject/config.js +296 -0
- package/dist/src/evaluation/localRepositorySubject/evaluationCase.js +36 -0
- package/dist/src/evaluation/localRepositorySubject/expectedFiles.js +51 -0
- package/dist/src/evaluation/localRepositorySubject/gitRepository.js +128 -0
- package/dist/src/evaluation/localRepositorySubject/index.js +8 -0
- package/dist/src/evaluation/localRepositorySubject/inventory.js +127 -0
- package/dist/src/evaluation/localRepositorySubject/loadLocalRepositorySubject.js +48 -0
- package/dist/src/evaluation/localRepositorySubject/manifest.js +52 -0
- package/dist/src/evaluation/localRepositorySubject/types.js +55 -0
- package/dist/src/evaluation/projectFileTree.js +45 -1
- package/dist/src/evaluation/retrievalQuality/buildRetrievalEvidence.js +292 -0
- package/dist/src/evaluation/retrievalQuality/corpusCompleteness.js +122 -0
- package/dist/src/evaluation/retrievalQuality/factContextTargets.js +44 -0
- package/dist/src/evaluation/retrievalQuality/index.js +5 -0
- package/dist/src/evaluation/retrievalQuality/metrics.js +233 -0
- package/dist/src/evaluation/retrievalQuality/types.js +9 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +6 -1
- package/dist/src/evaluation/runRawFullFileBaseline.js +18 -4
- package/dist/src/evaluation/targetImmutability/captureTargetSnapshot.js +29 -7
- package/dist/src/evaluation/targetImmutability/compareTargetSnapshots.js +4 -0
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectErrors.js +35 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExclusions.js +75 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExecution.js +136 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +128 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectScratch.js +85 -0
- package/dist/src/experiments/plugins/contextWindowScaling/metadata.js +2 -2
- package/dist/src/experiments/plugins/contextWindowScaling/plugin.js +93 -0
- package/dist/src/experiments/plugins/contextWindowScaling/projectProfile.js +4 -9
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/config.js +39 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/execution.js +152 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/executionArtifact.js +15 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/index.js +9 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectExecution.js +139 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectPrivacy.js +119 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/metadata.js +21 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/metrics.js +158 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/plugin.js +253 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/types.js +8 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +21 -2
- package/dist/src/report/experiments/buildRetrievalPrecisionRecallReport.js +83 -0
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +7 -3
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +9 -0
- package/dist/src/report/experiments/renderRetrievalPrecisionRecallHtml.js +96 -0
- package/dist/src/report/experiments/renderRetrievalPrecisionRecallText.js +121 -0
- package/dist/src/report/experiments/retrievalPrecisionRecallReportModel.js +3 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +8 -2
- package/docs/METRICS.md +34 -2
- package/package.json +1 -1
|
@@ -3,8 +3,12 @@ import path from "node:path";
|
|
|
3
3
|
import { parseAgentCommandTemplate } from "../agents/index.js";
|
|
4
4
|
import { parseAgentId } from "../agents/agentRegistry.js";
|
|
5
5
|
import { readBenchmarkProjectProfiles, readEvaluationCases } from "../evaluation/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { summarizeRetrievalGroundTruthIssuesSafely, validateRetrievalPrecisionRecallCorpus } from "../evaluation/retrievalQuality/index.js";
|
|
7
|
+
import { loadLocalRepositorySubject } from "../evaluation/localRepositorySubject/index.js";
|
|
8
|
+
import { contextStrategyComparisonPlugin, contextWindowScalingPlugin, createDefaultExperimentPluginRegistry, getWarmIndexCampaignPreset, incrementalChangeStalenessPlugin, parseContextBudgetsCliValue, parseWarmIndexCampaignPresetId, resolveExperimentTarget, retrievalPrecisionRecallPlugin, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
|
|
7
9
|
import { prepareSyntheticContextWindowScalingInputs } from "../experiments/plugins/contextWindowScaling/index.js";
|
|
10
|
+
import { projectExternalLocalTarget, projectRunForExternalLocalPersistence, } from "../experiments/plugins/contextWindowScaling/localSubjectPrivacy.js";
|
|
11
|
+
import { assertWorkRootOutsideTarget } from "../experiments/plugins/contextWindowScaling/localSubjectScratch.js";
|
|
8
12
|
import { buildDefaultExperimentOutputRoot } from "../experiments/outputPaths.js";
|
|
9
13
|
import { parsePromptComplexityLevel, parsePromptStrategy } from "../prompts/index.js";
|
|
10
14
|
import { writePluginExperimentReports } from "../report/index.js";
|
|
@@ -38,7 +42,23 @@ const DEFAULT_PROJECT_PROFILES_RESOURCE = "benchmarks/contracts/benchmark-projec
|
|
|
38
42
|
// catalog's package-relative targetRoot, so both resolve from the package root, never the cwd.
|
|
39
43
|
const CONTEXT_WINDOW_SCALING_CASES_RESOURCE = "benchmarks/contracts/context-window-scaling-cases.json";
|
|
40
44
|
// Options the context-window-scaling plugin accepts; every other flag is rejected, not ignored.
|
|
41
|
-
const CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS = [
|
|
45
|
+
const CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS = [
|
|
46
|
+
"--experiment",
|
|
47
|
+
"--out",
|
|
48
|
+
"--target",
|
|
49
|
+
"--case",
|
|
50
|
+
"--synthetic-config",
|
|
51
|
+
"--local-subject-config",
|
|
52
|
+
"--context-budgets",
|
|
53
|
+
"--kit-command"
|
|
54
|
+
];
|
|
55
|
+
// retrieval-precision-recall owns one frozen bundled corpus (the warm-index benchmark cases) and its project profiles,
|
|
56
|
+
// both resolved from the package root, never the cwd. It accepts only these options; every other flag is rejected.
|
|
57
|
+
const RETRIEVAL_PRECISION_RECALL_CASES_RESOURCE = "benchmarks/contracts/warm-index-benchmark-cases.json";
|
|
58
|
+
const RETRIEVAL_PRECISION_RECALL_ALLOWED_FLAGS = ["--experiment", "--out", "--case", "--benchmark-project", "--kit-command"];
|
|
59
|
+
// External-local mode (--target with --local-subject-config) is a different subject source: the local-subject config owns
|
|
60
|
+
// the case set, so the bundled corpus filters are not accepted there.
|
|
61
|
+
const RETRIEVAL_PRECISION_RECALL_EXTERNAL_ALLOWED_FLAGS = ["--experiment", "--out", "--target", "--local-subject-config", "--kit-command"];
|
|
42
62
|
export async function runExperimentRunCommandFromArgs(argv, options = {}) {
|
|
43
63
|
try {
|
|
44
64
|
const args = parseRunExperimentArgs(argv);
|
|
@@ -53,8 +73,8 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
|
|
|
53
73
|
experimentId: args.experimentId,
|
|
54
74
|
runId
|
|
55
75
|
});
|
|
56
|
-
if (args.syntheticConfigPath !== undefined && outputRoot === undefined) {
|
|
57
|
-
// Synthetic generation
|
|
76
|
+
if ((args.syntheticConfigPath !== undefined || args.localSubjectConfigPath !== undefined) && outputRoot === undefined) {
|
|
77
|
+
// Synthetic generation and external-local runs need the concrete root before the runner runs; use the exact default the
|
|
58
78
|
// runner itself would compute for this self-targeted invocation.
|
|
59
79
|
outputRoot = buildDefaultExperimentOutputRoot({
|
|
60
80
|
toolRoot,
|
|
@@ -74,22 +94,53 @@ export async function runExperimentRunCommandFromArgs(argv, options = {}) {
|
|
|
74
94
|
toolRoot,
|
|
75
95
|
runId
|
|
76
96
|
});
|
|
97
|
+
const localSubject = readLoadedLocalSubject(inputs);
|
|
98
|
+
// External-local runs persist only a projected run: no target, tool, or output machine-local paths.
|
|
99
|
+
const persistedRun = localSubject
|
|
100
|
+
? projectRunForExternalLocalPersistence(result, projectExternalLocalTarget(localSubject.manifest))
|
|
101
|
+
: result;
|
|
102
|
+
if (localSubject && result.status === "failed") {
|
|
103
|
+
// A failed local run has no execution evidence, so no report is produced and none could be mistaken for a
|
|
104
|
+
// completed experiment. The safe failure description (codes and kinds only) is the whole outcome.
|
|
105
|
+
console.log([
|
|
106
|
+
`Experiment: ${result.pluginId}`,
|
|
107
|
+
`Run ID: ${result.runId}`,
|
|
108
|
+
"Status: failed",
|
|
109
|
+
"Mode: external-local repository subject",
|
|
110
|
+
`Subject: ${localSubject.subjectId}`,
|
|
111
|
+
...result.failures.map((failure) => `Failure: ${failure.message}`)
|
|
112
|
+
].join("\n"));
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
77
115
|
const reports = await writePluginExperimentReports({
|
|
78
|
-
run:
|
|
116
|
+
run: persistedRun,
|
|
79
117
|
plugin: registry.describe(result.pluginId),
|
|
80
|
-
outputRoot: String(result.metadata?.outputRoot ?? "")
|
|
118
|
+
outputRoot: String(result.metadata?.outputRoot ?? ""),
|
|
119
|
+
redactOutputRoot: localSubject !== undefined
|
|
81
120
|
});
|
|
82
|
-
const outputLines =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
const outputLines = localSubject
|
|
122
|
+
? [
|
|
123
|
+
`Experiment: ${result.pluginId}`,
|
|
124
|
+
`Run ID: ${result.runId}`,
|
|
125
|
+
`Status: ${result.status}`,
|
|
126
|
+
"Mode: external-local repository subject",
|
|
127
|
+
`Subject: ${localSubject.subjectId}`,
|
|
128
|
+
`Output: ${String(result.metadata?.outputRoot ?? "")}`,
|
|
129
|
+
`Report JSON: ${reports.outputPaths.jsonPath}`,
|
|
130
|
+
`Report HTML: ${reports.outputPaths.htmlPath}`,
|
|
131
|
+
...result.failures.map((failure) => `Failure: ${failure.message}`)
|
|
132
|
+
]
|
|
133
|
+
: [
|
|
134
|
+
`Experiment: ${result.pluginId}`,
|
|
135
|
+
`Run ID: ${result.runId}`,
|
|
136
|
+
`Status: ${result.status}`,
|
|
137
|
+
`Mode: ${result.target.isSelf ? "self" : "external target"}`,
|
|
138
|
+
`Tool root: ${result.target.toolRoot}`,
|
|
139
|
+
`Target root: ${result.target.targetRoot}`,
|
|
140
|
+
`Output: ${String(result.metadata?.outputRoot ?? "")}`,
|
|
141
|
+
`Report JSON: ${reports.outputPaths.jsonPath}`,
|
|
142
|
+
`Report HTML: ${reports.outputPaths.htmlPath}`
|
|
143
|
+
];
|
|
93
144
|
// Automatic presentation (report already produced above) applies only to warm-index-reuse
|
|
94
145
|
// campaign runs; legacy fake-agent warm-index runs and context-strategy-comparison stay
|
|
95
146
|
// report-only, unchanged.
|
|
@@ -161,6 +212,7 @@ export function parseRunExperimentArgs(argv) {
|
|
|
161
212
|
let campaignPreset;
|
|
162
213
|
let contextBudgets;
|
|
163
214
|
let syntheticConfigPath;
|
|
215
|
+
let localSubjectConfigPath;
|
|
164
216
|
const seenFlags = [];
|
|
165
217
|
const commandTemplates = {};
|
|
166
218
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -188,6 +240,12 @@ export function parseRunExperimentArgs(argv) {
|
|
|
188
240
|
}
|
|
189
241
|
syntheticConfigPath = readRequiredValue(argv, ++index, "--synthetic-config");
|
|
190
242
|
}
|
|
243
|
+
else if (arg === "--local-subject-config") {
|
|
244
|
+
if (localSubjectConfigPath !== undefined) {
|
|
245
|
+
throw new Error("--local-subject-config may be supplied only once.");
|
|
246
|
+
}
|
|
247
|
+
localSubjectConfigPath = readRequiredValue(argv, ++index, "--local-subject-config");
|
|
248
|
+
}
|
|
191
249
|
else if (arg === "--case") {
|
|
192
250
|
caseIds.push(...splitList(readRequiredValue(argv, ++index, "--case")));
|
|
193
251
|
}
|
|
@@ -250,14 +308,44 @@ export function parseRunExperimentArgs(argv) {
|
|
|
250
308
|
const KIT_COMMAND_PLUGIN_IDS = [
|
|
251
309
|
warmIndexReusePlugin.metadata.id,
|
|
252
310
|
incrementalChangeStalenessPlugin.metadata.id,
|
|
253
|
-
contextWindowScalingPlugin.metadata.id
|
|
311
|
+
contextWindowScalingPlugin.metadata.id,
|
|
312
|
+
retrievalPrecisionRecallPlugin.metadata.id
|
|
254
313
|
];
|
|
314
|
+
if (experimentId === retrievalPrecisionRecallPlugin.metadata.id) {
|
|
315
|
+
// Mode matrix, no precedence rules: neither flag = bundled; both = external-local; exactly one is rejected.
|
|
316
|
+
if (targetPath !== undefined && localSubjectConfigPath === undefined) {
|
|
317
|
+
throw new Error(`External ${experimentId} targets require --local-subject-config.`);
|
|
318
|
+
}
|
|
319
|
+
if (localSubjectConfigPath !== undefined && targetPath === undefined) {
|
|
320
|
+
throw new Error(`--local-subject-config requires an external --target for ${experimentId}.`);
|
|
321
|
+
}
|
|
322
|
+
const externalMode = targetPath !== undefined;
|
|
323
|
+
if (externalMode && (seenFlags.includes("--case") || seenFlags.includes("--benchmark-project"))) {
|
|
324
|
+
throw new Error("--case and --benchmark-project cannot be combined with --local-subject-config; the local subject config owns the case set.");
|
|
325
|
+
}
|
|
326
|
+
const allowedFlags = externalMode ? RETRIEVAL_PRECISION_RECALL_EXTERNAL_ALLOWED_FLAGS : RETRIEVAL_PRECISION_RECALL_ALLOWED_FLAGS;
|
|
327
|
+
const unsupported = [...new Set(seenFlags.filter((flag) => !allowedFlags.includes(flag)))];
|
|
328
|
+
if (unsupported.length > 0) {
|
|
329
|
+
throw new Error(`${unsupported.join(", ")} ${unsupported.length === 1 ? "is" : "are"} not supported for --experiment ${experimentId}${externalMode ? " in external-local mode" : ""}; supported options: ${allowedFlags.join(", ")}.`);
|
|
330
|
+
}
|
|
331
|
+
if (seenFlags.includes("--case") && caseIds.length === 0) {
|
|
332
|
+
throw new Error("--case must list at least one case id.");
|
|
333
|
+
}
|
|
334
|
+
if (seenFlags.includes("--benchmark-project") && benchmarkProjects.length === 0) {
|
|
335
|
+
throw new Error("--benchmark-project must list at least one benchmark project id.");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
255
338
|
if (contextBudgets !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
|
|
256
339
|
throw new Error(`--context-budgets is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
|
|
257
340
|
}
|
|
258
341
|
if (syntheticConfigPath !== undefined && experimentId !== contextWindowScalingPlugin.metadata.id) {
|
|
259
342
|
throw new Error(`--synthetic-config is only supported for --experiment ${contextWindowScalingPlugin.metadata.id}.`);
|
|
260
343
|
}
|
|
344
|
+
if (localSubjectConfigPath !== undefined &&
|
|
345
|
+
experimentId !== contextWindowScalingPlugin.metadata.id &&
|
|
346
|
+
experimentId !== retrievalPrecisionRecallPlugin.metadata.id) {
|
|
347
|
+
throw new Error(`--local-subject-config is only supported for --experiment ${contextWindowScalingPlugin.metadata.id} or ${retrievalPrecisionRecallPlugin.metadata.id}.`);
|
|
348
|
+
}
|
|
261
349
|
if (experimentId === contextWindowScalingPlugin.metadata.id) {
|
|
262
350
|
const unsupported = [...new Set(seenFlags.filter((flag) => !CONTEXT_WINDOW_SCALING_ALLOWED_FLAGS.includes(flag)))];
|
|
263
351
|
if (unsupported.length > 0) {
|
|
@@ -269,6 +357,23 @@ export function parseRunExperimentArgs(argv) {
|
|
|
269
357
|
if (syntheticConfigPath !== undefined && seenFlags.includes("--case")) {
|
|
270
358
|
throw new Error("--case and --synthetic-config are mutually exclusive; the synthetic config owns the generated case set.");
|
|
271
359
|
}
|
|
360
|
+
// Subject-mode matrix: bundled (no source flags), synthetic (--synthetic-config), external-local
|
|
361
|
+
// (--target with --local-subject-config). Every other combination is rejected, never given a precedence.
|
|
362
|
+
if (localSubjectConfigPath !== undefined && syntheticConfigPath !== undefined) {
|
|
363
|
+
throw new Error("--synthetic-config and --local-subject-config are mutually exclusive; choose one subject source.");
|
|
364
|
+
}
|
|
365
|
+
if (localSubjectConfigPath !== undefined && seenFlags.includes("--case")) {
|
|
366
|
+
throw new Error("--case cannot be combined with --local-subject-config; the local subject config owns the case set.");
|
|
367
|
+
}
|
|
368
|
+
if (localSubjectConfigPath !== undefined && targetPath === undefined) {
|
|
369
|
+
throw new Error(`--local-subject-config requires an external --target for ${experimentId}.`);
|
|
370
|
+
}
|
|
371
|
+
if (syntheticConfigPath !== undefined && targetPath !== undefined) {
|
|
372
|
+
throw new Error("--synthetic-config cannot be combined with --target; synthetic subjects are generated beneath the experiment output directory.");
|
|
373
|
+
}
|
|
374
|
+
if (targetPath !== undefined && localSubjectConfigPath === undefined) {
|
|
375
|
+
throw new Error(`External ${experimentId} targets require --local-subject-config.`);
|
|
376
|
+
}
|
|
272
377
|
}
|
|
273
378
|
if (kitCommand !== undefined && !KIT_COMMAND_PLUGIN_IDS.includes(experimentId)) {
|
|
274
379
|
throw new Error(`--kit-command is only supported for --experiment ${KIT_COMMAND_PLUGIN_IDS.join(" or ")}.`);
|
|
@@ -292,6 +397,7 @@ export function parseRunExperimentArgs(argv) {
|
|
|
292
397
|
targetPath,
|
|
293
398
|
outDir,
|
|
294
399
|
syntheticConfigPath,
|
|
400
|
+
localSubjectConfigPath,
|
|
295
401
|
config: withoutUndefined({
|
|
296
402
|
casesPath,
|
|
297
403
|
projectProfilesPath,
|
|
@@ -355,8 +461,64 @@ async function loadPluginInputs(args, toolRoot, context, outputRoot) {
|
|
|
355
461
|
if (args.experimentId === contextWindowScalingPlugin.metadata.id) {
|
|
356
462
|
return loadContextWindowScalingInputs(args, toolRoot, context, outputRoot);
|
|
357
463
|
}
|
|
464
|
+
if (args.experimentId === retrievalPrecisionRecallPlugin.metadata.id) {
|
|
465
|
+
return args.localSubjectConfigPath !== undefined
|
|
466
|
+
? loadRetrievalPrecisionRecallLocalSubjectInputs(args, toolRoot, context, outputRoot)
|
|
467
|
+
: loadRetrievalPrecisionRecallInputs(args, toolRoot, context);
|
|
468
|
+
}
|
|
358
469
|
return undefined;
|
|
359
470
|
}
|
|
471
|
+
// External-local retrieval-precision-recall: the existing local-subject loader owns the repository, inventory and case
|
|
472
|
+
// adapter. The only additions are the physical output-root check before anything is created and a ground-truth gate
|
|
473
|
+
// whose message carries logical case ids and counts only (an external subject may have private fact ids and names).
|
|
474
|
+
async function loadRetrievalPrecisionRecallLocalSubjectInputs(args, toolRoot, context, outputRoot) {
|
|
475
|
+
const validation = retrievalPrecisionRecallPlugin.validateConfig(args.config);
|
|
476
|
+
if (!validation.valid || !validation.config) {
|
|
477
|
+
throw new Error(`Invalid retrieval precision/recall config: ${validation.errors.join("; ")}`);
|
|
478
|
+
}
|
|
479
|
+
if (outputRoot === undefined || args.localSubjectConfigPath === undefined) {
|
|
480
|
+
throw new Error("Internal error: external-local runs require a resolved experiment output root and config path.");
|
|
481
|
+
}
|
|
482
|
+
const target = resolveExperimentTarget(args.targetPath, toolRoot);
|
|
483
|
+
if (target.isSelf || target.kind !== "external-local") {
|
|
484
|
+
throw new Error("Local-repository subject mode requires an external-local target; --target must not be the Lab repository.");
|
|
485
|
+
}
|
|
486
|
+
// Fail closed before anything is created: the output root (and the private scratch beneath it) must be physically outside the target.
|
|
487
|
+
try {
|
|
488
|
+
await assertWorkRootOutsideTarget(outputRoot, target.targetRoot);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
throw new Error("Experiment output root must not be inside the external target project.");
|
|
492
|
+
}
|
|
493
|
+
const configPath = path.resolve(context.invocationCwd, args.localSubjectConfigPath);
|
|
494
|
+
const localSubjectConfig = await readJsonConfigFile("--local-subject-config", configPath, args.localSubjectConfigPath);
|
|
495
|
+
const localSubject = await loadLocalRepositorySubject({ config: localSubjectConfig, repositoryPath: target.targetRoot });
|
|
496
|
+
const groundTruthIssues = summarizeRetrievalGroundTruthIssuesSafely(localSubject.evaluationCases);
|
|
497
|
+
if (groundTruthIssues.length > 0) {
|
|
498
|
+
throw new Error(`Invalid retrieval-precision-recall ground truth for the local subject (${groundTruthIssues
|
|
499
|
+
.map((issue) => `case ${issue.caseId}: ${issue.issueCount} issue${issue.issueCount === 1 ? "" : "s"}`)
|
|
500
|
+
.join("; ")}); details withheld.`);
|
|
501
|
+
}
|
|
502
|
+
return { cases: localSubject.evaluationCases, localSubject, env: process.env };
|
|
503
|
+
}
|
|
504
|
+
// The frozen bundled corpus is read through the established resource resolver and validation path, then must satisfy
|
|
505
|
+
// the retrieval-precision-recall ground-truth completeness rules. Selection filters are applied by the plugin.
|
|
506
|
+
async function loadRetrievalPrecisionRecallInputs(args, toolRoot, context) {
|
|
507
|
+
const validation = retrievalPrecisionRecallPlugin.validateConfig(args.config);
|
|
508
|
+
if (!validation.valid || !validation.config) {
|
|
509
|
+
throw new Error(`Invalid retrieval precision/recall config: ${validation.errors.join("; ")}`);
|
|
510
|
+
}
|
|
511
|
+
const projectProfiles = await readBenchmarkProjectProfiles(resolvePackageResource(context, DEFAULT_PROJECT_PROFILES_RESOURCE), toolRoot);
|
|
512
|
+
const cases = await readEvaluationCases(resolvePackageResource(context, RETRIEVAL_PRECISION_RECALL_CASES_RESOURCE), toolRoot, {
|
|
513
|
+
projectProfiles,
|
|
514
|
+
requireProjectProfileRef: true
|
|
515
|
+
});
|
|
516
|
+
const corpusErrors = validateRetrievalPrecisionRecallCorpus(cases);
|
|
517
|
+
if (corpusErrors.length > 0) {
|
|
518
|
+
throw new Error(`Invalid retrieval-precision-recall corpus: ${corpusErrors.join(" ")}`);
|
|
519
|
+
}
|
|
520
|
+
return { cases };
|
|
521
|
+
}
|
|
360
522
|
// --case is a selection over the frozen bundled catalog (catalog order, not argument order);
|
|
361
523
|
// the rest of the CLI config is the plugin's own ContextWindowScalingConfig.
|
|
362
524
|
function pluginConfigFor(args) {
|
|
@@ -370,6 +532,27 @@ async function loadContextWindowScalingInputs(args, toolRoot, context, outputRoo
|
|
|
370
532
|
if (!validation.valid || !validation.config) {
|
|
371
533
|
throw new Error(`Invalid context window scaling config: ${validation.errors.join("; ")}`);
|
|
372
534
|
}
|
|
535
|
+
if (args.localSubjectConfigPath !== undefined) {
|
|
536
|
+
if (outputRoot === undefined) {
|
|
537
|
+
throw new Error("Internal error: external-local runs require a resolved experiment output root.");
|
|
538
|
+
}
|
|
539
|
+
const target = resolveExperimentTarget(args.targetPath, toolRoot);
|
|
540
|
+
if (target.isSelf || target.kind !== "external-local") {
|
|
541
|
+
throw new Error("Local-repository subject mode requires an external-local target; --target must not be the Lab repository.");
|
|
542
|
+
}
|
|
543
|
+
// Fail closed before anything is created: the output root (and the private scratch beneath it) must be outside the target.
|
|
544
|
+
try {
|
|
545
|
+
await assertWorkRootOutsideTarget(outputRoot, target.targetRoot);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
throw new Error("Experiment output root must not be inside the external target project.");
|
|
549
|
+
}
|
|
550
|
+
const configPath = path.resolve(context.invocationCwd, args.localSubjectConfigPath);
|
|
551
|
+
const localSubjectConfig = await readJsonConfigFile("--local-subject-config", configPath, args.localSubjectConfigPath);
|
|
552
|
+
// The selected --target is the physical root handed to the Batch 1 loader; the config carries no path.
|
|
553
|
+
const localSubject = await loadLocalRepositorySubject({ config: localSubjectConfig, repositoryPath: target.targetRoot });
|
|
554
|
+
return { cases: localSubject.evaluationCases, localSubject, env: process.env };
|
|
555
|
+
}
|
|
373
556
|
if (args.syntheticConfigPath !== undefined) {
|
|
374
557
|
if (outputRoot === undefined) {
|
|
375
558
|
throw new Error("Internal error: synthetic generation requires a resolved experiment output root.");
|
|
@@ -396,20 +579,27 @@ async function loadContextWindowScalingInputs(args, toolRoot, context, outputRoo
|
|
|
396
579
|
}
|
|
397
580
|
// The command only reads and parses the caller-supplied file; schema validation belongs to the synthetic planner.
|
|
398
581
|
async function readSyntheticConfigJson(resolvedPath, displayPath) {
|
|
582
|
+
return readJsonConfigFile("--synthetic-config", resolvedPath, displayPath);
|
|
583
|
+
}
|
|
584
|
+
async function readJsonConfigFile(flag, resolvedPath, displayPath) {
|
|
399
585
|
let text;
|
|
400
586
|
try {
|
|
401
587
|
text = await readFile(resolvedPath, "utf8");
|
|
402
588
|
}
|
|
403
589
|
catch (error) {
|
|
404
|
-
throw new Error(`Cannot read
|
|
590
|
+
throw new Error(`Cannot read ${flag} ${displayPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
405
591
|
}
|
|
406
592
|
try {
|
|
407
593
|
return JSON.parse(text);
|
|
408
594
|
}
|
|
409
595
|
catch (error) {
|
|
410
|
-
throw new Error(
|
|
596
|
+
throw new Error(`${flag} ${displayPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
411
597
|
}
|
|
412
598
|
}
|
|
599
|
+
function readLoadedLocalSubject(inputs) {
|
|
600
|
+
const value = inputs?.localSubject;
|
|
601
|
+
return value && typeof value === "object" ? value : undefined;
|
|
602
|
+
}
|
|
413
603
|
// Shared by both registered plugins: explicit paths resolve against toolRoot, defaults come from
|
|
414
604
|
// bundled package resources so they do not depend on the invocation cwd.
|
|
415
605
|
async function loadCasesAndProjectProfiles(args, toolRoot, context) {
|
|
@@ -98,3 +98,47 @@ export function collectFilesForGlobs(targetRoot, globs) {
|
|
|
98
98
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
99
99
|
.map(([relativePath, absolutePath]) => ({ relativePath, absolutePath }));
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Pure counterpart of `collectFilesForGlobs` for a caller-supplied, already validated list of repository-relative
|
|
103
|
+
* file paths. It applies the same glob matching and the same walk exclusions (excluded directory names below the
|
|
104
|
+
* glob base directory, temp files) and the same ordering, but never touches the filesystem, so the caller decides
|
|
105
|
+
* which files may be opened at all. Unlike the walking variant it does not require the base directory to exist.
|
|
106
|
+
*/
|
|
107
|
+
export function selectRelativePathsForGlobs(relativePaths, globs) {
|
|
108
|
+
const selected = new Set();
|
|
109
|
+
for (const globPattern of globs) {
|
|
110
|
+
if (!globPattern || typeof globPattern !== "string") {
|
|
111
|
+
throw new Error("Invalid glob pattern.");
|
|
112
|
+
}
|
|
113
|
+
const baseDir = baseDirectoryFromGlob(globPattern);
|
|
114
|
+
if (baseDir.split("/").includes("..") || path.isAbsolute(baseDir)) {
|
|
115
|
+
throw new Error(`Resolved path escapes target root: ${globPattern}`);
|
|
116
|
+
}
|
|
117
|
+
// Linear scan for the trailing "/" run (a regular expression here is a polynomial-backtracking risk on
|
|
118
|
+
// glob text that comes from user configuration).
|
|
119
|
+
let baseEnd = baseDir.length;
|
|
120
|
+
while (baseEnd > 0 && baseDir.charCodeAt(baseEnd - 1) === 47)
|
|
121
|
+
baseEnd -= 1;
|
|
122
|
+
const normalizedBase = baseDir === "." ? "" : baseDir.slice(0, baseEnd);
|
|
123
|
+
for (const relativePath of relativePaths) {
|
|
124
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
125
|
+
if (normalizedPath === normalizedBase) {
|
|
126
|
+
// An exact file base is used as-is by the walking variant (no walk exclusions apply).
|
|
127
|
+
if (matchesGlob(normalizedPath, globPattern))
|
|
128
|
+
selected.add(normalizedPath);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (normalizedBase !== "" && !normalizedPath.startsWith(`${normalizedBase}/`))
|
|
132
|
+
continue;
|
|
133
|
+
const belowBase = normalizedBase === "" ? normalizedPath : normalizedPath.slice(normalizedBase.length + 1);
|
|
134
|
+
const directorySegments = belowBase.split("/").slice(0, -1);
|
|
135
|
+
if (directorySegments.some((segment) => excludedDirNames.has(segment)))
|
|
136
|
+
continue;
|
|
137
|
+
if (isTempFile(normalizedPath))
|
|
138
|
+
continue;
|
|
139
|
+
if (matchesGlob(normalizedPath, globPattern))
|
|
140
|
+
selected.add(normalizedPath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return [...selected].sort((a, b) => a.localeCompare(b));
|
|
144
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore, roundToTwo } from "./projectComplexity.js";
|
|
5
|
+
import { hasFactMapping, interpretFactContextTarget } from "./retrievalQuality/factContextTargets.js";
|
|
5
6
|
import { TASK_LOCALITIES } from "./types.js";
|
|
6
7
|
export const REQUIRED_BENCHMARK_PROJECT_IDS = [
|
|
7
8
|
"todo-ts",
|
|
@@ -90,6 +91,18 @@ export function validateAnswerKey(answerKey, label) {
|
|
|
90
91
|
errors.push(`${label}: answerKey.minimumCorrectFacts is not satisfiable.`);
|
|
91
92
|
}
|
|
92
93
|
}
|
|
94
|
+
if (Array.isArray(candidate.expectedContextTargets) && Array.isArray(candidate.expectedFacts)) {
|
|
95
|
+
// Only targets that opt into fact mapping with `factIds` are validated here; legacy targets stay valid as-is.
|
|
96
|
+
const knownFactIds = new Set(candidate.expectedFacts.flatMap((fact) => (fact && typeof fact === "object" && typeof fact.id === "string" && fact.id.length > 0 ? [fact.id] : [])));
|
|
97
|
+
candidate.expectedContextTargets.forEach((target, index) => {
|
|
98
|
+
if (!hasFactMapping(target))
|
|
99
|
+
return;
|
|
100
|
+
const interpretation = interpretFactContextTarget(target, knownFactIds);
|
|
101
|
+
if (!interpretation.ok) {
|
|
102
|
+
errors.push(`${label}: answerKey.expectedContextTargets[${index}]: ${interpretation.problem}.`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
93
106
|
if (Array.isArray(candidate.expectedFiles) && candidate.expectedFiles.length === 0) {
|
|
94
107
|
errors.push(`${label}: answerKey.expectedFiles must not be empty.`);
|
|
95
108
|
}
|