@dailephd/my-dev-kit-lab 0.9.3 → 0.9.5
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 +26 -0
- package/README.md +7 -3
- package/dist/src/evaluation/affectedNeighborhood.js +140 -32
- package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +9 -1
- package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +91 -1
- package/dist/src/evaluation/runMyDevKitRetrieval.js +137 -4
- package/dist/src/evaluation/syntheticRepository/materialize.js +103 -7
- package/dist/src/experiments/plugins/incrementalChangeStaleness/comparison.js +10 -0
- package/dist/src/experiments/plugins/incrementalChangeStaleness/comparisonV2.js +9 -1
- package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +41 -5
- package/dist/src/experiments/plugins/incrementalChangeStaleness/executionV2.js +4 -4
- package/dist/src/report/experiments/buildIncrementalChangeStalenessReportV2.js +4 -0
- package/dist/src/report/experiments/incrementalChangeStalenessReportRowsV2.js +1 -1
- package/dist/src/securityValidation/attackScenarios/boundedSourceScan.js +112 -25
- package/dist/src/securityValidation/attackScenarios/scenarios/networkAssumptionScenario.js +58 -6
- package/dist/src/securityValidation/cliAdversarial/pathBoundaryChecks.js +108 -70
- package/dist/src/securityValidation/cliAdversarial/tempWorkspace.js +57 -8
- package/dist/src/securityValidation/cliAdversarial/traversalContainment.js +161 -0
- package/dist/src/securityValidation/commandRunner.js +52 -26
- package/dist/src/securityValidation/config.js +10 -0
- package/dist/src/securityValidation/packageChecks/parseNpmPackDryRun.js +46 -21
- package/dist/src/securityValidation/validate/runCliSecuritySuiteCheck.js +21 -5
- package/dist/src/securityValidation/validate/runSecurityValidation.js +2 -2
- package/dist/src/tutorial/tutorialAssertions.js +7 -0
- package/docs/METRICS.md +11 -1
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../core/countTokens.js";
|
|
3
3
|
import { runMeasuredCommand } from "../core/runMeasuredCommand.js";
|
|
4
4
|
import { interpretToolVersionOutput } from "./indexSnapshot.js";
|
|
5
|
-
import { buildRetrievalEvidenceFromCommands } from "./retrievalQuality/buildRetrievalEvidence.js";
|
|
5
|
+
import { buildRetrievalEvidenceFromCommands, normalizeRetrievedRepositoryPath } from "./retrievalQuality/buildRetrievalEvidence.js";
|
|
6
6
|
import { buildQueryStrategyEvidenceFromRetrievalEvidence } from "./retrievalQueryStrategyEvidence.js";
|
|
7
7
|
const VERSION_PROBE_TIMEOUT_MS = 30_000;
|
|
8
8
|
function parseJsonIfPossible(text) {
|
|
@@ -253,6 +253,39 @@ function selectRetrievalStrategyContextText(strategyId, outputs) {
|
|
|
253
253
|
: [outputs.source, outputs.slice, outputs.lookup, outputs.search];
|
|
254
254
|
return ordered.find((text) => text && text.trim().length > 0) ?? "";
|
|
255
255
|
}
|
|
256
|
+
export const BOUNDED_SOURCE_PROTOCOL_ID = "bounded-multi-source-v1";
|
|
257
|
+
export const DEFAULT_BOUNDED_SOURCE_OPTIONS = { maxSourceFiles: 8, searchLimit: 40 };
|
|
258
|
+
export function isBoundedRetrievalResult(result) {
|
|
259
|
+
return result.boundedCoverage?.protocol === BOUNDED_SOURCE_PROTOCOL_ID;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Pure, deterministic candidate plan over search results. Only `file`/`symbol` entries (or legacy entries that
|
|
263
|
+
* carry no `kind`) with a node id and a safe repository-relative path are eligible; edges and other kinds are
|
|
264
|
+
* ignored. The first occurrence of each normalized file wins, rank order is preserved, and the plan is cut at
|
|
265
|
+
* `maxSourceFiles`. Never inspects an answer key.
|
|
266
|
+
*/
|
|
267
|
+
export function planBoundedSourceCandidates(results, maxSourceFiles) {
|
|
268
|
+
const budget = Number.isInteger(maxSourceFiles) && maxSourceFiles > 0 ? maxSourceFiles : 0;
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
const eligible = [];
|
|
271
|
+
let rejectedCount = 0;
|
|
272
|
+
for (const entry of results) {
|
|
273
|
+
const kind = entry.kind;
|
|
274
|
+
if (kind !== undefined && kind !== "file" && kind !== "symbol")
|
|
275
|
+
continue;
|
|
276
|
+
const fields = pickCandidateFields(entry);
|
|
277
|
+
const file = normalizeRetrievedRepositoryPath(fields.file);
|
|
278
|
+
if (file === null || fields.nodeId === undefined) {
|
|
279
|
+
rejectedCount += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (seen.has(file))
|
|
283
|
+
continue;
|
|
284
|
+
seen.add(file);
|
|
285
|
+
eligible.push({ file, nodeId: fields.nodeId });
|
|
286
|
+
}
|
|
287
|
+
return { candidates: eligible.slice(0, budget), eligibleFileCount: eligible.length, rejectedCount };
|
|
288
|
+
}
|
|
256
289
|
const STRATEGY_DOWNSTREAM_COMMANDS = {
|
|
257
290
|
"keyword-search": [],
|
|
258
291
|
"symbol-lookup": ["lookup"],
|
|
@@ -275,7 +308,15 @@ export async function runMyDevKitRetrievalStrategyFromIndex(options) {
|
|
|
275
308
|
commandString: options.kitCommand,
|
|
276
309
|
cwd: process.cwd(),
|
|
277
310
|
outDir: commandsDir,
|
|
278
|
-
extraArgs: [
|
|
311
|
+
extraArgs: [
|
|
312
|
+
"search",
|
|
313
|
+
"--index",
|
|
314
|
+
indexDir,
|
|
315
|
+
"--query",
|
|
316
|
+
options.evaluationCase.query,
|
|
317
|
+
...(options.boundedSource ? ["--limit", String(options.boundedSource.searchLimit)] : []),
|
|
318
|
+
"--json"
|
|
319
|
+
]
|
|
279
320
|
});
|
|
280
321
|
commands.push(searchCommand);
|
|
281
322
|
if (!searchCommand.ok) {
|
|
@@ -324,8 +365,28 @@ export async function runMyDevKitRetrievalStrategyFromIndex(options) {
|
|
|
324
365
|
warnings.push("No my-dev-kit node id was available after search.");
|
|
325
366
|
}
|
|
326
367
|
}
|
|
327
|
-
|
|
328
|
-
|
|
368
|
+
let contextText = selectRetrievalStrategyContextText(options.strategyId, outputs);
|
|
369
|
+
let filesRead = selectedFile ? [selectedFile] : [];
|
|
370
|
+
let boundedCoverage;
|
|
371
|
+
if (options.boundedSource && downstream.includes("source")) {
|
|
372
|
+
const bounded = await expandBoundedSourceCandidates({
|
|
373
|
+
bounded: options.boundedSource,
|
|
374
|
+
candidates,
|
|
375
|
+
selectedNodeId,
|
|
376
|
+
selectedSourceOk: commands.some((command) => command.commandId === "source" && command.ok),
|
|
377
|
+
kitCommand: options.kitCommand,
|
|
378
|
+
indexDir,
|
|
379
|
+
commandsDir,
|
|
380
|
+
commands,
|
|
381
|
+
warnings
|
|
382
|
+
});
|
|
383
|
+
boundedCoverage = bounded.coverage;
|
|
384
|
+
// Bounded mode: filesRead names only files whose own `source` command succeeded, in rank order.
|
|
385
|
+
filesRead = bounded.coverage.attempts.filter((attempt) => attempt.sourced).map((attempt) => attempt.file);
|
|
386
|
+
if (bounded.extraSourceTexts.length > 0) {
|
|
387
|
+
contextText = [contextText, ...bounded.extraSourceTexts].filter((text) => text.length > 0).join("\n\n");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
329
390
|
const retrievalEvidence = buildRetrievalEvidenceFromCommands(commands, { nodeId: selectedNodeId, file: selectedFile });
|
|
330
391
|
return {
|
|
331
392
|
caseId: options.evaluationCase.id,
|
|
@@ -342,9 +403,81 @@ export async function runMyDevKitRetrievalStrategyFromIndex(options) {
|
|
|
342
403
|
selectedSymbol,
|
|
343
404
|
retrievalEvidence,
|
|
344
405
|
queryStrategyEvidence: buildQueryStrategyEvidenceFromRetrievalEvidence(options.strategyId, retrievalEvidence),
|
|
406
|
+
...(boundedCoverage ? { boundedCoverage } : {}),
|
|
345
407
|
durationMs: Date.now() - started
|
|
346
408
|
};
|
|
347
409
|
}
|
|
410
|
+
async function expandBoundedSourceCandidates(args) {
|
|
411
|
+
const plan = planBoundedSourceCandidates(args.candidates, args.bounded.maxSourceFiles);
|
|
412
|
+
const attempts = [];
|
|
413
|
+
const extraSourceTexts = [];
|
|
414
|
+
for (const [index, candidate] of plan.candidates.entries()) {
|
|
415
|
+
if (candidate.nodeId === args.selectedNodeId) {
|
|
416
|
+
// The first search candidate already received its `source` command in the historical lifecycle.
|
|
417
|
+
attempts.push({ ...candidate, sourced: args.selectedSourceOk });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const command = await runMeasuredCommand({
|
|
421
|
+
commandId: `source-candidate-${index + 1}`,
|
|
422
|
+
commandString: args.kitCommand,
|
|
423
|
+
cwd: process.cwd(),
|
|
424
|
+
outDir: args.commandsDir,
|
|
425
|
+
extraArgs: ["source", "--index", args.indexDir, "--node", candidate.nodeId, "--max-lines", "160", "--format", "numbered"]
|
|
426
|
+
});
|
|
427
|
+
args.commands.push(command);
|
|
428
|
+
if (!command.ok) {
|
|
429
|
+
args.warnings.push(`my-dev-kit source command failed for bounded candidate ${index + 1}.`);
|
|
430
|
+
}
|
|
431
|
+
else if (command.stdout.length > 0) {
|
|
432
|
+
extraSourceTexts.push(command.stdout);
|
|
433
|
+
}
|
|
434
|
+
attempts.push({ ...candidate, sourced: command.ok });
|
|
435
|
+
}
|
|
436
|
+
const resultCount = args.candidates.length;
|
|
437
|
+
return {
|
|
438
|
+
coverage: {
|
|
439
|
+
protocol: BOUNDED_SOURCE_PROTOCOL_ID,
|
|
440
|
+
maxSourceFiles: args.bounded.maxSourceFiles,
|
|
441
|
+
searchLimit: args.bounded.searchLimit,
|
|
442
|
+
searchResultCount: resultCount,
|
|
443
|
+
searchTruncated: resultCount >= args.bounded.searchLimit,
|
|
444
|
+
eligibleCandidateFileCount: plan.eligibleFileCount,
|
|
445
|
+
candidateBudgetExhausted: plan.eligibleFileCount > plan.candidates.length,
|
|
446
|
+
rejectedCandidateCount: plan.rejectedCount,
|
|
447
|
+
attempts
|
|
448
|
+
},
|
|
449
|
+
extraSourceTexts
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Opt-in bounded multi-source retrieval for evaluations that must observe more than one file (for example
|
|
454
|
+
* multi-file required-file evidence). Reuses the shared strategy executor; default top-one retrieval and the
|
|
455
|
+
* meaning of its `filesRead` are unchanged for every other caller. The result always carries
|
|
456
|
+
* `boundedCoverage`, including when the search yielded no usable candidate.
|
|
457
|
+
*/
|
|
458
|
+
export async function runMyDevKitBoundedRetrievalFromIndex(options) {
|
|
459
|
+
const boundedSource = options.boundedSource ?? DEFAULT_BOUNDED_SOURCE_OPTIONS;
|
|
460
|
+
const result = await runMyDevKitRetrievalStrategyFromIndex({ ...options, strategyId: "combined-graph-guided", boundedSource });
|
|
461
|
+
if (isBoundedRetrievalResult(result))
|
|
462
|
+
return result;
|
|
463
|
+
// Skipped before expansion (search failed or yielded no candidate): still declare the protocol, nothing sourced.
|
|
464
|
+
const skipped = {
|
|
465
|
+
...result,
|
|
466
|
+
filesRead: [],
|
|
467
|
+
boundedCoverage: {
|
|
468
|
+
protocol: BOUNDED_SOURCE_PROTOCOL_ID,
|
|
469
|
+
maxSourceFiles: boundedSource.maxSourceFiles,
|
|
470
|
+
searchLimit: boundedSource.searchLimit,
|
|
471
|
+
searchResultCount: 0,
|
|
472
|
+
searchTruncated: false,
|
|
473
|
+
eligibleCandidateFileCount: 0,
|
|
474
|
+
candidateBudgetExhausted: false,
|
|
475
|
+
rejectedCandidateCount: 0,
|
|
476
|
+
attempts: []
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
return skipped;
|
|
480
|
+
}
|
|
348
481
|
/**
|
|
349
482
|
* v0.8.0 compatibility entrypoint: the historical search -> lookup -> slice -> source lifecycle,
|
|
350
483
|
* delegating to the `combined-graph-guided` strategy. Never invokes `index`.
|
|
@@ -20,6 +20,19 @@ export class SyntheticRepositoryMaterializationError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
const STAGING_PREFIX = ".synthetic-staging-";
|
|
23
|
+
/** Total attempts at the publication rename (initial try plus two retries); Windows EPERM only. */
|
|
24
|
+
const PUBLISH_RENAME_MAX_ATTEMPTS = 3;
|
|
25
|
+
/** Fixed waits before the second and third attempts (100 ms maximum for one materialization). */
|
|
26
|
+
const PUBLISH_RENAME_RETRY_DELAYS_MS = [25, 75];
|
|
27
|
+
/** Raised when the owned staging directory can no longer be proven to be ours; cleanup must not touch it. */
|
|
28
|
+
class UnownedStagingError extends SyntheticRepositoryMaterializationError {
|
|
29
|
+
}
|
|
30
|
+
function sleepSync(milliseconds) {
|
|
31
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
32
|
+
}
|
|
33
|
+
function isTransientWindowsRenameError(error, platform) {
|
|
34
|
+
return platform === "win32" && typeof error === "object" && error !== null && error.code === "EPERM";
|
|
35
|
+
}
|
|
23
36
|
function withinRealRoot(realRoot, candidate) {
|
|
24
37
|
try {
|
|
25
38
|
relativeWithinRoot(realRoot, realpathSync(candidate));
|
|
@@ -63,12 +76,64 @@ function describeExisting(caseDirectory, expectedManifestText) {
|
|
|
63
76
|
return { reusable: false, reasons: verification.issues };
|
|
64
77
|
return { reusable: true };
|
|
65
78
|
}
|
|
79
|
+
/** Proves, before a rename retry, that the owned staging directory is still ours and the destination is still free. */
|
|
80
|
+
function assertStagingStillPublishable(realRoot, staging, caseDirectory, caseId) {
|
|
81
|
+
let rootStillVerified = false;
|
|
82
|
+
try {
|
|
83
|
+
rootStillVerified = realpathSync(realRoot) === realRoot;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
rootStillVerified = false;
|
|
87
|
+
}
|
|
88
|
+
if (!rootStillVerified) {
|
|
89
|
+
throw new UnownedStagingError("path-escape", ["output root no longer resolves to the verified physical root; the staging rename was not retried."]);
|
|
90
|
+
}
|
|
91
|
+
let stats;
|
|
92
|
+
try {
|
|
93
|
+
stats = lstatSync(staging);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new UnownedStagingError("io-failure", ["owned staging directory disappeared before the staging rename retry; nothing was published."]);
|
|
97
|
+
}
|
|
98
|
+
if (stats.isSymbolicLink() || !stats.isDirectory() || path.dirname(staging) !== realRoot || !withinRealRoot(realRoot, staging)) {
|
|
99
|
+
throw new UnownedStagingError("path-escape", ["owned staging directory is no longer a plain directory inside the output root; the staging rename was not retried."]);
|
|
100
|
+
}
|
|
101
|
+
if (exists(caseDirectory)) {
|
|
102
|
+
throw new SyntheticRepositoryMaterializationError("collision", [`output child ${caseId} appeared during materialization; nothing was overwritten.`]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The single atomic publication of a verified staging directory. On Windows an externally induced, brief EPERM from this
|
|
107
|
+
* one rename is retried up to the fixed budget (3 attempts, 25 ms then 75 ms apart) after the ownership and destination
|
|
108
|
+
* guards are re-proven; every other error, and every non-Windows platform, gets exactly one attempt. Returns the number
|
|
109
|
+
* of attempts used.
|
|
110
|
+
*/
|
|
111
|
+
function publishStagingDirectory(options) {
|
|
112
|
+
for (let attempt = 1;; attempt += 1) {
|
|
113
|
+
try {
|
|
114
|
+
options.renameDirectory(options.staging, options.caseDirectory);
|
|
115
|
+
return attempt;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (!isTransientWindowsRenameError(error, options.platform))
|
|
119
|
+
throw error;
|
|
120
|
+
if (attempt >= PUBLISH_RENAME_MAX_ATTEMPTS) {
|
|
121
|
+
throw new SyntheticRepositoryMaterializationError("io-failure", [
|
|
122
|
+
`final staging directory rename failed with EPERM after ${attempt} attempts (retry budget exhausted); nothing was copied, overwritten or published.`,
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
options.sleep(PUBLISH_RENAME_RETRY_DELAYS_MS[attempt - 1]);
|
|
126
|
+
assertStagingStillPublishable(options.realRoot, options.staging, options.caseDirectory, options.caseId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
66
130
|
/**
|
|
67
131
|
* Materializes a frozen Batch 1 plan below `outputRoot/<caseId>/` as `repository/` plus a sibling
|
|
68
132
|
* `synthetic-repository-manifest.json`. The plan is the single semantic source of truth: it is verified, never
|
|
69
133
|
* re-planned or repaired. A deterministic existing directory is reused only when its manifest and every file
|
|
70
134
|
* match the requested generation exactly; any other existing child is a collision. Writes go to an owned staging
|
|
71
|
-
* directory that is verified and then renamed into place
|
|
135
|
+
* directory that is verified and then renamed into place (a brief, externally induced Windows EPERM at that single
|
|
136
|
+
* rename is retried within a fixed budget, see `publishStagingDirectory`); on failure only that staging directory is removed.
|
|
72
137
|
*/
|
|
73
138
|
export function materializeSyntheticRepository(plan, outputRoot, io = {}) {
|
|
74
139
|
try {
|
|
@@ -156,15 +221,46 @@ export function materializeSyntheticRepository(plan, outputRoot, io = {}) {
|
|
|
156
221
|
if (exists(caseDirectory)) {
|
|
157
222
|
throw new SyntheticRepositoryMaterializationError("collision", [`output child ${plan.caseId} appeared during materialization; nothing was overwritten.`]);
|
|
158
223
|
}
|
|
159
|
-
|
|
224
|
+
const attempts = publishStagingDirectory({
|
|
225
|
+
staging,
|
|
226
|
+
caseDirectory,
|
|
227
|
+
caseId: plan.caseId,
|
|
228
|
+
realRoot,
|
|
229
|
+
renameDirectory: io.renameDirectory ?? renameSync,
|
|
230
|
+
sleep: io.sleep ?? sleepSync,
|
|
231
|
+
platform: io.platform ?? process.platform,
|
|
232
|
+
});
|
|
160
233
|
staging = undefined;
|
|
234
|
+
if (attempts > 1) {
|
|
235
|
+
// The staged tree was verified before the rename; after a retried publication the destination is re-verified too.
|
|
236
|
+
// It now holds this invocation's output, so a failure here is reported and the destination is left in place.
|
|
237
|
+
const publishedManifest = path.join(caseDirectory, SYNTHETIC_REPOSITORY_MANIFEST_FILE_NAME);
|
|
238
|
+
const published = verifySyntheticRepositoryMaterialization({
|
|
239
|
+
manifestPath: publishedManifest,
|
|
240
|
+
repositoryRoot: path.join(caseDirectory, SYNTHETIC_REPOSITORY_REPOSITORY_DIRECTORY),
|
|
241
|
+
});
|
|
242
|
+
if (!published.ok || readFileSync(publishedManifest, "utf8") !== manifestText) {
|
|
243
|
+
throw new SyntheticRepositoryMaterializationError("io-failure", [
|
|
244
|
+
`published output for ${plan.caseId} failed verification after the staging rename succeeded on attempt ${attempts}; the destination was left in place for inspection.`,
|
|
245
|
+
...published.issues,
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
161
249
|
}
|
|
162
250
|
catch (error) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
251
|
+
let cleanupFailure;
|
|
252
|
+
if (staging !== undefined && !(error instanceof UnownedStagingError)) {
|
|
253
|
+
try {
|
|
254
|
+
rmSync(staging, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
catch (cleanupError) {
|
|
257
|
+
cleanupFailure = `cleanup of the owned staging directory failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const reported = error instanceof SyntheticRepositoryMaterializationError ? error : new SyntheticRepositoryMaterializationError("io-failure", [error instanceof Error ? error.message : String(error)]);
|
|
261
|
+
if (cleanupFailure === undefined)
|
|
262
|
+
throw reported;
|
|
263
|
+
throw new SyntheticRepositoryMaterializationError(reported.code, [...reported.errors, cleanupFailure]);
|
|
168
264
|
}
|
|
169
265
|
return result(false);
|
|
170
266
|
}
|
|
@@ -71,6 +71,16 @@ export function compareRequiredFileEvidence(stale, fullRefresh) {
|
|
|
71
71
|
return "stale-better";
|
|
72
72
|
return "same";
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Reference-adequacy gate over the section 30 rules. Equal evidence only supports "same" when the full-refresh
|
|
76
|
+
* reference itself observed every required file: two treatments that both lack the required files are equally
|
|
77
|
+
* insufficient, which is not evidence that the candidate did not regress. That case is `unknown`. A candidate
|
|
78
|
+
* that observed files the insufficient reference did not is still reported as better; nothing else changes.
|
|
79
|
+
*/
|
|
80
|
+
export function compareRequiredFileEvidenceAgainstReference(candidate, reference) {
|
|
81
|
+
const relation = compareRequiredFileEvidence(candidate, reference);
|
|
82
|
+
return relation === "same" && reference.status !== "present" ? "unknown" : relation;
|
|
83
|
+
}
|
|
74
84
|
/**
|
|
75
85
|
* Sections 31-35: the frozen precedence algorithm. Correctness and required-file evidence are the
|
|
76
86
|
* only two inputs; affected-neighborhood evidence and reindexRecommendation are never inputs here.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compareCorrectness, compareRequiredFileEvidence } from "./comparison.js";
|
|
1
|
+
import { compareCorrectness, compareRequiredFileEvidence, compareRequiredFileEvidenceAgainstReference } from "./comparison.js";
|
|
2
2
|
function neutral(relation) {
|
|
3
3
|
if (relation === "stale-worse")
|
|
4
4
|
return "candidate-worse";
|
|
@@ -14,6 +14,14 @@ export function compareCandidateCorrectness(candidate, reference) {
|
|
|
14
14
|
export function compareCandidateRequiredFileEvidence(candidate, reference) {
|
|
15
15
|
return neutral(compareRequiredFileEvidence(candidate, reference));
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Candidate-vs-reference required-file relation with the reference-adequacy gate: equal weakness (both
|
|
19
|
+
* missing) is `unknown`, not `same`, because an insufficient full-refresh reference cannot support a
|
|
20
|
+
* no-regression statement. Every other outcome follows `compareCandidateRequiredFileEvidence`.
|
|
21
|
+
*/
|
|
22
|
+
export function compareCandidateRequiredFileEvidenceAgainstReference(candidate, reference) {
|
|
23
|
+
return neutral(compareRequiredFileEvidenceAgainstReference(candidate, reference));
|
|
24
|
+
}
|
|
17
25
|
/**
|
|
18
26
|
* Frozen precedence. Rule 0 (realization gate) first: a full fallback is never evidence about
|
|
19
27
|
* partial-refresh performance. Then observed regression (either dimension worse), then inconclusive
|
|
@@ -5,10 +5,10 @@ import { assessAffectedNeighborhood, mapAffectedNeighborhoodSeeds } from "../../
|
|
|
5
5
|
import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
|
|
6
6
|
import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
|
|
7
7
|
import { readEvaluationCases } from "../../../evaluation/readEvaluationCases.js";
|
|
8
|
-
import {
|
|
8
|
+
import { isBoundedRetrievalResult, runMyDevKitBoundedRetrievalFromIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
|
|
9
9
|
import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
|
|
10
10
|
import { generatePromptVariants } from "../../../prompts/index.js";
|
|
11
|
-
import { buildRequiredFileEvidence, classifyStaleRisk, compareCorrectness,
|
|
11
|
+
import { buildRequiredFileEvidence, classifyStaleRisk, compareCorrectness, compareRequiredFileEvidenceAgainstReference } from "./comparison.js";
|
|
12
12
|
import { WARM_INDEX_BENCHMARK_CASES_PATH } from "./scenarioCatalog.js";
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// v0.6.2 Batch 4 -- treatment retrieval, correctness, and required-file
|
|
@@ -134,7 +134,9 @@ export function classifyIncrementalChangeStalenessRetrieval(result) {
|
|
|
134
134
|
return result.commands.some((command) => !command.ok) ? "failed" : "skipped";
|
|
135
135
|
}
|
|
136
136
|
export async function runIncrementalChangeStalenessTreatmentRetrieval(args) {
|
|
137
|
-
|
|
137
|
+
// Opt-in bounded multi-source protocol: the staleness experiment must observe multi-file requirements,
|
|
138
|
+
// which the default first-candidate-only retrieval cannot. Candidates derive from the search output only.
|
|
139
|
+
return runMyDevKitBoundedRetrievalFromIndex({
|
|
138
140
|
evaluationCase: args.derivedTask,
|
|
139
141
|
kitCommand: args.kitCommand,
|
|
140
142
|
indexDir: args.indexDir,
|
|
@@ -142,10 +144,41 @@ export async function runIncrementalChangeStalenessTreatmentRetrieval(args) {
|
|
|
142
144
|
requireKit: false
|
|
143
145
|
});
|
|
144
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Required-file evidence under the bounded multi-source protocol. "Observed" means a successful `source`
|
|
149
|
+
* command for that file, never a search match. Fails closed: a protocol that cannot source as many files as
|
|
150
|
+
* are required, a failed source command, or rejected candidate identities leave unsourced required files
|
|
151
|
+
* `unknown` instead of `missing`. A `missing` result is relative to the fixed candidate budget and says so.
|
|
152
|
+
*/
|
|
153
|
+
function buildBoundedRequiredFileEvidence(args) {
|
|
154
|
+
const coverage = args.retrieval.boundedCoverage;
|
|
155
|
+
const observedFiles = coverage.attempts.filter((attempt) => attempt.sourced).map((attempt) => attempt.file);
|
|
156
|
+
const unknown = (unknownReason) => buildRequiredFileEvidence({ requiredFiles: args.requiredFiles, observedFiles, observedFilesComplete: false, unknownReason });
|
|
157
|
+
const required = buildRequiredFileEvidence({ requiredFiles: args.requiredFiles, observedFiles, observedFilesComplete: true });
|
|
158
|
+
if (required.requiredFiles.length > coverage.maxSourceFiles) {
|
|
159
|
+
return unknown(`Coverage-limited: the bounded retrieval protocol sources at most ${coverage.maxSourceFiles} candidate file(s) but ${required.requiredFiles.length} are required.`);
|
|
160
|
+
}
|
|
161
|
+
if (required.status === "present")
|
|
162
|
+
return required;
|
|
163
|
+
const failedAttempts = coverage.attempts.filter((attempt) => !attempt.sourced).length;
|
|
164
|
+
if (failedAttempts > 0) {
|
|
165
|
+
return unknown(`${failedAttempts} bounded source command(s) failed; unsourced required files cannot be classified as absent.`);
|
|
166
|
+
}
|
|
167
|
+
if (coverage.rejectedCandidateCount > 0) {
|
|
168
|
+
return unknown(`${coverage.rejectedCandidateCount} search candidate(s) were rejected (unsafe file identity or no node id); unsourced required files cannot be classified as absent.`);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
...required,
|
|
172
|
+
reason: `Required file(s) were not sourced within the bounded retrieval protocol (${observedFiles.length} of up to ${coverage.maxSourceFiles} candidate file(s) sourced; ` +
|
|
173
|
+
`${coverage.eligibleCandidateFileCount} eligible; search truncated: ${coverage.searchTruncated ? "yes" : "no"}; candidate budget exhausted: ${coverage.candidateBudgetExhausted ? "yes" : "no"}). ` +
|
|
174
|
+
"This is not an exhaustive source-set assessment."
|
|
175
|
+
};
|
|
176
|
+
}
|
|
145
177
|
/**
|
|
146
178
|
* Section 27: a `failed` retrieval status means the actual-files-read set cannot be trusted as
|
|
147
179
|
* complete (unknown); `completed` and `skipped` (search ran and legitimately found no candidate)
|
|
148
|
-
* both provide a complete, trustworthy files-read set, possibly empty.
|
|
180
|
+
* both provide a complete, trustworthy files-read set, possibly empty. A result produced by the opt-in
|
|
181
|
+
* bounded protocol is evaluated against its own declared coverage instead.
|
|
149
182
|
*/
|
|
150
183
|
export function buildIncrementalChangeStalenessRequiredFileEvidence(args) {
|
|
151
184
|
if (!args.retrieval) {
|
|
@@ -157,6 +190,9 @@ export function buildIncrementalChangeStalenessRequiredFileEvidence(args) {
|
|
|
157
190
|
});
|
|
158
191
|
}
|
|
159
192
|
const status = classifyIncrementalChangeStalenessRetrieval(args.retrieval);
|
|
193
|
+
if (status !== "failed" && isBoundedRetrievalResult(args.retrieval)) {
|
|
194
|
+
return buildBoundedRequiredFileEvidence({ requiredFiles: args.requiredFiles, retrieval: args.retrieval });
|
|
195
|
+
}
|
|
160
196
|
return buildRequiredFileEvidence({
|
|
161
197
|
requiredFiles: args.requiredFiles,
|
|
162
198
|
observedFiles: args.retrieval.filesRead,
|
|
@@ -412,7 +448,7 @@ export async function executeIncrementalChangeStalenessScenario(options) {
|
|
|
412
448
|
deps
|
|
413
449
|
});
|
|
414
450
|
const correctnessRelation = compareCorrectness(correctnessComparable(stale.fakeAgent), correctnessComparable(fullRefresh.fakeAgent));
|
|
415
|
-
const requiredFileEvidenceRelation =
|
|
451
|
+
const requiredFileEvidenceRelation = compareRequiredFileEvidenceAgainstReference(stale.requiredFileEvidence, fullRefresh.requiredFileEvidence);
|
|
416
452
|
const classification = classifyStaleRisk(correctnessRelation, requiredFileEvidenceRelation);
|
|
417
453
|
return {
|
|
418
454
|
...scenarioBase,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
3
|
-
import { classifyStaleRisk, compareCorrectness,
|
|
4
|
-
import { compareCandidateCorrectness,
|
|
3
|
+
import { classifyStaleRisk, compareCorrectness, compareRequiredFileEvidenceAgainstReference } from "./comparison.js";
|
|
4
|
+
import { compareCandidateCorrectness, compareCandidateRequiredFileEvidenceAgainstReference, classifyPartialRefreshAgainstFull } from "./comparisonV2.js";
|
|
5
5
|
import { INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS } from "./disposableTarget.js";
|
|
6
6
|
import { assessIncrementalChangeStalenessAffectedNeighborhood, checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry, correctnessComparable, defaultIncrementalChangeStalenessExecutionDeps, executeTreatmentEvaluation, INCONCLUSIVE_NO_EXECUTION, resolveIncrementalChangeStalenessScenarioTask } from "./execution.js";
|
|
7
7
|
/**
|
|
@@ -153,7 +153,7 @@ export function buildReferenceComparisons(treatments) {
|
|
|
153
153
|
throw new Error("Reference comparisons require the stale-index and full-refresh treatment executions.");
|
|
154
154
|
// Historical v0.6.2 semantics, unchanged.
|
|
155
155
|
const staleCorrectness = compareCorrectness(correctnessComparable(stale.fakeAgent), correctnessComparable(reference.fakeAgent));
|
|
156
|
-
const staleRequired =
|
|
156
|
+
const staleRequired = compareRequiredFileEvidenceAgainstReference(stale.requiredFileEvidence, reference.requiredFileEvidence);
|
|
157
157
|
const staleRisk = classifyStaleRisk(staleCorrectness, staleRequired);
|
|
158
158
|
const comparisons = [
|
|
159
159
|
{
|
|
@@ -174,7 +174,7 @@ export function buildReferenceComparisons(treatments) {
|
|
|
174
174
|
throw new Error(`${candidateTreatmentId} has no incremental refresh execution to compare.`);
|
|
175
175
|
}
|
|
176
176
|
const correctnessRelation = compareCandidateCorrectness(correctnessComparable(candidate.fakeAgent), correctnessComparable(reference.fakeAgent));
|
|
177
|
-
const requiredFileEvidenceRelation =
|
|
177
|
+
const requiredFileEvidenceRelation = compareCandidateRequiredFileEvidenceAgainstReference(candidate.requiredFileEvidence, reference.requiredFileEvidence);
|
|
178
178
|
const classified = classifyPartialRefreshAgainstFull({
|
|
179
179
|
realization: candidate.refreshExecution.realization,
|
|
180
180
|
correctnessRelation,
|
|
@@ -14,6 +14,10 @@ export const LIMITATIONS_V2 = [
|
|
|
14
14
|
"\"no-observed-regression-relative-to-full\" means only that the executed scenario did not show a regression under the frozen correctness and required-file evidence dimensions. It does not prove general equivalence or safety, and does not show that full refresh is unnecessary.",
|
|
15
15
|
"Required-file evidence is a bounded answer-key file-presence check, not retrieval precision/recall.",
|
|
16
16
|
"Correctness remains deterministic fake-agent/harness evidence in v0.6.3.",
|
|
17
|
+
"Fake-agent correctness is a synthetic score computed from the answer key (the fake agent deliberately reports only two expected facts, so scenarios requiring three or more facts cannot pass). It is not measured my-dev-kit answer correctness and does not discriminate between treatments; a failed synthetic flag is not a my-dev-kit regression and a passing one would not be evidence of Kit correctness.",
|
|
18
|
+
"Files Read and Observed Files count a file only when the bounded retrieval protocol ran a successful source command for it; a file merely surfaced by search, lookup or slice was not read. Artifacts recorded before v0.9.5 name only the single selected search candidate.",
|
|
19
|
+
"A Missing required-file result is relative to the fixed bounded candidate budget and is not an exhaustive source-set assessment; evidence the protocol could not observe (coverage limits, failed source commands, rejected candidates) is reported as unknown. Equal required-file evidence supports a same relation only when the full-refresh reference itself observed every required file; two equally insufficient treatments are reported as unknown/inconclusive.",
|
|
20
|
+
"Command log paths in persisted retrieval telemetry name run-owned files that the mandatory run cleanup removes after the artifact is written; they are not retained and must not be relied on as links.",
|
|
17
21
|
"Lab affected-neighborhood evidence (changed baseline neighborhood relative to the task) is distinct from upstream refresh execution evidence (what my-dev-kit actually refreshed).",
|
|
18
22
|
"\"reindexRecommendation\" remains observational evidence; it did not select or trigger any treatment.",
|
|
19
23
|
"\"changedSymbolCount\" does not mean source-level symbol-diff proof; it retains the released v0.6.1 baseline-symbol-identity definition.",
|
|
@@ -27,7 +27,7 @@ export function treatmentBlock(scenario, treatmentId, label) {
|
|
|
27
27
|
["Estimated context tokens", retrieval.status === "not-run" ? "unavailable" : String(retrieval.totalEstimatedTokens)],
|
|
28
28
|
["Retrieval duration (ms)", retrieval.status === "not-run" ? "unavailable" : String(retrieval.durationMs)],
|
|
29
29
|
["Fake-agent status", display(treatment.fakeAgent?.status, "not run")],
|
|
30
|
-
["
|
|
30
|
+
["Synthetic fake-agent correctness score", correctness && correctness.available && correctness.score !== null ? String(correctness.score) : "unavailable"],
|
|
31
31
|
["Required-file evidence", treatment.requiredFileEvidence.status],
|
|
32
32
|
["Required-file reason", display(treatment.requiredFileEvidence.reason, "not applicable")],
|
|
33
33
|
["Affected-neighborhood relationship", treatment.affectedNeighborhood.relationship],
|