@dailephd/my-dev-kit-lab 0.9.4 → 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 +15 -0
- package/README.md +4 -2
- 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/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 +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to my-dev-kit-lab are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.9.5] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Staleness experiment evidence correction
|
|
8
|
+
|
|
9
|
+
- The `incremental-change-staleness` experiment now retrieves through an opt-in bounded multi-source protocol in the shared retrieval owner (`runMyDevKitBoundedRetrievalFromIndex`) so multi-file required evidence can be observed; default top-one retrieval and the meaning of its `filesRead` are unchanged for every other caller. Only files whose own `source` command succeeded count as read.
|
|
10
|
+
- Required-file evidence reports `unknown` instead of `missing` when the bounded protocol cannot observe the requirement, and a full-refresh reference that did not itself observe the required files can no longer make equal results look like `same`/no observed regression: the relation is `unknown` and the classification `inconclusive`.
|
|
11
|
+
- The V2 report labels the fake-agent score as synthetic, distinguishes read from merely surfaced files, and states that runtime command-log paths are not retained after cleanup. Persisted V1/V2 schemas, treatment ordering, scenarios, answer keys and the fake agent and scorer are unchanged.
|
|
12
|
+
- The validation harness now gives disposable provider shims an explicit CommonJS package boundary; pins benchmark Git operations to their own initialized repository and rejects ancestor discovery and dangerous per-call redirects; and contains adversarial `--out`/`--index` traversal tests inside disposable enclosures before launching the tested CLI.
|
|
13
|
+
- Tutorial and non-Git fixtures now establish their package and repository boundaries explicitly. On Windows, synthetic repository publication retries only transient `EPERM` rename failures, with a finite three-attempt limit; if staging cleanup also fails, its diagnostic is appended while the primary materialization error remains the reported error.
|
|
14
|
+
- Packed-consumer staleness acceptance reuses the installed producer's reference-aware comparisons for all six scenarios and all three comparisons per scenario, including ordered reason codes; it retains the production artifact validator and does not change the scientific standard.
|
|
15
|
+
- Replaced drive-root adversarial test probes with enclosed safe/unsafe witnesses. Network scanning recognizes two narrowly reviewed local HTTP sites only with strict source identities (CRLF-to-LF normalization only), expected fetch counts, caller evidence, and physical provenance; unreviewed calls and reportable provenance refusals remain major findings.
|
|
16
|
+
- Corrected shared bounded source collection so ordinary files named like generated directories remain eligible, while actual generated directories stay excluded and case aliases cannot inherit reviewed trust.
|
|
17
|
+
- Security command timeouts now use bounded process-tree termination; required nested security-suite failures retain actual timeout, exit, signal, start-error and cleanup evidence without weakening major-finding severity.
|
|
18
|
+
- No Lab CLI flags, experiment schemas, or dependencies were added. Default retrieval behavior outside this experiment remains unchanged; persisted V1/V2 schemas, treatment ordering, scenarios, answer keys, fake agent, and scorer remain unchanged.
|
|
19
|
+
|
|
5
20
|
## [0.9.4] - 2026-10-23
|
|
6
21
|
|
|
7
22
|
### Kit schema-3 and npm 12 compatibility
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The v0.9.3 release added hardened prompt contracts and output validation, extens
|
|
|
10
10
|
|
|
11
11
|
v0.9.0 adds the `agent-success-rate` plugin, the eighth registered experiment plugin, which evaluates implementation patches under the `raw-full-file` and `context-pack` treatments.
|
|
12
12
|
|
|
13
|
-
v0.9.
|
|
13
|
+
v0.9.4 is the previous package release.
|
|
14
14
|
|
|
15
15
|
The v0.9.2 release corrected external npm package entrypoint validation, preserved strict Lab self-package validation, eliminated nine inherited false positives, corrected Semgrep failed/incomplete-scan evidence, and included campaign filesystem identity, path-safety, and cross-platform corrections. Security verdict strictness is unchanged. The v0.9.2 release-document state is final; actual npm publication is established separately through registry evidence.
|
|
16
16
|
|
|
@@ -18,11 +18,13 @@ The v0.9.3 release adds configurable Codex/Claude prompt contracts, strict struc
|
|
|
18
18
|
|
|
19
19
|
The v0.9.4 release supports my-dev-kit symbol-index schema 3 alongside schema 2, preserves qualified symbol identities, and handles ambiguous names conservatively. Package inventory validation supports npm 11 and npm 12 formats, and the packed-package verifier reports early failures reliably. Development-only Vite and Vitest dependencies are patched; security-verdict policy is unchanged. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md), [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), [docs/COMMANDS.md](docs/COMMANDS.md), and [docs/WORKFLOWS.md](docs/WORKFLOWS.md).
|
|
20
20
|
|
|
21
|
+
The v0.9.5 release corrects incremental-change-staleness evidence interpretation and strengthens disposable validation boundaries. Its bounded multi-source evidence path is opt-in; default top-one retrieval is unchanged. Insufficient evidence remains unknown or inconclusive, and synthetic fake-agent scores stay distinct from observed retrieval. No Lab CLI flags or experiment schemas were added.
|
|
22
|
+
|
|
21
23
|
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.
|
|
22
24
|
|
|
23
25
|
## Current capabilities
|
|
24
26
|
|
|
25
|
-
Package metadata is `0.9.
|
|
27
|
+
Package metadata is `0.9.5`. The v0.9.5 release corrects bounded evidence collection and uncertainty handling in the incremental-change-staleness experiment, preserves default retrieval behavior, and strengthens disposable test and validation boundaries. It adds no command, flag, experiment schema, or dependency. The v0.9.4 release supports my-dev-kit symbol-index schema 3 alongside schema 2, preserves qualified identities, handles ambiguity conservatively, supports npm 11 and npm 12 package inventories, corrects packed-package verifier behavior, and patches development-only security dependencies. Security-verdict policy is unchanged. The v0.9.3 release added configurable Codex/Claude prompt contracts, strict structured-diff output handling, generic report sections, HTML metric evidence and caveats, source-based gallery items including tutorial manifests, and prompt-template comparison. Registered metrics have evidence-based glossary definitions and links, and prompt comparison validates persisted identity and checkpoint evidence. Real-provider improvement remains NOT_VERIFIED because no paid smoke was authorized; its measurement is a separate opt-in empirical objective, not a v0.9.3 release gate. Bounded tool-use remains prompt guidance, not provider enforcement. See [Current State](docs/CURRENT_STATE.md) and [the roadmap](docs/ROADMAP.md).
|
|
26
28
|
|
|
27
29
|
- **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
|
|
28
30
|
- **Measure warm-index reuse (v0.5.0–v0.5.1, released):** the `warm-index-reuse` experiment plugin builds one my-dev-kit index per benchmark project, reuses it across that project's tasks, and compares every task with a matched `raw-full-file` baseline. Reports separate one-time index-build cost from per-task retrieval cost, show amortized index cost and cumulative measurements, and include deterministic fake-agent correctness and token evidence; `plots generate` renders four warm-index SVG charts. The v0.5.1 release adds a dedicated 12-task benchmark corpus (`benchmarks/contracts/warm-index-benchmark-cases.json`: six medium and six large/mixed tasks tagged `localized`, `cross-module`, or `broad-change`) for comparing warm-index behavior as task count grows. Estimated context tokens (a character-based context-size estimate) and fake-agent total tokens (simulated harness telemetry) are reported separately, and neither is provider billing telemetry.
|
|
@@ -3,7 +3,7 @@ import { copyFile, lstat, mkdir, realpath, rm, stat, writeFile } from "node:fs/p
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { resolveWithinRoot } from "../../core/pathSafety.js";
|
|
5
5
|
import { BenchmarkSandboxError } from "./errors.js";
|
|
6
|
-
import { excerptGitFailure, runSandboxGit } from "./gitExecutor.js";
|
|
6
|
+
import { excerptGitFailure, runSandboxGit, verifySandboxRepositoryBinding } from "./gitExecutor.js";
|
|
7
7
|
import { BENCHMARK_SANDBOX_EXCLUDED_NAMES, isSameOrInside, resolvePhysicalPath, SANDBOX_ID_PATTERN } from "./pathPolicy.js";
|
|
8
8
|
import { digestTreeSnapshot, scanProjectTree, snapshotProjectTree } from "./treeSnapshot.js";
|
|
9
9
|
import { BENCHMARK_SANDBOX_BASELINE_BRANCH, BENCHMARK_SANDBOX_OWNER_MARKER_FILE, BENCHMARK_SANDBOX_SCHEMA_VERSION } from "./types.js";
|
|
@@ -93,6 +93,14 @@ export async function createBenchmarkSandbox(options) {
|
|
|
93
93
|
}
|
|
94
94
|
const git = { projectRoot, evidenceRoot };
|
|
95
95
|
await mustGit(git, ["init", "-q", `--initial-branch=${BENCHMARK_SANDBOX_BASELINE_BRANCH}`], "init");
|
|
96
|
+
// A zero exit from init is not proof that later commands reach this repository: on some hosts Git discovery
|
|
97
|
+
// skips the new .git and selects an ancestor. Nothing may be configured, staged or committed before this passes.
|
|
98
|
+
const binding = await verifySandboxRepositoryBinding(git);
|
|
99
|
+
if (!binding.ok) {
|
|
100
|
+
throw binding.unavailable
|
|
101
|
+
? new BenchmarkSandboxError("GIT_UNAVAILABLE", "the git executable is not available.")
|
|
102
|
+
: new BenchmarkSandboxError("GIT_REPOSITORY_MISMATCH", binding.reason);
|
|
103
|
+
}
|
|
96
104
|
await mustGit(git, ["config", "user.name", "my-dev-kit-lab sandbox"], "config-name");
|
|
97
105
|
await mustGit(git, ["config", "user.email", "sandbox@my-dev-kit-lab.invalid"], "config-email");
|
|
98
106
|
await mustGit(git, ["config", "commit.gpgsign", "false"], "config-gpg");
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { realpath as realpathCallback } from "node:fs";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
1
3
|
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
2
5
|
import { runMeasuredCommand } from "../../core/runMeasuredCommand.js";
|
|
3
6
|
import { buildMinimalHostEnv } from "./minimalHostEnv.js";
|
|
4
7
|
// Git (including Git for Windows) opens this as an empty config file; os.devNull (\\.\nul) is rejected by Git on Windows.
|
|
5
8
|
const NULL_CONFIG_PATH = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
9
|
+
/** Variables that can redirect which repository, index or object store a Git command operates on. */
|
|
10
|
+
const REPOSITORY_REDIRECT_ENV = /^GIT_(DIR|WORK_TREE|COMMON_DIR|INDEX_FILE|OBJECT_DIRECTORY|ALTERNATE_OBJECT_DIRECTORIES|NAMESPACE|CEILING_DIRECTORIES|PREFIX|DISCOVERY_ACROSS_FILESYSTEM)$/i;
|
|
6
11
|
const DEFAULT_GIT_TIMEOUT_MS = 60_000;
|
|
7
12
|
const GIT_OUTPUT_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
8
13
|
const sequenceByEvidenceRoot = new Map();
|
|
@@ -17,6 +22,18 @@ function nextSequence(evidenceRoot) {
|
|
|
17
22
|
* and the system's Git configuration so hashes and diffs do not depend on the developer machine.
|
|
18
23
|
*/
|
|
19
24
|
export async function runSandboxGit(target, args, options = {}) {
|
|
25
|
+
const redirecting = Object.keys(options.env ?? {}).find((key) => REPOSITORY_REDIRECT_ENV.test(key));
|
|
26
|
+
if (redirecting !== undefined) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
exitCode: null,
|
|
30
|
+
stdout: "",
|
|
31
|
+
stderr: "",
|
|
32
|
+
timedOut: false,
|
|
33
|
+
unavailable: false,
|
|
34
|
+
error: `sandbox git refuses an environment override that can redirect the repository (${redirecting}).`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
20
37
|
const label = (options.label ?? args[0] ?? "git").replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 32);
|
|
21
38
|
const commandId = `git-${String(nextSequence(target.evidenceRoot)).padStart(4, "0")}-${label}`;
|
|
22
39
|
const env = buildMinimalHostEnv(process.env, {
|
|
@@ -26,7 +43,15 @@ export async function runSandboxGit(target, args, options = {}) {
|
|
|
26
43
|
GIT_OPTIONAL_LOCKS: "0",
|
|
27
44
|
HOME: target.evidenceRoot,
|
|
28
45
|
USERPROFILE: target.evidenceRoot,
|
|
29
|
-
...options.env
|
|
46
|
+
...options.env,
|
|
47
|
+
// The sandbox project sits below other directories that may be Git repositories. Never let discovery walk
|
|
48
|
+
// above it, and (unless verifying) never rely on discovery at all. The pin is relative to the project working
|
|
49
|
+
// directory on purpose: Git for Windows rejects an absolute GIT_DIR of roughly 245 characters or more
|
|
50
|
+
// ("'$GIT_DIR' too big") although the repository itself works at those lengths.
|
|
51
|
+
GIT_CEILING_DIRECTORIES: path.dirname(target.projectRoot),
|
|
52
|
+
...(options.repositoryBinding === "discovered"
|
|
53
|
+
? {}
|
|
54
|
+
: { GIT_DIR: ".git", GIT_WORK_TREE: "." })
|
|
30
55
|
});
|
|
31
56
|
const result = await runMeasuredCommand({
|
|
32
57
|
commandId,
|
|
@@ -60,3 +85,68 @@ export function excerptGitFailure(result) {
|
|
|
60
85
|
const text = (result.stderr || result.stdout || result.error || "no output").trim().replace(/\s+/g, " ");
|
|
61
86
|
return text.length > 300 ? `${text.slice(0, 300)}...` : text;
|
|
62
87
|
}
|
|
88
|
+
function samePhysicalPath(left, right) {
|
|
89
|
+
const caseFold = process.platform === "win32" || process.platform === "darwin";
|
|
90
|
+
const a = path.normalize(left);
|
|
91
|
+
const b = path.normalize(right);
|
|
92
|
+
return caseFold ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
93
|
+
}
|
|
94
|
+
// The native variant resolves Windows 8.3 short names and reports the on-disk case, matching what Git prints.
|
|
95
|
+
const realpathNative = promisify(realpathCallback.native);
|
|
96
|
+
async function physical(candidate) {
|
|
97
|
+
return realpathNative(path.resolve(candidate));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Proves that Git, discovering from the sandbox project (bounded by a ceiling at its parent), resolves to the
|
|
101
|
+
* project's own physically contained `.git` directory and to the project as work tree. Exit status of
|
|
102
|
+
* `git init` is not evidence of this; on some hosts `init` succeeds while discovery selects an ancestor.
|
|
103
|
+
*/
|
|
104
|
+
export async function verifySandboxRepositoryBinding(target) {
|
|
105
|
+
const fail = (reason, unavailable = false) => ({ ok: false, unavailable, reason });
|
|
106
|
+
const gitEntry = path.join(target.projectRoot, ".git");
|
|
107
|
+
let stats;
|
|
108
|
+
try {
|
|
109
|
+
stats = await lstat(gitEntry);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return fail("the sandbox project has no .git entry after initialization.");
|
|
113
|
+
}
|
|
114
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
115
|
+
return fail("the sandbox .git entry is not a real local directory.");
|
|
116
|
+
}
|
|
117
|
+
let expectedRoot;
|
|
118
|
+
try {
|
|
119
|
+
expectedRoot = await physical(target.projectRoot);
|
|
120
|
+
if (!samePhysicalPath(await physical(gitEntry), path.join(expectedRoot, ".git"))) {
|
|
121
|
+
return fail("the sandbox .git directory is not physically contained in the sandbox project.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return fail(`the sandbox repository paths could not be resolved: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
+
}
|
|
127
|
+
const result = await runSandboxGit(target, ["rev-parse", "--absolute-git-dir", "--show-toplevel"], {
|
|
128
|
+
label: "verify-root",
|
|
129
|
+
repositoryBinding: "discovered"
|
|
130
|
+
});
|
|
131
|
+
if (result.unavailable)
|
|
132
|
+
return fail("the git executable is not available.", true);
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
return fail(`git does not recognize the new sandbox repository from its project directory: ${excerptGitFailure(result)}`);
|
|
135
|
+
}
|
|
136
|
+
const lines = result.stdout.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
137
|
+
if (lines.length !== 2)
|
|
138
|
+
return fail("git repository discovery returned an unexpected shape.");
|
|
139
|
+
try {
|
|
140
|
+
const [discoveredGitDir, discoveredTopLevel] = await Promise.all([physical(lines[0]), physical(lines[1])]);
|
|
141
|
+
if (!samePhysicalPath(discoveredGitDir, path.join(expectedRoot, ".git"))) {
|
|
142
|
+
return fail("git discovered a repository administration directory other than the sandbox project's own .git.");
|
|
143
|
+
}
|
|
144
|
+
if (!samePhysicalPath(discoveredTopLevel, expectedRoot)) {
|
|
145
|
+
return fail("git discovered a work tree other than the sandbox project.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return fail(`the discovered repository paths could not be resolved: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
}
|
|
151
|
+
return { ok: true };
|
|
152
|
+
}
|
|
@@ -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,
|