@unbrained/pm-web 2026.8.17 → 2026.8.29

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/routes/pm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Router } from "express";
2
2
  import { requireAuth } from "../middleware/auth.js";
3
- import { ensureGraphExtension, runPm, runGetItemAt, readPmSettings, PmCliError, EXIT_CODE } from "../services/pm-runner.js";
3
+ import { ensureGraphExtension, readCompletePmItems, runPm, runGetItemAt, readPmSettings, PmCliError, EXIT_CODE } from "../services/pm-runner.js";
4
4
  // The search-tuning resolvers live only on the narrow sdk/query entrypoint — the
5
5
  // aggregate sdk barrel documents itself as re-exporting every supported export but
6
6
  // omits 45 of them, these three included (upstream: unbraind/pm-cli#740).
@@ -392,7 +392,10 @@ function scheduleGraphSync(projectId, project, reason) {
392
392
  });
393
393
  })
394
394
  .catch((err) => {
395
- console.error(`Neo4j graph auto-sync failed for ${projectId} after ${reason}:`, err);
395
+ // Use `%s` specifiers and pass the values as arguments instead of
396
+ // interpolating them into the format string, so a tainted `reason` or
397
+ // `projectId` cannot inject `console` format specifiers.
398
+ console.error("Neo4j graph auto-sync failed for %s after %s:", projectId, reason, err);
396
399
  broadcastProjectEvent(projectId, {
397
400
  type: "graph-sync-failed",
398
401
  data: { reason, error: err instanceof Error ? err.message : String(err) },
@@ -428,31 +431,26 @@ function broadcastDependencyEvent(projectId, kind, data) {
428
431
  },
429
432
  });
430
433
  }
431
- function itemsFromListAll(parsed) {
434
+ function itemsFromCompleteList(parsed) {
432
435
  return ((parsed?.items) ?? []);
433
436
  }
434
437
  /**
435
- * Build a project graph from `pm list-all` when no graph extension is available.
438
+ * Build a project graph from a certified complete pm read when no graph extension is available.
436
439
  *
437
- * Runs `list-all` as the project owner and assembles the graph from the returned
438
- * items using their embedded `deps`/`dependencies` (no per-item subprocess calls),
439
- * avoiding an N+1 fan-out. Throws when the items cannot be loaded.
440
+ * Reads every item through the public SDK's high-level complete-list operation
441
+ * and assembles the graph from embedded `deps`/`dependencies` (no per-item
442
+ * calls), avoiding an N+1 fan-out. Throws when the corpus cannot be certified.
440
443
  *
441
444
  * @param ownerUserId - The project owner's user id.
442
445
  * @param slug - The project slug.
443
446
  * @returns A pm-web-sourced project graph.
444
447
  */
445
448
  async function fallbackGraphForProject(ownerUserId, slug) {
446
- const itemsResult = await runPm({
447
- args: ["list-all"],
448
- userId: ownerUserId,
449
- slug,
450
- jsonOutput: true,
451
- });
449
+ const itemsResult = await readCompletePmItems(ownerUserId, slug);
452
450
  if (!itemsResult.ok)
453
451
  throw new Error(itemsResult.stderr || "Failed to load items for graph");
454
- const items = itemsFromListAll(itemsResult.parsed);
455
- // Deps are already embedded in list-all output (item.deps / item.dependencies).
452
+ const items = itemsFromCompleteList(itemsResult.result);
453
+ // Deps are already embedded in the full item rows (item.deps / item.dependencies).
456
454
  // Avoid N+1 subprocess calls by using only the embedded data.
457
455
  return graphFromItems(items, new Map());
458
456
  }
@@ -694,7 +692,10 @@ router.get("/list-all", async (req, res) => {
694
692
  return;
695
693
  }
696
694
  const { type, limit, after } = req.query;
697
- const args = ["list-all"];
695
+ // Preserve the public HTTP compatibility route while invoking the canonical
696
+ // CLI/SDK command internally. This route is intentionally paginated and is
697
+ // therefore distinct from readCompletePmItems used by whole-corpus views.
698
+ const args = ["list", "--all"];
698
699
  if (type)
699
700
  args.push("--type", type);
700
701
  if (limit)
@@ -727,16 +728,16 @@ router.get("/board", async (req, res) => {
727
728
  ? contracts.parsed["runtime_schema"]
728
729
  : undefined;
729
730
  const statuses = Array.isArray(rt?.["statuses"]) ? rt["statuses"] : [];
730
- const listed = await runPm({ args: ["list-all", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
731
+ const listed = await readCompletePmItems(project.ownerUserId, project.slug);
731
732
  if (!listed.ok) {
732
733
  res.json({ error: listed.stderr, columns: [] });
733
734
  return;
734
735
  }
735
- const items = itemsFromListAll(listed.parsed);
736
+ const items = itemsFromCompleteList(listed.result);
736
737
  res.json({ columns: boardColumns(items, statuses), statuses, count: items.length });
737
738
  });
738
739
  // GET /api/projects/:projectId/pm/search?q=...
739
- // Full-text search over id/title/tags/body via a single list-all read.
740
+ // Full-text search over id/title/tags/body via one certified complete read.
740
741
  router.get("/search", async (req, res) => {
741
742
  const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
742
743
  if (!project) {
@@ -744,12 +745,12 @@ router.get("/search", async (req, res) => {
744
745
  return;
745
746
  }
746
747
  const query = String(req.query["q"] ?? "");
747
- const listed = await runPm({ args: ["list-all", "--json", "--include-body"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
748
+ const listed = await readCompletePmItems(project.ownerUserId, project.slug, true);
748
749
  if (!listed.ok) {
749
750
  res.json({ error: listed.stderr, items: [] });
750
751
  return;
751
752
  }
752
- const items = filterItemsByQuery(itemsFromListAll(listed.parsed), query);
753
+ const items = filterItemsByQuery(itemsFromCompleteList(listed.result), query);
753
754
  res.json({ query, items, count: items.length });
754
755
  });
755
756
  // POST /api/projects/:projectId/pm/create
@@ -1215,8 +1216,8 @@ router.get("/calendar", async (req, res) => {
1215
1216
  });
1216
1217
  // GET /api/projects/:projectId/pm/calendar.ics
1217
1218
  // RFC 5545 iCalendar feed of item deadlines, for subscribing in Google
1218
- // Calendar / Outlook / Apple Calendar. Reuses the same list-all read as the
1219
- // calendar view. Auth works via the usual token (header/cookie) or a
1219
+ // Calendar / Outlook / Apple Calendar. Reads one certified complete corpus.
1220
+ // Auth works via the usual token (header/cookie) or a
1220
1221
  // `?token=` query param, since calendar clients cannot send cookies.
1221
1222
  router.get("/calendar.ics", async (req, res) => {
1222
1223
  const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
@@ -1224,17 +1225,12 @@ router.get("/calendar.ics", async (req, res) => {
1224
1225
  res.status(404).json({ error: "Project not found" });
1225
1226
  return;
1226
1227
  }
1227
- const listed = await runPm({
1228
- args: ["list-all", "--json"],
1229
- userId: project.ownerUserId,
1230
- slug: project.slug,
1231
- jsonOutput: true,
1232
- });
1228
+ const listed = await readCompletePmItems(project.ownerUserId, project.slug);
1233
1229
  if (!listed.ok) {
1234
1230
  res.status(502).json({ error: listed.stderr || "Failed to load items" });
1235
1231
  return;
1236
1232
  }
1237
- const items = itemsFromListAll(listed.parsed)
1233
+ const items = itemsFromCompleteList(listed.result)
1238
1234
  .filter((i) => Boolean(i.deadline))
1239
1235
  .map((i) => ({
1240
1236
  id: i.id,
@@ -1963,18 +1959,12 @@ router.get("/export", async (req, res) => {
1963
1959
  return;
1964
1960
  }
1965
1961
  const format = req.query["format"] || "json";
1966
- // Use --full --include-body to get the richest available list-level metadata
1967
- const result = await runPm({
1968
- args: ["list-all", "--limit", "10000", "--full", "--include-body"],
1969
- userId: project.ownerUserId,
1970
- slug: project.slug,
1971
- jsonOutput: true,
1972
- });
1962
+ const result = await readCompletePmItems(project.ownerUserId, project.slug, true);
1973
1963
  if (!result.ok) {
1974
1964
  res.status(500).json({ error: result.stderr || "Export failed" });
1975
1965
  return;
1976
1966
  }
1977
- const data = result.parsed;
1967
+ const data = result.result;
1978
1968
  const exportedAt = new Date().toISOString();
1979
1969
  if (format === "csv") {
1980
1970
  const items = data?.items ?? [];