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.
package/dist/index.js CHANGED
@@ -5829,6 +5829,30 @@ function setUpdatedField(content, value) {
5829
5829
  function escapeRegExp2(value) {
5830
5830
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5831
5831
  }
5832
+ function createTraces() {
5833
+ return { entries: [], subPhases: /* @__PURE__ */ new Map() };
5834
+ }
5835
+ async function timed(traces, label, fn) {
5836
+ if (!traces) return fn();
5837
+ const start = performance.now();
5838
+ try {
5839
+ return await fn();
5840
+ } finally {
5841
+ traces.entries.push({ label, ms: performance.now() - start });
5842
+ }
5843
+ }
5844
+ function accumulatePhase(traces, label, ms) {
5845
+ if (!traces) return;
5846
+ traces.subPhases.set(label, (traces.subPhases.get(label) ?? 0) + ms);
5847
+ }
5848
+ function emitTrace(traces, meta) {
5849
+ if (process.env.SYNTAUR_PERF_TRACE !== "1") return;
5850
+ const totalMs = traces.entries.reduce((sum, entry) => sum + entry.ms, 0);
5851
+ const subPhases = Object.fromEntries(traces.subPhases);
5852
+ console.log(
5853
+ JSON.stringify({ kind: "overview-trace", totalMs, phases: traces.entries, subPhases, ...meta })
5854
+ );
5855
+ }
5832
5856
  async function listStandaloneRecords(assignmentsDir2) {
5833
5857
  if (!assignmentsDir2) return [];
5834
5858
  if (!await fileExists(assignmentsDir2)) return [];
@@ -5994,8 +6018,19 @@ async function deleteWorkspace(projectsDir2, name, opts = {}) {
5994
6018
  return { rewroteFiles };
5995
6019
  }
5996
6020
  async function getOverview(projectsDir2, serversDir2, assignmentsDir2, options = {}) {
5997
- const projectRecords = await listProjectRecords(projectsDir2);
5998
- const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
6021
+ const traceEnabled = process.env.SYNTAUR_PERF_TRACE === "1";
6022
+ const traces = traceEnabled ? createTraces() : void 0;
6023
+ const overallStart = traceEnabled ? performance.now() : 0;
6024
+ const projectRecords = await timed(
6025
+ traces,
6026
+ "list-project-records",
6027
+ () => listProjectRecords(projectsDir2, traces)
6028
+ );
6029
+ const standaloneRecords = await timed(
6030
+ traces,
6031
+ "list-standalone-records",
6032
+ () => listStandaloneRecords(assignmentsDir2)
6033
+ );
5999
6034
  const recentActivity = buildRecentActivity(projectRecords, standaloneRecords);
6000
6035
  const staleLimit = clamp(
6001
6036
  Number.isFinite(options.staleLimit) ? Number(options.staleLimit) : STALE_LIMIT_DEFAULT,
@@ -6003,12 +6038,16 @@ async function getOverview(projectsDir2, serversDir2, assignmentsDir2, options =
6003
6038
  STALE_LIMIT_MAX
6004
6039
  );
6005
6040
  const staleOffset = Math.max(0, Number.isFinite(options.staleOffset) ? Number(options.staleOffset) : 0);
6006
- const buckets = await buildOverviewSegmentBuckets(projectsDir2, projectRecords, standaloneRecords);
6041
+ const buckets = await timed(
6042
+ traces,
6043
+ "build-segment-buckets",
6044
+ () => buildOverviewSegmentBuckets(projectsDir2, projectRecords, standaloneRecords, traces)
6045
+ );
6007
6046
  const segments = toOverviewSegments(buckets, { staleLimit, staleOffset });
6008
6047
  const hero = pickOverviewHero(buckets);
6009
6048
  let recentSessions = [];
6010
6049
  try {
6011
- const all = await listAllSessions(projectsDir2);
6050
+ const all = await timed(traces, "list-recent-sessions", () => listAllSessions(projectsDir2));
6012
6051
  recentSessions = all.slice(0, RECENT_SESSIONS_LIMIT);
6013
6052
  } catch {
6014
6053
  }
@@ -6016,7 +6055,11 @@ async function getOverview(projectsDir2, serversDir2, assignmentsDir2, options =
6016
6055
  if (serversDir2) {
6017
6056
  try {
6018
6057
  const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
6019
- const servers = await scanAllSessions2(serversDir2, projectsDir2, { assignmentsDir: assignmentsDir2 });
6058
+ const servers = await timed(
6059
+ traces,
6060
+ "scan-tmux-sessions",
6061
+ () => scanAllSessions2(serversDir2, projectsDir2, { assignmentsDir: assignmentsDir2 })
6062
+ );
6020
6063
  if (servers.tmuxAvailable) {
6021
6064
  const alive = servers.sessions.filter((s) => s.alive).length;
6022
6065
  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);
@@ -6030,6 +6073,14 @@ async function getOverview(projectsDir2, serversDir2, assignmentsDir2, options =
6030
6073
  } catch {
6031
6074
  }
6032
6075
  }
6076
+ if (traces) {
6077
+ const wallMs = performance.now() - overallStart;
6078
+ const totalAssignments = projectRecords.reduce((sum, r) => sum + r.assignments.length, 0) + standaloneRecords.length;
6079
+ emitTrace(traces, {
6080
+ wallMs,
6081
+ fixture: { projects: projectRecords.length, assignments: totalAssignments }
6082
+ });
6083
+ }
6033
6084
  return {
6034
6085
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6035
6086
  firstRun: projectRecords.length === 0 && standaloneRecords.length === 0,
@@ -6560,7 +6611,7 @@ async function buildStandaloneAssignmentDetail(resolved) {
6560
6611
  };
6561
6612
  return detail;
6562
6613
  }
6563
- async function listProjectRecords(projectsDir2) {
6614
+ async function listProjectRecords(projectsDir2, traces) {
6564
6615
  if (!await fileExists(projectsDir2)) {
6565
6616
  return [];
6566
6617
  }
@@ -6570,61 +6621,75 @@ async function listProjectRecords(projectsDir2) {
6570
6621
  }
6571
6622
  const entries = await readdir9(projectsDir2, { withFileTypes: true });
6572
6623
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
6573
- const records = [];
6574
- for (const entry of projectDirs) {
6575
- const projectPath = resolve16(projectsDir2, entry.name);
6576
- const projectMdPath = resolve16(projectPath, "project.md");
6577
- if (!await fileExists(projectMdPath)) {
6578
- continue;
6579
- }
6580
- const projectContent = await readFile11(projectMdPath, "utf-8");
6581
- const project = parseProject(projectContent);
6582
- const assignments = await listAssignmentRecords(projectPath);
6583
- const rollup = await buildProjectRollup(projectPath, project, assignments);
6584
- const updated = getProjectActivityTimestamp(project.updated, assignments);
6585
- records.push({
6586
- projectPath,
6587
- project,
6588
- assignments,
6589
- dependencyGraph: await loadDependencyGraph(projectPath, assignments),
6590
- summary: {
6591
- slug: project.slug || entry.name,
6592
- title: project.title,
6593
- status: rollup.status,
6594
- statusOverride: project.statusOverride,
6595
- archived: project.archived,
6596
- archivedAt: project.archivedAt,
6597
- archivedReason: project.archivedReason,
6598
- created: project.created,
6599
- updated,
6600
- tags: project.tags,
6601
- progress: rollup.progress,
6602
- needsAttention: rollup.needsAttention,
6603
- workspace: project.workspace
6624
+ const maybeRecords = await Promise.all(
6625
+ projectDirs.map(async (entry) => {
6626
+ const projectPath = resolve16(projectsDir2, entry.name);
6627
+ const projectMdPath = resolve16(projectPath, "project.md");
6628
+ if (!await fileExists(projectMdPath)) {
6629
+ return null;
6604
6630
  }
6605
- });
6606
- }
6631
+ const t0 = traces ? performance.now() : 0;
6632
+ const projectContent = await readFile11(projectMdPath, "utf-8");
6633
+ const project = parseProject(projectContent);
6634
+ if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
6635
+ const t1 = traces ? performance.now() : 0;
6636
+ const assignments = await listAssignmentRecords(projectPath, traces);
6637
+ if (traces) accumulatePhase(traces, "list-assignments", performance.now() - t1);
6638
+ const t2 = traces ? performance.now() : 0;
6639
+ const rollup = await buildProjectRollup(projectPath, project, assignments, traces);
6640
+ if (traces) accumulatePhase(traces, "build-rollup", performance.now() - t2);
6641
+ const updated = getProjectActivityTimestamp(project.updated, assignments);
6642
+ const t3 = traces ? performance.now() : 0;
6643
+ const dependencyGraph = await loadDependencyGraph(projectPath, assignments);
6644
+ if (traces) accumulatePhase(traces, "load-dep-graph", performance.now() - t3);
6645
+ return {
6646
+ projectPath,
6647
+ project,
6648
+ assignments,
6649
+ dependencyGraph,
6650
+ summary: {
6651
+ slug: project.slug || entry.name,
6652
+ title: project.title,
6653
+ status: rollup.status,
6654
+ statusOverride: project.statusOverride,
6655
+ archived: project.archived,
6656
+ archivedAt: project.archivedAt,
6657
+ archivedReason: project.archivedReason,
6658
+ created: project.created,
6659
+ updated,
6660
+ tags: project.tags,
6661
+ progress: rollup.progress,
6662
+ needsAttention: rollup.needsAttention,
6663
+ workspace: project.workspace
6664
+ }
6665
+ };
6666
+ })
6667
+ );
6668
+ const records = maybeRecords.filter((r) => r !== null);
6607
6669
  records.sort((left, right) => compareTimestamps(right.summary.updated, left.summary.updated));
6608
6670
  return records;
6609
6671
  }
6610
- async function listAssignmentRecords(projectPath) {
6672
+ async function listAssignmentRecords(projectPath, traces) {
6611
6673
  const assignmentsDir2 = resolve16(projectPath, "assignments");
6612
6674
  if (!await fileExists(assignmentsDir2)) {
6613
6675
  return [];
6614
6676
  }
6615
6677
  const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
6616
- const records = [];
6617
- for (const entry of entries) {
6618
- if (!entry.isDirectory()) {
6619
- continue;
6620
- }
6621
- const assignmentMd = resolve16(assignmentsDir2, entry.name, "assignment.md");
6622
- if (!await fileExists(assignmentMd)) {
6623
- continue;
6624
- }
6625
- const content = await readFile11(assignmentMd, "utf-8");
6626
- records.push(parseAssignmentFull(content));
6627
- }
6678
+ const dirEntries = entries.filter((entry) => entry.isDirectory());
6679
+ const maybeRecords = await Promise.all(
6680
+ dirEntries.map(async (entry) => {
6681
+ const assignmentMd = resolve16(assignmentsDir2, entry.name, "assignment.md");
6682
+ if (!await fileExists(assignmentMd)) {
6683
+ return null;
6684
+ }
6685
+ const t0 = traces ? performance.now() : 0;
6686
+ const content = await readFile11(assignmentMd, "utf-8");
6687
+ const parsed = parseAssignmentFull(content);
6688
+ if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
6689
+ return parsed;
6690
+ })
6691
+ );
6692
+ const records = maybeRecords.filter((r) => r !== null);
6628
6693
  records.sort((left, right) => compareTimestamps(right.updated, left.updated));
6629
6694
  return records;
6630
6695
  }
@@ -6782,13 +6847,20 @@ async function loadDependencyGraph(projectPath, assignments) {
6782
6847
  }
6783
6848
  return buildDependencyGraph(assignments);
6784
6849
  }
6785
- async function buildProjectRollup(projectPath, project, assignments) {
6850
+ async function buildProjectRollup(projectPath, project, assignments, traces) {
6786
6851
  const progress = { total: assignments.length };
6852
+ const perAssignment = await Promise.all(
6853
+ assignments.map(async (assignment) => {
6854
+ const t0 = traces ? performance.now() : 0;
6855
+ const openQuestions2 = await countOpenQuestions(projectPath, assignment.slug);
6856
+ if (traces) accumulatePhase(traces, "count-open-questions", performance.now() - t0);
6857
+ return { status: assignment.status, openQuestions: openQuestions2 };
6858
+ })
6859
+ );
6787
6860
  let openQuestions = 0;
6788
- for (const assignment of assignments) {
6789
- const s = assignment.status;
6790
- progress[s] = (progress[s] ?? 0) + 1;
6791
- openQuestions += await countOpenQuestions(projectPath, assignment.slug);
6861
+ for (const entry of perAssignment) {
6862
+ progress[entry.status] = (progress[entry.status] ?? 0) + 1;
6863
+ openQuestions += entry.openQuestions;
6792
6864
  }
6793
6865
  const needsAttention = {
6794
6866
  blockedCount: progress["blocked"] ?? 0,
@@ -6869,18 +6941,26 @@ function buildDependencyGraph(assignments) {
6869
6941
  function findAssignmentStatus(assignments, slug) {
6870
6942
  return assignments.find((assignment) => assignment.slug === slug)?.status ?? "pending";
6871
6943
  }
6872
- async function getAvailableTransitions(projectsDir2, projectSlug, assignmentSlug, assignment) {
6944
+ async function getAvailableTransitions(projectsDir2, projectSlug, assignmentSlug, assignment, options) {
6873
6945
  const config = await getStatusConfig();
6874
6946
  const transitionDefs = getTransitionDefinitions(config);
6875
6947
  const actions = [];
6876
6948
  const projectPath = resolve16(projectsDir2, projectSlug);
6949
+ const traces = options?.traces;
6877
6950
  for (const definition of transitionDefs) {
6878
6951
  let warning = null;
6879
6952
  if (definition.command === "start" && !assignment.assignee) {
6880
6953
  warning = "No assignee set \u2014 consider assigning before starting.";
6881
6954
  }
6882
6955
  if (definition.command === "start" && assignment.dependsOn.length > 0) {
6883
- const unmetDependencies = await getUnmetDependencies(projectPath, assignment.dependsOn, config.terminalStatuses);
6956
+ const t0 = traces ? performance.now() : 0;
6957
+ const unmetDependencies = await getUnmetDependencies(
6958
+ projectPath,
6959
+ assignment.dependsOn,
6960
+ config.terminalStatuses,
6961
+ options?.dependencyStatusMap
6962
+ );
6963
+ if (traces) accumulatePhase(traces, "get-unmet-dependencies", performance.now() - t0);
6884
6964
  if (unmetDependencies.length > 0) {
6885
6965
  warning = `Unmet dependencies: ${unmetDependencies.join(", ")}.`;
6886
6966
  }
@@ -6899,10 +6979,19 @@ async function getAvailableTransitions(projectsDir2, projectSlug, assignmentSlug
6899
6979
  }
6900
6980
  return actions;
6901
6981
  }
6902
- async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses3) {
6982
+ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses3, dependencyStatusMap) {
6903
6983
  const terminals = terminalStatuses3 ?? /* @__PURE__ */ new Set(["completed"]);
6904
6984
  const unmet = [];
6905
6985
  for (const dependency of dependsOn) {
6986
+ if (dependencyStatusMap) {
6987
+ const mappedStatus = dependencyStatusMap.get(dependency);
6988
+ if (mappedStatus !== void 0) {
6989
+ if (!terminals.has(mappedStatus)) {
6990
+ unmet.push(`${dependency} (${mappedStatus})`);
6991
+ }
6992
+ continue;
6993
+ }
6994
+ }
6906
6995
  const dependencyPath = resolve16(projectPath, "assignments", dependency, "assignment.md");
6907
6996
  if (!await fileExists(dependencyPath)) {
6908
6997
  unmet.push(`${dependency} (missing)`);
@@ -6940,23 +7029,35 @@ function segmentSeverity(segment) {
6940
7029
  return "medium";
6941
7030
  }
6942
7031
  }
6943
- async function buildOverviewSegmentBuckets(projectsDir2, projectRecords, standaloneRecords) {
7032
+ async function buildOverviewSegmentBuckets(projectsDir2, projectRecords, standaloneRecords, traces) {
6944
7033
  const now = Date.now();
6945
7034
  const buckets = emptyBuckets();
6946
7035
  const newestPool = [];
6947
7036
  for (const record of projectRecords) {
6948
- for (const assignment of record.assignments) {
7037
+ const depMap = /* @__PURE__ */ new Map();
7038
+ for (const a of record.assignments) {
7039
+ depMap.set(a.slug, a.status);
7040
+ }
7041
+ const resolvedTransitions = await Promise.all(
7042
+ record.assignments.map(async (assignment) => {
7043
+ const t0 = traces ? performance.now() : 0;
7044
+ const availableTransitions = await getAvailableTransitions(
7045
+ projectsDir2,
7046
+ record.summary.slug,
7047
+ assignment.slug,
7048
+ assignment,
7049
+ { traces, dependencyStatusMap: depMap }
7050
+ );
7051
+ if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
7052
+ return { assignment, availableTransitions };
7053
+ })
7054
+ );
7055
+ for (const { assignment, availableTransitions } of resolvedTransitions) {
6949
7056
  const segmentId = STATUS_TO_SEGMENT[assignment.status];
6950
7057
  const stale = isStale(assignment.updated);
6951
7058
  const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
6952
7059
  const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
6953
7060
  const baseId = `${record.summary.slug}:${assignment.slug}`;
6954
- const availableTransitions = await getAvailableTransitions(
6955
- projectsDir2,
6956
- record.summary.slug,
6957
- assignment.slug,
6958
- assignment
6959
- );
6960
7061
  const shared = {
6961
7062
  projectSlug: record.summary.slug,
6962
7063
  projectTitle: record.summary.title,
@@ -7006,14 +7107,21 @@ async function buildOverviewSegmentBuckets(projectsDir2, projectRecords, standal
7006
7107
  }
7007
7108
  }
7008
7109
  }
7009
- for (const sr of standaloneRecords) {
7110
+ const resolvedStandaloneTransitions = await Promise.all(
7111
+ standaloneRecords.map(async (sr) => {
7112
+ const t0 = traces ? performance.now() : 0;
7113
+ const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
7114
+ if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
7115
+ return { sr, availableTransitions };
7116
+ })
7117
+ );
7118
+ for (const { sr, availableTransitions } of resolvedStandaloneTransitions) {
7010
7119
  const assignment = sr.record;
7011
7120
  const segmentId = STATUS_TO_SEGMENT[assignment.status];
7012
7121
  const stale = isStale(assignment.updated);
7013
7122
  const isTerminal = TERMINAL_STATUSES2.has(assignment.status);
7014
7123
  const agingMs = Math.max(0, now - parseTimestamp(assignment.updated));
7015
7124
  const baseId = `standalone:${sr.id}`;
7016
- const availableTransitions = await getStandaloneAvailableTransitions(assignment);
7017
7125
  const shared = {
7018
7126
  projectSlug: null,
7019
7127
  projectTitle: null,