labgate 0.5.31 → 0.5.33

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 (73) hide show
  1. package/README.md +50 -2
  2. package/dist/cli.js +533 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/lib/config.d.ts +11 -0
  5. package/dist/lib/config.js +45 -4
  6. package/dist/lib/config.js.map +1 -1
  7. package/dist/lib/container.d.ts +3 -3
  8. package/dist/lib/container.js +144 -12
  9. package/dist/lib/container.js.map +1 -1
  10. package/dist/lib/display-mcp.d.ts +10 -0
  11. package/dist/lib/display-mcp.js +160 -0
  12. package/dist/lib/display-mcp.js.map +1 -0
  13. package/dist/lib/display-store.d.ts +24 -0
  14. package/dist/lib/display-store.js +150 -0
  15. package/dist/lib/display-store.js.map +1 -0
  16. package/dist/lib/explorer-autopilot.d.ts +16 -0
  17. package/dist/lib/explorer-autopilot.js +573 -0
  18. package/dist/lib/explorer-autopilot.js.map +1 -0
  19. package/dist/lib/explorer-claude.d.ts +16 -0
  20. package/dist/lib/explorer-claude.js +361 -0
  21. package/dist/lib/explorer-claude.js.map +1 -0
  22. package/dist/lib/explorer-compare.d.ts +9 -0
  23. package/dist/lib/explorer-compare.js +190 -0
  24. package/dist/lib/explorer-compare.js.map +1 -0
  25. package/dist/lib/explorer-eval.d.ts +23 -0
  26. package/dist/lib/explorer-eval.js +161 -0
  27. package/dist/lib/explorer-eval.js.map +1 -0
  28. package/dist/lib/explorer-gc.d.ts +11 -0
  29. package/dist/lib/explorer-gc.js +304 -0
  30. package/dist/lib/explorer-gc.js.map +1 -0
  31. package/dist/lib/explorer-git.d.ts +14 -0
  32. package/dist/lib/explorer-git.js +136 -0
  33. package/dist/lib/explorer-git.js.map +1 -0
  34. package/dist/lib/explorer-lock.d.ts +5 -0
  35. package/dist/lib/explorer-lock.js +100 -0
  36. package/dist/lib/explorer-lock.js.map +1 -0
  37. package/dist/lib/explorer-mcp.d.ts +11 -0
  38. package/dist/lib/explorer-mcp.js +611 -0
  39. package/dist/lib/explorer-mcp.js.map +1 -0
  40. package/dist/lib/explorer-retention.d.ts +4 -0
  41. package/dist/lib/explorer-retention.js +58 -0
  42. package/dist/lib/explorer-retention.js.map +1 -0
  43. package/dist/lib/explorer-store.d.ts +77 -0
  44. package/dist/lib/explorer-store.js +950 -0
  45. package/dist/lib/explorer-store.js.map +1 -0
  46. package/dist/lib/explorer-types.d.ts +161 -0
  47. package/dist/lib/explorer-types.js +3 -0
  48. package/dist/lib/explorer-types.js.map +1 -0
  49. package/dist/lib/explorer.d.ts +31 -0
  50. package/dist/lib/explorer.js +247 -0
  51. package/dist/lib/explorer.js.map +1 -0
  52. package/dist/lib/results-store.js +37 -3
  53. package/dist/lib/results-store.js.map +1 -1
  54. package/dist/lib/test/integration-harness.js +1 -1
  55. package/dist/lib/test/integration-harness.js.map +1 -1
  56. package/dist/lib/ui.html +5115 -2052
  57. package/dist/lib/ui.js +906 -39
  58. package/dist/lib/ui.js.map +1 -1
  59. package/dist/lib/web-terminal.js +4 -3
  60. package/dist/lib/web-terminal.js.map +1 -1
  61. package/dist/mcp-bundles/dataset-mcp.bundle.mjs +0 -8
  62. package/dist/mcp-bundles/display-mcp.bundle.mjs +30209 -0
  63. package/dist/mcp-bundles/explorer-mcp.bundle.mjs +40036 -0
  64. package/dist/mcp-bundles/results-mcp.bundle.mjs +30 -4
  65. package/package.json +3 -2
  66. package/templates/tsp-lab/API_CONTRACT.md +20 -0
  67. package/templates/tsp-lab/EVAL.md +20 -0
  68. package/templates/tsp-lab/PROBLEM.md +18 -0
  69. package/templates/tsp-lab/data/generate_instances.py +51 -0
  70. package/templates/tsp-lab/data/instances.jsonl +12 -0
  71. package/templates/tsp-lab/eval.py +148 -0
  72. package/templates/tsp-lab/solver.py +88 -0
  73. package/templates/tsp-lab/stub-patches/enable_two_opt.patch +14 -0
@@ -29940,7 +29940,7 @@ var StdioServerTransport = class {
29940
29940
 
29941
29941
  // src/lib/results-store.ts
29942
29942
  import { randomUUID } from "crypto";
29943
- import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
29943
+ import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
29944
29944
  import { dirname, join as join2 } from "path";
29945
29945
 
29946
29946
  // src/lib/config.ts
@@ -29971,6 +29971,10 @@ function getResultsDbPath() {
29971
29971
  }
29972
29972
 
29973
29973
  // src/lib/results-store.ts
29974
+ function isRenameFallbackError(err) {
29975
+ const code = err?.code;
29976
+ return code === "EBUSY" || code === "EXDEV";
29977
+ }
29974
29978
  function nowIso() {
29975
29979
  return (/* @__PURE__ */ new Date()).toISOString();
29976
29980
  }
@@ -30285,12 +30289,34 @@ var ResultsStore = class {
30285
30289
  }
30286
30290
  }
30287
30291
  writeFile(next) {
30288
- const tmpPath = join2(dirname(this.dbPath), `.${Date.now()}.${process.pid}.results.tmp`);
30289
- writeFileSync(tmpPath, JSON.stringify(next, null, 2) + "\n", {
30292
+ const payload = JSON.stringify(next, null, 2) + "\n";
30293
+ const tmpPath = join2(dirname(this.dbPath), `.${Date.now()}.${process.pid}.${randomUUID()}.results.tmp`);
30294
+ writeFileSync(tmpPath, payload, {
30290
30295
  encoding: "utf-8",
30291
30296
  mode: PRIVATE_FILE_MODE
30292
30297
  });
30293
- renameSync(tmpPath, this.dbPath);
30298
+ try {
30299
+ renameSync(tmpPath, this.dbPath);
30300
+ } catch (err) {
30301
+ if (!isRenameFallbackError(err)) {
30302
+ try {
30303
+ unlinkSync(tmpPath);
30304
+ } catch {
30305
+ }
30306
+ throw err;
30307
+ }
30308
+ try {
30309
+ writeFileSync(this.dbPath, payload, {
30310
+ encoding: "utf-8",
30311
+ mode: PRIVATE_FILE_MODE
30312
+ });
30313
+ } finally {
30314
+ try {
30315
+ unlinkSync(tmpPath);
30316
+ } catch {
30317
+ }
30318
+ }
30319
+ }
30294
30320
  ensurePrivateFile(this.dbPath);
30295
30321
  }
30296
30322
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "labgate",
3
- "version": "0.5.31",
3
+ "version": "0.5.33",
4
4
  "description": "Secure HPC wrapper for AI coding agents (Claude-first): policy controls, Apptainer sandboxes, and SLURM workflows — https://labgate.dev",
5
5
  "homepage": "https://labgate.dev",
6
6
  "keywords": [
@@ -39,7 +39,8 @@
39
39
  "files": [
40
40
  "bin/",
41
41
  "dist/",
42
- "sandbox/"
42
+ "sandbox/",
43
+ "templates/"
43
44
  ],
44
45
  "dependencies": {
45
46
  "@modelcontextprotocol/sdk": "^1.26.0",
@@ -0,0 +1,20 @@
1
+ # API Contract
2
+
3
+ Implement in `solver.py`:
4
+
5
+ ```python
6
+ def solve(points):
7
+ ...
8
+ ```
9
+
10
+ Input:
11
+ - `points`: list of `[x, y]` coordinates.
12
+
13
+ Output:
14
+ - A list of integer node indices representing a Hamiltonian cycle order.
15
+ - Must contain each node exactly once.
16
+ - `len(tour) == len(points)`.
17
+
18
+ Evaluation behavior:
19
+ - Evaluator treats output as a closed tour (last node reconnects to first).
20
+ - Invalid tours fail evaluation.
@@ -0,0 +1,20 @@
1
+ # Evaluation Contract
2
+
3
+ Run:
4
+
5
+ ```bash
6
+ python eval.py
7
+ ```
8
+
9
+ Output contract:
10
+ - The **last line of stdout** must be valid JSON.
11
+ - JSON must include at least:
12
+
13
+ ```json
14
+ {"score": 0.7812, "metrics": {"runtime_ms": 31.2}}
15
+ ```
16
+
17
+ Rules:
18
+ - `score` is a float and **higher is better**.
19
+ - `metrics` is optional but recommended.
20
+ - Non-zero exit code or missing/invalid final JSON line is treated as evaluation failure.
@@ -0,0 +1,18 @@
1
+ # TSP Lab Problem
2
+
3
+ You are optimizing a Euclidean Traveling Salesperson Problem (TSP) solver.
4
+
5
+ Goal: maximize evaluation score over a fixed benchmark set of TSP instances.
6
+
7
+ Current baseline:
8
+ - `solver.py` uses nearest-neighbor construction.
9
+
10
+ What to improve:
11
+ - Better construction heuristics.
12
+ - Local search (for example 2-opt).
13
+ - Runtime-aware improvements (score includes a tiny runtime penalty).
14
+
15
+ Constraints:
16
+ - Keep the `solve(points)` API contract intact.
17
+ - Do not modify `eval.py` unless explicitly allowed.
18
+ - Keep runtime practical on the provided benchmark instances.
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import random
6
+ from pathlib import Path
7
+
8
+
9
+ def generate_instances(count: int, points_per_instance: int, seed: int):
10
+ for i in range(count):
11
+ instance_seed = seed + i * 1009
12
+ rng = random.Random(instance_seed)
13
+
14
+ # Mix uniform cloud + a weak center cluster so local search has room to improve.
15
+ points = []
16
+ for j in range(points_per_instance):
17
+ if j % 4 == 0:
18
+ x = rng.gauss(500.0, 180.0)
19
+ y = rng.gauss(500.0, 180.0)
20
+ else:
21
+ x = rng.uniform(0.0, 1000.0)
22
+ y = rng.uniform(0.0, 1000.0)
23
+ points.append([round(max(0.0, min(1000.0, x)), 3), round(max(0.0, min(1000.0, y)), 3)])
24
+
25
+ yield {
26
+ "id": f"inst_{i:03d}",
27
+ "seed": instance_seed,
28
+ "points": points,
29
+ }
30
+
31
+
32
+ def main() -> None:
33
+ parser = argparse.ArgumentParser(description="Generate deterministic TSP instances")
34
+ parser.add_argument("--count", type=int, default=12)
35
+ parser.add_argument("--points", type=int, default=48)
36
+ parser.add_argument("--seed", type=int, default=20260217)
37
+ parser.add_argument("--output", type=Path, default=Path(__file__).parent / "instances.jsonl")
38
+ args = parser.parse_args()
39
+
40
+ rows = list(generate_instances(args.count, args.points, args.seed))
41
+ args.output.parent.mkdir(parents=True, exist_ok=True)
42
+
43
+ with args.output.open("w", encoding="utf-8") as f:
44
+ for row in rows:
45
+ f.write(json.dumps(row, separators=(",", ":")) + "\n")
46
+
47
+ print(f"wrote {len(rows)} instances to {args.output}")
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,12 @@
1
+ {"id":"inst_000","seed":20260217,"points":[[683.986,501.225],[48.285,616.705],[95.326,485.406],[544.661,644.98],[313.227,335.701],[451.726,736.463],[73.332,119.632],[295.362,488.05],[311.43,779.077],[357.013,188.147],[735.16,553.627],[848.313,875.275],[398.858,466.456],[226.985,616.271],[69.756,634.261],[265.573,105.813],[233.576,811.84],[312.43,718.167],[239.106,350.77],[339.89,171.019],[742.279,292.358],[764.341,400.574],[127.158,600.417],[568.032,274.537],[594.684,383.743],[639.212,67.937],[115.345,365.784],[736.814,311.384],[740.74,570.347],[195.542,181.487],[298.135,258.17],[665.133,359.811],[611.428,614.12],[465.802,327.782],[799.408,362.669],[265.021,972.475],[438.721,711.95],[475.786,986.801],[471.422,605.738],[441.864,435.809],[284.47,413.165],[760.204,741.432],[982.797,777.703],[955.065,578.705],[737.265,165.287],[578.27,391.485],[471.846,812.339],[13.765,200.115]]}
2
+ {"id":"inst_001","seed":20261226,"points":[[758.149,461.363],[851.146,805.955],[430.355,800.31],[971.87,405.734],[550.929,667.964],[859.758,602.053],[924.812,961.257],[600.163,822.698],[730.743,339.486],[9.34,419.11],[639.862,104.342],[973.502,557.276],[799.078,681.569],[547.909,532.516],[533.686,963.74],[963.989,822.733],[392.439,431.405],[74.622,679.043],[220.185,11.819],[3.289,538.228],[420.322,59.957],[143.799,240.156],[665.072,794.167],[670.759,630.144],[481.194,175.496],[966.604,504.127],[484.721,144.518],[843.851,483.177],[656.323,331.667],[252.814,282.728],[868.854,125.048],[949.193,310.906],[603.574,384.519],[773.865,354.254],[609.883,644.556],[11.029,210.353],[460.114,0.0],[499.468,626.871],[861.828,301.748],[462.611,608.424],[705.026,498.391],[477.975,839.622],[971.42,21.302],[238.133,188.17],[440.616,333.614],[134.819,416.892],[618.327,304.204],[465.456,505.684]]}
3
+ {"id":"inst_002","seed":20262235,"points":[[496.916,252.128],[96.719,288.477],[942.606,776.231],[688.433,568.783],[841.444,280.24],[240.097,357.931],[545.074,639.018],[373.152,830.405],[482.891,333.48],[899.981,432.971],[761.074,752.657],[891.145,461.803],[517.896,215.047],[690.35,2.163],[504.503,229.068],[191.792,579.626],[440.06,843.853],[487.079,320.721],[261.753,105.716],[346.012,314.983],[459.032,881.831],[494.771,54.279],[939.599,317.541],[160.038,781.17],[522.234,440.998],[822.49,113.743],[976.201,819.109],[33.871,934.614],[709.26,680.366],[477.231,83.252],[998.901,333.22],[829.078,137.085],[694.856,443.495],[303.571,121.059],[541.667,994.17],[933.671,868.551],[529.366,349.896],[474.558,537.771],[860.547,318.204],[751.796,473.336],[386.233,1000.0],[244.326,102.88],[280.562,576.871],[37.7,310.58],[533.925,222.075],[854.821,699.527],[815.267,762.728],[571.439,732.607]]}
4
+ {"id":"inst_003","seed":20263244,"points":[[301.076,315.01],[459.462,531.21],[188.241,177.274],[877.538,697.583],[447.279,322.692],[20.894,845.64],[117.812,758.162],[215.185,834.401],[371.434,564.083],[669.227,351.045],[960.485,754.741],[748.294,765.059],[551.814,588.54],[890.745,420.275],[582.675,897.548],[948.884,733.335],[472.147,575.595],[358.38,24.062],[196.822,809.055],[420.217,868.723],[286.741,406.368],[504.742,668.391],[448.042,388.253],[112.842,951.214],[288.67,560.341],[675.691,2.539],[646.583,90.151],[180.528,818.052],[408.731,477.44],[565.289,773.972],[173.542,994.296],[291.604,269.296],[680.216,477.521],[709.86,650.679],[234.329,334.261],[193.763,777.916],[556.573,426.064],[414.369,517.428],[263.123,662.036],[779.982,452.75],[579.311,483.535],[468.707,858.286],[478.315,161.212],[616.676,263.567],[422.942,596.815],[426.278,511.533],[742.47,463.636],[142.728,69.862]]}
5
+ {"id":"inst_004","seed":20264253,"points":[[477.275,445.019],[713.318,690.026],[162.492,172.005],[708.856,590.955],[294.784,220.955],[968.088,176.944],[295.353,663.014],[959.2,219.799],[0.0,132.236],[195.596,997.599],[571.184,434.251],[75.529,847.041],[333.996,671.191],[158.553,62.985],[823.791,35.811],[660.504,310.887],[533.609,389.294],[744.346,806.863],[134.658,142.041],[998.478,189.994],[323.262,865.75],[766.532,232.886],[604.468,284.027],[244.75,627.983],[343.15,703.776],[242.779,100.301],[738.63,791.076],[450.267,747.873],[452.002,427.453],[359.364,431.152],[826.691,261.663],[196.57,988.612],[182.785,348.418],[739.867,266.065],[624.33,493.646],[214.097,427.601],[831.911,555.777],[76.229,137.542],[791.282,318.506],[133.671,932.746],[419.426,779.566],[383.235,248.101],[842.463,164.796],[753.289,838.648],[325.607,716.687],[878.634,657.879],[842.152,180.623],[592.637,354.988]]}
6
+ {"id":"inst_005","seed":20265262,"points":[[157.518,371.13],[345.067,559.144],[4.414,133.838],[187.812,152.753],[447.618,633.502],[104.686,259.276],[903.362,163.241],[36.62,527.156],[447.772,489.729],[5.65,660.49],[223.545,672.598],[5.703,741.502],[299.445,581.059],[890.692,407.233],[410.673,66.233],[367.166,90.754],[805.714,821.309],[44.707,464.108],[356.679,909.184],[35.608,678.973],[606.104,724.143],[694.888,638.609],[111.129,286.524],[189.116,375.919],[450.877,654.209],[292.398,845.669],[596.806,290.811],[706.703,993.717],[395.052,378.591],[853.291,626.575],[685.965,850.644],[863.8,461.043],[376.231,121.579],[467.304,370.037],[310.814,728.899],[753.779,705.546],[725.532,272.545],[112.84,844.732],[212.528,685.117],[409.057,275.355],[729.216,345.717],[329.332,300.94],[623.699,117.554],[56.406,276.527],[368.941,520.93],[967.409,354.837],[193.038,790.596],[215.633,882.689]]}
7
+ {"id":"inst_006","seed":20266271,"points":[[369.18,520.158],[781.424,853.095],[805.066,404.252],[855.487,283.411],[405.009,789.122],[356.77,49.03],[937.196,815.471],[741.649,828.02],[379.573,381.529],[956.674,607.888],[739.591,340.122],[87.502,454.533],[124.863,493.345],[198.938,283.145],[594.598,630.482],[853.394,470.878],[626.752,403.06],[791.812,429.881],[689.167,926.958],[382.919,559.091],[463.865,348.772],[98.963,141.94],[900.643,457.124],[866.771,21.613],[335.004,189.301],[225.91,121.077],[526.673,395.053],[735.59,462.632],[287.721,393.298],[314.761,503.284],[186.22,245.111],[795.104,186.028],[417.321,447.111],[90.72,745.326],[560.205,876.282],[295.316,93.318],[459.482,618.948],[259.625,204.077],[219.822,615.395],[296.853,784.923],[627.916,635.752],[144.558,673.147],[682.152,823.043],[994.625,745.034],[772.634,216.363],[610.511,54.331],[980.268,724.8],[902.83,327.88]]}
8
+ {"id":"inst_007","seed":20267280,"points":[[524.239,321.274],[310.7,711.026],[129.47,457.573],[190.19,844.786],[277.811,479.541],[917.985,760.527],[921.935,429.161],[949.765,124.865],[560.536,641.004],[49.144,747.157],[401.924,45.803],[57.108,301.513],[419.857,270.514],[817.786,596.601],[193.859,955.26],[475.229,578.162],[462.166,415.531],[924.967,153.19],[819.08,506.761],[754.151,603.422],[392.487,373.325],[454.878,836.289],[143.077,970.386],[966.773,698.647],[674.642,266.247],[507.459,432.111],[284.105,350.058],[426.2,989.7],[666.184,189.744],[927.322,381.603],[31.2,600.569],[504.914,541.385],[794.168,371.21],[13.993,419.452],[91.18,365.595],[653.871,242.699],[353.611,335.591],[783.145,33.754],[115.445,992.465],[559.295,980.463],[289.921,320.239],[204.278,244.854],[891.546,856.759],[852.192,993.472],[872.517,480.099],[847.871,384.758],[29.051,113.588],[158.815,541.152]]}
9
+ {"id":"inst_008","seed":20268289,"points":[[626.028,687.447],[504.66,878.94],[868.05,860.11],[718.904,590.225],[453.606,571.127],[216.998,296.112],[930.59,491.676],[755.134,953.937],[368.473,747.776],[911.451,30.758],[960.926,343.573],[286.728,270.919],[541.846,503.02],[354.972,476.538],[884.738,499.398],[287.827,528.119],[417.682,376.11],[416.954,971.462],[675.428,771.707],[730.801,89.765],[355.811,232.117],[564.431,558.233],[542.062,582.815],[15.451,805.778],[627.626,378.726],[646.924,854.211],[784.669,247.572],[61.749,754.82],[654.68,565.584],[897.78,677.814],[705.336,632.456],[256.343,366.313],[216.039,545.031],[436.725,768.376],[289.669,663.62],[166.282,872.595],[425.369,950.71],[545.379,814.457],[544.404,723.767],[771.28,316.682],[523.788,376.575],[658.694,444.634],[213.996,283.628],[827.381,837.748],[372.559,528.4],[556.852,306.885],[886.203,884.83],[492.777,909.567]]}
10
+ {"id":"inst_009","seed":20269298,"points":[[753.864,514.102],[968.088,670.402],[153.493,149.5],[12.014,976.5],[281.651,536.746],[767.052,678.295],[472.5,671.237],[921.943,363.685],[1000.0,387.734],[940.096,560.942],[15.452,106.042],[12.961,846.44],[833.9,640.565],[174.537,845.716],[822.671,710.894],[314.718,999.232],[350.427,645.603],[954.735,463.851],[987.593,37.501],[619.551,85.026],[344.848,532.907],[808.037,812.685],[892.838,947.412],[868.442,830.687],[839.77,300.43],[937.261,168.978],[402.515,864.858],[442.578,934.554],[371.979,933.403],[437.1,754.582],[463.169,427.443],[951.892,470.011],[210.253,477.777],[664.592,13.714],[167.591,373.092],[296.046,968.374],[476.459,361.492],[330.979,113.478],[701.83,353.04],[595.925,961.351],[578.704,653.863],[68.055,729.963],[942.226,822.455],[965.433,531.93],[444.878,377.716],[233.479,894.353],[327.394,795.227],[303.043,524.204]]}
11
+ {"id":"inst_010","seed":20270307,"points":[[315.615,400.062],[503.968,236.176],[134.923,524.789],[925.554,241.383],[440.014,568.787],[134.199,532.952],[694.19,171.554],[456.662,363.543],[391.569,444.021],[592.26,801.592],[151.964,761.087],[456.951,905.873],[521.253,306.487],[809.057,659.169],[761.223,923.406],[538.494,320.908],[545.827,287.852],[299.502,776.694],[853.674,221.131],[862.68,919.452],[619.702,252.15],[377.518,895.258],[11.224,934.665],[419.355,39.437],[679.989,677.02],[639.098,738.968],[718.369,38.97],[339.119,154.48],[279.784,849.77],[311.958,792.925],[440.741,481.346],[659.838,762.043],[367.186,740.524],[750.092,941.72],[714.563,454.886],[594.889,394.258],[329.603,307.553],[771.642,219.997],[393.004,684.189],[593.515,473.802],[721.476,552.305],[472.337,61.207],[22.473,559.454],[370.486,786.227],[250.702,678.314],[775.339,442.749],[265.463,444.563],[20.119,98.301]]}
12
+ {"id":"inst_011","seed":20271316,"points":[[282.151,481.786],[10.994,916.475],[882.863,72.019],[205.716,130.974],[541.009,815.753],[569.001,455.751],[325.352,501.697],[968.888,889.175],[509.237,497.72],[727.008,376.312],[979.482,169.553],[834.617,55.585],[542.724,714.038],[183.634,242.702],[757.456,928.698],[695.503,819.934],[216.495,409.335],[385.031,892.905],[165.766,498.839],[316.598,854.91],[715.282,472.478],[458.152,332.918],[826.924,58.25],[222.041,655.671],[482.981,505.717],[291.735,45.164],[923.305,848.919],[288.386,26.115],[589.648,616.647],[766.389,352.905],[32.107,795.771],[159.765,375.757],[702.303,453.156],[556.369,549.713],[208.305,269.991],[685.616,690.388],[629.062,437.984],[713.096,390.803],[36.88,863.567],[220.44,601.364],[203.336,560.472],[154.731,367.911],[983.927,129.027],[299.802,356.034],[341.897,605.937],[783.284,105.391],[638.869,712.713],[396.054,926.216]]}
@@ -0,0 +1,148 @@
1
+ """Evaluator for TSP Lab.
2
+
3
+ Contract: print a JSON object on the last stdout line:
4
+ {"score": <float>, "metrics": {...}}
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Iterable, List, Sequence, Tuple
13
+
14
+ import solver
15
+
16
+ Point = Tuple[float, float]
17
+
18
+ ALPHA_RUNTIME = 0.0005
19
+ INSTANCES_PATH = Path(__file__).parent / "data" / "instances.jsonl"
20
+
21
+
22
+ def _euclid(a: Point, b: Point) -> float:
23
+ dx = a[0] - b[0]
24
+ dy = a[1] - b[1]
25
+ return (dx * dx + dy * dy) ** 0.5
26
+
27
+
28
+ def _distance_matrix(points: Sequence[Point]) -> List[List[float]]:
29
+ n = len(points)
30
+ dist = [[0.0] * n for _ in range(n)]
31
+ for i in range(n):
32
+ for j in range(i + 1, n):
33
+ d = _euclid(points[i], points[j])
34
+ dist[i][j] = d
35
+ dist[j][i] = d
36
+ return dist
37
+
38
+
39
+ def _mst_weight(dist: List[List[float]]) -> float:
40
+ n = len(dist)
41
+ if n <= 1:
42
+ return 0.0
43
+
44
+ in_tree = [False] * n
45
+ best = [float("inf")] * n
46
+ best[0] = 0.0
47
+ total = 0.0
48
+
49
+ for _ in range(n):
50
+ u = -1
51
+ best_val = float("inf")
52
+ for i in range(n):
53
+ if not in_tree[i] and best[i] < best_val:
54
+ best_val = best[i]
55
+ u = i
56
+ if u < 0:
57
+ raise RuntimeError("MST failed: disconnected candidate")
58
+
59
+ in_tree[u] = True
60
+ total += best_val
61
+
62
+ for v in range(n):
63
+ if in_tree[v]:
64
+ continue
65
+ w = dist[u][v]
66
+ if w < best[v]:
67
+ best[v] = w
68
+
69
+ return total
70
+
71
+
72
+ def _tour_length(tour: Sequence[int], dist: List[List[float]]) -> float:
73
+ n = len(tour)
74
+ if n <= 1:
75
+ return 0.0
76
+
77
+ total = 0.0
78
+ for i in range(n):
79
+ a = tour[i]
80
+ b = tour[(i + 1) % n]
81
+ total += dist[a][b]
82
+ return total
83
+
84
+
85
+ def _validate_tour(tour: Sequence[int], n: int) -> None:
86
+ if len(tour) != n:
87
+ raise ValueError(f"invalid tour length: expected {n}, got {len(tour)}")
88
+ seen = set(tour)
89
+ if len(seen) != n:
90
+ raise ValueError("tour contains duplicate nodes")
91
+ if min(seen) < 0 or max(seen) >= n:
92
+ raise ValueError("tour contains out-of-range node indices")
93
+
94
+
95
+ def _load_instances(path: Path) -> Iterable[dict]:
96
+ if not path.exists():
97
+ raise FileNotFoundError(f"instances file not found: {path}")
98
+ for line in path.read_text(encoding="utf-8").splitlines():
99
+ raw = line.strip()
100
+ if not raw:
101
+ continue
102
+ yield json.loads(raw)
103
+
104
+
105
+ def main() -> None:
106
+ rows = list(_load_instances(INSTANCES_PATH))
107
+ if not rows:
108
+ raise RuntimeError("no instances found")
109
+
110
+ instance_scores: List[float] = []
111
+ total_runtime_ms = 0.0
112
+
113
+ for row in rows:
114
+ points = [(float(x), float(y)) for x, y in row["points"]]
115
+ n = len(points)
116
+
117
+ start = time.perf_counter()
118
+ tour = solver.solve(points)
119
+ elapsed_ms = (time.perf_counter() - start) * 1000.0
120
+ total_runtime_ms += elapsed_ms
121
+
122
+ _validate_tour(tour, n)
123
+
124
+ dist = _distance_matrix(points)
125
+ mst = _mst_weight(dist)
126
+ length = _tour_length(tour, dist)
127
+ if length <= 0.0:
128
+ raise ValueError("tour length must be > 0")
129
+
130
+ instance_scores.append(mst / length)
131
+
132
+ mean_ratio = sum(instance_scores) / len(instance_scores)
133
+ runtime_penalty = ALPHA_RUNTIME * (total_runtime_ms / 1000.0)
134
+ score = mean_ratio - runtime_penalty
135
+
136
+ metrics = {
137
+ "instances": len(rows),
138
+ "mean_lb_over_len": mean_ratio,
139
+ "runtime_ms": total_runtime_ms,
140
+ "runtime_penalty": runtime_penalty,
141
+ }
142
+
143
+ print(f"instances={metrics['instances']} mean_lb_over_len={mean_ratio:.6f} runtime_ms={total_runtime_ms:.2f}")
144
+ print(json.dumps({"score": score, "metrics": metrics}, separators=(",", ":")))
145
+
146
+
147
+ if __name__ == "__main__":
148
+ main()
@@ -0,0 +1,88 @@
1
+ """Baseline Euclidean TSP solver for the LabGate Solution Explorer demo.
2
+
3
+ API contract:
4
+ - solve(points) -> tour (list of node indices)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from math import hypot
10
+ from typing import List, Sequence, Tuple
11
+
12
+ Point = Tuple[float, float]
13
+
14
+ # Dummy-agent patch flips this to True for deterministic improvement tests.
15
+ ENABLE_TWO_OPT = False
16
+ MAX_TWO_OPT_PASSES = 2
17
+
18
+
19
+ def _dist(a: Point, b: Point) -> float:
20
+ return hypot(a[0] - b[0], a[1] - b[1])
21
+
22
+
23
+ def _build_distance_matrix(points: Sequence[Point]) -> List[List[float]]:
24
+ n = len(points)
25
+ dist = [[0.0] * n for _ in range(n)]
26
+ for i in range(n):
27
+ for j in range(i + 1, n):
28
+ d = _dist(points[i], points[j])
29
+ dist[i][j] = d
30
+ dist[j][i] = d
31
+ return dist
32
+
33
+
34
+ def _nearest_neighbor_tour(dist: List[List[float]]) -> List[int]:
35
+ n = len(dist)
36
+ if n <= 1:
37
+ return list(range(n))
38
+
39
+ unvisited = set(range(1, n))
40
+ tour = [0]
41
+
42
+ while unvisited:
43
+ last = tour[-1]
44
+ nxt = min(unvisited, key=lambda idx: dist[last][idx])
45
+ unvisited.remove(nxt)
46
+ tour.append(nxt)
47
+
48
+ return tour
49
+
50
+
51
+ def _apply_two_opt(tour: List[int], dist: List[List[float]], max_passes: int) -> List[int]:
52
+ n = len(tour)
53
+ if n < 5:
54
+ return tour
55
+
56
+ route = tour[:]
57
+ for _ in range(max_passes):
58
+ improved = False
59
+ for i in range(n - 1):
60
+ a = route[i]
61
+ b = route[(i + 1) % n]
62
+ for k in range(i + 2, n if i > 0 else n - 1):
63
+ c = route[k]
64
+ d = route[(k + 1) % n]
65
+ current = dist[a][b] + dist[c][d]
66
+ swapped = dist[a][c] + dist[b][d]
67
+ if swapped + 1e-12 < current:
68
+ route[i + 1:k + 1] = reversed(route[i + 1:k + 1])
69
+ improved = True
70
+ if not improved:
71
+ break
72
+
73
+ return route
74
+
75
+
76
+ def solve(points: Sequence[Sequence[float]]) -> List[int]:
77
+ n = len(points)
78
+ if n == 0:
79
+ return []
80
+
81
+ typed_points: List[Point] = [(float(p[0]), float(p[1])) for p in points]
82
+ dist = _build_distance_matrix(typed_points)
83
+ tour = _nearest_neighbor_tour(dist)
84
+
85
+ if ENABLE_TWO_OPT:
86
+ tour = _apply_two_opt(tour, dist, max_passes=MAX_TWO_OPT_PASSES)
87
+
88
+ return tour
@@ -0,0 +1,14 @@
1
+ diff --git a/solver.py b/solver.py
2
+ --- a/solver.py
3
+ +++ b/solver.py
4
+ @@ -12,8 +12,8 @@ from typing import List, Sequence, Tuple
5
+ Point = Tuple[float, float]
6
+
7
+ # Dummy-agent patch flips this to True for deterministic improvement tests.
8
+ -ENABLE_TWO_OPT = False
9
+ -MAX_TWO_OPT_PASSES = 2
10
+ +ENABLE_TWO_OPT = True
11
+ +MAX_TWO_OPT_PASSES = 4
12
+
13
+
14
+ def _dist(a: Point, b: Point) -> float: