@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,170 @@
|
|
|
1
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { relativeWithinRoot, resolveWithinRoot } from "../../core/pathSafety.js";
|
|
4
|
+
import { buildSyntheticEvaluationCase } from "./evaluationCase.js";
|
|
5
|
+
import { SYNTHETIC_REPOSITORY_MANIFEST_FILE_NAME, SYNTHETIC_REPOSITORY_REPOSITORY_DIRECTORY, buildSyntheticRepositoryManifest, serializeSyntheticRepositoryManifest, } from "./manifest.js";
|
|
6
|
+
import { verifySyntheticRepositoryMaterialization } from "./manifestVerification.js";
|
|
7
|
+
import { renderSyntheticRepository } from "./renderRepository.js";
|
|
8
|
+
import { SyntheticRepositoryRenderError } from "./renderShared.js";
|
|
9
|
+
import { SyntheticRepositoryPlanningError } from "./types.js";
|
|
10
|
+
import { verifySyntheticRepositoryPlan } from "./planning.js";
|
|
11
|
+
/** Thrown for every materialization failure; `code` classifies it and `errors` carries bounded evidence. */
|
|
12
|
+
export class SyntheticRepositoryMaterializationError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
errors;
|
|
15
|
+
constructor(code, errors) {
|
|
16
|
+
super(`Synthetic repository materialization failed (${code}): ${errors.join("; ")}`);
|
|
17
|
+
this.name = "SyntheticRepositoryMaterializationError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.errors = [...errors];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const STAGING_PREFIX = ".synthetic-staging-";
|
|
23
|
+
function withinRealRoot(realRoot, candidate) {
|
|
24
|
+
try {
|
|
25
|
+
relativeWithinRoot(realRoot, realpathSync(candidate));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function exists(target) {
|
|
33
|
+
try {
|
|
34
|
+
lstatSync(target);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function describeExisting(caseDirectory, expectedManifestText) {
|
|
42
|
+
const stats = lstatSync(caseDirectory);
|
|
43
|
+
if (stats.isSymbolicLink() || !stats.isDirectory())
|
|
44
|
+
return { reusable: false, reasons: ["existing output child is not a plain directory."] };
|
|
45
|
+
const manifestPath = path.join(caseDirectory, SYNTHETIC_REPOSITORY_MANIFEST_FILE_NAME);
|
|
46
|
+
const repositoryRoot = path.join(caseDirectory, SYNTHETIC_REPOSITORY_REPOSITORY_DIRECTORY);
|
|
47
|
+
let manifestText;
|
|
48
|
+
try {
|
|
49
|
+
manifestText = readFileSync(manifestPath, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { reusable: false, reasons: ["existing output child has no readable synthetic manifest."] };
|
|
53
|
+
}
|
|
54
|
+
if (manifestText !== expectedManifestText) {
|
|
55
|
+
return { reusable: false, reasons: ["existing manifest differs from the manifest of the requested generation."] };
|
|
56
|
+
}
|
|
57
|
+
const repositoryStats = exists(repositoryRoot) ? lstatSync(repositoryRoot) : undefined;
|
|
58
|
+
if (!repositoryStats || repositoryStats.isSymbolicLink() || !repositoryStats.isDirectory()) {
|
|
59
|
+
return { reusable: false, reasons: ["existing repository directory is missing or not a plain directory."] };
|
|
60
|
+
}
|
|
61
|
+
const verification = verifySyntheticRepositoryMaterialization({ manifestPath, repositoryRoot });
|
|
62
|
+
if (!verification.ok)
|
|
63
|
+
return { reusable: false, reasons: verification.issues };
|
|
64
|
+
return { reusable: true };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Materializes a frozen Batch 1 plan below `outputRoot/<caseId>/` as `repository/` plus a sibling
|
|
68
|
+
* `synthetic-repository-manifest.json`. The plan is the single semantic source of truth: it is verified, never
|
|
69
|
+
* re-planned or repaired. A deterministic existing directory is reused only when its manifest and every file
|
|
70
|
+
* 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; on failure only that staging directory is removed.
|
|
72
|
+
*/
|
|
73
|
+
export function materializeSyntheticRepository(plan, outputRoot, io = {}) {
|
|
74
|
+
try {
|
|
75
|
+
verifySyntheticRepositoryPlan(plan);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof SyntheticRepositoryPlanningError)
|
|
79
|
+
throw new SyntheticRepositoryMaterializationError("invalid-plan", error.errors);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
let files;
|
|
83
|
+
try {
|
|
84
|
+
files = renderSyntheticRepository(plan);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof SyntheticRepositoryRenderError)
|
|
88
|
+
throw new SyntheticRepositoryMaterializationError("render-failed", error.errors);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const manifest = buildSyntheticRepositoryManifest(plan, files);
|
|
92
|
+
const manifestText = serializeSyntheticRepositoryManifest(manifest);
|
|
93
|
+
let realRoot;
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(path.resolve(outputRoot), { recursive: true });
|
|
96
|
+
realRoot = realpathSync(path.resolve(outputRoot));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new SyntheticRepositoryMaterializationError("io-failure", [`output root is not usable: ${error instanceof Error ? error.message : String(error)}`]);
|
|
100
|
+
}
|
|
101
|
+
let caseDirectory;
|
|
102
|
+
try {
|
|
103
|
+
caseDirectory = resolveWithinRoot(realRoot, plan.caseId);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new SyntheticRepositoryMaterializationError("path-escape", [error instanceof Error ? error.message : String(error)]);
|
|
107
|
+
}
|
|
108
|
+
const result = (reused) => {
|
|
109
|
+
const repositoryRoot = path.join(caseDirectory, SYNTHETIC_REPOSITORY_REPOSITORY_DIRECTORY);
|
|
110
|
+
return {
|
|
111
|
+
plan,
|
|
112
|
+
manifest,
|
|
113
|
+
manifestPath: path.join(caseDirectory, SYNTHETIC_REPOSITORY_MANIFEST_FILE_NAME),
|
|
114
|
+
repositoryRoot,
|
|
115
|
+
evaluationCase: buildSyntheticEvaluationCase(plan, manifest, repositoryRoot),
|
|
116
|
+
reusedExistingMaterialization: reused,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
if (exists(caseDirectory)) {
|
|
120
|
+
if (!withinRealRoot(realRoot, caseDirectory)) {
|
|
121
|
+
throw new SyntheticRepositoryMaterializationError("path-escape", ["existing output child resolves outside the output root."]);
|
|
122
|
+
}
|
|
123
|
+
const existing = describeExisting(caseDirectory, manifestText);
|
|
124
|
+
if (!existing.reusable) {
|
|
125
|
+
throw new SyntheticRepositoryMaterializationError("collision", [
|
|
126
|
+
`output child ${plan.caseId} already exists and is not an exact materialization of this generation; nothing was changed.`,
|
|
127
|
+
...existing.reasons,
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
return result(true);
|
|
131
|
+
}
|
|
132
|
+
const writeFile = io.writeFile ?? ((absolutePath, bytes) => writeFileSync(absolutePath, bytes, { flag: "wx" }));
|
|
133
|
+
let staging;
|
|
134
|
+
try {
|
|
135
|
+
staging = mkdtempSync(path.join(realRoot, STAGING_PREFIX));
|
|
136
|
+
if (!withinRealRoot(realRoot, staging))
|
|
137
|
+
throw new SyntheticRepositoryMaterializationError("path-escape", ["staging directory resolves outside the output root."]);
|
|
138
|
+
const stagedRepository = path.join(staging, SYNTHETIC_REPOSITORY_REPOSITORY_DIRECTORY);
|
|
139
|
+
mkdirSync(stagedRepository);
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
let target;
|
|
142
|
+
try {
|
|
143
|
+
target = resolveWithinRoot(stagedRepository, file.path);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new SyntheticRepositoryMaterializationError("path-escape", [error instanceof Error ? error.message : String(error)]);
|
|
147
|
+
}
|
|
148
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
149
|
+
writeFile(target, Buffer.from(file.content, "utf8"));
|
|
150
|
+
}
|
|
151
|
+
const stagedManifest = path.join(staging, SYNTHETIC_REPOSITORY_MANIFEST_FILE_NAME);
|
|
152
|
+
writeFile(stagedManifest, Buffer.from(manifestText, "utf8"));
|
|
153
|
+
const verification = verifySyntheticRepositoryMaterialization({ manifestPath: stagedManifest, repositoryRoot: stagedRepository });
|
|
154
|
+
if (!verification.ok)
|
|
155
|
+
throw new SyntheticRepositoryMaterializationError("io-failure", ["staged files failed manifest verification.", ...verification.issues]);
|
|
156
|
+
if (exists(caseDirectory)) {
|
|
157
|
+
throw new SyntheticRepositoryMaterializationError("collision", [`output child ${plan.caseId} appeared during materialization; nothing was overwritten.`]);
|
|
158
|
+
}
|
|
159
|
+
renameSync(staging, caseDirectory);
|
|
160
|
+
staging = undefined;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (staging !== undefined)
|
|
164
|
+
rmSync(staging, { recursive: true, force: true });
|
|
165
|
+
if (error instanceof SyntheticRepositoryMaterializationError)
|
|
166
|
+
throw error;
|
|
167
|
+
throw new SyntheticRepositoryMaterializationError("io-failure", [error instanceof Error ? error.message : String(error)]);
|
|
168
|
+
}
|
|
169
|
+
return result(false);
|
|
170
|
+
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { normalizeSyntheticRepositoryConfig, validateSyntheticRepositoryCaseSpec } from "./config.js";
|
|
2
|
+
import { computeSyntheticRepositoryCaseIdentity } from "./identity.js";
|
|
3
|
+
import { createSyntheticRepositoryPrng } from "./prng.js";
|
|
4
|
+
import { planTopology } from "./topology.js";
|
|
5
|
+
import { SYNTHETIC_REPOSITORY_IDENTITY_ALGORITHM_VERSION, SYNTHETIC_REPOSITORY_PATTERN_ROLES, SYNTHETIC_REPOSITORY_PLAN_SCHEMA_ID, SYNTHETIC_REPOSITORY_PLAN_SCHEMA_VERSION, SYNTHETIC_REPOSITORY_PRNG_VERSION, SyntheticRepositoryConfigError, SyntheticRepositoryPlanningError, } from "./types.js";
|
|
6
|
+
const REQUIRED_FACT_WEIGHT = 2;
|
|
7
|
+
const OPTIONAL_FACT_WEIGHT = 1;
|
|
8
|
+
const MAX_TASK_SYMBOLS_PER_MODULE = 3;
|
|
9
|
+
const BROAD_CHANGE_MODULE_COUNT = 4;
|
|
10
|
+
const SAFE_PLANNED_PATH_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]*(\/[A-Za-z0-9_][A-Za-z0-9_.-]*)*$/;
|
|
11
|
+
function pad(value, width) {
|
|
12
|
+
return String(value).padStart(width, "0");
|
|
13
|
+
}
|
|
14
|
+
function requestedDimensions(spec) {
|
|
15
|
+
return {
|
|
16
|
+
sourceFileCount: spec.sourceFileCount,
|
|
17
|
+
moduleDepth: spec.moduleDepth,
|
|
18
|
+
internalImportCount: spec.internalImportCount,
|
|
19
|
+
symbolCount: spec.symbolCount,
|
|
20
|
+
testFileCount: spec.testFileCount,
|
|
21
|
+
repeatedPatternCount: spec.repeatedPatternCount,
|
|
22
|
+
taskLocality: spec.taskLocality,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function planModules(spec, levelOfModule, prng) {
|
|
26
|
+
const moduleCount = spec.sourceFileCount;
|
|
27
|
+
const extension = spec.language === "typescript" ? ".ts" : ".py";
|
|
28
|
+
const symbolCounts = new Array(moduleCount).fill(1);
|
|
29
|
+
for (let extra = moduleCount; extra < spec.symbolCount; extra += 1) {
|
|
30
|
+
symbolCounts[prng.nextInt(moduleCount)] += 1;
|
|
31
|
+
}
|
|
32
|
+
const modules = [];
|
|
33
|
+
const symbols = [];
|
|
34
|
+
const symbolsByModule = [];
|
|
35
|
+
let ordinal = 0;
|
|
36
|
+
for (let index = 0; index < moduleCount; index += 1) {
|
|
37
|
+
const moduleId = `mod_${pad(index + 1, 5)}`;
|
|
38
|
+
const own = [];
|
|
39
|
+
for (let count = 0; count < symbolCounts[index]; count += 1) {
|
|
40
|
+
ordinal += 1;
|
|
41
|
+
const name = `sym_${pad(ordinal, 6)}`;
|
|
42
|
+
own.push({ symbolId: `${moduleId}#${name}`, name, ordinal, moduleId });
|
|
43
|
+
}
|
|
44
|
+
symbols.push(...own);
|
|
45
|
+
symbolsByModule.push(own);
|
|
46
|
+
modules.push({
|
|
47
|
+
moduleId,
|
|
48
|
+
index,
|
|
49
|
+
level: levelOfModule[index],
|
|
50
|
+
path: `src/group_${pad(Math.floor(index / 100) + 1, 3)}/${moduleId}${extension}`,
|
|
51
|
+
symbolIds: own.map((symbol) => symbol.symbolId),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { modules, symbols, symbolsByModule };
|
|
55
|
+
}
|
|
56
|
+
function pickConsecutiveSymbols(own, maxCount, prng) {
|
|
57
|
+
const count = 1 + prng.nextInt(Math.min(maxCount, own.length));
|
|
58
|
+
const start = prng.nextInt(own.length);
|
|
59
|
+
const indices = new Set();
|
|
60
|
+
for (let offset = 0; offset < count; offset += 1)
|
|
61
|
+
indices.add((start + offset) % own.length);
|
|
62
|
+
return [...indices].sort((left, right) => left - right).map((index) => own[index]);
|
|
63
|
+
}
|
|
64
|
+
function planTestFiles(spec, modules, symbolsByModule, prng) {
|
|
65
|
+
if (spec.testFileCount === 0)
|
|
66
|
+
return [];
|
|
67
|
+
const suffix = spec.language === "typescript" ? ".test.ts" : ".py";
|
|
68
|
+
const order = prng.shuffle(Array.from({ length: modules.length }, (_, index) => index));
|
|
69
|
+
const tests = [];
|
|
70
|
+
for (let position = 0; position < spec.testFileCount; position += 1) {
|
|
71
|
+
const target = order[position % modules.length];
|
|
72
|
+
const testId = `test_${pad(position + 1, 5)}`;
|
|
73
|
+
tests.push({
|
|
74
|
+
testId,
|
|
75
|
+
path: `tests/${testId}${suffix}`,
|
|
76
|
+
targetModuleId: modules[target].moduleId,
|
|
77
|
+
targetSymbolIds: pickConsecutiveSymbols(symbolsByModule[target], MAX_TASK_SYMBOLS_PER_MODULE, prng).map((symbol) => symbol.symbolId),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return tests;
|
|
81
|
+
}
|
|
82
|
+
function planRepeatedPatterns(spec, modules, prng) {
|
|
83
|
+
const patterns = [];
|
|
84
|
+
for (let position = 0; position < spec.repeatedPatternCount; position += 1) {
|
|
85
|
+
const target = prng.nextInt(modules.length);
|
|
86
|
+
const role = SYNTHETIC_REPOSITORY_PATTERN_ROLES[prng.nextInt(SYNTHETIC_REPOSITORY_PATTERN_ROLES.length)];
|
|
87
|
+
patterns.push({
|
|
88
|
+
patternId: `pattern_${pad(position + 1, 6)}`,
|
|
89
|
+
ordinal: position + 1,
|
|
90
|
+
role,
|
|
91
|
+
targetModuleId: modules[target].moduleId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return patterns;
|
|
95
|
+
}
|
|
96
|
+
function planTask(spec, modules, symbolsByModule, edges, tests, prng) {
|
|
97
|
+
const taskId = `${spec.id}-task`;
|
|
98
|
+
const moduleIndexById = new Map(modules.map((module) => [module.moduleId, module.index]));
|
|
99
|
+
if (spec.taskLocality === "localized") {
|
|
100
|
+
const owner = prng.nextInt(modules.length);
|
|
101
|
+
const symbols = pickConsecutiveSymbols(symbolsByModule[owner], MAX_TASK_SYMBOLS_PER_MODULE, prng);
|
|
102
|
+
const symbolIds = symbols.map((symbol) => symbol.symbolId);
|
|
103
|
+
return {
|
|
104
|
+
taskId,
|
|
105
|
+
locality: "localized",
|
|
106
|
+
ownerModuleIds: [modules[owner].moduleId],
|
|
107
|
+
symbolIds,
|
|
108
|
+
relationEdge: null,
|
|
109
|
+
associatedTestIds: tests.filter((test) => test.targetModuleId === modules[owner].moduleId).map((test) => test.testId),
|
|
110
|
+
queryPlan: { kind: "locate-symbols", subjectSymbolIds: symbolIds },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
let selected;
|
|
114
|
+
let relationEdge = null;
|
|
115
|
+
if (spec.taskLocality === "cross-module") {
|
|
116
|
+
if (edges.length === 0 || modules.length < 2) {
|
|
117
|
+
throw new SyntheticRepositoryPlanningError(["cross-module task requires at least two modules and one import edge."]);
|
|
118
|
+
}
|
|
119
|
+
relationEdge = edges[prng.nextInt(edges.length)];
|
|
120
|
+
selected = [moduleIndexById.get(relationEdge.from), moduleIndexById.get(relationEdge.to)];
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (modules.length < BROAD_CHANGE_MODULE_COUNT) {
|
|
124
|
+
throw new SyntheticRepositoryPlanningError(["broad-change task requires at least four modules."]);
|
|
125
|
+
}
|
|
126
|
+
selected = [];
|
|
127
|
+
for (let stratum = 0; stratum < BROAD_CHANGE_MODULE_COUNT; stratum += 1) {
|
|
128
|
+
const low = Math.floor((stratum * modules.length) / BROAD_CHANGE_MODULE_COUNT);
|
|
129
|
+
const high = Math.floor(((stratum + 1) * modules.length) / BROAD_CHANGE_MODULE_COUNT);
|
|
130
|
+
selected.push(low + prng.nextInt(high - low));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
selected.sort((left, right) => left - right);
|
|
134
|
+
const symbolIds = selected.map((index) => {
|
|
135
|
+
const own = symbolsByModule[index];
|
|
136
|
+
return own[prng.nextInt(own.length)].symbolId;
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
taskId,
|
|
140
|
+
locality: spec.taskLocality,
|
|
141
|
+
ownerModuleIds: selected.map((index) => modules[index].moduleId),
|
|
142
|
+
symbolIds,
|
|
143
|
+
relationEdge,
|
|
144
|
+
associatedTestIds: [],
|
|
145
|
+
queryPlan: {
|
|
146
|
+
kind: spec.taskLocality === "cross-module" ? "trace-import" : "broad-change-survey",
|
|
147
|
+
subjectSymbolIds: symbolIds,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function planAnswerKey(task, modules, symbols) {
|
|
152
|
+
const moduleById = new Map(modules.map((module) => [module.moduleId, module]));
|
|
153
|
+
const symbolById = new Map(symbols.map((symbol) => [symbol.symbolId, symbol]));
|
|
154
|
+
const taskSymbols = task.symbolIds.map((symbolId) => symbolById.get(symbolId));
|
|
155
|
+
const facts = [];
|
|
156
|
+
const addFact = (fact) => {
|
|
157
|
+
facts.push({
|
|
158
|
+
factId: `fact_${pad(facts.length + 1, 3)}`,
|
|
159
|
+
weight: fact.required ? REQUIRED_FACT_WEIGHT : OPTIONAL_FACT_WEIGHT,
|
|
160
|
+
...fact,
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
for (const symbol of taskSymbols) {
|
|
164
|
+
addFact({ role: "symbol-definition", required: true, symbolIds: [symbol.symbolId], edge: null, testId: null });
|
|
165
|
+
}
|
|
166
|
+
if (task.relationEdge) {
|
|
167
|
+
addFact({ role: "import-relation", required: true, symbolIds: [], edge: task.relationEdge, testId: null });
|
|
168
|
+
}
|
|
169
|
+
for (const testId of task.associatedTestIds) {
|
|
170
|
+
addFact({ role: "associated-test", required: false, symbolIds: [], edge: null, testId });
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
expectedFiles: task.ownerModuleIds.map((moduleId) => moduleById.get(moduleId).path),
|
|
174
|
+
expectedSymbols: taskSymbols.map((symbol) => symbol.name),
|
|
175
|
+
expectedSymbolIds: [...task.symbolIds],
|
|
176
|
+
facts,
|
|
177
|
+
minimumCorrectFacts: facts.filter((fact) => fact.required).length,
|
|
178
|
+
expectedContextTargets: task.ownerModuleIds.map((moduleId) => ({
|
|
179
|
+
file: moduleById.get(moduleId).path,
|
|
180
|
+
symbols: taskSymbols.filter((symbol) => symbol.moduleId === moduleId).map((symbol) => symbol.name),
|
|
181
|
+
required: true,
|
|
182
|
+
})),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Broad-change stratum boundaries: stratum s starts at floor(s * moduleCount / 4). */
|
|
186
|
+
function stratumOf(moduleIndex, moduleCount) {
|
|
187
|
+
for (let stratum = BROAD_CHANGE_MODULE_COUNT - 1; stratum > 0; stratum -= 1) {
|
|
188
|
+
if (moduleIndex >= Math.floor((stratum * moduleCount) / BROAD_CHANGE_MODULE_COUNT))
|
|
189
|
+
return stratum;
|
|
190
|
+
}
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
function classifyLocality(ownerCount) {
|
|
194
|
+
if (ownerCount === 1)
|
|
195
|
+
return "localized";
|
|
196
|
+
if (ownerCount >= BROAD_CHANGE_MODULE_COUNT)
|
|
197
|
+
return "broad-change";
|
|
198
|
+
if (ownerCount >= 2)
|
|
199
|
+
return "cross-module";
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Recomputes every measurable dimension of a plan from its own arrays and checks reference integrity, path
|
|
204
|
+
* safety, graph shape, locality semantics and answer-key satisfiability. Returns what was found; never throws.
|
|
205
|
+
*/
|
|
206
|
+
function inspectPlan(plan) {
|
|
207
|
+
const errors = [];
|
|
208
|
+
const moduleCount = plan.modules.length;
|
|
209
|
+
const moduleIndexById = new Map();
|
|
210
|
+
plan.modules.forEach((module, index) => {
|
|
211
|
+
if (moduleIndexById.has(module.moduleId))
|
|
212
|
+
errors.push(`duplicate module id ${module.moduleId}.`);
|
|
213
|
+
moduleIndexById.set(module.moduleId, index);
|
|
214
|
+
if (module.index !== index)
|
|
215
|
+
errors.push(`module ${module.moduleId} index ${module.index} does not match its position ${index}.`);
|
|
216
|
+
});
|
|
217
|
+
const seenPaths = new Set();
|
|
218
|
+
for (const path of [...plan.modules.map((module) => module.path), ...plan.testFiles.map((test) => test.path)]) {
|
|
219
|
+
if (!SAFE_PLANNED_PATH_PATTERN.test(path))
|
|
220
|
+
errors.push(`unsafe planned path ${JSON.stringify(path)}.`);
|
|
221
|
+
if (seenPaths.has(path))
|
|
222
|
+
errors.push(`duplicate planned path ${path}.`);
|
|
223
|
+
seenPaths.add(path);
|
|
224
|
+
}
|
|
225
|
+
const symbolById = new Map();
|
|
226
|
+
const symbolNames = new Set();
|
|
227
|
+
const symbolsPerModule = new Map();
|
|
228
|
+
for (const symbol of plan.symbols) {
|
|
229
|
+
if (symbolById.has(symbol.symbolId))
|
|
230
|
+
errors.push(`duplicate symbol id ${symbol.symbolId}.`);
|
|
231
|
+
if (symbolNames.has(symbol.name))
|
|
232
|
+
errors.push(`duplicate symbol name ${symbol.name}.`);
|
|
233
|
+
symbolById.set(symbol.symbolId, symbol);
|
|
234
|
+
symbolNames.add(symbol.name);
|
|
235
|
+
if (!moduleIndexById.has(symbol.moduleId)) {
|
|
236
|
+
errors.push(`symbol ${symbol.symbolId} references missing module ${symbol.moduleId}.`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const list = symbolsPerModule.get(symbol.moduleId) ?? [];
|
|
240
|
+
list.push(symbol.symbolId);
|
|
241
|
+
symbolsPerModule.set(symbol.moduleId, list);
|
|
242
|
+
}
|
|
243
|
+
for (const module of plan.modules) {
|
|
244
|
+
const owned = symbolsPerModule.get(module.moduleId) ?? [];
|
|
245
|
+
if (owned.length === 0)
|
|
246
|
+
errors.push(`module ${module.moduleId} owns no symbol.`);
|
|
247
|
+
if (owned.length !== module.symbolIds.length || owned.some((symbolId, index) => symbolId !== module.symbolIds[index])) {
|
|
248
|
+
errors.push(`module ${module.moduleId} symbolIds do not match the symbols that reference it.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const adjacency = Array.from({ length: moduleCount }, () => []);
|
|
252
|
+
const indegree = new Array(moduleCount).fill(0);
|
|
253
|
+
const edgeKeys = new Set();
|
|
254
|
+
for (const edge of plan.importEdges) {
|
|
255
|
+
const from = moduleIndexById.get(edge.from);
|
|
256
|
+
const to = moduleIndexById.get(edge.to);
|
|
257
|
+
if (from === undefined || to === undefined) {
|
|
258
|
+
errors.push(`edge ${edge.from} -> ${edge.to} references a missing module.`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (from === to)
|
|
262
|
+
errors.push(`self edge on ${edge.from}.`);
|
|
263
|
+
const key = `${edge.from}>${edge.to}`;
|
|
264
|
+
if (edgeKeys.has(key))
|
|
265
|
+
errors.push(`duplicate edge ${edge.from} -> ${edge.to}.`);
|
|
266
|
+
edgeKeys.add(key);
|
|
267
|
+
adjacency[from].push(to);
|
|
268
|
+
indegree[to] += 1;
|
|
269
|
+
}
|
|
270
|
+
// Longest chain (in modules) via Kahn's algorithm; a short count means the graph has a cycle.
|
|
271
|
+
const chainLength = new Array(moduleCount).fill(1);
|
|
272
|
+
const queue = [];
|
|
273
|
+
for (let index = 0; index < moduleCount; index += 1)
|
|
274
|
+
if (indegree[index] === 0)
|
|
275
|
+
queue.push(index);
|
|
276
|
+
let processed = 0;
|
|
277
|
+
let moduleDepth = 0;
|
|
278
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
279
|
+
const current = queue[head];
|
|
280
|
+
processed += 1;
|
|
281
|
+
moduleDepth = Math.max(moduleDepth, chainLength[current]);
|
|
282
|
+
for (const next of adjacency[current]) {
|
|
283
|
+
chainLength[next] = Math.max(chainLength[next], chainLength[current] + 1);
|
|
284
|
+
indegree[next] -= 1;
|
|
285
|
+
if (indegree[next] === 0)
|
|
286
|
+
queue.push(next);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (processed < moduleCount)
|
|
290
|
+
errors.push("import graph contains a cycle.");
|
|
291
|
+
const testIds = new Set();
|
|
292
|
+
for (const test of plan.testFiles) {
|
|
293
|
+
if (testIds.has(test.testId))
|
|
294
|
+
errors.push(`duplicate test id ${test.testId}.`);
|
|
295
|
+
testIds.add(test.testId);
|
|
296
|
+
if (!moduleIndexById.has(test.targetModuleId))
|
|
297
|
+
errors.push(`test ${test.testId} targets missing module ${test.targetModuleId}.`);
|
|
298
|
+
if (test.targetSymbolIds.length === 0)
|
|
299
|
+
errors.push(`test ${test.testId} references no symbol.`);
|
|
300
|
+
for (const symbolId of test.targetSymbolIds) {
|
|
301
|
+
if (symbolById.get(symbolId)?.moduleId !== test.targetModuleId) {
|
|
302
|
+
errors.push(`test ${test.testId} references symbol ${symbolId} that is not owned by ${test.targetModuleId}.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const patternIds = new Set();
|
|
307
|
+
for (const pattern of plan.repeatedPatterns) {
|
|
308
|
+
if (patternIds.has(pattern.patternId))
|
|
309
|
+
errors.push(`duplicate pattern id ${pattern.patternId}.`);
|
|
310
|
+
patternIds.add(pattern.patternId);
|
|
311
|
+
if (!moduleIndexById.has(pattern.targetModuleId)) {
|
|
312
|
+
errors.push(`pattern ${pattern.patternId} targets missing module ${pattern.targetModuleId}.`);
|
|
313
|
+
}
|
|
314
|
+
if (!SYNTHETIC_REPOSITORY_PATTERN_ROLES.includes(pattern.role)) {
|
|
315
|
+
errors.push(`pattern ${pattern.patternId} has unknown role ${pattern.role}.`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const { task, answerKey } = plan;
|
|
319
|
+
const ownerSet = new Set(task.ownerModuleIds);
|
|
320
|
+
if (ownerSet.size !== task.ownerModuleIds.length)
|
|
321
|
+
errors.push("task owner modules are not unique.");
|
|
322
|
+
const symbolOwners = new Set();
|
|
323
|
+
for (const symbolId of task.symbolIds) {
|
|
324
|
+
const symbol = symbolById.get(symbolId);
|
|
325
|
+
if (!symbol) {
|
|
326
|
+
errors.push(`task references missing symbol ${symbolId}.`);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
symbolOwners.add(symbol.moduleId);
|
|
330
|
+
if (!ownerSet.has(symbol.moduleId))
|
|
331
|
+
errors.push(`task symbol ${symbolId} is outside the task owner modules.`);
|
|
332
|
+
}
|
|
333
|
+
for (const owner of task.ownerModuleIds) {
|
|
334
|
+
if (!moduleIndexById.has(owner))
|
|
335
|
+
errors.push(`task owner ${owner} is missing from the plan.`);
|
|
336
|
+
if (!symbolOwners.has(owner))
|
|
337
|
+
errors.push(`task owner ${owner} has no task symbol.`);
|
|
338
|
+
}
|
|
339
|
+
let taskLocality = classifyLocality(symbolOwners.size);
|
|
340
|
+
if (taskLocality === undefined) {
|
|
341
|
+
errors.push("task spans no source module.");
|
|
342
|
+
taskLocality = task.locality;
|
|
343
|
+
}
|
|
344
|
+
if (task.locality !== taskLocality) {
|
|
345
|
+
errors.push(`task locality ${task.locality} does not match its structure (${taskLocality}).`);
|
|
346
|
+
}
|
|
347
|
+
if (taskLocality === "cross-module") {
|
|
348
|
+
const relation = task.relationEdge;
|
|
349
|
+
if (!relation || !edgeKeys.has(`${relation.from}>${relation.to}`) || !ownerSet.has(relation.from) || !ownerSet.has(relation.to)) {
|
|
350
|
+
errors.push("cross-module task owners are not connected by a planned import edge.");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (taskLocality === "broad-change") {
|
|
354
|
+
const strata = new Set(task.ownerModuleIds.map((owner) => stratumOf(moduleIndexById.get(owner) ?? 0, moduleCount)));
|
|
355
|
+
if (strata.size < BROAD_CHANGE_MODULE_COUNT)
|
|
356
|
+
errors.push("broad-change task owners are not spread over four module strata.");
|
|
357
|
+
}
|
|
358
|
+
for (const testId of task.associatedTestIds) {
|
|
359
|
+
const test = plan.testFiles.find((candidate) => candidate.testId === testId);
|
|
360
|
+
if (!test || !ownerSet.has(test.targetModuleId))
|
|
361
|
+
errors.push(`task associated test ${testId} does not resolve to a task owner.`);
|
|
362
|
+
}
|
|
363
|
+
const modulePathById = new Map(plan.modules.map((module) => [module.moduleId, module.path]));
|
|
364
|
+
const expectedFiles = task.ownerModuleIds.map((owner) => modulePathById.get(owner));
|
|
365
|
+
if (answerKey.expectedFiles.length === 0 || answerKey.expectedFiles.join("\n") !== expectedFiles.join("\n")) {
|
|
366
|
+
errors.push("answer key expectedFiles do not match the task owner modules.");
|
|
367
|
+
}
|
|
368
|
+
const expectedNames = task.symbolIds.map((symbolId) => symbolById.get(symbolId)?.name);
|
|
369
|
+
if (answerKey.expectedSymbols.length === 0 || answerKey.expectedSymbols.join("\n") !== expectedNames.join("\n")) {
|
|
370
|
+
errors.push("answer key expectedSymbols do not match the task symbols.");
|
|
371
|
+
}
|
|
372
|
+
if (answerKey.expectedSymbolIds.join("\n") !== task.symbolIds.join("\n")) {
|
|
373
|
+
errors.push("answer key expectedSymbolIds do not match the task symbols.");
|
|
374
|
+
}
|
|
375
|
+
for (const target of answerKey.expectedContextTargets) {
|
|
376
|
+
if (!seenPaths.has(target.file))
|
|
377
|
+
errors.push(`context target file ${target.file} is not planned.`);
|
|
378
|
+
for (const name of target.symbols)
|
|
379
|
+
if (!symbolNames.has(name))
|
|
380
|
+
errors.push(`context target symbol ${name} is not planned.`);
|
|
381
|
+
}
|
|
382
|
+
const factIds = new Set();
|
|
383
|
+
let requiredFacts = 0;
|
|
384
|
+
for (const fact of answerKey.facts) {
|
|
385
|
+
if (factIds.has(fact.factId))
|
|
386
|
+
errors.push(`duplicate fact id ${fact.factId}.`);
|
|
387
|
+
factIds.add(fact.factId);
|
|
388
|
+
if (!(fact.weight > 0))
|
|
389
|
+
errors.push(`fact ${fact.factId} must have a positive weight.`);
|
|
390
|
+
if (fact.required)
|
|
391
|
+
requiredFacts += 1;
|
|
392
|
+
for (const symbolId of fact.symbolIds) {
|
|
393
|
+
if (!task.symbolIds.includes(symbolId))
|
|
394
|
+
errors.push(`fact ${fact.factId} references symbol ${symbolId} outside the task.`);
|
|
395
|
+
}
|
|
396
|
+
if (fact.edge && !edgeKeys.has(`${fact.edge.from}>${fact.edge.to}`))
|
|
397
|
+
errors.push(`fact ${fact.factId} references a missing edge.`);
|
|
398
|
+
if (fact.testId !== null && !testIds.has(fact.testId))
|
|
399
|
+
errors.push(`fact ${fact.factId} references missing test ${fact.testId}.`);
|
|
400
|
+
}
|
|
401
|
+
if (!Number.isInteger(answerKey.minimumCorrectFacts) ||
|
|
402
|
+
answerKey.minimumCorrectFacts < 1 ||
|
|
403
|
+
answerKey.minimumCorrectFacts > requiredFacts) {
|
|
404
|
+
errors.push("answer key minimumCorrectFacts is not satisfiable by the required facts.");
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
errors,
|
|
408
|
+
realized: {
|
|
409
|
+
sourceFileCount: moduleCount,
|
|
410
|
+
moduleDepth,
|
|
411
|
+
internalImportCount: plan.importEdges.length,
|
|
412
|
+
symbolCount: plan.symbols.length,
|
|
413
|
+
testFileCount: plan.testFiles.length,
|
|
414
|
+
repeatedPatternCount: plan.repeatedPatterns.length,
|
|
415
|
+
taskLocality,
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** Verifies a plan against its own structure and its requested dimensions; throws on any mismatch. */
|
|
420
|
+
export function verifySyntheticRepositoryPlan(plan) {
|
|
421
|
+
const { realized, errors } = inspectPlan(plan);
|
|
422
|
+
for (const key of Object.keys(plan.requested)) {
|
|
423
|
+
if (plan.requested[key] !== realized[key]) {
|
|
424
|
+
errors.push(`realized ${key} ${String(realized[key])} does not equal requested ${String(plan.requested[key])}.`);
|
|
425
|
+
}
|
|
426
|
+
if (plan.realized[key] !== realized[key]) {
|
|
427
|
+
errors.push(`recorded realized ${key} ${String(plan.realized[key])} does not match the plan (${String(realized[key])}).`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (errors.length > 0)
|
|
431
|
+
throw new SyntheticRepositoryPlanningError(errors);
|
|
432
|
+
return realized;
|
|
433
|
+
}
|
|
434
|
+
/** Plans one repository. Re-validates the case, never repairs, and never returns a partial plan. */
|
|
435
|
+
export function planSyntheticRepositoryCase(input) {
|
|
436
|
+
const validation = validateSyntheticRepositoryCaseSpec(input);
|
|
437
|
+
if (!validation.ok)
|
|
438
|
+
throw new SyntheticRepositoryConfigError(validation.errors);
|
|
439
|
+
const spec = validation.spec;
|
|
440
|
+
const generationIdentity = computeSyntheticRepositoryCaseIdentity(spec);
|
|
441
|
+
const prng = createSyntheticRepositoryPrng(generationIdentity);
|
|
442
|
+
const topology = planTopology(spec, prng);
|
|
443
|
+
const { modules, symbols, symbolsByModule } = planModules(spec, topology.levelOfModule, prng);
|
|
444
|
+
const importEdges = topology.edges.map(([from, to]) => ({
|
|
445
|
+
from: modules[from].moduleId,
|
|
446
|
+
to: modules[to].moduleId,
|
|
447
|
+
}));
|
|
448
|
+
const testFiles = planTestFiles(spec, modules, symbolsByModule, prng);
|
|
449
|
+
const repeatedPatterns = planRepeatedPatterns(spec, modules, prng);
|
|
450
|
+
const task = planTask(spec, modules, symbolsByModule, importEdges, testFiles, prng);
|
|
451
|
+
const answerKey = planAnswerKey(task, modules, symbols);
|
|
452
|
+
const requested = requestedDimensions(spec);
|
|
453
|
+
const plan = {
|
|
454
|
+
schemaId: SYNTHETIC_REPOSITORY_PLAN_SCHEMA_ID,
|
|
455
|
+
schemaVersion: SYNTHETIC_REPOSITORY_PLAN_SCHEMA_VERSION,
|
|
456
|
+
prngVersion: SYNTHETIC_REPOSITORY_PRNG_VERSION,
|
|
457
|
+
identityAlgorithmVersion: SYNTHETIC_REPOSITORY_IDENTITY_ALGORITHM_VERSION,
|
|
458
|
+
caseId: spec.id,
|
|
459
|
+
generationIdentity,
|
|
460
|
+
language: spec.language,
|
|
461
|
+
logicalProjectId: `synthetic-${spec.id}`,
|
|
462
|
+
requested,
|
|
463
|
+
realized: requested,
|
|
464
|
+
modules,
|
|
465
|
+
symbols,
|
|
466
|
+
importEdges,
|
|
467
|
+
testFiles,
|
|
468
|
+
repeatedPatterns,
|
|
469
|
+
task,
|
|
470
|
+
answerKey,
|
|
471
|
+
};
|
|
472
|
+
plan.realized = inspectPlan(plan).realized;
|
|
473
|
+
verifySyntheticRepositoryPlan(plan);
|
|
474
|
+
return plan;
|
|
475
|
+
}
|
|
476
|
+
/** Normalizes a configuration (throwing on invalid input) and plans every case in canonical id order. */
|
|
477
|
+
export function planSyntheticRepositories(input) {
|
|
478
|
+
return normalizeSyntheticRepositoryConfig(input).cases.map((spec) => planSyntheticRepositoryCase(spec));
|
|
479
|
+
}
|