@fabiofiorita/porcelain 0.60.0 → 0.60.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @fabiofiorita/porcelain (0.60.0)
1
+ # @fabiofiorita/porcelain (0.60.1)
2
2
 
3
3
  Headless **Porcelain** backend — the Electron-free daemon + renderer, packaged for
4
4
  plain Node on any machine (Linux mini-PC, cloud VM, laptop). Same credential-gated
@@ -3261,6 +3261,8 @@ var evidenceAssetBodySchema = import_zod22.z.object({
3261
3261
  }).strict();
3262
3262
  var reviewCommentSchema = import_zod22.z.object({
3263
3263
  id: import_zod22.z.string(),
3264
+ /** Absent only for comments written before authorship was recorded; clients treat those as user. */
3265
+ author: import_zod22.z.enum(["user", "agent"]).optional(),
3264
3266
  path: import_zod22.z.string().min(1),
3265
3267
  startLine: import_zod22.z.number().int().positive().optional(),
3266
3268
  endLine: import_zod22.z.number().int().positive().optional(),
@@ -7496,11 +7498,11 @@ function createCommitGeneration() {
7496
7498
  });
7497
7499
  }
7498
7500
  function createGitDiffReadingSources(options) {
7499
- const layersFor = async (repoPath) => options === void 0 ? [] : options.review.layersForRepo(repoPath);
7501
+ const layersFor = async (repoPath, commitHash) => options === void 0 ? [] : options.review.layersForRepo(repoPath, commitHash);
7500
7502
  return Object.freeze({
7501
7503
  loadWorkingFlow: async (repoPath) => loadWorkingFlow(repoPath, await layersFor(repoPath)),
7502
7504
  loadRangeFlow: async (repoPath, base) => loadRangeFlow(repoPath, await layersFor(repoPath), base),
7503
- loadCommitFlow: async (repoPath, hash) => loadCommitFlow(repoPath, hash, await layersFor(repoPath)),
7505
+ loadCommitFlow: async (repoPath, hash) => loadCommitFlow(repoPath, hash, await layersFor(repoPath, hash)),
7504
7506
  workingHunks: (repoPath, path, context) => gitDiffFile(repoPath, path, context).then((result) => result.hunks),
7505
7507
  rangeHunks: (repoPath, base, path, context) => gitRangeDiffFile(repoPath, base, path, context).then((result) => result.hunks),
7506
7508
  diffFile: gitDiffFile,
@@ -10677,9 +10679,10 @@ function structuredSource(document, assetsDir, extraFiles = []) {
10677
10679
  ...extraFiles.length === 0 ? {} : { extraFiles }
10678
10680
  };
10679
10681
  }
10680
- function reviewBundleSource(data, assetsDir) {
10682
+ function reviewBundleSource(data, assetsDir, commitHash) {
10681
10683
  const metadata = {
10682
10684
  name: data.title,
10685
+ ...commitHash === void 0 ? {} : { commitHash },
10683
10686
  layers: data.layers,
10684
10687
  files: data.files,
10685
10688
  sections: []
@@ -10830,7 +10833,7 @@ function createMcpToolHandlers(deps) {
10830
10833
  const resolved = await resolveWorkspace(ref, inventory.value);
10831
10834
  return resolved.ok ? { ok: true, value: resolved.value } : { ok: false, result: fail(resolved.message) };
10832
10835
  }
10833
- async function canvasSource(args) {
10836
+ async function canvasSource(args, reviewCommitHash) {
10834
10837
  if (args.document !== void 0) {
10835
10838
  if (args.templateData !== void 0 || args.template !== void 0 || args.files !== void 0 || args.kind !== void 0 || args.entry !== void 0 || args.title !== void 0) {
10836
10839
  return {
@@ -10884,7 +10887,11 @@ function createMcpToolHandlers(deps) {
10884
10887
  };
10885
10888
  }
10886
10889
  const assetsDir = stringField(args, "sourceDir");
10887
- const source = template === "review" ? reviewBundleSource(reviewCanvasTemplateDataSchema.parse(parsed.data), assetsDir) : planBundleSource(planCanvasTemplateDataSchema.parse(parsed.data), assetsDir);
10890
+ const source = template === "review" ? reviewBundleSource(
10891
+ reviewCanvasTemplateDataSchema.parse(parsed.data),
10892
+ assetsDir,
10893
+ reviewCommitHash
10894
+ ) : planBundleSource(planCanvasTemplateDataSchema.parse(parsed.data), assetsDir);
10888
10895
  return {
10889
10896
  ok: true,
10890
10897
  title: parsed.data.title,
@@ -10984,7 +10991,17 @@ function createMcpToolHandlers(deps) {
10984
10991
  }
10985
10992
  if (op !== "create" && op !== "update")
10986
10993
  return fail("op must be list, get, create, update, delete, or promote.");
10987
- const source = await canvasSource(args);
10994
+ const reviewCommitHash = await (async () => {
10995
+ if (args.template !== "review" || place.worktreePath === null) return void 0;
10996
+ try {
10997
+ const status = await operations.git.statusGit(place.worktreePath);
10998
+ if (!status.ok || status.value.length > 0) return void 0;
10999
+ return (await operations.git.logGit({ repoPath: place.worktreePath, limit: 1 }))[0]?.hash;
11000
+ } catch {
11001
+ return void 0;
11002
+ }
11003
+ })();
11004
+ const source = await canvasSource(args, reviewCommitHash);
10988
11005
  if (!source.ok) return source.result;
10989
11006
  const canvasId = stringField(args, "id");
10990
11007
  if (op === "create" && canvasId !== void 0)
@@ -11022,10 +11039,17 @@ function createMcpToolHandlers(deps) {
11022
11039
  ...replacing ? { replace: true } : {}
11023
11040
  });
11024
11041
  return promoted.ok ? ok(
11025
- `Canvas ${written.value.id} written to the tracked overlay. Files written; nothing staged or committed.`
11042
+ source.template === "review" ? `Review Canvas ${written.value.id} written to the tracked overlay${reviewCommitHash === void 0 ? " for live Changes; update it after committing to bind History" : ` and bound to commit ${reviewCommitHash} for History`}. Files written; nothing staged or committed.` : `Canvas ${written.value.id} written to the tracked overlay. Files written; nothing staged or committed.`
11026
11043
  ) : fail(`Could not update the tracked Canvas: ${describeError(promoted.error)}`);
11027
11044
  }
11028
- return workspaceResult(place, written.value);
11045
+ return workspaceResult(
11046
+ place,
11047
+ source.template === "review" ? {
11048
+ ...written.value,
11049
+ historyCommit: reviewCommitHash ?? null,
11050
+ history: reviewCommitHash === void 0 ? "live-only; update this Canvas after committing to bind History" : "bound"
11051
+ } : written.value
11052
+ );
11029
11053
  },
11030
11054
  async porcelain_comment(args, place) {
11031
11055
  if (place?.worktreePath === null || place === null)
@@ -11054,6 +11078,7 @@ function createMcpToolHandlers(deps) {
11054
11078
  return fail("comment.path and comment.body are required to create a comment.");
11055
11079
  const created = await operations.review.addReviewComment({
11056
11080
  projectPath,
11081
+ author: "agent",
11057
11082
  path,
11058
11083
  body,
11059
11084
  ...typeof comment.startLine === "number" ? { startLine: comment.startLine } : {},
@@ -11404,7 +11429,7 @@ var MCP_TOOLS = Object.freeze([
11404
11429
  {
11405
11430
  name: "porcelain_canvas",
11406
11431
  title: "Manage a Canvas",
11407
- description: "List, read, create, update, delete, or promote an agent-authored Canvas. Prefer document for a custom validated structured Canvas; sourceDir may provide bundle assets. Built-in review and plan templates compile into that same renderer. Review fixes Why/How and owns its file layers; Plan chooses bounded tabs. Each Review create is scoped to the addressed Worktree, and update requires its id. Promotion writes files but never stages or commits.",
11432
+ description: "List, read, create, update, delete, or promote an agent-authored Canvas. Prefer document for a custom validated structured Canvas; sourceDir may provide bundle assets. Built-in review and plan templates compile into that same renderer. Review fixes Why/How and owns its file layers; Plan chooses bounded tabs. Each Review create is scoped to the addressed Worktree, and update requires its id. A clean Review write binds current HEAD for cross-Worktree History; a dirty write reports live-only and must be updated after commit. Promotion writes files but never stages or commits.",
11408
11433
  inputSchema: {
11409
11434
  type: "object",
11410
11435
  properties: {
@@ -12688,7 +12713,7 @@ function parseComment(value, index) {
12688
12713
  throw new CommentsFileParseError("invalid-comment", `comments[${index}] is not an object`);
12689
12714
  }
12690
12715
  for (const key of Object.keys(value)) {
12691
- if (key !== "id" && key !== "path" && key !== "startLine" && key !== "endLine" && key !== "anchorText" && key !== "body" && key !== "resolved" && key !== "createdAt" && key !== "agentReply") {
12716
+ if (key !== "id" && key !== "author" && key !== "path" && key !== "startLine" && key !== "endLine" && key !== "anchorText" && key !== "body" && key !== "resolved" && key !== "createdAt" && key !== "agentReply") {
12692
12717
  throw new CommentsFileParseError(
12693
12718
  "invalid-comment",
12694
12719
  `comments[${index}] has unknown field ${key}`
@@ -12698,6 +12723,9 @@ function parseComment(value, index) {
12698
12723
  if (typeof value.id !== "string" || value.id.length === 0) {
12699
12724
  throw new CommentsFileParseError("invalid-comment", `comments[${index}].id is invalid`);
12700
12725
  }
12726
+ if (value.author !== void 0 && value.author !== "user" && value.author !== "agent") {
12727
+ throw new CommentsFileParseError("invalid-comment", `comments[${index}].author is invalid`);
12728
+ }
12701
12729
  if (typeof value.path !== "string" || value.path.length === 0) {
12702
12730
  throw new CommentsFileParseError("invalid-comment", `comments[${index}].path is invalid`);
12703
12731
  }
@@ -12732,6 +12760,7 @@ function parseComment(value, index) {
12732
12760
  resolved: value.resolved,
12733
12761
  createdAt: value.createdAt
12734
12762
  };
12763
+ if (value.author !== void 0) comment.author = value.author;
12735
12764
  if (value.startLine !== void 0) comment.startLine = value.startLine;
12736
12765
  if (value.endLine !== void 0) comment.endLine = value.endLine;
12737
12766
  if (value.anchorText !== void 0) comment.anchorText = value.anchorText;
@@ -12800,6 +12829,7 @@ function cloneComment(comment) {
12800
12829
  resolved: comment.resolved,
12801
12830
  createdAt: comment.createdAt
12802
12831
  };
12832
+ if (comment.author !== void 0) next.author = comment.author;
12803
12833
  if (comment.startLine !== void 0) next.startLine = comment.startLine;
12804
12834
  if (comment.endLine !== void 0) next.endLine = comment.endLine;
12805
12835
  if (comment.anchorText !== void 0) next.anchorText = comment.anchorText;
@@ -12817,6 +12847,7 @@ function planAddReviewComment(file, input) {
12817
12847
  }
12818
12848
  const comment = {
12819
12849
  id: input.id,
12850
+ author: input.author ?? "user",
12820
12851
  path: input.path,
12821
12852
  body: input.body,
12822
12853
  resolved: false,
@@ -12909,6 +12940,7 @@ function createAddReviewComment(deps) {
12909
12940
  const result = await deps.store.transact(input.projectPath, (current) => {
12910
12941
  const planned = planAddReviewComment(current, {
12911
12942
  id,
12943
+ author: input.author,
12912
12944
  path: input.path,
12913
12945
  startLine: input.startLine,
12914
12946
  endLine: input.endLine,
@@ -14844,7 +14876,7 @@ function daemonIdentity(host = process.env.PORCELAIN_DAEMON_HOST?.trim() || (0,
14844
14876
 
14845
14877
  // ../daemon/src/net/daemon-version.ts
14846
14878
  function daemonVersion() {
14847
- return "0.60.0";
14879
+ return "0.60.1";
14848
14880
  }
14849
14881
 
14850
14882
  // ../daemon/src/stores/review-store.ts
@@ -14878,6 +14910,8 @@ var reviewSectionSchema2 = import_zod49.z.object({
14878
14910
  var reviewSetSchema = import_zod49.z.object({
14879
14911
  name: import_zod49.z.string().default("Active review"),
14880
14912
  thesis: import_zod49.z.string().max(4096).optional(),
14913
+ /** The clean checkout HEAD this Review was last authored against. */
14914
+ commitHash: import_zod49.z.string().regex(/^[0-9a-f]{40,64}$/).optional(),
14881
14915
  layers: import_zod49.z.array(profileLayerSchema).default([]),
14882
14916
  files: import_zod49.z.array(reviewSetFileSchema).default([]),
14883
14917
  sections: import_zod49.z.array(reviewSectionSchema2).max(30).default([])
@@ -14950,7 +14984,21 @@ function projectIdentity(repoPath) {
14950
14984
  return null;
14951
14985
  }
14952
14986
  }
14953
- async function readCanvasReviewSet(repoPath) {
14987
+ async function readReviewCandidate(projectId, record) {
14988
+ try {
14989
+ const raw = JSON.parse(
14990
+ await (0, import_promises33.readFile)(
14991
+ (0, import_node_path48.join)(canvasBundleDir(porcelainHome(), projectId, record.id), "review.json"),
14992
+ "utf8"
14993
+ )
14994
+ );
14995
+ const parsed = reviewSetSchema.safeParse(raw);
14996
+ return parsed.success ? parsed.data : null;
14997
+ } catch {
14998
+ return null;
14999
+ }
15000
+ }
15001
+ async function readCanvasReviewSet(repoPath, commitHash) {
14954
15002
  const identity2 = projectIdentity(repoPath);
14955
15003
  if (identity2 === null) return null;
14956
15004
  try {
@@ -14960,29 +15008,31 @@ async function readCanvasReviewSet(repoPath) {
14960
15008
  const candidates = (index.value?.canvases ?? []).filter(
14961
15009
  (entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string"
14962
15010
  );
14963
- const reviews = candidates.filter((entry) => entry.template === "review");
15011
+ const reviews = candidates.filter((entry) => entry.template === "review").sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
15012
+ if (commitHash !== void 0) {
15013
+ for (const record2 of reviews) {
15014
+ const review = await readReviewCandidate(identity2.projectId, record2);
15015
+ if (review?.commitHash !== void 0 && (review.commitHash === commitHash || review.commitHash.startsWith(commitHash))) {
15016
+ return review;
15017
+ }
15018
+ }
15019
+ return null;
15020
+ }
14964
15021
  const exact = reviews.filter((entry) => entry.worktreeId === identity2.worktreeId);
14965
- const record = (exact.length === 0 ? reviews.filter((entry) => entry.worktreeId == null) : exact).sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))[0];
15022
+ const record = (exact.length === 0 ? reviews.filter((entry) => entry.worktreeId == null) : exact)[0];
14966
15023
  if (record === void 0) return null;
14967
- const raw = JSON.parse(
14968
- await (0, import_promises33.readFile)(
14969
- (0, import_node_path48.join)(canvasBundleDir(porcelainHome(), identity2.projectId, record.id), "review.json"),
14970
- "utf8"
14971
- )
14972
- );
14973
- const parsed = reviewSetSchema.safeParse(raw);
14974
- return parsed.success ? parsed.data : null;
15024
+ return readReviewCandidate(identity2.projectId, record);
14975
15025
  } catch {
14976
15026
  return null;
14977
15027
  }
14978
15028
  }
14979
- async function readReviewSet(repoPath) {
14980
- const canvas = await readCanvasReviewSet(repoPath);
15029
+ async function readReviewSet(repoPath, commitHash) {
15030
+ const canvas = await readCanvasReviewSet(repoPath, commitHash);
14981
15031
  if (canvas !== null) return sanitizeReview(repoPath, lenientReviewSetSchema.parse(canvas));
14982
15032
  return null;
14983
15033
  }
14984
- async function reviewLayersForRepo(repoPath) {
14985
- return (await readReviewSet(repoPath))?.layers ?? [];
15034
+ async function reviewLayersForRepo(repoPath, commitHash) {
15035
+ return (await readReviewSet(repoPath, commitHash))?.layers ?? [];
14986
15036
  }
14987
15037
 
14988
15038
  // ../daemon/src/daemon-composition/daemon-operations.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabiofiorita/porcelain",
3
- "version": "0.60.0",
3
+ "version": "0.60.1",
4
4
  "description": "Headless Porcelain daemon — plain Node backend for remote machines (npx @fabiofiorita/porcelain@latest serve)",
5
5
  "license": "MIT",
6
6
  "author": "Fabio Fiorita <fabiolfp@gmail.com>",