erdos-problems 0.1.13 → 0.2.1

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 (60) hide show
  1. package/README.md +117 -4
  2. package/docs/RESEARCH_LOOP.md +21 -2
  3. package/package.json +1 -1
  4. package/packs/number-theory/README.md +17 -0
  5. package/packs/number-theory/problems/1/CHECKPOINT_TEMPLATE.md +7 -0
  6. package/packs/number-theory/problems/1/CONTEXT.md +8 -0
  7. package/packs/number-theory/problems/1/FRONTIER_NOTE.md +8 -0
  8. package/packs/number-theory/problems/1/OPS_DETAILS.yaml +25 -0
  9. package/packs/number-theory/problems/1/REPORT_TEMPLATE.md +7 -0
  10. package/packs/number-theory/problems/1/ROUTE_HISTORY.md +5 -0
  11. package/packs/number-theory/problems/1/ROUTE_PACKET.yaml +13 -0
  12. package/packs/number-theory/problems/1/context.yaml +25 -0
  13. package/packs/number-theory/problems/2/CHECKPOINT_TEMPLATE.md +7 -0
  14. package/packs/number-theory/problems/2/CONTEXT.md +8 -0
  15. package/packs/number-theory/problems/2/FRONTIER_NOTE.md +8 -0
  16. package/packs/number-theory/problems/2/OPS_DETAILS.yaml +25 -0
  17. package/packs/number-theory/problems/2/REPORT_TEMPLATE.md +7 -0
  18. package/packs/number-theory/problems/2/ROUTE_HISTORY.md +5 -0
  19. package/packs/number-theory/problems/2/ROUTE_PACKET.yaml +13 -0
  20. package/packs/number-theory/problems/2/context.yaml +25 -0
  21. package/packs/sunflower/README.md +17 -4
  22. package/packs/sunflower/problems/20/CHECKPOINT_TEMPLATE.md +29 -0
  23. package/packs/sunflower/problems/20/FRONTIER_NOTE.md +13 -0
  24. package/packs/sunflower/problems/20/OPS_DETAILS.yaml +44 -0
  25. package/packs/sunflower/problems/20/REPORT_TEMPLATE.md +23 -0
  26. package/packs/sunflower/problems/20/ROUTE_HISTORY.md +18 -0
  27. package/packs/sunflower/problems/536/CHECKPOINT_TEMPLATE.md +7 -0
  28. package/packs/sunflower/problems/536/FRONTIER_NOTE.md +8 -0
  29. package/packs/sunflower/problems/536/OPS_DETAILS.yaml +39 -0
  30. package/packs/sunflower/problems/536/REPORT_TEMPLATE.md +7 -0
  31. package/packs/sunflower/problems/536/ROUTE_HISTORY.md +5 -0
  32. package/packs/sunflower/problems/856/CHECKPOINT_TEMPLATE.md +7 -0
  33. package/packs/sunflower/problems/856/FRONTIER_NOTE.md +8 -0
  34. package/packs/sunflower/problems/856/OPS_DETAILS.yaml +39 -0
  35. package/packs/sunflower/problems/856/REPORT_TEMPLATE.md +7 -0
  36. package/packs/sunflower/problems/856/ROUTE_HISTORY.md +5 -0
  37. package/packs/sunflower/problems/857/CHECKPOINT_TEMPLATE.md +32 -0
  38. package/packs/sunflower/problems/857/FRONTIER_NOTE.md +18 -0
  39. package/packs/sunflower/problems/857/OPS_DETAILS.yaml +65 -0
  40. package/packs/sunflower/problems/857/REPORT_TEMPLATE.md +26 -0
  41. package/packs/sunflower/problems/857/ROUTE_HISTORY.md +25 -0
  42. package/src/cli/index.js +22 -3
  43. package/src/commands/archive.js +46 -0
  44. package/src/commands/cluster.js +4 -0
  45. package/src/commands/maintainer.js +20 -2
  46. package/src/commands/number-theory.js +199 -0
  47. package/src/commands/problem.js +3 -0
  48. package/src/commands/pull.js +180 -5
  49. package/src/commands/sunflower.js +290 -12
  50. package/src/commands/upstream.js +129 -0
  51. package/src/commands/workspace.js +20 -0
  52. package/src/runtime/archive.js +87 -0
  53. package/src/runtime/checkpoints.js +27 -0
  54. package/src/runtime/maintainer-seed.js +70 -0
  55. package/src/runtime/number-theory.js +169 -0
  56. package/src/runtime/paths.js +16 -0
  57. package/src/runtime/state.js +63 -3
  58. package/src/runtime/sunflower.js +329 -2
  59. package/src/runtime/workspace.js +4 -0
  60. package/src/upstream/literature.js +83 -0
@@ -1,9 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parse } from 'yaml';
4
- import { writeJson } from './files.js';
4
+ import { ensureDir, writeJson, writeText } from './files.js';
5
5
  import { buildBreakthroughsComputeView } from './breakthroughs.js';
6
- import { getPackDir, getPackProblemDir, getWorkspaceComputeRegistryDir } from './paths.js';
6
+ import {
7
+ getPackDir,
8
+ getPackProblemDir,
9
+ getWorkspaceComputeRegistryDir,
10
+ getWorkspaceRunDir,
11
+ } from './paths.js';
7
12
 
8
13
  const CLAIM_LEVEL_PRIORITY = {
9
14
  Exact: 4,
@@ -63,6 +68,26 @@ function getSunflowerAtomicBoardMarkdownPath(problemId) {
63
68
  return path.join(getSunflowerProblemDir(problemId), 'ATOMIC_BOARD.md');
64
69
  }
65
70
 
71
+ function getSunflowerFrontierNotePath(problemId) {
72
+ return path.join(getSunflowerProblemDir(problemId), 'FRONTIER_NOTE.md');
73
+ }
74
+
75
+ function getSunflowerRouteHistoryPath(problemId) {
76
+ return path.join(getSunflowerProblemDir(problemId), 'ROUTE_HISTORY.md');
77
+ }
78
+
79
+ function getSunflowerCheckpointTemplatePath(problemId) {
80
+ return path.join(getSunflowerProblemDir(problemId), 'CHECKPOINT_TEMPLATE.md');
81
+ }
82
+
83
+ function getSunflowerReportTemplatePath(problemId) {
84
+ return path.join(getSunflowerProblemDir(problemId), 'REPORT_TEMPLATE.md');
85
+ }
86
+
87
+ function getSunflowerOpsDetailsPath(problemId) {
88
+ return path.join(getSunflowerProblemDir(problemId), 'OPS_DETAILS.yaml');
89
+ }
90
+
66
91
  function parseStringList(value) {
67
92
  if (!Array.isArray(value)) {
68
93
  return [];
@@ -247,6 +272,73 @@ function parseMirageFrontiers(value) {
247
272
  return text ? [text] : [];
248
273
  }
249
274
 
275
+ function parseOpsRouteEntries(value) {
276
+ if (!Array.isArray(value)) {
277
+ return [];
278
+ }
279
+
280
+ return value
281
+ .filter((entry) => entry && typeof entry === 'object')
282
+ .map((entry) => ({
283
+ routeId: compactText(entry.route_id ?? entry.route),
284
+ title: compactText(entry.title),
285
+ status: compactText(entry.status),
286
+ theoremModule: compactText(entry.theorem_module),
287
+ summary: compactText(entry.summary),
288
+ whyNow: compactText(entry.why_now),
289
+ nextMove: compactText(entry.next_move),
290
+ ticketIds: parseStringList(entry.ticket_ids),
291
+ sourcePaths: parseStringList(entry.source_paths),
292
+ }));
293
+ }
294
+
295
+ function parseOpsTicketEntries(value) {
296
+ if (!Array.isArray(value)) {
297
+ return [];
298
+ }
299
+
300
+ return value
301
+ .filter((entry) => entry && typeof entry === 'object')
302
+ .map((entry) => ({
303
+ ticketId: compactText(entry.ticket_id),
304
+ title: compactText(entry.title ?? entry.ticket_name),
305
+ routeId: compactText(entry.route_id),
306
+ routeLeaf: compactText(entry.route_leaf),
307
+ status: compactText(entry.status),
308
+ summary: compactText(entry.summary),
309
+ gateStory: compactText(entry.gate_story),
310
+ currentBlocker: compactText(entry.current_blocker),
311
+ nextMove: compactText(entry.next_move),
312
+ atomIds: parseStringList(entry.atom_ids),
313
+ sourcePaths: parseStringList(entry.source_paths),
314
+ }));
315
+ }
316
+
317
+ function parseOpsAtomEntries(value) {
318
+ if (!Array.isArray(value)) {
319
+ return [];
320
+ }
321
+
322
+ return value
323
+ .filter((entry) => entry && typeof entry === 'object')
324
+ .map((entry) => ({
325
+ atomId: compactText(entry.atom_id),
326
+ title: compactText(entry.title),
327
+ ticketId: compactText(entry.ticket_id),
328
+ routeId: compactText(entry.route_id),
329
+ gateId: compactText(entry.gate_id),
330
+ tier: compactText(entry.tier),
331
+ kind: compactText(entry.kind),
332
+ status: compactText(entry.status),
333
+ summary: compactText(entry.summary),
334
+ whyNow: compactText(entry.why_now),
335
+ nextMove: compactText(entry.next_move),
336
+ verificationHook: parseStringList(entry.verification_hook),
337
+ dependencies: parseStringList(entry.dependencies),
338
+ sourcePaths: parseStringList(entry.source_paths),
339
+ }));
340
+ }
341
+
250
342
  function chooseActiveTicket(board) {
251
343
  if (!board) {
252
344
  return null;
@@ -301,6 +393,48 @@ function readSunflowerAtomicBoard(problemId) {
301
393
  };
302
394
  }
303
395
 
396
+ function readSunflowerOpsDetails(problemId) {
397
+ const opsDetailsPath = getSunflowerOpsDetailsPath(problemId);
398
+ if (!fs.existsSync(opsDetailsPath)) {
399
+ return null;
400
+ }
401
+
402
+ const parsed = parse(fs.readFileSync(opsDetailsPath, 'utf8')) ?? {};
403
+ const routes = parseOpsRouteEntries(parsed.routes);
404
+ const tickets = parseOpsTicketEntries(parsed.tickets);
405
+ const atoms = parseOpsAtomEntries(parsed.atoms);
406
+
407
+ return {
408
+ packetId: compactText(parsed.packet_id),
409
+ summary: compactText(parsed.summary),
410
+ routes,
411
+ tickets,
412
+ atoms,
413
+ path: opsDetailsPath,
414
+ };
415
+ }
416
+
417
+ function findRouteDetail(opsDetails, routeId) {
418
+ if (!opsDetails || !routeId) {
419
+ return null;
420
+ }
421
+ return opsDetails.routes.find((entry) => entry.routeId === routeId) ?? null;
422
+ }
423
+
424
+ function findTicketDetail(opsDetails, ticketId) {
425
+ if (!opsDetails || !ticketId) {
426
+ return null;
427
+ }
428
+ return opsDetails.tickets.find((entry) => entry.ticketId === ticketId) ?? null;
429
+ }
430
+
431
+ function findAtomDetail(opsDetails, atomId) {
432
+ if (!opsDetails || !atomId) {
433
+ return null;
434
+ }
435
+ return opsDetails.atoms.find((entry) => entry.atomId === atomId) ?? null;
436
+ }
437
+
304
438
  function chooseActivePacket(packets) {
305
439
  if (packets.length === 0) {
306
440
  return null;
@@ -436,6 +570,21 @@ function compactAtomicBoard(board) {
436
570
  };
437
571
  }
438
572
 
573
+ function compactOpsDetails(opsDetails) {
574
+ if (!opsDetails) {
575
+ return null;
576
+ }
577
+
578
+ return {
579
+ packetId: opsDetails.packetId,
580
+ summary: opsDetails.summary,
581
+ path: opsDetails.path,
582
+ routes: opsDetails.routes,
583
+ tickets: opsDetails.tickets,
584
+ atoms: opsDetails.atoms,
585
+ };
586
+ }
587
+
439
588
  function deriveRouteState(problem, context) {
440
589
  const researchState = problem.researchState ?? {};
441
590
  const solvedBySite = String(problem.siteStatus ?? '').toLowerCase() === 'solved';
@@ -470,6 +619,7 @@ export function buildSunflowerStatusSnapshot(problem) {
470
619
  const context = readSunflowerContext(problem.problemId);
471
620
  const routePacket = readSunflowerRoutePacket(problem.problemId);
472
621
  const atomicBoard = readSunflowerAtomicBoard(problem.problemId);
622
+ const opsDetails = readSunflowerOpsDetails(problem.problemId);
473
623
  const packets = listSunflowerComputePackets(problem.problemId);
474
624
  const activePacket = chooseActivePacket(packets);
475
625
  const summary = deriveSummary(activePacket);
@@ -478,8 +628,15 @@ export function buildSunflowerStatusSnapshot(problem) {
478
628
  const agentStartPath = getSunflowerAgentStartPath(problem.problemId);
479
629
  const checkpointPacketPath = getSunflowerCheckpointPacketPath(problem.problemId);
480
630
  const reportPacketPath = getSunflowerReportPacketPath(problem.problemId);
631
+ const frontierNotePath = getSunflowerFrontierNotePath(problem.problemId);
632
+ const routeHistoryPath = getSunflowerRouteHistoryPath(problem.problemId);
633
+ const checkpointTemplatePath = getSunflowerCheckpointTemplatePath(problem.problemId);
634
+ const reportTemplatePath = getSunflowerReportTemplatePath(problem.problemId);
481
635
 
482
636
  const firstReadyAtom = atomicBoard?.readyQueue?.[0] ?? null;
637
+ const activeRouteDetail = findRouteDetail(opsDetails, routeState.activeRoute ?? atomicBoard?.activeRoute);
638
+ const activeTicketDetail = findTicketDetail(opsDetails, atomicBoard?.activeTicket?.ticketId);
639
+ const activeAtomDetail = findAtomDetail(opsDetails, firstReadyAtom?.atomId);
483
640
 
484
641
  return {
485
642
  generatedAt: new Date().toISOString(),
@@ -524,6 +681,14 @@ export function buildSunflowerStatusSnapshot(problem) {
524
681
  checkpointPacketPath: fs.existsSync(checkpointPacketPath) ? checkpointPacketPath : null,
525
682
  reportPacketPresent: fs.existsSync(reportPacketPath),
526
683
  reportPacketPath: fs.existsSync(reportPacketPath) ? reportPacketPath : null,
684
+ frontierNotePresent: fs.existsSync(frontierNotePath),
685
+ frontierNotePath: fs.existsSync(frontierNotePath) ? frontierNotePath : null,
686
+ routeHistoryPresent: fs.existsSync(routeHistoryPath),
687
+ routeHistoryPath: fs.existsSync(routeHistoryPath) ? routeHistoryPath : null,
688
+ checkpointTemplatePresent: fs.existsSync(checkpointTemplatePath),
689
+ checkpointTemplatePath: fs.existsSync(checkpointTemplatePath) ? checkpointTemplatePath : null,
690
+ reportTemplatePresent: fs.existsSync(reportTemplatePath),
691
+ reportTemplatePath: fs.existsSync(reportTemplatePath) ? reportTemplatePath : null,
527
692
  atomicBoardPresent: Boolean(atomicBoard),
528
693
  atomicBoardPath: atomicBoard?.atomicBoardPath ?? null,
529
694
  atomicBoardMarkdownPath: atomicBoard?.atomicBoardMarkdownExists ? atomicBoard.atomicBoardMarkdownPath : null,
@@ -532,6 +697,11 @@ export function buildSunflowerStatusSnapshot(problem) {
532
697
  readyAtomCount: atomicBoard?.readyQueue?.length ?? 0,
533
698
  firstReadyAtom,
534
699
  mirageFrontierCount: atomicBoard?.mirageFrontiers?.length ?? 0,
700
+ opsDetailsPresent: Boolean(opsDetails),
701
+ opsDetailsPath: opsDetails?.path ?? null,
702
+ activeRouteDetail,
703
+ activeTicketDetail,
704
+ activeAtomDetail,
535
705
  computeLanePresent: Boolean(activePacket),
536
706
  computeLaneCount: packets.length,
537
707
  computeSummary: summary.computeSummary,
@@ -543,6 +713,7 @@ export function buildSunflowerStatusSnapshot(problem) {
543
713
  activePacket: compactPacket(activePacket),
544
714
  computePackets: packets.map((packet) => compactPacket(packet)),
545
715
  atomicBoardSummary: compactAtomicBoard(atomicBoard),
716
+ opsDetails: compactOpsDetails(opsDetails),
546
717
  };
547
718
  }
548
719
 
@@ -559,3 +730,159 @@ export function writeSunflowerStatusRecord(problem, snapshot, workspaceRoot) {
559
730
  latestPath,
560
731
  };
561
732
  }
733
+
734
+ export function getSunflowerRouteSnapshot(problem, routeId) {
735
+ const snapshot = buildSunflowerStatusSnapshot(problem);
736
+ const boardRoute = snapshot.atomicBoardSummary?.routeStatus?.find((route) => route.route === routeId) ?? null;
737
+ const routeDetail = findRouteDetail(snapshot.opsDetails, routeId);
738
+ if (!boardRoute && !routeDetail) {
739
+ return null;
740
+ }
741
+
742
+ return {
743
+ problemId: problem.problemId,
744
+ displayName: problem.displayName,
745
+ routeId,
746
+ activeRoute: snapshot.activeRoute,
747
+ routeBreakthrough: snapshot.routeBreakthrough,
748
+ boardRoute,
749
+ routeDetail,
750
+ activeTicket: snapshot.activeTicket,
751
+ firstReadyAtom: snapshot.firstReadyAtom,
752
+ snapshot,
753
+ };
754
+ }
755
+
756
+ export function getSunflowerTicketSnapshot(problem, ticketId) {
757
+ const snapshot = buildSunflowerStatusSnapshot(problem);
758
+ const boardTicket = snapshot.atomicBoardSummary?.tickets?.find((ticket) => ticket.ticketId === ticketId) ?? null;
759
+ const ticketDetail = findTicketDetail(snapshot.opsDetails, ticketId);
760
+ if (!boardTicket && !ticketDetail) {
761
+ return null;
762
+ }
763
+
764
+ return {
765
+ problemId: problem.problemId,
766
+ displayName: problem.displayName,
767
+ ticketId,
768
+ activeTicketId: snapshot.activeTicket?.ticketId ?? null,
769
+ boardTicket,
770
+ ticketDetail,
771
+ firstReadyAtom: snapshot.firstReadyAtom,
772
+ snapshot,
773
+ };
774
+ }
775
+
776
+ export function getSunflowerAtomSnapshot(problem, atomId) {
777
+ const snapshot = buildSunflowerStatusSnapshot(problem);
778
+ const boardAtom = snapshot.atomicBoardSummary?.readyQueue?.find((atom) => atom.atomId === atomId) ?? null;
779
+ const atomDetail = findAtomDetail(snapshot.opsDetails, atomId);
780
+ if (!boardAtom && !atomDetail) {
781
+ return null;
782
+ }
783
+
784
+ return {
785
+ problemId: problem.problemId,
786
+ displayName: problem.displayName,
787
+ atomId,
788
+ boardAtom,
789
+ atomDetail,
790
+ firstReadyAtom: snapshot.firstReadyAtom,
791
+ snapshot,
792
+ };
793
+ }
794
+
795
+ export function runSunflowerLocalScout(problem, workspaceRoot) {
796
+ const snapshot = buildSunflowerStatusSnapshot(problem);
797
+ const governance = snapshot.computeGovernance;
798
+
799
+ if (!snapshot.activePacket || !governance) {
800
+ throw new Error(`Problem ${problem.problemId} does not have an admitted sunflower compute packet.`);
801
+ }
802
+
803
+ if (governance.dispatchResult.action !== 'run_local' || governance.selectedRung.spendClass !== 'local_unmetered') {
804
+ throw new Error(
805
+ `Problem ${problem.problemId} is not currently admitted for a local scout run. Current action: ${governance.dispatchResult.action}.`,
806
+ );
807
+ }
808
+
809
+ const runId = `${new Date().toISOString().replaceAll(':', '-')}__sunflower_p${problem.problemId}__${snapshot.activePacket.laneId}`;
810
+ const runDir = getWorkspaceRunDir(runId, workspaceRoot);
811
+ ensureDir(runDir);
812
+
813
+ const runRecord = {
814
+ runId,
815
+ generatedAt: new Date().toISOString(),
816
+ problemId: problem.problemId,
817
+ displayName: problem.displayName,
818
+ cluster: problem.cluster,
819
+ laneId: snapshot.activePacket.laneId,
820
+ packetStatus: snapshot.activePacket.status,
821
+ dispatchAction: governance.dispatchResult.action,
822
+ selectedRung: governance.selectedRung,
823
+ question: governance.question,
824
+ currentFrontier: snapshot.firstReadyAtom
825
+ ? `${snapshot.firstReadyAtom.atomId} — ${snapshot.firstReadyAtom.title}`
826
+ : snapshot.frontierDetail,
827
+ artifacts: {
828
+ statusRecordPath: path.join(runDir, 'STATUS_RECORD.json'),
829
+ runSummaryPath: path.join(runDir, 'RUN_SUMMARY.md'),
830
+ runLogPath: path.join(runDir, 'RUN_LOG.txt'),
831
+ governancePath: path.join(runDir, 'GOVERNANCE.json'),
832
+ orpPacketPath: path.join(runDir, 'ORP_COMPUTE_PACKET.json'),
833
+ },
834
+ };
835
+
836
+ writeJson(path.join(runDir, 'RUN.json'), runRecord);
837
+ writeJson(path.join(runDir, 'STATUS_RECORD.json'), snapshot);
838
+ writeJson(path.join(runDir, 'GOVERNANCE.json'), governance);
839
+ writeJson(path.join(runDir, 'ORP_COMPUTE_PACKET.json'), governance.orpPacket);
840
+ writeText(
841
+ path.join(runDir, 'RUN_LOG.txt'),
842
+ [
843
+ `sunflower local scout`,
844
+ `problem=${problem.problemId}`,
845
+ `lane=${snapshot.activePacket.laneId}`,
846
+ `action=${governance.dispatchResult.action}`,
847
+ `rung=${governance.selectedRung.label}`,
848
+ `question=${governance.question}`,
849
+ `frontier=${runRecord.currentFrontier}`,
850
+ `when=${governance.when}`,
851
+ ].join('\n') + '\n',
852
+ );
853
+ writeText(
854
+ path.join(runDir, 'RUN_SUMMARY.md'),
855
+ [
856
+ `# Sunflower Local Scout Run`,
857
+ '',
858
+ `- Problem: ${problem.displayName}`,
859
+ `- Lane: ${snapshot.activePacket.laneId}`,
860
+ `- Dispatch action: ${governance.dispatchResult.action}`,
861
+ `- Selected rung: ${governance.selectedRung.label} [${governance.selectedRung.spendClass}]`,
862
+ `- Question: ${governance.question}`,
863
+ `- Current frontier: ${runRecord.currentFrontier}`,
864
+ '',
865
+ 'Why this run exists:',
866
+ `- ${snapshot.computeSummary}`,
867
+ `- ${governance.when}`,
868
+ '',
869
+ 'Run outputs:',
870
+ '- RUN.json',
871
+ '- STATUS_RECORD.json',
872
+ '- GOVERNANCE.json',
873
+ '- ORP_COMPUTE_PACKET.json',
874
+ '- RUN_LOG.txt',
875
+ '',
876
+ 'Important boundary:',
877
+ '- This is a governed local-scout artifact bundle. It does not upgrade problem-level claims by itself.',
878
+ '',
879
+ ].join('\n'),
880
+ );
881
+
882
+ return {
883
+ runId,
884
+ runDir,
885
+ runRecord,
886
+ snapshot,
887
+ };
888
+ }
@@ -13,7 +13,9 @@ import {
13
13
  getWorkspaceProblemPullDir,
14
14
  getWorkspaceProblemScaffoldDir,
15
15
  getWorkspaceQuestionLedgerPath,
16
+ getWorkspaceArchivesDir,
16
17
  getWorkspaceRoot,
18
+ getWorkspaceRunsDir,
17
19
  getWorkspaceSeededProblemDir,
18
20
  getWorkspaceSeededProblemsDir,
19
21
  getWorkspaceStateMarkdownPath,
@@ -93,6 +95,8 @@ export function getWorkspaceSummary(workspaceRoot = getWorkspaceRoot()) {
93
95
  artifactDir: activeProblem ? getWorkspaceProblemArtifactDir(activeProblem, workspaceRoot) : getWorkspaceProblemArtifactDir('<problem-id>', workspaceRoot),
94
96
  literatureDir: activeProblem ? getWorkspaceProblemLiteratureDir(activeProblem, workspaceRoot) : getWorkspaceProblemLiteratureDir('<problem-id>', workspaceRoot),
95
97
  seededProblemDir: activeProblem ? getWorkspaceSeededProblemDir(activeProblem, workspaceRoot) : getWorkspaceSeededProblemDir('<problem-id>', workspaceRoot),
98
+ runsDir: getWorkspaceRunsDir(workspaceRoot),
99
+ archivesDir: getWorkspaceArchivesDir(workspaceRoot),
96
100
  updatedAt: state?.updatedAt ?? null,
97
101
  continuationMode: state?.continuation?.mode ?? null,
98
102
  activeRoute: state?.activeRoute ?? null,
@@ -0,0 +1,83 @@
1
+ function normalizeText(value) {
2
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
3
+ }
4
+
5
+ function uniqueStrings(values) {
6
+ return [...new Set(values.map((value) => normalizeText(value)).filter(Boolean))];
7
+ }
8
+
9
+ export function buildLiteratureQueries(problemId, title) {
10
+ const cleanTitle = normalizeText(title);
11
+ return uniqueStrings([
12
+ `Erdos Problem ${problemId}`,
13
+ cleanTitle,
14
+ cleanTitle ? `${cleanTitle} Erdos` : null,
15
+ cleanTitle ? `${cleanTitle} combinatorics` : null,
16
+ ]);
17
+ }
18
+
19
+ export async function fetchCrossrefLiterature(problemId, title) {
20
+ const query = buildLiteratureQueries(problemId, title)[0] ?? `Erdos Problem ${problemId}`;
21
+ const url = `https://api.crossref.org/works?query.title=${encodeURIComponent(query)}&rows=5&sort=relevance`;
22
+ const response = await fetch(url, {
23
+ headers: {
24
+ 'User-Agent': 'erdos-problems-cli',
25
+ Accept: 'application/json',
26
+ },
27
+ });
28
+
29
+ if (!response.ok) {
30
+ throw new Error(`Crossref lookup failed for problem ${problemId}: ${response.status}`);
31
+ }
32
+
33
+ const payload = await response.json();
34
+ const items = Array.isArray(payload?.message?.items) ? payload.message.items : [];
35
+
36
+ return {
37
+ provider: 'crossref',
38
+ fetchedAt: new Date().toISOString(),
39
+ problemId: String(problemId),
40
+ query,
41
+ results: items.slice(0, 5).map((item) => ({
42
+ title: normalizeText(Array.isArray(item?.title) ? item.title[0] : item?.title),
43
+ url: normalizeText(item?.URL),
44
+ doi: normalizeText(item?.DOI),
45
+ publisher: normalizeText(item?.publisher),
46
+ published:
47
+ item?.issued?.['date-parts']?.[0]?.filter((value) => value !== null && value !== undefined).join('-')
48
+ ?? null,
49
+ })),
50
+ };
51
+ }
52
+
53
+ export async function fetchOpenAlexLiterature(problemId, title) {
54
+ const query = buildLiteratureQueries(problemId, title)[0] ?? `Erdos Problem ${problemId}`;
55
+ const url = `https://api.openalex.org/works?search=${encodeURIComponent(query)}&per-page=5&sort=relevance_score:desc`;
56
+ const response = await fetch(url, {
57
+ headers: {
58
+ 'User-Agent': 'erdos-problems-cli',
59
+ Accept: 'application/json',
60
+ },
61
+ });
62
+
63
+ if (!response.ok) {
64
+ throw new Error(`OpenAlex lookup failed for problem ${problemId}: ${response.status}`);
65
+ }
66
+
67
+ const payload = await response.json();
68
+ const results = Array.isArray(payload?.results) ? payload.results : [];
69
+
70
+ return {
71
+ provider: 'openalex',
72
+ fetchedAt: new Date().toISOString(),
73
+ problemId: String(problemId),
74
+ query,
75
+ results: results.slice(0, 5).map((item) => ({
76
+ title: normalizeText(item?.title),
77
+ url: normalizeText(item?.primary_location?.landing_page_url ?? item?.id),
78
+ doi: normalizeText(item?.doi),
79
+ citedByCount: Number(item?.cited_by_count ?? 0),
80
+ publicationYear: item?.publication_year ?? null,
81
+ })),
82
+ };
83
+ }