@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,47 @@
1
+ const UINT32_RANGE = 4294967296;
2
+ const IDENTITY_PATTERN = /^[0-9a-f]{64}$/;
3
+ /** Initial uint32 state: the first four bytes of the SHA-256 identity digest, big-endian. */
4
+ export function syntheticRepositoryPrngInitialState(identity) {
5
+ if (!IDENTITY_PATTERN.test(identity)) {
6
+ throw new Error("PRNG identity must be a lowercase 64-character SHA-256 hex string.");
7
+ }
8
+ const b0 = Number.parseInt(identity.slice(0, 2), 16);
9
+ const b1 = Number.parseInt(identity.slice(2, 4), 16);
10
+ const b2 = Number.parseInt(identity.slice(4, 6), 16);
11
+ const b3 = Number.parseInt(identity.slice(6, 8), 16);
12
+ return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) >>> 0;
13
+ }
14
+ /** Generator-owned Mulberry32 stream (version "mulberry32-v1"). Never reads Math.random. */
15
+ export function createSyntheticRepositoryPrng(identity) {
16
+ let state = syntheticRepositoryPrngInitialState(identity);
17
+ const nextUint32 = () => {
18
+ state = (state + 0x6d2b79f5) >>> 0;
19
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
20
+ t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
21
+ return (t ^ (t >>> 14)) >>> 0;
22
+ };
23
+ const nextInt = (maxExclusive) => {
24
+ if (!Number.isInteger(maxExclusive) || maxExclusive < 1 || maxExclusive > UINT32_RANGE) {
25
+ throw new RangeError(`nextInt bound must be an integer between 1 and ${UINT32_RANGE} (received ${maxExclusive}).`);
26
+ }
27
+ const limit = UINT32_RANGE - (UINT32_RANGE % maxExclusive);
28
+ let value = nextUint32();
29
+ while (value >= limit)
30
+ value = nextUint32();
31
+ return value % maxExclusive;
32
+ };
33
+ return {
34
+ nextUint32,
35
+ nextFloat: () => nextUint32() / UINT32_RANGE,
36
+ nextInt,
37
+ shuffle(items) {
38
+ for (let index = items.length - 1; index > 0; index -= 1) {
39
+ const swapIndex = nextInt(index + 1);
40
+ const held = items[index];
41
+ items[index] = items[swapIndex];
42
+ items[swapIndex] = held;
43
+ }
44
+ return items;
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,68 @@
1
+ import { assertLogicalPath, buildRenderContext, canonicalText, compareLogicalPaths, exportedSymbolOf, patternCommentLines, SyntheticRepositoryRenderError, } from "./renderShared.js";
2
+ const PY_PROJECT_TOML = canonicalText([
3
+ "[project]",
4
+ 'name = "synthetic-repository"',
5
+ 'version = "0.0.0"',
6
+ "",
7
+ "[tool.pytest.ini_options]",
8
+ 'pythonpath = ["src"]',
9
+ 'testpaths = ["tests"]',
10
+ ]);
11
+ /**
12
+ * Dotted module name of a planned source path. Source modules live under `src/`, which is the Python
13
+ * import root (`pythonpath = ["src"]`); directories are implicit namespace packages, so no `__init__.py`
14
+ * support files are needed and none can inflate the planned source-file count.
15
+ */
16
+ export function pythonModuleName(logicalPath) {
17
+ if (!logicalPath.startsWith("src/") || !logicalPath.endsWith(".py")) {
18
+ throw new SyntheticRepositoryRenderError([`python source path ${JSON.stringify(logicalPath)} must be under src/ and end in .py.`]);
19
+ }
20
+ return logicalPath.slice("src/".length, -".py".length).split("/").join(".");
21
+ }
22
+ function renderModule(module, context, pathOf) {
23
+ const dependencies = (context.importsByModule.get(module.moduleId) ?? []).map((target) => ({
24
+ symbol: exportedSymbolOf(context, target),
25
+ moduleName: pythonModuleName(pathOf(target)),
26
+ }));
27
+ const lines = [`"""Synthetic module ${module.moduleId} (dependency level ${module.level}). Generated by my-dev-kit-lab."""`];
28
+ for (const dependency of dependencies)
29
+ lines.push(`from ${dependency.moduleName} import ${dependency.symbol.name}`);
30
+ for (const pattern of context.patternsByModule.get(module.moduleId) ?? []) {
31
+ lines.push("", ...patternCommentLines(pattern, "#"));
32
+ }
33
+ (context.symbolsByModule.get(module.moduleId) ?? []).forEach((symbol, position) => {
34
+ lines.push("", "", `def ${symbol.name}(input_value: int) -> int:`, ` """Synthetic symbol ${symbol.name} of ${module.moduleId}."""`, ` total = input_value + ${symbol.ordinal}`);
35
+ if (position === 0) {
36
+ for (const dependency of dependencies)
37
+ lines.push(` total += ${dependency.symbol.name}(total) % 7`);
38
+ }
39
+ lines.push(" return total");
40
+ });
41
+ return canonicalText(lines);
42
+ }
43
+ function renderTest(test, context, pathOf) {
44
+ const names = test.targetSymbolIds.map((symbolId) => context.symbolById.get(symbolId).name);
45
+ return canonicalText([
46
+ `from ${pythonModuleName(pathOf(test.targetModuleId))} import ${names.join(", ")}`,
47
+ "",
48
+ "",
49
+ `def test_case_${test.testId.slice("test_".length)}():`,
50
+ ...names.map((name) => ` assert isinstance(${name}(1), int)`),
51
+ ]);
52
+ }
53
+ /** Deterministic projection of a Python plan into files, sorted by logical path. */
54
+ export function renderPythonRepository(plan) {
55
+ const context = buildRenderContext(plan);
56
+ const pathOf = (moduleId) => context.moduleById.get(moduleId).path;
57
+ const files = [];
58
+ for (const module of plan.modules) {
59
+ assertLogicalPath(module.path, ".py");
60
+ files.push({ path: module.path, role: "source", language: "python", content: renderModule(module, context, pathOf) });
61
+ }
62
+ for (const test of plan.testFiles) {
63
+ assertLogicalPath(test.path, ".py");
64
+ files.push({ path: test.path, role: "test", language: "python", content: renderTest(test, context, pathOf) });
65
+ }
66
+ files.push({ path: "pyproject.toml", role: "support", content: PY_PROJECT_TOML });
67
+ return files.sort(compareLogicalPaths);
68
+ }
@@ -0,0 +1,39 @@
1
+ import { inspectRepositoryFiles } from "./inspection.js";
2
+ import { renderPythonRepository } from "./renderPython.js";
3
+ import { SyntheticRepositoryRenderError } from "./renderShared.js";
4
+ import { renderTypeScriptRepository } from "./renderTypeScript.js";
5
+ /**
6
+ * Renders a frozen plan into its complete generated file set (sorted by logical path) and proves the files
7
+ * realize the plan: counts, import edges, patterns and depth are re-measured from the rendered bytes and must
8
+ * equal the plan, otherwise rendering fails closed. No filesystem access.
9
+ */
10
+ export function renderSyntheticRepository(plan) {
11
+ const files = plan.language === "typescript" ? renderTypeScriptRepository(plan) : renderPythonRepository(plan);
12
+ const inspection = inspectRepositoryFiles(plan.language, files);
13
+ const errors = [...inspection.issues];
14
+ const expected = {
15
+ sourceFileCount: plan.requested.sourceFileCount,
16
+ moduleDepth: plan.requested.moduleDepth,
17
+ internalImportCount: plan.requested.internalImportCount,
18
+ symbolCount: plan.requested.symbolCount,
19
+ testFileCount: plan.requested.testFileCount,
20
+ repeatedPatternCount: plan.requested.repeatedPatternCount,
21
+ };
22
+ for (const key of Object.keys(expected)) {
23
+ if (inspection.dimensions[key] !== expected[key]) {
24
+ errors.push(`rendered ${key} ${inspection.dimensions[key]} does not equal planned ${expected[key]}.`);
25
+ }
26
+ }
27
+ const plannedEdges = new Map(plan.modules.map((module) => [module.moduleId, module.path]));
28
+ const expectedEdges = plan.importEdges
29
+ .map((edge) => `${plannedEdges.get(edge.from)} -> ${plannedEdges.get(edge.to)}`)
30
+ .sort();
31
+ if (expectedEdges.join("\n") !== inspection.importEdges.join("\n"))
32
+ errors.push("rendered import edges do not equal the planned import edges.");
33
+ const plannedNames = plan.symbols.map((symbol) => symbol.name).sort();
34
+ if (plannedNames.join("\n") !== [...inspection.symbolNames].sort().join("\n"))
35
+ errors.push("rendered symbols do not equal the planned symbols.");
36
+ if (errors.length > 0)
37
+ throw new SyntheticRepositoryRenderError(errors);
38
+ return files;
39
+ }
@@ -0,0 +1,93 @@
1
+ import path from "node:path";
2
+ /** Thrown when a plan cannot be faithfully rendered or a rendered file set disagrees with its plan. */
3
+ export class SyntheticRepositoryRenderError extends Error {
4
+ errors;
5
+ constructor(errors) {
6
+ super(`Synthetic repository rendering failed: ${errors.join("; ")}`);
7
+ this.name = "SyntheticRepositoryRenderError";
8
+ this.errors = [...errors];
9
+ }
10
+ }
11
+ /** Distinct marker token of one counted repeated pattern occurrence. Appears nowhere else in generated text. */
12
+ export const REPEATED_PATTERN_MARKER = "@synthetic-pattern";
13
+ const PATTERN_ROLE_LINES = {
14
+ "helper-block": ["Repeated helper block: shared utility shape reused across modules."],
15
+ "table-block": ["Repeated table block: static lookup shape reused across modules."],
16
+ "doc-block": ["Repeated documentation block: explanatory text reused across modules."],
17
+ "guard-block": ["Repeated guard block: input validation shape reused across modules."],
18
+ };
19
+ /** Canonical text: LF only, no BOM, exactly one trailing newline. Rejects any host line ending or BOM in input lines. */
20
+ export function canonicalText(lines) {
21
+ for (const line of lines) {
22
+ if (line.includes("\r") || line.includes("\n") || line.includes("")) {
23
+ throw new SyntheticRepositoryRenderError(["rendered line contains a carriage return, newline or byte-order mark."]);
24
+ }
25
+ }
26
+ return `${lines.join("\n")}\n`;
27
+ }
28
+ export function patternCommentLines(pattern, comment) {
29
+ return [
30
+ `${comment} ${REPEATED_PATTERN_MARKER} ${pattern.patternId} ordinal=${pattern.ordinal} role=${pattern.role}`,
31
+ ...PATTERN_ROLE_LINES[pattern.role].map((line) => `${comment} ${line}`),
32
+ `${comment} @end-synthetic-pattern ${pattern.patternId}`,
33
+ ];
34
+ }
35
+ export function buildRenderContext(plan) {
36
+ const symbolById = new Map(plan.symbols.map((symbol) => [symbol.symbolId, symbol]));
37
+ const moduleById = new Map(plan.modules.map((module) => [module.moduleId, module]));
38
+ const symbolsByModule = new Map();
39
+ for (const module of plan.modules) {
40
+ const own = module.symbolIds.map((symbolId) => symbolById.get(symbolId));
41
+ if (own.length === 0 || own.some((symbol) => symbol === undefined)) {
42
+ throw new SyntheticRepositoryRenderError([`module ${module.moduleId} has no resolvable planned symbol.`]);
43
+ }
44
+ symbolsByModule.set(module.moduleId, own);
45
+ }
46
+ const patternsByModule = new Map();
47
+ for (const pattern of plan.repeatedPatterns) {
48
+ if (!moduleById.has(pattern.targetModuleId)) {
49
+ throw new SyntheticRepositoryRenderError([`pattern ${pattern.patternId} targets missing module ${pattern.targetModuleId}.`]);
50
+ }
51
+ const list = patternsByModule.get(pattern.targetModuleId) ?? [];
52
+ list.push(pattern);
53
+ patternsByModule.set(pattern.targetModuleId, list);
54
+ }
55
+ const importsByModule = new Map();
56
+ for (const edge of plan.importEdges) {
57
+ if (!moduleById.has(edge.from) || !moduleById.has(edge.to)) {
58
+ throw new SyntheticRepositoryRenderError([`import edge ${edge.from} -> ${edge.to} references a missing module.`]);
59
+ }
60
+ const list = importsByModule.get(edge.from) ?? [];
61
+ list.push(edge.to);
62
+ importsByModule.set(edge.from, list);
63
+ }
64
+ return { symbolsByModule, patternsByModule, importsByModule, moduleById, symbolById };
65
+ }
66
+ /** The symbol other modules import from `moduleId`: its first planned symbol. */
67
+ export function exportedSymbolOf(context, moduleId) {
68
+ return context.symbolsByModule.get(moduleId)[0];
69
+ }
70
+ /** Rejects planned paths that are not plain relative POSIX logical paths. */
71
+ export function assertLogicalPath(logicalPath, extension) {
72
+ const segments = logicalPath.split("/");
73
+ if (logicalPath.length === 0 ||
74
+ logicalPath.startsWith("/") ||
75
+ logicalPath.includes("\\") ||
76
+ logicalPath.includes(":") ||
77
+ segments.some((segment) => segment === "" || segment === "." || segment === "..") ||
78
+ !logicalPath.endsWith(extension)) {
79
+ throw new SyntheticRepositoryRenderError([`planned path ${JSON.stringify(logicalPath)} cannot be rendered as a safe ${extension} path.`]);
80
+ }
81
+ }
82
+ /** Relative POSIX specifier from the directory of `fromPath` to `toPath`. */
83
+ export function relativeSpecifier(fromPath, toPath) {
84
+ const relative = path.posix.relative(path.posix.dirname(fromPath), toPath);
85
+ return relative.startsWith(".") ? relative : `./${relative}`;
86
+ }
87
+ export function compareLogicalPaths(left, right) {
88
+ if (left.path < right.path)
89
+ return -1;
90
+ if (left.path > right.path)
91
+ return 1;
92
+ return 0;
93
+ }
@@ -0,0 +1,82 @@
1
+ import { assertLogicalPath, buildRenderContext, canonicalText, compareLogicalPaths, exportedSymbolOf, patternCommentLines, relativeSpecifier, } from "./renderShared.js";
2
+ const TS_PACKAGE_JSON = canonicalText([
3
+ "{",
4
+ ' "name": "synthetic-repository",',
5
+ ' "version": "0.0.0",',
6
+ ' "private": true,',
7
+ ' "type": "module",',
8
+ ' "scripts": {',
9
+ ' "test": "vitest run"',
10
+ " }",
11
+ "}",
12
+ ]);
13
+ const TS_CONFIG_JSON = canonicalText([
14
+ "{",
15
+ ' "compilerOptions": {',
16
+ ' "target": "ES2022",',
17
+ ' "module": "NodeNext",',
18
+ ' "moduleResolution": "NodeNext",',
19
+ ' "strict": true,',
20
+ ' "noEmit": true,',
21
+ ' "skipLibCheck": true',
22
+ " },",
23
+ ' "include": ["src", "tests"]',
24
+ "}",
25
+ ]);
26
+ /** `.ts` logical path to the NodeNext `.js` import specifier. */
27
+ function importSpecifier(fromPath, toPath) {
28
+ return relativeSpecifier(fromPath, toPath).replace(/\.ts$/, ".js");
29
+ }
30
+ function renderModule(module, context, pathOf) {
31
+ const lines = [`// Synthetic module ${module.moduleId} (dependency level ${module.level}). Generated by my-dev-kit-lab.`];
32
+ const dependencies = (context.importsByModule.get(module.moduleId) ?? []).map((target) => ({
33
+ target,
34
+ symbol: exportedSymbolOf(context, target),
35
+ }));
36
+ for (const dependency of dependencies) {
37
+ lines.push(`import { ${dependency.symbol.name} } from "${importSpecifier(module.path, pathOf(dependency.target))}";`);
38
+ }
39
+ for (const pattern of context.patternsByModule.get(module.moduleId) ?? []) {
40
+ lines.push("", ...patternCommentLines(pattern, "//"));
41
+ }
42
+ (context.symbolsByModule.get(module.moduleId) ?? []).forEach((symbol, position) => {
43
+ lines.push("", `/** Synthetic symbol ${symbol.name} of ${module.moduleId}. */`, `export function ${symbol.name}(input: number): number {`, ` let total = input + ${symbol.ordinal};`);
44
+ if (position === 0) {
45
+ for (const dependency of dependencies)
46
+ lines.push(` total += ${dependency.symbol.name}(total) % 7;`);
47
+ }
48
+ lines.push(" return total;", "}");
49
+ });
50
+ return canonicalText(lines);
51
+ }
52
+ function renderTest(test, context) {
53
+ const target = context.moduleById.get(test.targetModuleId);
54
+ const names = test.targetSymbolIds.map((symbolId) => context.symbolById.get(symbolId).name);
55
+ return canonicalText([
56
+ 'import { describe, expect, it } from "vitest";',
57
+ `import { ${names.join(", ")} } from "${importSpecifier(test.path, target.path)}";`,
58
+ "",
59
+ `describe("${test.testId}", () => {`,
60
+ ` it("exercises ${target.moduleId}", () => {`,
61
+ ...names.map((name) => ` expect(typeof ${name}(1)).toBe("number");`),
62
+ " });",
63
+ "});",
64
+ ]);
65
+ }
66
+ /** Deterministic projection of a TypeScript plan into files, sorted by logical path. */
67
+ export function renderTypeScriptRepository(plan) {
68
+ const context = buildRenderContext(plan);
69
+ const pathOf = (moduleId) => context.moduleById.get(moduleId).path;
70
+ const files = [];
71
+ for (const module of plan.modules) {
72
+ assertLogicalPath(module.path, ".ts");
73
+ files.push({ path: module.path, role: "source", language: "typescript", content: renderModule(module, context, pathOf) });
74
+ }
75
+ for (const test of plan.testFiles) {
76
+ assertLogicalPath(test.path, ".test.ts");
77
+ files.push({ path: test.path, role: "test", language: "typescript", content: renderTest(test, context) });
78
+ }
79
+ files.push({ path: "package.json", role: "support", content: TS_PACKAGE_JSON });
80
+ files.push({ path: "tsconfig.json", role: "support", content: TS_CONFIG_JSON });
81
+ return files.sort(compareLogicalPaths);
82
+ }
@@ -0,0 +1,96 @@
1
+ import { maxImportEdges } from "./config.js";
2
+ import { SyntheticRepositoryPlanningError } from "./types.js";
3
+ /**
4
+ * Plans the acyclic import graph: exactly `internalImportCount` unique edges whose longest chain has exactly
5
+ * `moduleDepth` modules. Modules live on `moduleDepth` levels and may only import modules on a deeper level,
6
+ * so the longest chain can never exceed `moduleDepth`; one spine chain guarantees it reaches it.
7
+ * Assumes a validated request (feasibility is also re-checked defensively).
8
+ */
9
+ export function planTopology(request, prng) {
10
+ const { sourceFileCount, moduleDepth, internalImportCount } = request;
11
+ if (internalImportCount > maxImportEdges(sourceFileCount, moduleDepth) || internalImportCount < moduleDepth - 1) {
12
+ throw new SyntheticRepositoryPlanningError([
13
+ `topology request is infeasible: ${sourceFileCount} modules, depth ${moduleDepth}, ${internalImportCount} edges.`,
14
+ ]);
15
+ }
16
+ const levelSizes = new Array(moduleDepth).fill(1);
17
+ for (let extra = moduleDepth; extra < sourceFileCount; extra += 1) {
18
+ levelSizes[prng.nextInt(moduleDepth)] += 1;
19
+ }
20
+ let sumOfSquares = levelSizes.reduce((total, size) => total + size * size, 0);
21
+ let capacity = (sourceFileCount * sourceFileCount - sumOfSquares) / 2;
22
+ while (capacity < internalImportCount) {
23
+ let largest = 0;
24
+ let smallest = 0;
25
+ for (let level = 1; level < moduleDepth; level += 1) {
26
+ if (levelSizes[level] > levelSizes[largest])
27
+ largest = level;
28
+ if (levelSizes[level] < levelSizes[smallest])
29
+ smallest = level;
30
+ }
31
+ if (levelSizes[largest] - levelSizes[smallest] < 2) {
32
+ throw new SyntheticRepositoryPlanningError(["topology rebalancing exhausted before reaching the requested edge count."]);
33
+ }
34
+ const from = levelSizes[largest];
35
+ const to = levelSizes[smallest];
36
+ sumOfSquares += (from - 1) * (from - 1) + (to + 1) * (to + 1) - from * from - to * to;
37
+ levelSizes[largest] -= 1;
38
+ levelSizes[smallest] += 1;
39
+ capacity = (sourceFileCount * sourceFileCount - sumOfSquares) / 2;
40
+ }
41
+ const levelStart = [];
42
+ const levelEnd = [];
43
+ const levelOfModule = new Array(sourceFileCount);
44
+ let cursor = 0;
45
+ for (let level = 0; level < moduleDepth; level += 1) {
46
+ levelStart.push(cursor);
47
+ for (let count = 0; count < levelSizes[level]; count += 1) {
48
+ levelOfModule[cursor] = level;
49
+ cursor += 1;
50
+ }
51
+ levelEnd.push(cursor);
52
+ }
53
+ // Candidate edge ranks: modules in index order, each followed by the modules on strictly deeper levels.
54
+ const cumulative = new Array(sourceFileCount + 1);
55
+ cumulative[0] = 0;
56
+ for (let module = 0; module < sourceFileCount; module += 1) {
57
+ cumulative[module + 1] = cumulative[module] + (sourceFileCount - levelEnd[levelOfModule[module]]);
58
+ }
59
+ const totalRanks = cumulative[sourceFileCount];
60
+ const rankToEdge = (rank) => {
61
+ let low = 0;
62
+ let high = sourceFileCount - 1;
63
+ while (low < high) {
64
+ const middle = Math.ceil((low + high) / 2);
65
+ if (cumulative[middle] <= rank)
66
+ low = middle;
67
+ else
68
+ high = middle - 1;
69
+ }
70
+ return [low, levelEnd[levelOfModule[low]] + (rank - cumulative[low])];
71
+ };
72
+ const spineRanks = [];
73
+ for (let level = 0; level + 1 < moduleDepth; level += 1) {
74
+ spineRanks.push(cumulative[levelStart[level]]);
75
+ }
76
+ const extraEdges = internalImportCount - spineRanks.length;
77
+ const freeRanks = totalRanks - spineRanks.length;
78
+ const chosen = new Set();
79
+ for (let upper = freeRanks - extraEdges; upper < freeRanks; upper += 1) {
80
+ const pick = prng.nextInt(upper + 1);
81
+ chosen.add(chosen.has(pick) ? upper : pick);
82
+ }
83
+ const ranks = [...spineRanks];
84
+ for (const freeIndex of chosen) {
85
+ let rank = freeIndex;
86
+ for (const spineRank of spineRanks) {
87
+ if (spineRank <= rank)
88
+ rank += 1;
89
+ else
90
+ break;
91
+ }
92
+ ranks.push(rank);
93
+ }
94
+ ranks.sort((left, right) => left - right);
95
+ return { levelSizes, levelOfModule, edges: ranks.map(rankToEdge) };
96
+ }
@@ -0,0 +1,26 @@
1
+ export const SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_ID = "my-dev-kit-lab-synthetic-repository-config-v1";
2
+ export const SYNTHETIC_REPOSITORY_CONFIG_SCHEMA_VERSION = "1.0.0";
3
+ export const SYNTHETIC_REPOSITORY_PLAN_SCHEMA_ID = "my-dev-kit-lab-synthetic-repository-plan-v1";
4
+ export const SYNTHETIC_REPOSITORY_PLAN_SCHEMA_VERSION = "1.0.0";
5
+ export const SYNTHETIC_REPOSITORY_IDENTITY_ALGORITHM_VERSION = "sha256-canonical-json-v1";
6
+ export const SYNTHETIC_REPOSITORY_PRNG_VERSION = "mulberry32-v1";
7
+ export const SYNTHETIC_REPOSITORY_LANGUAGES = ["typescript", "python"];
8
+ export const SYNTHETIC_REPOSITORY_PATTERN_ROLES = ["helper-block", "table-block", "doc-block", "guard-block"];
9
+ /** Thrown for invalid configuration or case input; `errors` carries every labelled validation error. */
10
+ export class SyntheticRepositoryConfigError extends Error {
11
+ errors;
12
+ constructor(errors) {
13
+ super(`Invalid synthetic repository configuration: ${errors.join("; ")}`);
14
+ this.name = "SyntheticRepositoryConfigError";
15
+ this.errors = [...errors];
16
+ }
17
+ }
18
+ /** Thrown when a plan cannot be produced or fails its own invariant verification. */
19
+ export class SyntheticRepositoryPlanningError extends Error {
20
+ errors;
21
+ constructor(errors) {
22
+ super(`Synthetic repository planning failed: ${errors.join("; ")}`);
23
+ this.name = "SyntheticRepositoryPlanningError";
24
+ this.errors = [...errors];
25
+ }
26
+ }
@@ -18,7 +18,7 @@ function isInsideRoot(resolvedRoot, candidate) {
18
18
  const relative = path.relative(resolvedRoot, candidate);
19
19
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
20
20
  }
21
- async function snapshotPathEntry(resolvedPath) {
21
+ async function snapshotPathEntry(resolvedPath, maxHashedFileBytes) {
22
22
  let stats;
23
23
  try {
24
24
  stats = await lstat(resolvedPath);
@@ -38,6 +38,15 @@ async function snapshotPathEntry(resolvedPath) {
38
38
  return { state: "directory", sha256: null, symbolicLinkTarget: null };
39
39
  }
40
40
  if (stats.isFile()) {
41
+ if (maxHashedFileBytes !== undefined && stats.size > maxHashedFileBytes) {
42
+ // Safe mode: a file above the bound is fingerprinted by metadata and its contents are never read.
43
+ return {
44
+ state: "file",
45
+ sha256: null,
46
+ symbolicLinkTarget: null,
47
+ contentFingerprint: { sizeBytes: stats.size, mtimeMs: stats.mtimeMs }
48
+ };
49
+ }
41
50
  const contents = await readFile(resolvedPath);
42
51
  return { state: "file", sha256: sha256Hex(contents), symbolicLinkTarget: null };
43
52
  }
@@ -69,8 +78,9 @@ async function runRequiredGit(resolvedRoot, args, targetRootPath) {
69
78
  : `git ${args.join(" ")} failed: ${result.message}`;
70
79
  return { ok: false, code, targetRootPath, message };
71
80
  }
72
- export async function captureTargetSnapshot(config) {
81
+ export async function captureTargetSnapshot(config, options = {}) {
73
82
  const resolvedTargetRootPath = path.resolve(config.targetRootPath);
83
+ const maxHashedFileBytes = options.externalLocalSafe?.maxHashedFileBytes;
74
84
  let rootStats;
75
85
  try {
76
86
  rootStats = await lstat(resolvedTargetRootPath);
@@ -113,13 +123,14 @@ export async function captureTargetSnapshot(config) {
113
123
  };
114
124
  }
115
125
  try {
116
- const entry = await snapshotPathEntry(resolvedPath);
126
+ const entry = await snapshotPathEntry(resolvedPath, maxHashedFileBytes);
117
127
  configuredFiles.push({
118
128
  relativePath,
119
129
  resolvedPath,
120
130
  state: entry.state,
121
131
  sha256: entry.sha256,
122
- symbolicLinkTarget: entry.symbolicLinkTarget
132
+ symbolicLinkTarget: entry.symbolicLinkTarget,
133
+ ...(entry.contentFingerprint ? { contentFingerprint: entry.contentFingerprint } : {})
123
134
  });
124
135
  }
125
136
  catch (error) {
@@ -183,15 +194,25 @@ export async function captureTargetSnapshot(config) {
183
194
  const untrackedFiles = [];
184
195
  for (const untrackedRelativePath of untrackedPaths) {
185
196
  const resolvedPath = path.resolve(resolvedTargetRootPath, untrackedRelativePath);
186
- const entry = await snapshotPathEntry(resolvedPath);
197
+ const entry = await snapshotPathEntry(resolvedPath, maxHashedFileBytes);
187
198
  const state = entry.state === "directory" ? "other" : entry.state;
188
199
  untrackedFiles.push({
189
200
  path: untrackedRelativePath,
190
201
  state,
191
202
  sha256: entry.sha256,
192
- symbolicLinkTarget: entry.symbolicLinkTarget
203
+ symbolicLinkTarget: entry.symbolicLinkTarget,
204
+ ...(entry.contentFingerprint ? { contentFingerprint: entry.contentFingerprint } : {})
193
205
  });
194
206
  }
207
+ let ignoredPaths;
208
+ if (maxHashedFileBytes !== undefined) {
209
+ // Safe mode: Lab-created files in ignored locations (for example a stray index directory) are invisible to
210
+ // `git status`, so record the ignored path set by name. Contents are never read.
211
+ const ignoredOutcome = await runRequiredGit(resolvedTargetRootPath, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"], config.targetRootPath);
212
+ if (!ignoredOutcome.ok)
213
+ return ignoredOutcome;
214
+ ignoredPaths = ignoredOutcome.stdout.split("\0").filter((entry) => entry.length > 0).sort();
215
+ }
195
216
  const git = {
196
217
  availability: "available",
197
218
  branch,
@@ -199,7 +220,8 @@ export async function captureTargetSnapshot(config) {
199
220
  statusEntries,
200
221
  worktreeDiffSha256,
201
222
  stagedDiffSha256,
202
- untrackedFiles
223
+ untrackedFiles,
224
+ ...(ignoredPaths !== undefined ? { ignoredPaths } : {})
203
225
  };
204
226
  return {
205
227
  ok: true,
@@ -38,6 +38,10 @@ export function compareTargetSnapshots(before, after) {
38
38
  mutations.push(mutation(`git.untracked:${afterEntry.path}`, "git-untracked-file", `git.untrackedFiles.${afterEntry.path}`, null, afterEntry));
39
39
  }
40
40
  }
41
+ if ((before.git.ignoredPaths !== undefined || after.git.ignoredPaths !== undefined) &&
42
+ !isDeepStrictEqual(before.git.ignoredPaths ?? null, after.git.ignoredPaths ?? null)) {
43
+ mutations.push(mutation("git.ignored-paths", "git-ignored-paths", "git.ignoredPaths", (before.git.ignoredPaths ?? null), (after.git.ignoredPaths ?? null)));
44
+ }
41
45
  const afterConfiguredByPath = new Map(after.configuredFiles.map((entry) => [entry.relativePath, entry]));
42
46
  for (const beforeEntry of before.configuredFiles) {
43
47
  const afterEntry = afterConfiguredByPath.get(beforeEntry.relativePath);
@@ -12,3 +12,4 @@ export * from "./projectProfile.js";
12
12
  export * from "./relevantFiles.js";
13
13
  export * from "./successEvidence.js";
14
14
  export * from "./types.js";
15
+ export * from "./syntheticInputs.js";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Thrown when a local-subject execution did not complete cleanly. The primary code is the first issue in the
3
+ * ordered list (execution, mutation, snapshot, cleanup); every issue is retained so a secondary failure never
4
+ * masks the primary one. `caseEvidence` and `immutability` are runtime-only partial evidence for forensics and
5
+ * must not be persisted without the later privacy projection.
6
+ */
7
+ export class LocalSubjectExecutionError extends Error {
8
+ code;
9
+ issues;
10
+ caseEvidence;
11
+ immutability;
12
+ constructor(issues, partial = {}) {
13
+ if (issues.length === 0)
14
+ throw new Error("LocalSubjectExecutionError requires at least one issue.");
15
+ super(`Local subject execution failed (${issues[0].code}): ${issues.map((issue) => issue.message).join("; ")}`);
16
+ this.name = "LocalSubjectExecutionError";
17
+ this.code = issues[0].code;
18
+ this.issues = [...issues];
19
+ this.caseEvidence = partial.caseEvidence ?? null;
20
+ this.immutability = partial.immutability ?? null;
21
+ }
22
+ }
23
+ /** Replaces absolute private roots in a message so errors never carry the target or scratch location. */
24
+ export function redactPrivatePaths(message, privatePaths) {
25
+ let redacted = message;
26
+ for (const privatePath of privatePaths) {
27
+ if (!privatePath)
28
+ continue;
29
+ const variants = new Set([privatePath, privatePath.replace(/\\/g, "/"), privatePath.replace(/\//g, "\\")]);
30
+ for (const variant of variants) {
31
+ redacted = redacted.split(variant).join("<private-path>");
32
+ }
33
+ }
34
+ return redacted;
35
+ }