@dailephd/my-dev-kit-lab 0.7.0 → 0.7.2
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 +23 -0
- package/README.md +6 -4
- package/benchmarks/projects/README.md +3 -1
- package/dist/src/cli/help.js +20 -3
- package/dist/src/commands/runExperimentDescribeCommand.js +10 -1
- package/dist/src/commands/runExperimentRunCommand.js +169 -19
- package/dist/src/core/fileGlobs.js +44 -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/runMyDevKitRetrieval.js +3 -1
- package/dist/src/evaluation/runRawFullFileBaseline.js +18 -4
- package/dist/src/evaluation/syntheticRepository/config.js +304 -0
- package/dist/src/evaluation/syntheticRepository/evaluationCase.js +43 -0
- package/dist/src/evaluation/syntheticRepository/identity.js +41 -0
- package/dist/src/evaluation/syntheticRepository/index.js +16 -0
- package/dist/src/evaluation/syntheticRepository/inspection.js +92 -0
- package/dist/src/evaluation/syntheticRepository/layout.js +50 -0
- package/dist/src/evaluation/syntheticRepository/manifest.js +281 -0
- package/dist/src/evaluation/syntheticRepository/manifestVerification.js +112 -0
- package/dist/src/evaluation/syntheticRepository/materialize.js +170 -0
- package/dist/src/evaluation/syntheticRepository/planning.js +479 -0
- package/dist/src/evaluation/syntheticRepository/prng.js +47 -0
- package/dist/src/evaluation/syntheticRepository/renderPython.js +68 -0
- package/dist/src/evaluation/syntheticRepository/renderRepository.js +39 -0
- package/dist/src/evaluation/syntheticRepository/renderShared.js +93 -0
- package/dist/src/evaluation/syntheticRepository/renderTypeScript.js +82 -0
- package/dist/src/evaluation/syntheticRepository/topology.js +96 -0
- package/dist/src/evaluation/syntheticRepository/types.js +26 -0
- package/dist/src/evaluation/targetImmutability/captureTargetSnapshot.js +29 -7
- package/dist/src/evaluation/targetImmutability/compareTargetSnapshots.js +4 -0
- package/dist/src/experiments/plugins/contextWindowScaling/index.js +1 -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/contextWindowScaling/syntheticInputs.js +56 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +4 -3
- package/dist/src/report/experiments/writePluginExperimentReports.js +8 -2
- package/docs/METRICS.md +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { compareCodeUnits } from "../../../evaluation/localRepositorySubject/index.js";
|
|
2
|
+
import { LocalSubjectExecutionError } from "./localSubjectErrors.js";
|
|
3
|
+
/** Fail-closed ceilings so the exclusion arguments stay well inside Windows command-line limits. */
|
|
4
|
+
export const GUIDED_EXCLUSION_MAX_ENTRIES = 500;
|
|
5
|
+
export const GUIDED_EXCLUSION_MAX_CHARACTERS = 24_000;
|
|
6
|
+
/**
|
|
7
|
+
* Pure policy: turns Batch 1 runtime exclusion evidence into the exact `--exclude` paths for the guided index.
|
|
8
|
+
*
|
|
9
|
+
* Each excluded file is covered by the shallowest ancestor directory that contains no eligible file (so a fully
|
|
10
|
+
* ignored directory becomes one entry), otherwise by the file path itself. Bare names are never produced because
|
|
11
|
+
* my-dev-kit treats a bare name as "every directory with this name", which would drop eligible material. If the
|
|
12
|
+
* result is too large the call throws instead of truncating: an incomplete exclusion list would be unsafe.
|
|
13
|
+
*/
|
|
14
|
+
export function deriveGuidedIndexExclusions(input) {
|
|
15
|
+
const eligibleDirectories = new Set();
|
|
16
|
+
const eligibleSegments = new Set();
|
|
17
|
+
for (const eligible of input.eligibleFiles) {
|
|
18
|
+
const segments = eligible.split("/");
|
|
19
|
+
segments.forEach((segment) => eligibleSegments.add(segment));
|
|
20
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
21
|
+
eligibleDirectories.add(segments.slice(0, depth).join("/"));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const candidates = new Set();
|
|
25
|
+
for (const excluded of input.excludedFiles) {
|
|
26
|
+
const segments = excluded.split("/");
|
|
27
|
+
let covering = excluded;
|
|
28
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
29
|
+
const directory = segments.slice(0, depth).join("/");
|
|
30
|
+
if (!eligibleDirectories.has(directory)) {
|
|
31
|
+
covering = directory;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// A single-segment entry is a name match in my-dev-kit (any depth), and "./name" is silently ignored, so it is
|
|
36
|
+
// only safe when no eligible path uses that name. Otherwise descend to a multi-segment path, which is exact.
|
|
37
|
+
if (!covering.includes("/") && eligibleSegments.has(covering)) {
|
|
38
|
+
if (covering !== excluded && segments.length > 2) {
|
|
39
|
+
covering = segments.slice(0, 2).join("/");
|
|
40
|
+
}
|
|
41
|
+
else if (covering !== excluded) {
|
|
42
|
+
covering = excluded;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new LocalSubjectExecutionError([
|
|
46
|
+
{
|
|
47
|
+
code: "GUIDED_EXCLUSION_UNREPRESENTABLE",
|
|
48
|
+
message: `an excluded top-level entry shares its name with eligible files and cannot be excluded exactly (${excluded}).`,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
candidates.add(covering);
|
|
54
|
+
}
|
|
55
|
+
const exclusions = [...candidates]
|
|
56
|
+
.filter((candidate) => {
|
|
57
|
+
const segments = candidate.split("/");
|
|
58
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
59
|
+
if (candidates.has(segments.slice(0, depth).join("/")))
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
})
|
|
64
|
+
.sort(compareCodeUnits);
|
|
65
|
+
const characters = exclusions.reduce((total, entry) => total + entry.length + 1, 0);
|
|
66
|
+
if (exclusions.length > GUIDED_EXCLUSION_MAX_ENTRIES || characters > GUIDED_EXCLUSION_MAX_CHARACTERS) {
|
|
67
|
+
throw new LocalSubjectExecutionError([
|
|
68
|
+
{
|
|
69
|
+
code: "GUIDED_EXCLUSION_LIMIT",
|
|
70
|
+
message: `guided index exclusions exceed the safe limit (${exclusions.length} entries, ${characters} characters).`,
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
return exclusions;
|
|
75
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { relativeWithinRoot } from "../../../core/pathSafety.js";
|
|
3
|
+
import { runMyDevKitRetrieval } from "../../../evaluation/runMyDevKitRetrieval.js";
|
|
4
|
+
import { runRawFullFileBaseline } from "../../../evaluation/runRawFullFileBaseline.js";
|
|
5
|
+
import { captureTargetSnapshot, compareTargetSnapshots } from "../../../evaluation/targetImmutability/index.js";
|
|
6
|
+
import { executeContextWindowScalingCases, } from "./execution.js";
|
|
7
|
+
import { LocalSubjectExecutionError, redactPrivatePaths, } from "./localSubjectErrors.js";
|
|
8
|
+
import { deriveGuidedIndexExclusions } from "./localSubjectExclusions.js";
|
|
9
|
+
import { assertWorkRootOutsideTarget, createPrivateScratch, defaultPrivateScratchIo, removePrivateScratch, } from "./localSubjectScratch.js";
|
|
10
|
+
import { resolveScalingProjectProfiles } from "./projectProfile.js";
|
|
11
|
+
function toRepositoryRelative(repositoryRoot, file) {
|
|
12
|
+
const slashed = file.replace(/\\/g, "/");
|
|
13
|
+
if (path.isAbsolute(file)) {
|
|
14
|
+
try {
|
|
15
|
+
return relativeWithinRoot(repositoryRoot, file);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return slashed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return slashed.replace(/^\.\//, "");
|
|
22
|
+
}
|
|
23
|
+
/** Safe local constructors: raw context from eligible files only; guided retrieval over the original root. */
|
|
24
|
+
function createSafeLocalDependencies(subject, guidedExclusions) {
|
|
25
|
+
const eligible = new Set(subject.eligibleFiles);
|
|
26
|
+
return {
|
|
27
|
+
constructRawContext: (evaluationCase) => runRawFullFileBaseline(evaluationCase, { eligibleFiles: subject.eligibleFiles }),
|
|
28
|
+
constructGuidedContext: async ({ evaluationCase, kitCommand, outputDir }) => {
|
|
29
|
+
const result = await runMyDevKitRetrieval({
|
|
30
|
+
evaluationCase,
|
|
31
|
+
kitCommand,
|
|
32
|
+
outputDir,
|
|
33
|
+
requireKit: false,
|
|
34
|
+
excludePaths: guidedExclusions,
|
|
35
|
+
});
|
|
36
|
+
const returned = [...result.filesRead, ...(result.selectedFile ? [result.selectedFile] : [])];
|
|
37
|
+
const outside = returned.filter((file) => !eligible.has(toRepositoryRelative(subject.repositoryRoot, file)));
|
|
38
|
+
if (outside.length > 0) {
|
|
39
|
+
throw new Error(`Guided retrieval returned ${outside.length} file(s) outside the eligible subject universe: ${outside
|
|
40
|
+
.map((file) => toRepositoryRelative(subject.repositoryRoot, file))
|
|
41
|
+
.join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Internal programmatic seam: runs a loaded Batch 1 local subject through the existing context-window core with
|
|
49
|
+
* safe raw and guided constructors, a private scratch outside the target, and pre/post target-immutability
|
|
50
|
+
* enforcement. Order: validate work root, derive exclusions, capture before, create scratch, execute, capture after,
|
|
51
|
+
* compare, remove scratch, then decide. Success is returned only when execution, immutability and cleanup all pass.
|
|
52
|
+
* A mutated target is never reverted.
|
|
53
|
+
*/
|
|
54
|
+
export async function executeLocalRepositorySubjectContextWindowScaling(args) {
|
|
55
|
+
const { subject } = args;
|
|
56
|
+
await assertWorkRootOutsideTarget(args.workRoot, subject.repositoryRoot);
|
|
57
|
+
const guidedExclusions = deriveGuidedIndexExclusions({
|
|
58
|
+
eligibleFiles: subject.eligibleFiles,
|
|
59
|
+
excludedFiles: [...subject.runtimeSafetyExclusions.gitIgnoredFiles, ...subject.runtimeSafetyExclusions.oversizedFiles],
|
|
60
|
+
});
|
|
61
|
+
const snapshotConfig = { targetRootPath: subject.repositoryRoot, relativeFilePaths: subject.eligibleFiles };
|
|
62
|
+
const snapshotOptions = { externalLocalSafe: { maxHashedFileBytes: subject.manifest.safetyPolicy.maxFileBytes } };
|
|
63
|
+
const before = await captureTargetSnapshot(snapshotConfig, snapshotOptions);
|
|
64
|
+
if (!before.ok) {
|
|
65
|
+
throw new LocalSubjectExecutionError([
|
|
66
|
+
{ code: "BEFORE_SNAPSHOT_FAILED", message: `target snapshot before execution failed (${before.code}).` },
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
const scratch = await createPrivateScratch(args.workRoot, subject.repositoryRoot);
|
|
70
|
+
const privatePaths = [subject.repositoryRoot, scratch.path, path.resolve(args.workRoot)];
|
|
71
|
+
const redact = (message) => redactPrivatePaths(message, privatePaths);
|
|
72
|
+
const issues = [];
|
|
73
|
+
let caseEvidence = null;
|
|
74
|
+
try {
|
|
75
|
+
const projectProfiles = resolveScalingProjectProfiles(subject.evaluationCases, [], { eligibleFiles: subject.eligibleFiles });
|
|
76
|
+
caseEvidence = await executeContextWindowScalingCases({
|
|
77
|
+
cases: subject.evaluationCases,
|
|
78
|
+
contextBudgets: args.contextBudgets,
|
|
79
|
+
kitCommand: args.kitCommand,
|
|
80
|
+
outputRoot: scratch.path,
|
|
81
|
+
projectProfiles,
|
|
82
|
+
cwd: scratch.path,
|
|
83
|
+
env: args.env,
|
|
84
|
+
dependencies: { ...createSafeLocalDependencies(subject, guidedExclusions), ...args.dependencies },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
issues.push({
|
|
89
|
+
code: "EXECUTION_FAILED",
|
|
90
|
+
message: redact(`execution threw: ${error instanceof Error ? error.message : String(error)}`),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (caseEvidence) {
|
|
94
|
+
for (const caseResult of caseEvidence) {
|
|
95
|
+
for (const treatment of caseResult.treatments) {
|
|
96
|
+
if (treatment.status === "failed" || treatment.errors.length > 0) {
|
|
97
|
+
issues.push({
|
|
98
|
+
code: "EXECUTION_FAILED",
|
|
99
|
+
message: redact(`case ${caseResult.caseId} treatment ${treatment.variantId} failed: ${treatment.errors
|
|
100
|
+
.map((entry) => `${entry.code}: ${entry.message}`)
|
|
101
|
+
.join("; ")}`),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let comparison = null;
|
|
108
|
+
const after = await captureTargetSnapshot(snapshotConfig, snapshotOptions);
|
|
109
|
+
if (!after.ok) {
|
|
110
|
+
issues.push({ code: "AFTER_SNAPSHOT_FAILED", message: `target snapshot after execution failed (${after.code}).` });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
comparison = compareTargetSnapshots(before.snapshot, after.snapshot);
|
|
114
|
+
if (comparison.status === "mutated") {
|
|
115
|
+
issues.push({
|
|
116
|
+
code: "TARGET_MUTATED",
|
|
117
|
+
message: `the target changed during execution (${comparison.mutations.map((mutation) => mutation.id).join(", ")}); it was not restored.`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const cleanupFailure = await removePrivateScratch(scratch, args.scratchIo ?? defaultPrivateScratchIo);
|
|
122
|
+
if (cleanupFailure) {
|
|
123
|
+
issues.push({ code: "SCRATCH_CLEANUP_FAILED", message: redact(cleanupFailure) });
|
|
124
|
+
}
|
|
125
|
+
if (issues.length > 0 || !caseEvidence || !comparison) {
|
|
126
|
+
throw new LocalSubjectExecutionError(issues, { caseEvidence, immutability: comparison });
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
caseEvidence,
|
|
130
|
+
immutability: {
|
|
131
|
+
status: "unchanged",
|
|
132
|
+
preExistingGitStatusEntryCount: comparison.preExistingGitStatusEntryCount,
|
|
133
|
+
newMutationCount: 0,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { redactPrivatePaths } from "./localSubjectErrors.js";
|
|
3
|
+
/** Value used wherever a durable external-local artifact would otherwise show a machine-local path. */
|
|
4
|
+
export const EXTERNAL_LOCAL_REDACTED_VALUE = "[redacted]";
|
|
5
|
+
const REDACTED_FILE_TEXT = "<redacted file>";
|
|
6
|
+
/** One opaque placeholder per file: counts stay exact and an empty list still means "no files". */
|
|
7
|
+
export function redactedFileList(count) {
|
|
8
|
+
return Array.from({ length: count }, (_, index) => `<redacted file ${index + 1}>`);
|
|
9
|
+
}
|
|
10
|
+
/** Replaces known repository-relative paths (longest first) and private roots inside free text. */
|
|
11
|
+
export function redactKnownPaths(text, knownFiles, privateRoots = []) {
|
|
12
|
+
let redacted = redactPrivatePaths(text, privateRoots);
|
|
13
|
+
for (const file of [...knownFiles].sort((left, right) => right.length - left.length)) {
|
|
14
|
+
if (file.length === 0)
|
|
15
|
+
continue;
|
|
16
|
+
// Native tool errors on Windows may spell a repository-relative path with backslashes.
|
|
17
|
+
for (const variant of new Set([file, file.replace(/\//g, "\\")]))
|
|
18
|
+
redacted = redacted.split(variant).join(REDACTED_FILE_TEXT);
|
|
19
|
+
}
|
|
20
|
+
return redacted;
|
|
21
|
+
}
|
|
22
|
+
function redactStrings(value, redact) {
|
|
23
|
+
if (typeof value === "string")
|
|
24
|
+
return redact(value);
|
|
25
|
+
if (Array.isArray(value))
|
|
26
|
+
return value.map((entry) => redactStrings(entry, redact));
|
|
27
|
+
if (value && typeof value === "object") {
|
|
28
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, redactStrings(nested, redact)]));
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Projects execution evidence for durable persistence of an external-local run. File identity lists become
|
|
34
|
+
* placeholder lists of the same length (with an explicit per-treatment flag) and every free-text field has known
|
|
35
|
+
* repository paths and private roots replaced. Scientific numbers, statuses and identifiers are untouched.
|
|
36
|
+
*/
|
|
37
|
+
export function projectEvidenceForExternalLocalPersistence(evidence, privacy) {
|
|
38
|
+
const redact = (text) => redactKnownPaths(text, privacy.knownFiles, privacy.privateRoots);
|
|
39
|
+
return evidence.map((caseEvidence) => {
|
|
40
|
+
const cloned = structuredClone(caseEvidence);
|
|
41
|
+
return {
|
|
42
|
+
...redactStrings(cloned, redact),
|
|
43
|
+
treatments: cloned.treatments.map((treatment) => {
|
|
44
|
+
const safe = redactStrings(treatment, redact);
|
|
45
|
+
return {
|
|
46
|
+
...safe,
|
|
47
|
+
context: {
|
|
48
|
+
...safe.context,
|
|
49
|
+
observedFiles: treatment.context.observedFiles === null ? null : redactedFileList(treatment.context.observedFiles.length),
|
|
50
|
+
},
|
|
51
|
+
relevantFileEvidence: {
|
|
52
|
+
...safe.relevantFileEvidence,
|
|
53
|
+
expectedRelevantFiles: redactedFileList(treatment.relevantFileEvidence.expectedRelevantFiles.length),
|
|
54
|
+
omittedRelevantFiles: redactedFileList(treatment.relevantFileEvidence.omittedRelevantFiles.length),
|
|
55
|
+
},
|
|
56
|
+
fileIdentityRedaction: "redacted",
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Durable target identity for an external-local run: logical identity and Git identity, no filesystem facts. */
|
|
63
|
+
export function projectExternalLocalTarget(manifest) {
|
|
64
|
+
return {
|
|
65
|
+
kind: "external-local",
|
|
66
|
+
targetRoot: manifest.logicalTargetRoot,
|
|
67
|
+
toolRoot: EXTERNAL_LOCAL_REDACTED_VALUE,
|
|
68
|
+
packageName: null,
|
|
69
|
+
packageVersion: null,
|
|
70
|
+
hasPackageJson: false,
|
|
71
|
+
hasLockfile: false,
|
|
72
|
+
branch: manifest.repository.branch,
|
|
73
|
+
commit: manifest.repository.commit,
|
|
74
|
+
hasGit: true,
|
|
75
|
+
isSelf: false,
|
|
76
|
+
privacyProjection: "external-local-redacted",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Replaces target and machine-local metadata of a run before any report is serialized. */
|
|
80
|
+
export function projectRunForExternalLocalPersistence(run, target) {
|
|
81
|
+
const metadata = { ...(run.metadata ?? {}) };
|
|
82
|
+
if ("outputRoot" in metadata)
|
|
83
|
+
metadata.outputRoot = EXTERNAL_LOCAL_REDACTED_VALUE;
|
|
84
|
+
if (typeof metadata.executionArtifactPath === "string")
|
|
85
|
+
metadata.executionArtifactPath = path.basename(metadata.executionArtifactPath);
|
|
86
|
+
return { ...run, target, metadata: metadata };
|
|
87
|
+
}
|
|
88
|
+
const GENERIC_ISSUE_TEXT = {
|
|
89
|
+
WORK_ROOT_INSIDE_TARGET: "the output directory must be outside the local subject repository",
|
|
90
|
+
BEFORE_SNAPSHOT_FAILED: "the target snapshot before execution could not be captured",
|
|
91
|
+
AFTER_SNAPSHOT_FAILED: "the target snapshot after execution could not be captured",
|
|
92
|
+
SCRATCH_CLEANUP_FAILED: "the private scratch could not be removed",
|
|
93
|
+
GUIDED_EXCLUSION_LIMIT: "the guided-index exclusions exceeded the safe limit",
|
|
94
|
+
GUIDED_EXCLUSION_UNREPRESENTABLE: "the guided-index exclusions could not be expressed exactly",
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Safe, persistable description of a local-subject execution failure: issue codes, case and treatment identifiers,
|
|
98
|
+
* error codes and mutation kinds only. Repository file names, mutation identifiers and the full comparison are
|
|
99
|
+
* never included.
|
|
100
|
+
*/
|
|
101
|
+
export function describeLocalSubjectFailureForPersistence(error) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const issue of error.issues) {
|
|
105
|
+
if (seen.has(issue.code))
|
|
106
|
+
continue;
|
|
107
|
+
seen.add(issue.code);
|
|
108
|
+
if (issue.code === "EXECUTION_FAILED") {
|
|
109
|
+
const details = [];
|
|
110
|
+
for (const caseEvidence of error.caseEvidence ?? []) {
|
|
111
|
+
for (const treatment of caseEvidence.treatments) {
|
|
112
|
+
if (treatment.status === "failed" || treatment.errors.length > 0) {
|
|
113
|
+
details.push(`case ${caseEvidence.caseId} treatment ${treatment.variantId}: ${treatment.errors.map((entry) => entry.code).join(", ") || "failed"}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
parts.push(details.length > 0 ? `execution failed (${details.join("; ")})` : "execution failed before completing (details withheld)");
|
|
118
|
+
}
|
|
119
|
+
else if (issue.code === "TARGET_MUTATED") {
|
|
120
|
+
const kinds = [...new Set((error.immutability?.mutations ?? []).map((mutation) => mutation.kind))].sort();
|
|
121
|
+
parts.push(`the target changed during execution${kinds.length > 0 ? ` (${kinds.join(", ")})` : ""}; it was not restored`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
parts.push(GENERIC_ISSUE_TEXT[issue.code] ?? `issue ${issue.code}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return `Local subject execution failed (${error.code}): ${parts.join("; ")}.`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { realpath as nativeRealpath } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { relativeWithinRoot } from "../../../core/pathSafety.js";
|
|
6
|
+
import { LocalSubjectExecutionError } from "./localSubjectErrors.js";
|
|
7
|
+
const realpath = promisify(nativeRealpath.native);
|
|
8
|
+
export const defaultPrivateScratchIo = {
|
|
9
|
+
removeDirectory: (directory) => rm(directory, { recursive: true, force: true }),
|
|
10
|
+
};
|
|
11
|
+
/** Physical path of a path that may not exist yet: realpath of the deepest existing ancestor plus the remainder. */
|
|
12
|
+
async function resolvePhysicalPath(candidate) {
|
|
13
|
+
const resolved = path.resolve(candidate);
|
|
14
|
+
const remainder = [];
|
|
15
|
+
let current = resolved;
|
|
16
|
+
for (;;) {
|
|
17
|
+
try {
|
|
18
|
+
const physical = await realpath(current);
|
|
19
|
+
return remainder.length === 0 ? physical : path.join(physical, ...remainder.reverse());
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = error.code;
|
|
23
|
+
const parent = path.dirname(current);
|
|
24
|
+
if ((code !== "ENOENT" && code !== "ENOTDIR") || parent === current)
|
|
25
|
+
return resolved;
|
|
26
|
+
remainder.push(path.basename(current));
|
|
27
|
+
current = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isSameOrInside(root, candidate) {
|
|
32
|
+
try {
|
|
33
|
+
relativeWithinRoot(root, candidate);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** True when both paths resolve to the same physical directory (follows links; case-insensitive on Windows). */
|
|
41
|
+
export async function isSamePhysicalDirectory(left, right) {
|
|
42
|
+
const [physicalLeft, physicalRight] = await Promise.all([resolvePhysicalPath(left), resolvePhysicalPath(right)]);
|
|
43
|
+
return path.relative(physicalLeft, physicalRight) === "";
|
|
44
|
+
}
|
|
45
|
+
/** Rejects a Lab-owned work root that equals or lies inside the target. Performs no writes. */
|
|
46
|
+
export async function assertWorkRootOutsideTarget(workRoot, targetRoot) {
|
|
47
|
+
const physicalTarget = await resolvePhysicalPath(targetRoot);
|
|
48
|
+
const physicalWorkRoot = await resolvePhysicalPath(workRoot);
|
|
49
|
+
if (isSameOrInside(physicalTarget, physicalWorkRoot)) {
|
|
50
|
+
throw new LocalSubjectExecutionError([
|
|
51
|
+
{ code: "WORK_ROOT_INSIDE_TARGET", message: "the Lab work root must be outside the local subject repository." },
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Creates a short run-owned private scratch directory under the work root and re-verifies it is outside the target. */
|
|
56
|
+
export async function createPrivateScratch(workRoot, targetRoot) {
|
|
57
|
+
await mkdir(path.resolve(workRoot), { recursive: true });
|
|
58
|
+
const scratchPath = await mkdtemp(path.join(await realpath(path.resolve(workRoot)), "s-"));
|
|
59
|
+
if (isSameOrInside(await resolvePhysicalPath(targetRoot), await resolvePhysicalPath(scratchPath))) {
|
|
60
|
+
await rm(scratchPath, { recursive: true, force: true });
|
|
61
|
+
throw new LocalSubjectExecutionError([
|
|
62
|
+
{ code: "WORK_ROOT_INSIDE_TARGET", message: "the private scratch directory resolved inside the local subject repository." },
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
return { path: scratchPath };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Removes the scratch and verifies it is gone. Returns a failure description instead of throwing so the caller can
|
|
69
|
+
* keep the primary error; never retries and never sleeps.
|
|
70
|
+
*/
|
|
71
|
+
export async function removePrivateScratch(scratch, io = defaultPrivateScratchIo) {
|
|
72
|
+
try {
|
|
73
|
+
await io.removeDirectory(scratch.path);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return `private scratch could not be removed: ${error instanceof Error ? error.message : String(error)}`;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await lstat(scratch.path);
|
|
80
|
+
return "private scratch still exists after removal.";
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return error.code === "ENOENT" ? null : "private scratch state could not be verified after removal.";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -4,9 +4,9 @@ export const CONTEXT_WINDOW_SCALING_TREATMENT_IDS = ["raw-full-file", "my-dev-ki
|
|
|
4
4
|
export const contextWindowScalingMetadata = {
|
|
5
5
|
id: CONTEXT_WINDOW_SCALING_PLUGIN_ID,
|
|
6
6
|
name: "Context Window Scaling",
|
|
7
|
-
description: "Measure raw-full-file and my-dev-kit-guided strategies under increasing deterministic estimated-context-token budgets.",
|
|
7
|
+
description: "Measure raw-full-file and my-dev-kit-guided strategies under increasing deterministic estimated-context-token budgets, over the bundled fixed scaling corpus, caller-supplied deterministic synthetic repositories (--synthetic-config), or an explicitly selected local Git repository (--target with --local-subject-config). Operational context-fit measurement only; it does not measure retrieval precision, recall, or ranking quality.",
|
|
8
8
|
schemaVersion: "1.0.0",
|
|
9
9
|
status: "experimental",
|
|
10
|
-
supportedTargets: ["self"],
|
|
10
|
+
supportedTargets: ["self", "external-local"],
|
|
11
11
|
supportedOutputs: ["json", "text", "html", "plot"],
|
|
12
12
|
};
|
|
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { tokenCountMethod } from "../../../core/countTokens.js";
|
|
4
4
|
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
5
|
+
import { serializeLocalRepositorySubjectManifest, } from "../../../evaluation/localRepositorySubject/index.js";
|
|
5
6
|
import { summarizeExperimentRun } from "../../results.js";
|
|
6
7
|
import { aggregateStatus } from "../warmIndexReuse/execution.js";
|
|
7
8
|
import { contextWindowScalingConfigDefinition, defaultContextWindowScalingConfig, validateContextWindowScalingConfig, } from "./config.js";
|
|
@@ -9,7 +10,13 @@ import { executeContextWindowScalingCases, } from "./execution.js";
|
|
|
9
10
|
import { buildContextWindowScalingExecutionArtifact, CONTEXT_WINDOW_SCALING_EXECUTION_ARTIFACT_FILE, } from "./executionArtifact.js";
|
|
10
11
|
import { CONTEXT_WINDOW_SCALING_TREATMENT_IDS, contextWindowScalingMetadata } from "./metadata.js";
|
|
11
12
|
import { aggregateContextWindowScaling, toAggregateExperimentMetrics, toOutcomeBudgetMetrics, } from "./metrics.js";
|
|
13
|
+
import { LocalSubjectExecutionError } from "./localSubjectErrors.js";
|
|
14
|
+
import { executeLocalRepositorySubjectContextWindowScaling } from "./localSubjectExecution.js";
|
|
15
|
+
import { describeLocalSubjectFailureForPersistence, projectEvidenceForExternalLocalPersistence, projectExternalLocalTarget, redactKnownPaths, } from "./localSubjectPrivacy.js";
|
|
16
|
+
import { isSamePhysicalDirectory } from "./localSubjectScratch.js";
|
|
12
17
|
import { resolveScalingProjectProfiles } from "./projectProfile.js";
|
|
18
|
+
/** Privacy-safe Batch 1 manifest persisted beside the execution artifact for external-local runs. */
|
|
19
|
+
export const LOCAL_REPOSITORY_SUBJECT_MANIFEST_FILE = "local-repository-subject-manifest.json";
|
|
13
20
|
const VARIANTS = [
|
|
14
21
|
{
|
|
15
22
|
id: "raw-full-file",
|
|
@@ -31,6 +38,14 @@ export const contextWindowScalingPlugin = {
|
|
|
31
38
|
validateConfig: validateContextWindowScalingConfig,
|
|
32
39
|
async run(context) {
|
|
33
40
|
const startedAt = context.startedAt.toISOString();
|
|
41
|
+
const localSubject = readLocalSubjectInput(context.inputs);
|
|
42
|
+
if (localSubject)
|
|
43
|
+
return runLocalSubject(context, localSubject, startedAt);
|
|
44
|
+
if (context.target.kind === "external-local") {
|
|
45
|
+
// Never fall back to the legacy raw/guided path against an external repository: it is neither
|
|
46
|
+
// ignore-aware nor symlink-aware and writes artifacts it must not.
|
|
47
|
+
throw new Error("External-local context-window-scaling targets require a loaded local repository subject (--local-subject-config).");
|
|
48
|
+
}
|
|
34
49
|
const cases = readCasesInput(context.inputs);
|
|
35
50
|
const projectProfiles = resolveScalingProjectProfiles(cases, readProjectProfilesInput(context.inputs));
|
|
36
51
|
const outDir = context.outputRoot ?? path.resolve(context.toolRoot, "lab-output", "context-window-scaling");
|
|
@@ -72,6 +87,84 @@ export const contextWindowScalingPlugin = {
|
|
|
72
87
|
return result.summary ?? summarizeExperimentRun(result);
|
|
73
88
|
},
|
|
74
89
|
};
|
|
90
|
+
function readLocalSubjectInput(inputs) {
|
|
91
|
+
const value = inputs?.localSubject;
|
|
92
|
+
return value && typeof value === "object" ? value : undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* External-local path: the Batch 2 safe seam owns execution, immutability and scratch cleanup; this function only
|
|
96
|
+
* projects the privacy-lean result for persistence and feeds the same artifact and run builders as the legacy path.
|
|
97
|
+
*/
|
|
98
|
+
async function runLocalSubject(context, subject, startedAt) {
|
|
99
|
+
if (context.target.isSelf || context.target.kind !== "external-local") {
|
|
100
|
+
throw new Error("Local-repository subject mode requires an external-local target.");
|
|
101
|
+
}
|
|
102
|
+
if (!(await isSamePhysicalDirectory(context.target.targetRoot, subject.repositoryRoot))) {
|
|
103
|
+
throw new Error("The selected --target is not the repository the local subject was loaded from.");
|
|
104
|
+
}
|
|
105
|
+
const outDir = context.outputRoot;
|
|
106
|
+
if (!outDir)
|
|
107
|
+
throw new Error("Local-repository subject mode requires an experiment output directory.");
|
|
108
|
+
const knownFiles = [
|
|
109
|
+
...subject.eligibleFiles,
|
|
110
|
+
...subject.runtimeSafetyExclusions.gitIgnoredFiles,
|
|
111
|
+
...subject.runtimeSafetyExclusions.oversizedFiles,
|
|
112
|
+
];
|
|
113
|
+
const privateRoots = [subject.repositoryRoot, context.target.targetRoot, outDir, context.toolRoot];
|
|
114
|
+
let localResult;
|
|
115
|
+
try {
|
|
116
|
+
localResult = await executeLocalRepositorySubjectContextWindowScaling({
|
|
117
|
+
subject,
|
|
118
|
+
contextBudgets: context.config.contextBudgets,
|
|
119
|
+
kitCommand: context.config.kitCommand,
|
|
120
|
+
workRoot: outDir,
|
|
121
|
+
env: readEnvInput(context.inputs),
|
|
122
|
+
dependencies: readDependenciesInput(context.inputs),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof LocalSubjectExecutionError)
|
|
127
|
+
throw new Error(describeLocalSubjectFailureForPersistence(error));
|
|
128
|
+
throw new Error(redactKnownPaths(error instanceof Error ? error.message : String(error), knownFiles, privateRoots));
|
|
129
|
+
}
|
|
130
|
+
const startedAtIso = startedAt;
|
|
131
|
+
const evidence = projectEvidenceForExternalLocalPersistence(localResult.caseEvidence, { knownFiles, privateRoots });
|
|
132
|
+
await mkdir(outDir, { recursive: true });
|
|
133
|
+
const completedAt = new Date().toISOString();
|
|
134
|
+
const artifact = buildContextWindowScalingExecutionArtifact({
|
|
135
|
+
runId: context.runId,
|
|
136
|
+
pluginId: contextWindowScalingMetadata.id,
|
|
137
|
+
pluginSchemaVersion: contextWindowScalingMetadata.schemaVersion,
|
|
138
|
+
startedAt: startedAtIso,
|
|
139
|
+
completedAt,
|
|
140
|
+
tokenCountMethod,
|
|
141
|
+
contextBudgets: context.config.contextBudgets,
|
|
142
|
+
cases: evidence,
|
|
143
|
+
});
|
|
144
|
+
const artifactPath = resolveWithinRoot(outDir, CONTEXT_WINDOW_SCALING_EXECUTION_ARTIFACT_FILE);
|
|
145
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}
|
|
146
|
+
`, "utf8");
|
|
147
|
+
const manifestPath = resolveWithinRoot(outDir, LOCAL_REPOSITORY_SUBJECT_MANIFEST_FILE);
|
|
148
|
+
await writeFile(manifestPath, serializeLocalRepositorySubjectManifest(subject.manifest), "utf8");
|
|
149
|
+
const run = mapExecutionToRun({
|
|
150
|
+
runId: context.runId,
|
|
151
|
+
startedAt: startedAtIso,
|
|
152
|
+
completedAt,
|
|
153
|
+
target: projectExternalLocalTarget(subject.manifest),
|
|
154
|
+
contextBudgets: context.config.contextBudgets,
|
|
155
|
+
executionEvidence: evidence,
|
|
156
|
+
artifactPath,
|
|
157
|
+
});
|
|
158
|
+
run.artifacts.push({
|
|
159
|
+
id: "local-repository-subject-manifest",
|
|
160
|
+
label: "Privacy-safe local repository subject manifest",
|
|
161
|
+
path: manifestPath,
|
|
162
|
+
kind: "artifact",
|
|
163
|
+
mimeType: "application/json",
|
|
164
|
+
description: "Subject identity, Git commit, safety policy and aggregate inventory counts; no paths, source text or file lists.",
|
|
165
|
+
});
|
|
166
|
+
return run;
|
|
167
|
+
}
|
|
75
168
|
export function mapExecutionToRun(args) {
|
|
76
169
|
const cases = args.executionEvidence.map((caseEvidence) => ({
|
|
77
170
|
id: caseEvidence.caseId,
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { calculateProjectComplexityScore, computeProjectComplexityMetrics, PROJECT_COMPLEXITY_FORMULA, } from "../../../evaluation/projectComplexity.js";
|
|
3
3
|
import { buildProjectFileTree } from "../../../evaluation/projectFileTree.js";
|
|
4
|
-
|
|
5
|
-
* The fixed scaling project has no entry in the shared benchmark-project-profiles contract. The
|
|
6
|
-
* existing prompt owner requires a profile per case, so a missing one is derived in memory from
|
|
7
|
-
* the existing file-tree and complexity owners. Supplied profiles always win; nothing is written.
|
|
8
|
-
*/
|
|
9
|
-
export function resolveScalingProjectProfiles(cases, supplied) {
|
|
4
|
+
export function resolveScalingProjectProfiles(cases, supplied, options = {}) {
|
|
10
5
|
const profiles = [...supplied];
|
|
11
6
|
const known = new Set(profiles.map((profile) => profile.projectId));
|
|
12
7
|
for (const evaluationCase of cases) {
|
|
@@ -14,13 +9,13 @@ export function resolveScalingProjectProfiles(cases, supplied) {
|
|
|
14
9
|
if (known.has(profileId))
|
|
15
10
|
continue;
|
|
16
11
|
known.add(profileId);
|
|
17
|
-
profiles.push(deriveProfile(profileId, evaluationCase, cases));
|
|
12
|
+
profiles.push(deriveProfile(profileId, evaluationCase, cases, options));
|
|
18
13
|
}
|
|
19
14
|
return profiles;
|
|
20
15
|
}
|
|
21
|
-
function deriveProfile(profileId, evaluationCase, cases) {
|
|
16
|
+
function deriveProfile(profileId, evaluationCase, cases, options) {
|
|
22
17
|
const siblings = cases.filter((candidate) => (candidate.projectProfileRef ?? candidate.benchmarkProject) === profileId);
|
|
23
|
-
const fileTree = buildProjectFileTree(evaluationCase.absoluteTargetRoot);
|
|
18
|
+
const fileTree = buildProjectFileTree(evaluationCase.absoluteTargetRoot, options.eligibleFiles === undefined ? {} : { allowedRelativeFiles: options.eligibleFiles });
|
|
24
19
|
const average = (values) => (values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length);
|
|
25
20
|
const complexityMetrics = computeProjectComplexityMetrics(evaluationCase.absoluteTargetRoot, fileTree, {
|
|
26
21
|
taskCount: siblings.length,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { relativeWithinRoot, resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { materializeSyntheticRepository, planSyntheticRepositories } from "../../../evaluation/syntheticRepository/index.js";
|
|
5
|
+
import { resolveScalingProjectProfiles } from "./projectProfile.js";
|
|
6
|
+
/**
|
|
7
|
+
* Stable child of the experiment output root that owns every generated repository. Batch 2 lays each case out
|
|
8
|
+
* beneath it as `<case id>/repository` plus a sibling manifest. It never collides with the execution artifact,
|
|
9
|
+
* reports, `guided/`, `agents/` or `charts/`.
|
|
10
|
+
*/
|
|
11
|
+
export const SYNTHETIC_REPOSITORIES_OUTPUT_SUBDIRECTORY = "synthetic-repositories";
|
|
12
|
+
/**
|
|
13
|
+
* Internal input source for context-window-scaling: Batch 1 config -> plans -> Batch 2 materializations ->
|
|
14
|
+
* EvaluationCase[] -> in-memory project profiles. It owns only that sequencing; planning, rendering, manifests,
|
|
15
|
+
* profile derivation and execution all stay with their existing owners, and generated repositories are not
|
|
16
|
+
* serialized into bundled case JSON (readEvaluationCases is deliberately not used).
|
|
17
|
+
*
|
|
18
|
+
* Fail closed: an invalid or empty config, a planner failure, a materialization collision or a verification
|
|
19
|
+
* failure throws the owning typed error before any case exists. All plans are computed before the first
|
|
20
|
+
* repository is written. Repeating the call with the same config and output root reuses the existing
|
|
21
|
+
* materializations (Batch 2 idempotence) and returns the same identities and order.
|
|
22
|
+
*
|
|
23
|
+
* The caller must supply the final experiment output root; the public command resolves it before calling this
|
|
24
|
+
* and passes the same root to the generic runner.
|
|
25
|
+
*/
|
|
26
|
+
export function prepareSyntheticContextWindowScalingInputs(args) {
|
|
27
|
+
const plans = planSyntheticRepositories(args.syntheticRepositoryConfig);
|
|
28
|
+
const syntheticOutputRoot = resolveWithinRoot(args.outputRoot, SYNTHETIC_REPOSITORIES_OUTPUT_SUBDIRECTORY);
|
|
29
|
+
assertStaysInsideOutputRoot(args.outputRoot, syntheticOutputRoot);
|
|
30
|
+
const materializations = plans.map((plan) => {
|
|
31
|
+
const materialized = materializeSyntheticRepository(plan, syntheticOutputRoot);
|
|
32
|
+
return {
|
|
33
|
+
caseId: plan.caseId,
|
|
34
|
+
generationIdentity: plan.generationIdentity,
|
|
35
|
+
repositoryContentIdentity: materialized.manifest.repositoryContentIdentity,
|
|
36
|
+
repositoryRoot: materialized.repositoryRoot,
|
|
37
|
+
manifestPath: materialized.manifestPath,
|
|
38
|
+
evaluationCase: materialized.evaluationCase,
|
|
39
|
+
reusedExistingMaterialization: materialized.reusedExistingMaterialization,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
const cases = materializations.map((entry) => entry.evaluationCase);
|
|
43
|
+
return { cases, projectProfiles: resolveScalingProjectProfiles(cases, []), materializations, syntheticOutputRoot };
|
|
44
|
+
}
|
|
45
|
+
/** A pre-existing synthetic subtree that is a link resolving elsewhere must not redirect generation out of the experiment output. */
|
|
46
|
+
function assertStaysInsideOutputRoot(outputRoot, syntheticOutputRoot) {
|
|
47
|
+
mkdirSync(path.resolve(outputRoot), { recursive: true });
|
|
48
|
+
if (!existsSync(syntheticOutputRoot))
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
relativeWithinRoot(realpathSync(path.resolve(outputRoot)), realpathSync(syntheticOutputRoot));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`Synthetic repository output ${SYNTHETIC_REPOSITORIES_OUTPUT_SUBDIRECTORY} resolves outside the experiment output root.`);
|
|
55
|
+
}
|
|
56
|
+
}
|