@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +6 -4
  3. package/benchmarks/projects/README.md +3 -1
  4. package/dist/src/cli/help.js +20 -3
  5. package/dist/src/commands/runExperimentDescribeCommand.js +10 -1
  6. package/dist/src/commands/runExperimentRunCommand.js +169 -19
  7. package/dist/src/core/fileGlobs.js +44 -0
  8. package/dist/src/evaluation/localRepositorySubject/config.js +296 -0
  9. package/dist/src/evaluation/localRepositorySubject/evaluationCase.js +36 -0
  10. package/dist/src/evaluation/localRepositorySubject/expectedFiles.js +51 -0
  11. package/dist/src/evaluation/localRepositorySubject/gitRepository.js +128 -0
  12. package/dist/src/evaluation/localRepositorySubject/index.js +8 -0
  13. package/dist/src/evaluation/localRepositorySubject/inventory.js +127 -0
  14. package/dist/src/evaluation/localRepositorySubject/loadLocalRepositorySubject.js +48 -0
  15. package/dist/src/evaluation/localRepositorySubject/manifest.js +52 -0
  16. package/dist/src/evaluation/localRepositorySubject/types.js +55 -0
  17. package/dist/src/evaluation/projectFileTree.js +45 -1
  18. package/dist/src/evaluation/runMyDevKitRetrieval.js +3 -1
  19. package/dist/src/evaluation/runRawFullFileBaseline.js +18 -4
  20. package/dist/src/evaluation/syntheticRepository/config.js +304 -0
  21. package/dist/src/evaluation/syntheticRepository/evaluationCase.js +43 -0
  22. package/dist/src/evaluation/syntheticRepository/identity.js +41 -0
  23. package/dist/src/evaluation/syntheticRepository/index.js +16 -0
  24. package/dist/src/evaluation/syntheticRepository/inspection.js +92 -0
  25. package/dist/src/evaluation/syntheticRepository/layout.js +50 -0
  26. package/dist/src/evaluation/syntheticRepository/manifest.js +281 -0
  27. package/dist/src/evaluation/syntheticRepository/manifestVerification.js +112 -0
  28. package/dist/src/evaluation/syntheticRepository/materialize.js +170 -0
  29. package/dist/src/evaluation/syntheticRepository/planning.js +479 -0
  30. package/dist/src/evaluation/syntheticRepository/prng.js +47 -0
  31. package/dist/src/evaluation/syntheticRepository/renderPython.js +68 -0
  32. package/dist/src/evaluation/syntheticRepository/renderRepository.js +39 -0
  33. package/dist/src/evaluation/syntheticRepository/renderShared.js +93 -0
  34. package/dist/src/evaluation/syntheticRepository/renderTypeScript.js +82 -0
  35. package/dist/src/evaluation/syntheticRepository/topology.js +96 -0
  36. package/dist/src/evaluation/syntheticRepository/types.js +26 -0
  37. package/dist/src/evaluation/targetImmutability/captureTargetSnapshot.js +29 -7
  38. package/dist/src/evaluation/targetImmutability/compareTargetSnapshots.js +4 -0
  39. package/dist/src/experiments/plugins/contextWindowScaling/index.js +1 -0
  40. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectErrors.js +35 -0
  41. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExclusions.js +75 -0
  42. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectExecution.js +136 -0
  43. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +128 -0
  44. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectScratch.js +85 -0
  45. package/dist/src/experiments/plugins/contextWindowScaling/metadata.js +2 -2
  46. package/dist/src/experiments/plugins/contextWindowScaling/plugin.js +93 -0
  47. package/dist/src/experiments/plugins/contextWindowScaling/projectProfile.js +4 -9
  48. package/dist/src/experiments/plugins/contextWindowScaling/syntheticInputs.js +56 -0
  49. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +4 -3
  50. package/dist/src/report/experiments/writePluginExperimentReports.js +8 -2
  51. package/docs/METRICS.md +5 -1
  52. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ import { parseLocalRepositorySubjectConfig } from "./config.js";
2
+ import { buildLocalRepositorySubjectEvaluationCases } from "./evaluationCase.js";
3
+ import { validateExpectedFiles } from "./expectedFiles.js";
4
+ import { readRepositoryIdentity } from "./gitRepository.js";
5
+ import { buildLocalRepositorySubjectInventory, defaultLocalRepositorySubjectFsIo, validateSourceRootOnDisk, } from "./inventory.js";
6
+ import { buildLocalRepositorySubjectManifest } from "./manifest.js";
7
+ import { DEFAULT_LOCAL_REPOSITORY_SUBJECT_MAX_FILE_BYTES, LocalRepositorySubjectConfigError, compareCodeUnits, logicalTargetRootForSubject, } from "./types.js";
8
+ /**
9
+ * Loads a local repository subject in the frozen safety order: validate config, identify the Git repository,
10
+ * validate source roots on disk, inventory (without reading contents), validate required files, then build the
11
+ * privacy-safe manifest and in-memory EvaluationCase objects. Never mutates the subject repository.
12
+ */
13
+ export async function loadLocalRepositorySubject(options) {
14
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_LOCAL_REPOSITORY_SUBJECT_MAX_FILE_BYTES;
15
+ if (!Number.isSafeInteger(maxFileBytes) || maxFileBytes < 1) {
16
+ throw new LocalRepositorySubjectConfigError([`policy.maxFileBytes: must be a positive safe integer (received ${String(maxFileBytes)}).`]);
17
+ }
18
+ const io = options.io ?? defaultLocalRepositorySubjectFsIo;
19
+ const config = parseLocalRepositorySubjectConfig(options.config);
20
+ const { repositoryRoot, identity } = await readRepositoryIdentity(options.repositoryPath);
21
+ const rootErrors = [];
22
+ for (let caseIndex = 0; caseIndex < config.cases.length; caseIndex += 1) {
23
+ const roots = config.cases[caseIndex].sourceRoots;
24
+ for (let rootIndex = 0; rootIndex < roots.length; rootIndex += 1) {
25
+ rootErrors.push(...(await validateSourceRootOnDisk(repositoryRoot, roots[rootIndex], `config.cases[${caseIndex}].sourceRoots[${rootIndex}]`, io)));
26
+ }
27
+ }
28
+ if (rootErrors.length > 0)
29
+ throw new LocalRepositorySubjectConfigError(rootErrors);
30
+ const unionRoots = [...new Set(config.cases.flatMap((subjectCase) => subjectCase.sourceRoots))];
31
+ const inventory = await buildLocalRepositorySubjectInventory(repositoryRoot, unionRoots, maxFileBytes, io);
32
+ const expectedFileErrors = await validateExpectedFiles(repositoryRoot, config.cases, inventory, io);
33
+ if (expectedFileErrors.length > 0)
34
+ throw new LocalRepositorySubjectConfigError(expectedFileErrors);
35
+ const manifest = buildLocalRepositorySubjectManifest(config, identity, maxFileBytes, inventory);
36
+ return {
37
+ subjectId: config.subjectId,
38
+ logicalTargetRoot: logicalTargetRootForSubject(config.subjectId),
39
+ repositoryRoot,
40
+ manifest,
41
+ evaluationCases: buildLocalRepositorySubjectEvaluationCases(config, repositoryRoot),
42
+ eligibleFiles: inventory.eligibleFiles.map((file) => file.path),
43
+ runtimeSafetyExclusions: {
44
+ gitIgnoredFiles: [...inventory.ignoredFiles].sort(compareCodeUnits),
45
+ oversizedFiles: [...inventory.oversizedFiles].sort(compareCodeUnits),
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,52 @@
1
+ import { LOCAL_REPOSITORY_SUBJECT_MANIFEST_SCHEMA_ID, LOCAL_REPOSITORY_SUBJECT_MANIFEST_SCHEMA_VERSION, LOCAL_REPOSITORY_SUBJECT_SAFETY_POLICY_VERSION, compareCodeUnits, logicalTargetRootForSubject, } from "./types.js";
2
+ /** Lowercase text after the last "." of the basename; "(none)" when absent or only a leading dot. */
3
+ export function extensionOf(filePath) {
4
+ const base = filePath.slice(filePath.lastIndexOf("/") + 1);
5
+ const dot = base.lastIndexOf(".");
6
+ if (dot <= 0 || dot === base.length - 1)
7
+ return "(none)";
8
+ return base.slice(dot + 1).toLowerCase();
9
+ }
10
+ function summarizeExtensions(filePaths) {
11
+ const counts = new Map();
12
+ for (const filePath of filePaths) {
13
+ const extension = extensionOf(filePath);
14
+ counts.set(extension, (counts.get(extension) ?? 0) + 1);
15
+ }
16
+ return [...counts.entries()]
17
+ .sort(([left], [right]) => compareCodeUnits(left, right))
18
+ .map(([extension, fileCount]) => ({ extension, fileCount }));
19
+ }
20
+ /** Builds the privacy-safe manifest model. Inputs that are runtime-only (paths, file lists) are reduced to aggregates. */
21
+ export function buildLocalRepositorySubjectManifest(config, identity, maxFileBytes, inventory) {
22
+ const sourceRoots = [...new Set(config.cases.flatMap((subjectCase) => subjectCase.sourceRoots))].sort(compareCodeUnits);
23
+ return {
24
+ schemaId: LOCAL_REPOSITORY_SUBJECT_MANIFEST_SCHEMA_ID,
25
+ schemaVersion: LOCAL_REPOSITORY_SUBJECT_MANIFEST_SCHEMA_VERSION,
26
+ subjectId: config.subjectId,
27
+ logicalTargetRoot: logicalTargetRootForSubject(config.subjectId),
28
+ repository: { commit: identity.commit, branch: identity.branch, workingTreeDirty: identity.workingTreeDirty },
29
+ safetyPolicy: {
30
+ version: LOCAL_REPOSITORY_SUBJECT_SAFETY_POLICY_VERSION,
31
+ maxFileBytes,
32
+ gitIgnoreAuthority: "git",
33
+ symlinkPolicy: "excluded-not-followed",
34
+ },
35
+ sourceRoots,
36
+ caseCount: config.cases.length,
37
+ caseIds: config.cases.map((subjectCase) => subjectCase.id),
38
+ inventory: {
39
+ eligibleFileCount: inventory.counts.eligibleFileCount,
40
+ eligibleByteCount: inventory.counts.eligibleByteCount,
41
+ gitIgnoredCount: inventory.counts.gitIgnoredCount,
42
+ oversizedCount: inventory.counts.oversizedCount,
43
+ symlinkCount: inventory.counts.symlinkCount,
44
+ otherExcludedCount: inventory.counts.otherExcludedCount,
45
+ extensionSummary: summarizeExtensions(inventory.eligibleFiles.map((file) => file.path)),
46
+ },
47
+ };
48
+ }
49
+ /** Deterministic serialization (fixed key order from the builder, LF terminated). */
50
+ export function serializeLocalRepositorySubjectManifest(manifest) {
51
+ return `${JSON.stringify(manifest, null, 2)}\n`;
52
+ }
@@ -0,0 +1,55 @@
1
+ export const LOCAL_REPOSITORY_SUBJECT_CONFIG_SCHEMA_ID = "my-dev-kit-lab-local-repository-subject-config-v1";
2
+ export const LOCAL_REPOSITORY_SUBJECT_CONFIG_SCHEMA_VERSION = "1.0.0";
3
+ export const LOCAL_REPOSITORY_SUBJECT_MANIFEST_SCHEMA_ID = "my-dev-kit-lab-local-repository-subject-manifest-v1";
4
+ export const LOCAL_REPOSITORY_SUBJECT_MANIFEST_SCHEMA_VERSION = "1.0.0";
5
+ /** Operational safety policy (not a quality metric): files larger than this are never eligible subject material. */
6
+ export const DEFAULT_LOCAL_REPOSITORY_SUBJECT_MAX_FILE_BYTES = 1_048_576;
7
+ export const LOCAL_REPOSITORY_SUBJECT_SAFETY_POLICY_VERSION = "1.0.0";
8
+ export const LOCAL_REPOSITORY_SUBJECT_LIMITS = {
9
+ subjectIdMaxLength: 128,
10
+ };
11
+ export const LOCAL_REPOSITORY_SUBJECT_CONFIG_FIELDS = ["schemaVersion", "subjectId", "cases"];
12
+ export const LOCAL_REPOSITORY_SUBJECT_CASE_FIELDS = [
13
+ "id",
14
+ "title",
15
+ "sourceRoots",
16
+ "query",
17
+ "expectedFiles",
18
+ "expectedSymbols",
19
+ "rawIncludeGlobs",
20
+ "answerKey",
21
+ "expectedFacts",
22
+ "taskLocality",
23
+ "promptComplexityHint",
24
+ "projectComplexityRelevance",
25
+ "notes",
26
+ ];
27
+ /** Thrown when the config, or required paths it names, fail validation. Messages carry logical labels only. */
28
+ export class LocalRepositorySubjectConfigError extends Error {
29
+ errors;
30
+ constructor(errors) {
31
+ super(`Invalid local repository subject configuration: ${errors.join("; ")}`);
32
+ this.name = "LocalRepositorySubjectConfigError";
33
+ this.errors = [...errors];
34
+ }
35
+ }
36
+ /** Thrown when the selected repository cannot be identified or classified safely. Never carries raw Git output. */
37
+ export class LocalRepositorySubjectRepositoryError extends Error {
38
+ code;
39
+ constructor(code, message) {
40
+ super(`Local repository subject repository error (${code}): ${message}`);
41
+ this.name = "LocalRepositorySubjectRepositoryError";
42
+ this.code = code;
43
+ }
44
+ }
45
+ export function logicalTargetRootForSubject(subjectId) {
46
+ return `local-repository:${subjectId}`;
47
+ }
48
+ /** Locale-independent ordering by code unit. */
49
+ export function compareCodeUnits(left, right) {
50
+ if (left < right)
51
+ return -1;
52
+ if (left > right)
53
+ return 1;
54
+ return 0;
55
+ }
@@ -45,8 +45,52 @@ export function countFileLines(filePath) {
45
45
  }
46
46
  return content.split(/\r?\n/).length;
47
47
  }
48
- export function buildProjectFileTree(projectRoot) {
48
+ function buildAllowedFileTree(root, allowedRelativeFiles) {
49
+ const top = { children: new Map(), isFile: false };
50
+ for (const allowed of allowedRelativeFiles) {
51
+ const relativePath = allowed.replace(/\\/g, "/");
52
+ if (isExcludedProjectPath(relativePath))
53
+ continue;
54
+ let node = top;
55
+ const segments = relativePath.split("/");
56
+ segments.forEach((segment, index) => {
57
+ let child = node.children.get(segment);
58
+ if (!child) {
59
+ child = { children: new Map(), isFile: index === segments.length - 1 };
60
+ node.children.set(segment, child);
61
+ }
62
+ node = child;
63
+ });
64
+ }
65
+ const entries = [];
66
+ const walk = (node, prefix) => {
67
+ const names = [...node.children.keys()].sort((a, b) => a.localeCompare(b));
68
+ for (const name of names) {
69
+ const child = node.children.get(name);
70
+ const relativePath = prefix === "" ? name : `${prefix}/${name}`;
71
+ if (child.isFile) {
72
+ entries.push({
73
+ path: relativePath,
74
+ kind: "file",
75
+ role: inferFileRole(relativePath, "file"),
76
+ language: inferFileLanguage(relativePath),
77
+ lines: countFileLines(path.join(root, ...relativePath.split("/")))
78
+ });
79
+ }
80
+ else {
81
+ entries.push({ path: relativePath, kind: "directory", role: inferFileRole(relativePath, "directory") });
82
+ walk(child, relativePath);
83
+ }
84
+ }
85
+ };
86
+ walk(top, "");
87
+ return { entries };
88
+ }
89
+ export function buildProjectFileTree(projectRoot, options = {}) {
49
90
  const root = path.resolve(projectRoot);
91
+ if (options.allowedRelativeFiles !== undefined) {
92
+ return buildAllowedFileTree(root, options.allowedRelativeFiles);
93
+ }
50
94
  const entries = [];
51
95
  function walk(currentDir) {
52
96
  const dirEntries = readdirSync(currentDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
@@ -177,6 +177,7 @@ export async function buildMyDevKitIndex(options) {
177
177
  "--root",
178
178
  options.target.absoluteTargetRoot,
179
179
  ...options.target.sourceRoots.flatMap((sourceRoot) => ["--src", sourceRoot]),
180
+ ...(options.excludePaths ?? []).flatMap((excludePath) => ["--exclude", excludePath]),
180
181
  "--out",
181
182
  options.indexDir,
182
183
  ...(mode.kind === "incremental" ? ["--incremental", "--refresh-scope", mode.refreshScope] : []),
@@ -352,7 +353,8 @@ export async function runMyDevKitRetrieval(options) {
352
353
  kitCommand: options.kitCommand,
353
354
  indexDir,
354
355
  commandsDir,
355
- requireKit: options.requireKit
356
+ requireKit: options.requireKit,
357
+ excludePaths: options.excludePaths
356
358
  });
357
359
  if (!index.ok) {
358
360
  return skippedRetrieval(options.evaluationCase.id, [...index.warnings], [index.command], Date.now() - started);
@@ -1,7 +1,8 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { collectFilesForGlobs } from "../core/fileGlobs.js";
1
+ import { lstatSync, readFileSync, statSync } from "node:fs";
2
+ import { collectFilesForGlobs, selectRelativePathsForGlobs } from "../core/fileGlobs.js";
3
+ import { resolveWithinRoot } from "../core/pathSafety.js";
3
4
  import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../core/countTokens.js";
4
- export async function runRawFullFileBaseline(evaluationCase) {
5
+ export async function runRawFullFileBaseline(evaluationCase, options = {}) {
5
6
  const started = Date.now();
6
7
  let stats;
7
8
  try {
@@ -13,7 +14,9 @@ export async function runRawFullFileBaseline(evaluationCase) {
13
14
  if (!stats.isDirectory()) {
14
15
  throw new Error(`Target root is not a directory: ${evaluationCase.targetRoot}`);
15
16
  }
16
- const files = collectFilesForGlobs(evaluationCase.absoluteTargetRoot, evaluationCase.rawIncludeGlobs);
17
+ const files = options.eligibleFiles === undefined
18
+ ? collectFilesForGlobs(evaluationCase.absoluteTargetRoot, evaluationCase.rawIncludeGlobs)
19
+ : selectEligibleFiles(evaluationCase, options.eligibleFiles);
17
20
  const contextText = files
18
21
  .map(({ absolutePath, relativePath }) => `=== FILE: ${relativePath} ===\n${readFileSync(absolutePath, "utf8")}\n`)
19
22
  .join("\n");
@@ -29,3 +32,14 @@ export async function runRawFullFileBaseline(evaluationCase) {
29
32
  durationMs: Date.now() - started
30
33
  };
31
34
  }
35
+ function selectEligibleFiles(evaluationCase, eligibleFiles) {
36
+ return selectRelativePathsForGlobs(eligibleFiles, evaluationCase.rawIncludeGlobs).map((relativePath) => {
37
+ const absolutePath = resolveWithinRoot(evaluationCase.absoluteTargetRoot, relativePath);
38
+ // The eligible list was built earlier; refuse a file that has since been swapped for a link or removed.
39
+ const stats = lstatSync(absolutePath, { throwIfNoEntry: false });
40
+ if (!stats || stats.isSymbolicLink() || !stats.isFile()) {
41
+ throw new Error(`Eligible file is no longer a regular file: ${relativePath}`);
42
+ }
43
+ return { absolutePath, relativePath };
44
+ });
45
+ }
@@ -0,0 +1,304 @@
1
+ import { TASK_LOCALITIES } from "../types.js";
2
+ import { SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION, SYNTHETIC_REPOSITORY_LANGUAGES, SyntheticRepositoryConfigError, } from "./types.js";
3
+ export const SYNTHETIC_REPOSITORY_LIMITS = {
4
+ maxCases: 64,
5
+ caseIdMaxLength: 64,
6
+ seedMaxLength: 256,
7
+ sourceFileCount: { min: 1, max: 10000 },
8
+ moduleDepth: { min: 1, max: 64 },
9
+ internalImportCount: { min: 0, max: 100000 },
10
+ symbolCount: { min: 1, max: 100000 },
11
+ testFileCount: { min: 0, max: 10000 },
12
+ repeatedPatternCount: { min: 0, max: 100000 },
13
+ broadChangeMinSourceFiles: 4,
14
+ crossModuleMinSourceFiles: 2,
15
+ };
16
+ /** Fixed field order of one case; also the canonical serialization order. */
17
+ export const SYNTHETIC_REPOSITORY_CASE_FIELDS = [
18
+ "id",
19
+ "language",
20
+ "seed",
21
+ "sourceFileCount",
22
+ "moduleDepth",
23
+ "internalImportCount",
24
+ "symbolCount",
25
+ "testFileCount",
26
+ "taskLocality",
27
+ "repeatedPatternCount",
28
+ ];
29
+ const COUNT_FIELDS = [
30
+ "sourceFileCount",
31
+ "moduleDepth",
32
+ "internalImportCount",
33
+ "symbolCount",
34
+ "testFileCount",
35
+ "repeatedPatternCount",
36
+ ];
37
+ const CASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
38
+ const WINDOWS_DEVICE_NAME_PATTERN = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
39
+ /** Locale-independent ordering by UTF-16 code unit. */
40
+ export function compareCodeUnits(left, right) {
41
+ if (left < right)
42
+ return -1;
43
+ if (left > right)
44
+ return 1;
45
+ return 0;
46
+ }
47
+ /**
48
+ * Maximum number of unique directed acyclic import edges among `sourceFileCount` modules whose longest
49
+ * dependency chain has exactly `moduleDepth` modules: the edge count of the balanced complete
50
+ * multipartite arrangement, (N^2 - sum of squared layer sizes) / 2.
51
+ */
52
+ export function maxImportEdges(sourceFileCount, moduleDepth) {
53
+ const base = Math.floor(sourceFileCount / moduleDepth);
54
+ const larger = sourceFileCount % moduleDepth;
55
+ const sumOfSquares = larger * (base + 1) * (base + 1) + (moduleDepth - larger) * base * base;
56
+ return (sourceFileCount * sourceFileCount - sumOfSquares) / 2;
57
+ }
58
+ /** Returns the case in canonical field order. Assumes the spec is valid. */
59
+ export function orderCaseSpec(spec) {
60
+ return {
61
+ id: spec.id,
62
+ language: spec.language,
63
+ seed: spec.seed,
64
+ sourceFileCount: spec.sourceFileCount,
65
+ moduleDepth: spec.moduleDepth,
66
+ internalImportCount: spec.internalImportCount,
67
+ symbolCount: spec.symbolCount,
68
+ testFileCount: spec.testFileCount,
69
+ taskLocality: spec.taskLocality,
70
+ repeatedPatternCount: spec.repeatedPatternCount,
71
+ };
72
+ }
73
+ function isPlainObject(value) {
74
+ if (typeof value !== "object" || value === null || Array.isArray(value))
75
+ return false;
76
+ const prototype = Object.getPrototypeOf(value);
77
+ return prototype === Object.prototype || prototype === null;
78
+ }
79
+ function hasOwn(value, key) {
80
+ return Object.prototype.hasOwnProperty.call(value, key);
81
+ }
82
+ function describeValue(value) {
83
+ if (typeof value === "string") {
84
+ return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value);
85
+ }
86
+ if (typeof value === "number" || typeof value === "boolean")
87
+ return String(value);
88
+ if (value === null)
89
+ return "null";
90
+ if (Array.isArray(value))
91
+ return "array";
92
+ return typeof value;
93
+ }
94
+ function hasLoneSurrogate(value) {
95
+ for (let index = 0; index < value.length; index += 1) {
96
+ const unit = value.charCodeAt(index);
97
+ if (unit >= 0xd800 && unit <= 0xdbff) {
98
+ const next = value.charCodeAt(index + 1);
99
+ if (next >= 0xdc00 && next <= 0xdfff) {
100
+ index += 1;
101
+ continue;
102
+ }
103
+ return true;
104
+ }
105
+ if (unit >= 0xdc00 && unit <= 0xdfff)
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+ function validateCaseId(value, label, errors) {
111
+ if (typeof value !== "string") {
112
+ errors.push(`${label}.id: must be a string (received ${describeValue(value)}).`);
113
+ return false;
114
+ }
115
+ const before = errors.length;
116
+ if (value.length < 1 || value.length > SYNTHETIC_REPOSITORY_LIMITS.caseIdMaxLength) {
117
+ errors.push(`${label}.id: length must be between 1 and ${SYNTHETIC_REPOSITORY_LIMITS.caseIdMaxLength}.`);
118
+ }
119
+ else if (!CASE_ID_PATTERN.test(value)) {
120
+ errors.push(`${label}.id: must begin with an ASCII letter or digit and contain only ASCII letters, digits, ".", "_" and "-" (received ${describeValue(value)}).`);
121
+ }
122
+ else {
123
+ if (value.endsWith(".")) {
124
+ errors.push(`${label}.id: must not end with ".".`);
125
+ }
126
+ if (WINDOWS_DEVICE_NAME_PATTERN.test(value.split(".")[0])) {
127
+ errors.push(`${label}.id: must not use a reserved device name (received ${describeValue(value)}).`);
128
+ }
129
+ }
130
+ return errors.length === before;
131
+ }
132
+ function validateSeed(value, label, errors) {
133
+ if (typeof value !== "string") {
134
+ errors.push(`${label}.seed: must be a string (received ${describeValue(value)}).`);
135
+ return false;
136
+ }
137
+ const before = errors.length;
138
+ if (!/\S/.test(value)) {
139
+ errors.push(`${label}.seed: must contain at least one non-whitespace character.`);
140
+ }
141
+ if (value.length > SYNTHETIC_REPOSITORY_LIMITS.seedMaxLength) {
142
+ errors.push(`${label}.seed: must be at most ${SYNTHETIC_REPOSITORY_LIMITS.seedMaxLength} UTF-16 code units.`);
143
+ }
144
+ if (hasLoneSurrogate(value)) {
145
+ errors.push(`${label}.seed: must be well-formed Unicode (lone surrogate found).`);
146
+ }
147
+ return errors.length === before;
148
+ }
149
+ function validateCount(value, field, label, errors) {
150
+ const { min, max } = SYNTHETIC_REPOSITORY_LIMITS[field];
151
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
152
+ errors.push(`${label}.${field}: must be an integer between ${min} and ${max} (received ${describeValue(value)}).`);
153
+ return false;
154
+ }
155
+ return true;
156
+ }
157
+ function validateCase(value, label, errors) {
158
+ if (!isPlainObject(value)) {
159
+ errors.push(`${label}: must be an object (received ${describeValue(value)}).`);
160
+ return undefined;
161
+ }
162
+ const before = errors.length;
163
+ const allowed = new Set(SYNTHETIC_REPOSITORY_CASE_FIELDS);
164
+ for (const key of Object.keys(value).sort(compareCodeUnits)) {
165
+ if (!allowed.has(key))
166
+ errors.push(`${label}: unknown field ${JSON.stringify(key)}.`);
167
+ }
168
+ for (const field of SYNTHETIC_REPOSITORY_CASE_FIELDS) {
169
+ if (!hasOwn(value, field))
170
+ errors.push(`${label}: missing required field ${field}.`);
171
+ }
172
+ const idOk = hasOwn(value, "id") && validateCaseId(value.id, label, errors);
173
+ const languageOk = hasOwn(value, "language") &&
174
+ SYNTHETIC_REPOSITORY_LANGUAGES.includes(value.language);
175
+ if (hasOwn(value, "language") && !languageOk) {
176
+ errors.push(`${label}.language: must be one of ${SYNTHETIC_REPOSITORY_LANGUAGES.join(", ")} (received ${describeValue(value.language)}).`);
177
+ }
178
+ const seedOk = hasOwn(value, "seed") && validateSeed(value.seed, label, errors);
179
+ const countOk = new Map();
180
+ for (const field of COUNT_FIELDS) {
181
+ countOk.set(field, hasOwn(value, field) && validateCount(value[field], field, label, errors));
182
+ }
183
+ const localityOk = hasOwn(value, "taskLocality") && TASK_LOCALITIES.includes(value.taskLocality);
184
+ if (hasOwn(value, "taskLocality") && !localityOk) {
185
+ errors.push(`${label}.taskLocality: must be one of ${TASK_LOCALITIES.join(", ")} (received ${describeValue(value.taskLocality)}).`);
186
+ }
187
+ const sourceFileCount = value.sourceFileCount;
188
+ const moduleDepth = value.moduleDepth;
189
+ const internalImportCount = value.internalImportCount;
190
+ const symbolCount = value.symbolCount;
191
+ const taskLocality = value.taskLocality;
192
+ const sourceOk = countOk.get("sourceFileCount") === true;
193
+ const depthOk = countOk.get("moduleDepth") === true;
194
+ const importsOk = countOk.get("internalImportCount") === true;
195
+ const symbolsOk = countOk.get("symbolCount") === true;
196
+ if (sourceOk && depthOk) {
197
+ if (moduleDepth > sourceFileCount) {
198
+ errors.push(`${label}.moduleDepth: must not exceed sourceFileCount (${moduleDepth} > ${sourceFileCount}).`);
199
+ }
200
+ else if (importsOk) {
201
+ if (moduleDepth > 1 && internalImportCount < moduleDepth - 1) {
202
+ errors.push(`${label}.internalImportCount: must be at least moduleDepth - 1 (${moduleDepth - 1}) to realize moduleDepth ${moduleDepth}.`);
203
+ }
204
+ const capacity = maxImportEdges(sourceFileCount, moduleDepth);
205
+ if (internalImportCount > capacity) {
206
+ errors.push(`${label}.internalImportCount: exceeds the ${capacity} unique acyclic edges possible for sourceFileCount ${sourceFileCount} with exact moduleDepth ${moduleDepth}.`);
207
+ }
208
+ }
209
+ }
210
+ if (sourceOk && symbolsOk && symbolCount < sourceFileCount) {
211
+ errors.push(`${label}.symbolCount: must be at least sourceFileCount (${symbolCount} < ${sourceFileCount}).`);
212
+ }
213
+ if (sourceOk && localityOk) {
214
+ if (taskLocality === "cross-module") {
215
+ if (sourceFileCount < SYNTHETIC_REPOSITORY_LIMITS.crossModuleMinSourceFiles) {
216
+ errors.push(`${label}.taskLocality: cross-module requires sourceFileCount >= 2.`);
217
+ }
218
+ if (importsOk && internalImportCount < 1) {
219
+ errors.push(`${label}.taskLocality: cross-module requires internalImportCount >= 1.`);
220
+ }
221
+ }
222
+ else if (taskLocality === "broad-change" && sourceFileCount < SYNTHETIC_REPOSITORY_LIMITS.broadChangeMinSourceFiles) {
223
+ errors.push(`${label}.taskLocality: broad-change requires sourceFileCount >= 4.`);
224
+ }
225
+ }
226
+ if (errors.length !== before || !idOk || !languageOk || !seedOk || !localityOk)
227
+ return undefined;
228
+ return orderCaseSpec(value);
229
+ }
230
+ /** Validates one case spec and returns it in canonical field order. Never throws, never repairs. */
231
+ export function validateSyntheticRepositoryCaseSpec(input) {
232
+ const errors = [];
233
+ const spec = validateCase(input, "case", errors);
234
+ if (errors.length > 0 || spec === undefined)
235
+ return { ok: false, errors };
236
+ return { ok: true, errors: [], spec };
237
+ }
238
+ /**
239
+ * Strictly validates an unknown value as SyntheticRepositoryConfigV1. On success the returned config is
240
+ * canonical: cases sorted ascending by id (code-unit order), fixed field order, values untouched.
241
+ */
242
+ export function validateSyntheticRepositoryConfig(input) {
243
+ const errors = [];
244
+ if (!isPlainObject(input)) {
245
+ return { ok: false, errors: [`config: must be an object (received ${describeValue(input)}).`] };
246
+ }
247
+ for (const key of Object.keys(input).sort(compareCodeUnits)) {
248
+ if (key !== "schemaVersion" && key !== "cases")
249
+ errors.push(`config: unknown field ${JSON.stringify(key)}.`);
250
+ }
251
+ if (!hasOwn(input, "schemaVersion")) {
252
+ errors.push("config: missing required field schemaVersion.");
253
+ }
254
+ else if (input.schemaVersion !== SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION) {
255
+ errors.push(`config.schemaVersion: must be ${JSON.stringify(SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION)} (received ${describeValue(input.schemaVersion)}).`);
256
+ }
257
+ if (!hasOwn(input, "cases")) {
258
+ errors.push("config: missing required field cases.");
259
+ return { ok: false, errors };
260
+ }
261
+ const cases = input.cases;
262
+ if (!Array.isArray(cases)) {
263
+ errors.push(`config.cases: must be an array (received ${describeValue(cases)}).`);
264
+ return { ok: false, errors };
265
+ }
266
+ if (cases.length < 1 || cases.length > SYNTHETIC_REPOSITORY_LIMITS.maxCases) {
267
+ errors.push(`config.cases: must contain between 1 and ${SYNTHETIC_REPOSITORY_LIMITS.maxCases} cases (received ${cases.length}).`);
268
+ return { ok: false, errors };
269
+ }
270
+ const valid = [];
271
+ const firstIndexById = new Map();
272
+ cases.forEach((candidate, index) => {
273
+ const label = `cases[${index}]`;
274
+ const spec = validateCase(candidate, label, errors);
275
+ if (spec)
276
+ valid.push(spec);
277
+ const id = isPlainObject(candidate) ? candidate.id : undefined;
278
+ if (typeof id === "string") {
279
+ const key = id.toLowerCase();
280
+ const first = firstIndexById.get(key);
281
+ if (first === undefined) {
282
+ firstIndexById.set(key, index);
283
+ }
284
+ else {
285
+ errors.push(`${label}.id: duplicates cases[${first}].id (ids are compared without regard to letter case).`);
286
+ }
287
+ }
288
+ });
289
+ if (errors.length > 0)
290
+ return { ok: false, errors };
291
+ const sorted = [...valid].sort((left, right) => compareCodeUnits(left.id, right.id));
292
+ return {
293
+ ok: true,
294
+ errors: [],
295
+ config: { schemaVersion: SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION, cases: sorted },
296
+ };
297
+ }
298
+ /** Validates then returns the canonical configuration; throws SyntheticRepositoryConfigError when invalid. */
299
+ export function normalizeSyntheticRepositoryConfig(input) {
300
+ const result = validateSyntheticRepositoryConfig(input);
301
+ if (!result.ok)
302
+ throw new SyntheticRepositoryConfigError(result.errors);
303
+ return result.config;
304
+ }
@@ -0,0 +1,43 @@
1
+ import { describeSyntheticTask } from "./layout.js";
2
+ /** The plan's answer key expressed in the existing benchmark answer-key vocabulary (no second correctness contract). */
3
+ export function buildSyntheticAnswerKey(plan) {
4
+ const { factTextById } = describeSyntheticTask(plan);
5
+ return {
6
+ expectedFiles: [...plan.answerKey.expectedFiles],
7
+ expectedSymbols: [...plan.answerKey.expectedSymbols],
8
+ expectedFacts: plan.answerKey.facts.map((fact) => ({
9
+ id: fact.factId,
10
+ text: factTextById.get(fact.factId),
11
+ weight: fact.weight,
12
+ required: fact.required,
13
+ })),
14
+ expectedContextTargets: plan.answerKey.expectedContextTargets.map((target) => ({
15
+ file: target.file,
16
+ symbols: [...target.symbols],
17
+ required: target.required,
18
+ })),
19
+ minimumCorrectFacts: plan.answerKey.minimumCorrectFacts,
20
+ };
21
+ }
22
+ /**
23
+ * Builds the existing EvaluationCase directly in memory (never through readEvaluationCases, which owns
24
+ * repository-contained JSON cases). `targetRoot` is the stable logical root from the manifest; only
25
+ * `absoluteTargetRoot` carries the physical generated repository directory.
26
+ */
27
+ export function buildSyntheticEvaluationCase(plan, manifest, repositoryRoot) {
28
+ const text = describeSyntheticTask(plan);
29
+ return {
30
+ id: plan.task.taskId,
31
+ title: text.title,
32
+ benchmarkProject: plan.logicalProjectId,
33
+ targetRoot: manifest.logicalTargetRoot,
34
+ sourceRoots: [...manifest.sourceRoots],
35
+ query: text.query,
36
+ expectedFiles: [...plan.answerKey.expectedFiles],
37
+ expectedSymbols: [...plan.answerKey.expectedSymbols],
38
+ rawIncludeGlobs: [...manifest.rawIncludeGlobs],
39
+ answerKey: buildSyntheticAnswerKey(plan),
40
+ taskLocality: plan.task.locality,
41
+ absoluteTargetRoot: repositoryRoot,
42
+ };
43
+ }
@@ -0,0 +1,41 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizeSyntheticRepositoryConfig, orderCaseSpec, validateSyntheticRepositoryCaseSpec } from "./config.js";
3
+ import { SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_ID, SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION, SYNTHETIC_REPOSITORY_IDENTITY_ALGORITHM_VERSION, SyntheticRepositoryConfigError, } from "./types.js";
4
+ /** Canonical JSON text of one valid case: fixed key order, exact values. */
5
+ export function canonicalSyntheticRepositoryCaseText(spec) {
6
+ return JSON.stringify(orderCaseSpec(spec));
7
+ }
8
+ /** Canonical JSON text of a normalized configuration (cases already sorted by id). */
9
+ export function canonicalSyntheticRepositoryConfigText(config) {
10
+ return JSON.stringify({
11
+ schemaVersion: config.schemaVersion,
12
+ cases: config.cases.map(orderCaseSpec),
13
+ });
14
+ }
15
+ /**
16
+ * SHA-256 over the UTF-8 bytes of five newline-joined lines: schema id, schema version, identity
17
+ * algorithm version, a domain tag ("case" or "config") and the canonical JSON payload. JSON escapes every
18
+ * newline, so the payload cannot forge an earlier line.
19
+ */
20
+ function hashDomain(domain, payload) {
21
+ const text = [
22
+ SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_ID,
23
+ SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION,
24
+ SYNTHETIC_REPOSITORY_IDENTITY_ALGORITHM_VERSION,
25
+ domain,
26
+ payload,
27
+ ].join("\n");
28
+ return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
29
+ }
30
+ /** Per-case generation identity (lowercase 64-character SHA-256 hex). Throws on an invalid case. */
31
+ export function computeSyntheticRepositoryCaseIdentity(input) {
32
+ const result = validateSyntheticRepositoryCaseSpec(input);
33
+ if (!result.ok)
34
+ throw new SyntheticRepositoryConfigError(result.errors);
35
+ return hashDomain("case", canonicalSyntheticRepositoryCaseText(result.spec));
36
+ }
37
+ /** Whole-configuration identity over the normalized form; input case order never matters. */
38
+ export function computeSyntheticRepositoryConfigIdentity(input) {
39
+ const config = normalizeSyntheticRepositoryConfig(input);
40
+ return hashDomain("config", canonicalSyntheticRepositoryConfigText(config));
41
+ }