syntaur 0.16.2 → 0.16.3

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.
@@ -4658,6 +4658,30 @@ function setUpdatedField(content, value) {
4658
4658
  function escapeRegExp2(value) {
4659
4659
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4660
4660
  }
4661
+ function createTraces() {
4662
+ return { entries: [], subPhases: /* @__PURE__ */ new Map() };
4663
+ }
4664
+ async function timed(traces, label, fn) {
4665
+ if (!traces) return fn();
4666
+ const start = performance.now();
4667
+ try {
4668
+ return await fn();
4669
+ } finally {
4670
+ traces.entries.push({ label, ms: performance.now() - start });
4671
+ }
4672
+ }
4673
+ function accumulatePhase(traces, label, ms) {
4674
+ if (!traces) return;
4675
+ traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);
4676
+ }
4677
+ function emitTrace(traces, meta) {
4678
+ if (process.env.SYNTAUR_PERF_TRACE !== "1") return;
4679
+ const totalMs = traces.entries.reduce((sum, entry) => sum + entry.ms, 0);
4680
+ const subPhases = Object.fromEntries(traces.subPhases);
4681
+ console.log(
4682
+ JSON.stringify({ kind: "overview-trace", totalMs, phases: traces.entries, subPhases, ...meta })
4683
+ );
4684
+ }
4661
4685
  async function listStandaloneRecords(assignmentsDir2) {
4662
4686
  if (!assignmentsDir2) return [];
4663
4687
  if (!await fileExists(assignmentsDir2)) return [];
@@ -4823,8 +4847,19 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
4823
4847
  return { rewroteFiles };
4824
4848
  }
4825
4849
  async function getOverview(projectsDir, serversDir2, assignmentsDir2, options = {}) {
4826
- const projectRecords = await listProjectRecords(projectsDir);
4827
- const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
4850
+ const traceEnabled = process.env.SYNTAUR_PERF_TRACE === "1";
4851
+ const traces = traceEnabled ? createTraces() : void 0;
4852
+ const overallStart = traceEnabled ? performance.now() : 0;
4853
+ const projectRecords = await timed(
4854
+ traces,
4855
+ "list-project-records",
4856
+ () => listProjectRecords(projectsDir, traces)
4857
+ );
4858
+ const standaloneRecords = await timed(
4859
+ traces,
4860
+ "list-standalone-records",
4861
+ () => listStandaloneRecords(assignmentsDir2)
4862
+ );
4828
4863
  const recentActivity = buildRecentActivity(projectRecords, standaloneRecords);
4829
4864
  const staleLimit = clamp(
4830
4865
  Number.isFinite(options.staleLimit) ? Number(options.staleLimit) : STALE_LIMIT_DEFAULT,
@@ -4832,12 +4867,16 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
4832
4867
  STALE_LIMIT_MAX
4833
4868
  );
4834
4869
  const staleOffset = Math.max(0, Number.isFinite(options.staleOffset) ? Number(options.staleOffset) : 0);
4835
- const buckets = await buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords);
4870
+ const buckets = await timed(
4871
+ traces,
4872
+ "build-segment-buckets",
4873
+ () => buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces)
4874
+ );
4836
4875
  const segments = toOverviewSegments(buckets, { staleLimit, staleOffset });
4837
4876
  const hero = pickOverviewHero(buckets);
4838
4877
  let recentSessions = [];
4839
4878
  try {
4840
- const all = await listAllSessions(projectsDir);
4879
+ const all = await timed(traces, "list-recent-sessions", () => listAllSessions(projectsDir));
4841
4880
  recentSessions = all.slice(0, RECENT_SESSIONS_LIMIT);
4842
4881
  } catch {
4843
4882
  }
@@ -4845,7 +4884,11 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
4845
4884
  if (serversDir2) {
4846
4885
  try {
4847
4886
  const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
4848
- const servers = await scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 });
4887
+ const servers = await timed(
4888
+ traces,
4889
+ "scan-tmux-sessions",
4890
+ () => scanAllSessions2(serversDir2, projectsDir, { assignmentsDir: assignmentsDir2 })
4891
+ );
4849
4892
  if (servers.tmuxAvailable) {
4850
4893
  const alive = servers.sessions.filter((s) => s.alive).length;
4851
4894
  const totalPorts = servers.sessions.reduce((sum, s) => sum + s.windows.reduce((ws, w) => ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);
@@ -4859,6 +4902,14 @@ async function getOverview(projectsDir, serversDir2, assignmentsDir2, options =
4859
4902
  } catch {
4860
4903
  }
4861
4904
  }
4905
+ if (traces) {
4906
+ const wallMs = performance.now() - overallStart;
4907
+ const totalAssignments = projectRecords.reduce((sum, r) => sum + r.assignments.length, 0) + standaloneRecords.length;
4908
+ emitTrace(traces, {
4909
+ wallMs,
4910
+ fixture: { projects: projectRecords.length, assignments: totalAssignments }
4911
+ });
4912
+ }
4862
4913
  return {
4863
4914
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4864
4915
  firstRun: projectRecords.length === 0 && standaloneRecords.length === 0,
@@ -5389,7 +5440,7 @@ async function buildStandaloneAssignmentDetail(resolved) {
5389
5440
  };
5390
5441
  return detail;
5391
5442
  }
5392
- async function listProjectRecords(projectsDir) {
5443
+ async function listProjectRecords(projectsDir, traces) {
5393
5444
  if (!await fileExists(projectsDir)) {
5394
5445
  return [];
5395
5446
  }
@@ -5399,61 +5450,75 @@ async function listProjectRecords(projectsDir) {
5399
5450
  }
5400
5451
  const entries = await readdir8(projectsDir, { withFileTypes: true });
5401
5452
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
5402
- const records = [];
5403
- for (const entry of projectDirs) {
5404
- const projectPath = resolve13(projectsDir, entry.name);
5405
- const projectMdPath = resolve13(projectPath, "project.md");
5406
- if (!await fileExists(projectMdPath)) {
5407
- continue;
5408
- }
5409
- const projectContent = await readFile10(projectMdPath, "utf-8");
5410
- const project = parseProject(projectContent);
5411
- const assignments = await listAssignmentRecords(projectPath);
5412
- const rollup = await buildProjectRollup(projectPath, project, assignments);
5413
- const updated = getProjectActivityTimestamp(project.updated, assignments);
5414
- records.push({
5415
- projectPath,
5416
- project,
5417
- assignments,
5418
- dependencyGraph: await loadDependencyGraph(projectPath, assignments),
5419
- summary: {
5420
- slug: project.slug || entry.name,
5421
- title: project.title,
5422
- status: rollup.status,
5423
- statusOverride: project.statusOverride,
5424
- archived: project.archived,
5425
- archivedAt: project.archivedAt,
5426
- archivedReason: project.archivedReason,
5427
- created: project.created,
5428
- updated,
5429
- tags: project.tags,
5430
- progress: rollup.progress,
5431
- needsAttention: rollup.needsAttention,
5432
- workspace: project.workspace
5453
+ const maybeRecords = await Promise.all(
5454
+ projectDirs.map(async (entry) => {
5455
+ const projectPath = resolve13(projectsDir, entry.name);
5456
+ const projectMdPath = resolve13(projectPath, "project.md");
5457
+ if (!await fileExists(projectMdPath)) {
5458
+ return null;
5433
5459
  }
5434
- });
5435
- }
5460
+ const t0 = traces ? performance.now() : 0;
5461
+ const projectContent = await readFile10(projectMdPath, "utf-8");
5462
+ const project = parseProject(projectContent);
5463
+ if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
5464
+ const t1 = traces ? performance.now() : 0;
5465
+ const assignments = await listAssignmentRecords(projectPath, traces);
5466
+ if (traces) accumulatePhase(traces, "list-assignments", performance.now() - t1);
5467
+ const t2 = traces ? performance.now() : 0;
5468
+ const rollup = await buildProjectRollup(projectPath, project, assignments, traces);
5469
+ if (traces) accumulatePhase(traces, "build-rollup", performance.now() - t2);
5470
+ const updated = getProjectActivityTimestamp(project.updated, assignments);
5471
+ const t3 = traces ? performance.now() : 0;
5472
+ const dependencyGraph = await loadDependencyGraph(projectPath, assignments);
5473
+ if (traces) accumulatePhase(traces, "load-dep-graph", performance.now() - t3);
5474
+ return {
5475
+ projectPath,
5476
+ project,
5477
+ assignments,
5478
+ dependencyGraph,
5479
+ summary: {
5480
+ slug: project.slug || entry.name,
5481
+ title: project.title,
5482
+ status: rollup.status,
5483
+ statusOverride: project.statusOverride,
5484
+ archived: project.archived,
5485
+ archivedAt: project.archivedAt,
5486
+ archivedReason: project.archivedReason,
5487
+ created: project.created,
5488
+ updated,
5489
+ tags: project.tags,
5490
+ progress: rollup.progress,
5491
+ needsAttention: rollup.needsAttention,
5492
+ workspace: project.workspace
5493
+ }
5494
+ };
5495
+ })
5496
+ );
5497
+ const records = maybeRecords.filter((r) => r !== null);
5436
5498
  records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));
5437
5499
  return records;
5438
5500
  }
5439
- async function listAssignmentRecords(projectPath) {
5501
+ async function listAssignmentRecords(projectPath, traces) {
5440
5502
  const assignmentsDir2 = resolve13(projectPath, "assignments");
5441
5503
  if (!await fileExists(assignmentsDir2)) {
5442
5504
  return [];
5443
5505
  }
5444
5506
  const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
5445
- const records = [];
5446
- for (const entry of entries) {
5447
- if (!entry.isDirectory()) {
5448
- continue;
5449
- }
5450
- const assignmentMd = resolve13(assignmentsDir2, entry.name, "assignment.md");
5451
- if (!await fileExists(assignmentMd)) {
5452
- continue;
5453
- }
5454
- const content = await readFile10(assignmentMd, "utf-8");
5455
- records.push(parseAssignmentFull(content));
5456
- }
5507
+ const dirEntries = entries.filter((entry) => entry.isDirectory());
5508
+ const maybeRecords = await Promise.all(
5509
+ dirEntries.map(async (entry) => {
5510
+ const assignmentMd = resolve13(assignmentsDir2, entry.name, "assignment.md");
5511
+ if (!await fileExists(assignmentMd)) {
5512
+ return null;
5513
+ }
5514
+ const t0 = traces ? performance.now() : 0;
5515
+ const content = await readFile10(assignmentMd, "utf-8");
5516
+ const parsed = parseAssignmentFull(content);
5517
+ if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
5518
+ return parsed;
5519
+ })
5520
+ );
5521
+ const records = maybeRecords.filter((r) => r !== null);
5457
5522
  records.sort((left, right) => compareTimestamps(right.updated, left.updated));
5458
5523
  return records;
5459
5524
  }
@@ -5611,13 +5676,20 @@ async function loadDependencyGraph(projectPath, assignments) {
5611
5676
  }
5612
5677
  return buildDependencyGraph(assignments);
5613
5678
  }
5614
- async function buildProjectRollup(projectPath, project, assignments) {
5679
+ async function buildProjectRollup(projectPath, project, assignments, traces) {
5615
5680
  const progress = { total: assignments.length };
5681
+ const perAssignment = await Promise.all(
5682
+ assignments.map(async (assignment) => {
5683
+ const t0 = traces ? performance.now() : 0;
5684
+ const openQuestions2 = await countOpenQuestions(projectPath, assignment.slug);
5685
+ if (traces) accumulatePhase(traces, "count-open-questions", performance.now() - t0);
5686
+ return { status: assignment.status, openQuestions: openQuestions2 };
5687
+ })
5688
+ );
5616
5689
  let openQuestions = 0;
5617
- for (const assignment of assignments) {
5618
- const s = assignment.status;
5619
- progress[s] = (progress[s] ?? 0) + 1;
5620
- openQuestions += await countOpenQuestions(projectPath, assignment.slug);
5690
+ for (const entry of perAssignment) {
5691
+ progress[entry.status] = (progress[entry.status] ?? 0) + 1;
5692
+ openQuestions += entry.openQuestions;
5621
5693
  }
5622
5694
  const needsAttention = {
5623
5695
  blockedCount: progress["blocked"] ?? 0,
@@ -5698,18 +5770,26 @@ function buildDependencyGraph(assignments) {
5698
5770
  function findAssignmentStatus(assignments, slug) {
5699
5771
  return assignments.find((assignment) => assignment.slug === slug)?.status ?? "pending";
5700
5772
  }
5701
- async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment) {
5773
+ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug, assignment, options) {
5702
5774
  const config = await getStatusConfig();
5703
5775
  const transitionDefs = getTransitionDefinitions(config);
5704
5776
  const actions = [];
5705
5777
  const projectPath = resolve13(projectsDir, projectSlug);
5778
+ const traces = options?.traces;
5706
5779
  for (const definition of transitionDefs) {
5707
5780
  let warning = null;
5708
5781
  if (definition.command === "start" && !assignment.assignee) {
5709
5782
  warning = "No assignee set \u2014 consider assigning before starting.";
5710
5783
  }
5711
5784
  if (definition.command === "start" && assignment.dependsOn.length > 0) {
5712
- const unmetDependencies = await getUnmetDependencies(projectPath, assignment.dependsOn, config.terminalStatuses);
5785
+ const t0 = traces ? performance.now() : 0;
5786
+ const unmetDependencies = await getUnmetDependencies(
5787
+ projectPath,
5788
+ assignment.dependsOn,
5789
+ config.terminalStatuses,
5790
+ options?.dependencyStatusMap
5791
+ );
5792
+ if (traces) accumulatePhase(traces, "get-unmet-dependencies", performance.now() - t0);
5713
5793
  if (unmetDependencies.length > 0) {
5714
5794
  warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
5715
5795
  }
@@ -5728,10 +5808,19 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
5728
5808
  }
5729
5809
  return actions;
5730
5810
  }
5731
- async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses) {
5811
+ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, dependencyStatusMap) {
5732
5812
  const terminals = terminalStatuses ?? /* @__PURE__ */ new Set(["completed"]);
5733
5813
  const unmet = [];
5734
5814
  for (const dependency of dependsOn) {
5815
+ if (dependencyStatusMap) {
5816
+ const mappedStatus = dependencyStatusMap.get(dependency);
5817
+ if (mappedStatus !== void 0) {
5818
+ if (!terminals.has(mappedStatus)) {
5819
+ unmet.push(`${dependency} (${mappedStatus})`);
5820
+ }
5821
+ continue;
5822
+ }
5823
+ }
5735
5824
  const dependencyPath = resolve13(projectPath, "assignments", dependency, "assignment.md");
5736
5825
  if (!await fileExists(dependencyPath)) {
5737
5826
  unmet.push(`${dependency} (missing)`);
@@ -5769,23 +5858,35 @@ function segmentSeverity(segment) {
5769
5858
  return "medium";
5770
5859
  }
5771
5860
  }
5772
- async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords) {
5861
+ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standaloneRecords, traces) {
5773
5862
  const now = Date.now();
5774
5863
  const buckets = emptyBuckets();
5775
5864
  const newestPool = [];
5776
5865
  for (const record of projectRecords) {
5777
- for (const assignment of record.assignments) {
5866
+ const depMap = /* @__PURE__ */ new Map();
5867
+ for (const a of record.assignments) {
5868
+ depMap.set(a.slug, a.status);
5869
+ }
5870
+ const resolvedTransitions = await Promise.all(
5871
+ record.assignments.map(async (assignment) => {
5872
+ const t0 = traces ? performance.now() : 0;
5873
+ const availableTransitions = await getAvailableTransitions(
5874
+ projectsDir,
5875
+ record.summary.slug,
5876
+ assignment.slug,
5877
+ assignment,
5878
+ { traces, dependencyStatusMap: depMap }
5879
+ );
5880
+ if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
5881
+ return { assignment, availableTransitions };
5882
+ })
5883
+ );
5884
+ for (const { assignment, availableTransitions } of resolvedTransitions) {
5778
5885
  const segmentId = STATUS_TO_SEGMENT[assignment.status];
5779
5886
  const stale = isStale(assignment.updated);
5780
5887
  const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
5781
5888
  const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
5782
5889
  const baseId = `${record.summary.slug}:${assignment.slug}`;
5783
- const availableTransitions = await getAvailableTransitions(
5784
- projectsDir,
5785
- record.summary.slug,
5786
- assignment.slug,
5787
- assignment
5788
- );
5789
5890
  const shared = {
5790
5891
  projectSlug: record.summary.slug,
5791
5892
  projectTitle: record.summary.title,
@@ -5835,14 +5936,21 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
5835
5936
  }
5836
5937
  }
5837
5938
  }
5838
- for (const sr of standaloneRecords) {
5939
+ const resolvedStandaloneTransitions = await Promise.all(
5940
+ standaloneRecords.map(async (sr) => {
5941
+ const t0 = traces ? performance.now() : 0;
5942
+ const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
5943
+ if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
5944
+ return { sr, availableTransitions };
5945
+ })
5946
+ );
5947
+ for (const { sr, availableTransitions } of resolvedStandaloneTransitions) {
5839
5948
  const assignment = sr.record;
5840
5949
  const segmentId = STATUS_TO_SEGMENT[assignment.status];
5841
5950
  const stale = isStale(assignment.updated);
5842
5951
  const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
5843
5952
  const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
5844
5953
  const baseId = `standalone:${sr.id}`;
5845
- const availableTransitions = await getStandaloneAvailableTransitions(assignment);
5846
5954
  const shared = {
5847
5955
  projectSlug: null,
5848
5956
  projectTitle: null,