@aefree/pi-plastic 0.7.6 → 0.8.0
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/CHANGELOG.md +37 -0
- package/README.md +48 -174
- package/docs/2026-10-06-development.md +101 -0
- package/docs/2026-10-06-diffs-and-patches.md +136 -0
- package/docs/2026-10-06-discovery.md +135 -0
- package/docs/2026-10-06-integrations.md +49 -0
- package/docs/2026-10-06-merging.md +153 -0
- package/docs/2026-10-06-safety-and-receipts.md +95 -0
- package/docs/2026-10-06-setup.md +101 -0
- package/docs/2026-10-06-shelvesets-and-reviews.md +113 -0
- package/docs/2026-10-06-status.md +116 -0
- package/docs/2026-10-06-tools-and-loading.md +64 -0
- package/docs/2026-10-06-workspace-operations.md +142 -0
- package/docs/README.md +65 -0
- package/extensions/bash-cm-diff-guard.ts +1 -1
- package/index.ts +4 -687
- package/package.json +10 -4
- package/skills/using-plastic/SKILL.md +11 -5
- package/skills/using-plastic/references/branch-management.md +5 -1
- package/skills/using-plastic/references/changeset-operations.md +9 -8
- package/skills/using-plastic/references/code-review-creation.md +25 -18
- package/skills/using-plastic/references/integration.md +9 -5
- package/skills/using-plastic/references/quick-reference.md +4 -4
- package/skills/using-plastic/references/reviewing-changes.md +10 -10
- package/skills/using-plastic/references/shelving.md +26 -11
- package/skills/using-plastic/references/troubleshooting.md +4 -4
- package/src/diff/local-snapshot.ts +74 -0
- package/src/diff/text.ts +187 -0
- package/src/domain/add-contract.ts +24 -0
- package/src/domain/branch-create-contract.ts +38 -0
- package/src/domain/branch-delete-contract.ts +15 -0
- package/src/domain/branches.ts +263 -0
- package/src/domain/checkin-contract.ts +70 -0
- package/src/domain/closeout-contract.ts +30 -0
- package/src/domain/copied-merge.ts +42 -0
- package/src/domain/diff-base-xml.ts +65 -0
- package/src/domain/diff-pending-xml.ts +34 -0
- package/src/domain/merge-output.ts +327 -0
- package/src/domain/paths.ts +253 -0
- package/src/domain/pending.ts +499 -0
- package/src/domain/removal-contract.ts +25 -0
- package/src/domain/revisions.ts +301 -0
- package/src/domain/server-merge-contract.ts +45 -0
- package/src/domain/server-merge-resolutions.ts +61 -0
- package/src/domain/status-xml.ts +122 -0
- package/src/domain/switch-contract.ts +60 -0
- package/src/domain/switch-policy.ts +95 -0
- package/src/domain/undo-contract.ts +24 -0
- package/src/domain/update-contract.ts +16 -0
- package/src/domain/workspace-merge-contract.ts +41 -0
- package/src/execution/add-command.ts +118 -0
- package/src/execution/branch-create-command.ts +118 -0
- package/src/execution/branch-list-command.ts +75 -0
- package/src/execution/checkin-command.ts +116 -0
- package/src/execution/cli-version.ts +31 -0
- package/src/execution/cm.ts +59 -0
- package/src/execution/code-review-find-command.ts +75 -0
- package/src/execution/context.ts +20 -0
- package/src/execution/diff-revisions-command.ts +74 -0
- package/src/execution/process.ts +260 -0
- package/src/execution/removal-command.ts +118 -0
- package/src/execution/server-merge-command.ts +92 -0
- package/src/execution/shelveset-list-command.ts +75 -0
- package/src/execution/status-command.ts +29 -0
- package/src/execution/status-xml-command.ts +72 -0
- package/src/execution/status-xml-limits.ts +1 -0
- package/src/execution/switch-command.ts +118 -0
- package/src/execution/test-seams.ts +10 -0
- package/src/execution/undo-command.ts +118 -0
- package/src/execution/update-command.ts +118 -0
- package/src/execution/workspace-list-command.ts +75 -0
- package/src/execution/workspace-merge-command.ts +118 -0
- package/src/operations/add-receipt.ts +37 -0
- package/src/operations/arguments.ts +3 -0
- package/src/operations/branch-create.ts +74 -0
- package/src/operations/branch-delete-receipt.ts +25 -0
- package/src/operations/branch-list.ts +94 -0
- package/src/operations/branch-reads.ts +94 -0
- package/src/operations/branches.ts +111 -0
- package/src/operations/checkin-receipt.ts +140 -0
- package/src/operations/checkin.ts +102 -0
- package/src/operations/closeout-receipt.ts +74 -0
- package/src/operations/closeout.ts +19 -0
- package/src/operations/code-review-find.ts +97 -0
- package/src/operations/consolidated-diff.ts +144 -0
- package/src/operations/diff-revisions.ts +157 -0
- package/src/operations/diff.ts +53 -0
- package/src/operations/merge.ts +26 -0
- package/src/operations/object-delete-receipt.ts +21 -0
- package/src/operations/object-write-receipt.ts +105 -0
- package/src/operations/patch-receipt.ts +96 -0
- package/src/operations/patch-support.ts +375 -0
- package/src/operations/patch.ts +25 -0
- package/src/operations/removal-receipt.ts +42 -0
- package/src/operations/reviews.ts +90 -0
- package/src/operations/server-merge.ts +130 -0
- package/src/operations/shelveset-list.ts +90 -0
- package/src/operations/shelvesets.ts +90 -0
- package/src/operations/status.ts +129 -0
- package/src/operations/switch-receipt.ts +85 -0
- package/src/operations/switch.ts +25 -0
- package/src/operations/undo-receipt.ts +37 -0
- package/src/operations/update-receipt.ts +35 -0
- package/src/operations/workspace-list.ts +92 -0
- package/src/operations/workspace-merge-receipt.ts +50 -0
- package/src/operations/workspace.ts +120 -0
- package/src/pi/add-output.ts +52 -0
- package/src/pi/arguments.ts +298 -0
- package/src/pi/branch-create-output.ts +83 -0
- package/src/pi/branch-delete-output.ts +46 -0
- package/src/pi/branch-list-output.ts +59 -0
- package/src/pi/branch-output.ts +63 -0
- package/src/pi/capability-diagnostics.ts +23 -0
- package/src/pi/checkin-output.ts +68 -0
- package/src/pi/closeout-output.ts +87 -0
- package/src/pi/code-review-find-output.ts +50 -0
- package/src/pi/diff-output.ts +66 -0
- package/src/pi/diff-revisions-output.ts +55 -0
- package/src/pi/object-delete-output.ts +15 -0
- package/src/pi/object-write-output.ts +54 -0
- package/src/pi/patch-output.ts +69 -0
- package/src/pi/register.ts +188 -0
- package/src/pi/removal-output.ts +59 -0
- package/src/pi/schemas.ts +146 -0
- package/src/pi/server-merge-output.ts +83 -0
- package/src/pi/shelveset-list-output.ts +50 -0
- package/src/pi/status-output.ts +149 -0
- package/src/pi/switch-output.ts +80 -0
- package/src/pi/tool-registry.ts +72 -0
- package/src/pi/undo-output.ts +52 -0
- package/src/pi/update-output.ts +48 -0
- package/src/pi/workspace-list-output.ts +53 -0
- package/src/pi/workspace-merge-output.ts +46 -0
- package/src/plastic-core.ts +14 -5775
- package/src/plastic-renderers.ts +102 -22
- package/src/plastic-tool-loading.ts +7 -12
- package/src/presentation/add-results.ts +5 -0
- package/src/presentation/branch-create.ts +7 -0
- package/src/presentation/branch-delete-results.ts +7 -0
- package/src/presentation/checkin-results.ts +14 -0
- package/src/presentation/closeout-results.ts +2 -0
- package/src/presentation/diff-results.ts +237 -0
- package/src/presentation/removal-results.ts +7 -0
- package/src/presentation/results.ts +84 -0
- package/src/presentation/switch-results.ts +7 -0
- package/src/presentation/undo-results.ts +5 -0
- package/src/presentation/update-results.ts +5 -0
- package/src/presentation/workspace-merge-results.ts +6 -0
package/src/plastic-renderers.ts
CHANGED
|
@@ -13,8 +13,7 @@ const separator = " \u00b7 ";
|
|
|
13
13
|
|
|
14
14
|
const titles: Record<string, string> = {
|
|
15
15
|
status: "Status", currentBranch: "Current branch", update: "Update", add: "Add", checkin: "Check in", undo: "Undo",
|
|
16
|
-
resolveDeleteChangeConflict: "Resolve deletion", diff: "Diff
|
|
17
|
-
diffFile: "File diff", workspaceDiff: "Workspace diff", branchCreate: "Create branch", switchBranch: "Switch branch",
|
|
16
|
+
resolveDeleteChangeConflict: "Resolve deletion", diff: "Diff", patch: "Review patch", branchCreate: "Create branch", switchBranch: "Switch branch",
|
|
18
17
|
merge: "Merge", mergeBranches: "Server merge", mergeToBranch: "Merge and check in", finalizeMerge: "Finalize merge",
|
|
19
18
|
branchList: "Branches", branchExists: "Branch exists", branchDelete: "Delete branch", shelvesetCreate: "Create shelveset",
|
|
20
19
|
shelvesetApply: "Apply shelveset", shelvesetDelete: "Delete shelveset", shelvesetList: "Shelvesets",
|
|
@@ -60,10 +59,10 @@ function list(value: unknown): string {
|
|
|
60
59
|
}
|
|
61
60
|
function target(name: string, args: Args): string {
|
|
62
61
|
if (name === "tool_search") return list(args.toolNames) || scalar(args.query) || "Browse capabilities";
|
|
63
|
-
if (name === "
|
|
62
|
+
if (name === "diff" && args.mode === "revisions") return `${scalar(args.leftRevision) || "..."} -> ${scalar(args.rightRevision) || "..."}`;
|
|
64
63
|
if (name === "mergeBranches" || name === "mergeToBranch") return `${scalar(args.source) || "..."} -> ${scalar(args.target) || "parent branch"}`;
|
|
65
|
-
if (name === "patch"
|
|
66
|
-
if (name === "
|
|
64
|
+
if (name === "patch") return [scalar(args.source), scalar(args.destination)].filter(Boolean).join(" -> ");
|
|
65
|
+
if (name === "diff" && args.mode === "file") return [scalar(args.path), args.revision ? `vs ${scalar(args.revision)}` : "vs workspace base"].join(" ");
|
|
67
66
|
return scalar(args.branch) || scalar(args.source) || scalar(args.path) || list(args.paths) || scalar(args.shelveset)
|
|
68
67
|
|| list(args.ids) || scalar(args.id) || scalar(args.target) || scalar(args.nameLike) || scalar(args.titleLike)
|
|
69
68
|
|| (args.allPending === true ? "All pending files" : "");
|
|
@@ -73,7 +72,8 @@ export function renderPlasticCall(name: string, input: unknown, theme: RenderThe
|
|
|
73
72
|
const args = record(input);
|
|
74
73
|
const workdir = scalar(args.workdir) || context?.cwd || "";
|
|
75
74
|
const workspace = name === "mergeBranches" ? "server" : workdir.replace(/[\\/]+$/, "").split(/[\\/]/).pop();
|
|
76
|
-
|
|
75
|
+
const title=name==="diff" ? args.mode==="file"?"File diff":args.mode==="revisions"?"Revision diff":args.mode==="workspace"?"Workspace diff":"Diff" : titles[name] ?? name;
|
|
76
|
+
let text = theme.fg("toolTitle", theme.bold(`Plastic${separator}${title}`));
|
|
77
77
|
if (workspace && name !== "tool_search") text += theme.fg("dim", ` ${compact(workspace, 60)}`);
|
|
78
78
|
if (args.preflight === true) text += theme.fg("warning", `${separator}preview`);
|
|
79
79
|
const subtitle = target(name, args);
|
|
@@ -82,6 +82,8 @@ export function renderPlasticCall(name: string, input: unknown, theme: RenderThe
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function payload(result: Result, raw: string): Record<string, unknown> {
|
|
85
|
+
const structured = record((result as Result & {structuredContent?:unknown}).structuredContent);
|
|
86
|
+
if (["shelveset-create","shelveset-apply","code-review-create","code-review-update"].includes(scalar(structured.action)) || structured.action === "patch" || structured.action === "diff" || structured.action === "merge-branches" || structured.action === "checkin" || structured.action === "branch-create" || structured.action === "switch-branch" || structured.action === "update" || structured.action === "add" || structured.action === "undo" || structured.action === "resolve-delete-change-conflict" || structured.action === "merge" || structured.action === "finalize-merge" || structured.action === "merge-to-branch" || structured.action === "branch-delete" || structured.action === "shelveset-delete" || structured.action === "code-review-delete") return structured;
|
|
85
87
|
const rawResult = record(result.details).rawResult;
|
|
86
88
|
if (rawResult !== null && typeof rawResult === "object") return record(rawResult);
|
|
87
89
|
// Only decode the package's whole JSON envelope, never a JSON fragment in CLI or diff output.
|
|
@@ -109,33 +111,104 @@ function summarize(name: string, result: Result, raw: string, context?: Context)
|
|
|
109
111
|
const notices = strings(envelope.warnings);
|
|
110
112
|
const summary: Summary = { label: "Result returned", tone: "toolOutput", notices, rows: [] };
|
|
111
113
|
const textLines = raw.split(/\r?\n/).filter(line => line.trim());
|
|
112
|
-
const preview = record(context?.args).preflight === true || action.endsWith("-preflight") || envelope.outcome === "preflight";
|
|
113
|
-
if (context?.isError) {
|
|
114
|
+
const preview = !["update", "add", "undo", "resolve-delete-change-conflict", "merge", "finalize-merge", "merge-to-branch", "branch-delete", "shelveset-delete", "code-review-delete", "patch","shelveset-create","shelveset-apply","code-review-create","code-review-update"].includes(action) && (record(context?.args).preflight === true || action.endsWith("-preflight") || envelope.outcome === "preflight");
|
|
115
|
+
if (context?.isError && !(name === "diff" && Array.isArray(data.outcomes)) && !(name === "mergeBranches" && action === "merge-branches") && !["checkin", "branch-create", "switch-branch", "update", "add", "undo", "resolve-delete-change-conflict", "merge", "finalize-merge", "merge-to-branch", "branch-delete", "shelveset-delete", "code-review-delete", "patch","shelveset-create","shelveset-apply","code-review-create","code-review-update"].includes(action)) {
|
|
114
116
|
summary.label = "Failed";
|
|
115
117
|
summary.tone = "error";
|
|
116
118
|
summary.rows = textLines.slice(0, 3);
|
|
117
119
|
return summary;
|
|
118
120
|
}
|
|
119
|
-
if (
|
|
121
|
+
if (["shelveset-create","shelveset-apply","code-review-create","code-review-update"].includes(action)) {
|
|
122
|
+
summary.label = `Object write: ${scalar(envelope.outcome)}`;
|
|
123
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
124
|
+
summary.rows = [`Attempt: ${scalar(record(data.attempt).state)}`, `Effect: ${scalar(data.effect)}`, `Emitted identity: ${scalar(record(data.observedCreatedIdentity).value) || "unobserved"}`];
|
|
125
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
126
|
+
notices.push(envelope.outcome === "preflight" ? "Command-only preview; no conflict/state analysis." : "Applied items, requested review state, repository scope and rollback remain unverified.");
|
|
127
|
+
} else if (name === "patch" && action === "patch") {
|
|
128
|
+
summary.label = `Patch: ${scalar(envelope.outcome)}`;
|
|
129
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
130
|
+
summary.rows = [`Effect: ${scalar(data.effect)}`, `Publication: ${scalar(data.publication)}`, `Cleanup: ${scalar(data.cleanup)}`, `Observed bytes: ${count(record(data.artifact).bytes) ?? "unknown"}`];
|
|
131
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
132
|
+
notices.push("Generation only; workspace effects, move encoding and rollback unverified.");
|
|
133
|
+
} else if ((name === "shelvesetDelete" && action === "shelveset-delete") || (name === "codeReviewDelete" && action === "code-review-delete")) {
|
|
134
|
+
summary.label = `Object deletion: ${scalar(envelope.outcome)}`;
|
|
135
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
136
|
+
summary.rows = [`Attempt: ${scalar(record(data.attempt).state)}`, `Effects: ${scalar(data.effect)}`];
|
|
137
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
138
|
+
notices.push(envelope.outcome === "preflight" ? "Command-only preview; no existence/readiness analysis." : "Deleted identities, repository scope and rollback remain unverified.");
|
|
139
|
+
} else if (name === "branchDelete" && action === "branch-delete") {
|
|
140
|
+
summary.label = `Branch deletion: ${scalar(envelope.outcome)}`;
|
|
141
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
142
|
+
summary.rows = [`Attempt: ${scalar(record(data.attempt).state)}`, `Effects: ${scalar(data.effect)}`, `Delete changesets requested: ${scalar(data.deleteChangesetsRequested)}`];
|
|
143
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
144
|
+
notices.push(envelope.outcome === "preflight" ? "Command-only preview; no existence or readiness analysis." : "Deletion identity, history effects and repository scope remain unverified.");
|
|
145
|
+
} else if (name === "mergeToBranch" && action === "merge-to-branch") {
|
|
146
|
+
summary.label = `Closeout: ${scalar(envelope.outcome)}`;
|
|
147
|
+
summary.tone = envelope.ok === false ? "error" : envelope.outcome === "completed" ? "success" : "warning";
|
|
148
|
+
summary.rows = [`Effect: ${scalar(data.effect)}`, `Recorded stages: ${records(data.stages).length}`, `Created changeset: ${scalar(record(data.createdChangeset).id) || "unobserved"}`, `Target verification: ${scalar(data.targetVerification)}`];
|
|
149
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
150
|
+
notices.push("Earlier child effects remain recorded. Source links, branch head, aliases, exclusive scope and rollback unverified; no automatic replay.");
|
|
151
|
+
} else if ((name === "merge" || name === "finalizeMerge") && (action === "merge" || action === "finalize-merge")) {
|
|
152
|
+
summary.label = `Workspace merge: ${scalar(envelope.outcome)}`;
|
|
153
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
154
|
+
summary.rows = [`Applying command completed: ${String(data.commandCompleted)}`, `Checkin readiness: ${scalar(data.checkinReadiness)}`, `Effect: ${scalar(data.effect)}`];
|
|
155
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
156
|
+
notices.push(envelope.outcome === "preflight" ? "Command-only preview; no readiness analysis." : "Conflict resolution, finalized metadata, preservation and complete scope remain unverified.");
|
|
157
|
+
} else if (name === "resolveDeleteChangeConflict" && action === "resolve-delete-change-conflict") {
|
|
158
|
+
summary.label = `Removal: ${scalar(envelope.outcome)}`;
|
|
159
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
160
|
+
summary.rows = [`Attempt: ${scalar(record(data.attempt).state)}`, `Effect: ${scalar(data.effect)}`];
|
|
161
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
162
|
+
notices.push(envelope.outcome === "preflight" ? "Command-only preview; no conflict or execution readiness analysis." : "Conflict resolution, preservation, pending state and affected scope remain unverified.");
|
|
163
|
+
} else if (preview) {
|
|
120
164
|
const wouldRun = typeof data.wouldRun === "boolean" ? data.wouldRun : /^- Would run: no$/m.test(raw) ? false : /^- Would run: yes$/m.test(raw) ? true : undefined;
|
|
121
165
|
summary.label = wouldRun === false ? "Preview: would not run" : wouldRun === true ? "Preview: would run" : "Preview returned";
|
|
122
166
|
summary.tone = "warning";
|
|
123
167
|
summary.rows = action ? [scalar(data.reason) || scalar(data.errorCode), scalar(data.strategy)].filter(Boolean) : textLines.filter(line => /^- (?:Reason|Strategy|Command):/.test(line));
|
|
168
|
+
} else if ((name === "update" && action === "update" || name === "add" && action === "add" || name === "undo" && action === "undo") && typeof envelope.outcome === "string") {
|
|
169
|
+
summary.label = `${name === "update" ? "Update" : name === "add" ? "Add" : "Undo"}: ${envelope.outcome}`;
|
|
170
|
+
summary.tone = envelope.ok === false ? "error" : "warning";
|
|
171
|
+
summary.rows = [`Attempt: ${scalar(record(data.attempt).state)}`, `Effect: ${scalar(data.effect)}`];
|
|
172
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
173
|
+
notices.push(name === "update" ? "Workspace identity, changed items, preservation, cleanliness and merge readiness remain unverified." : name === "add" ? "Requested operands are not verified added items. Workspace identity, expanded scope and preservation remain unverified." : "Requested operands are not verified undone items. Workspace identity, restored content, expanded scope and preservation remain unverified.");
|
|
174
|
+
} else if (name === "switchBranch" && action === "switch-branch" && typeof envelope.outcome === "string") {
|
|
175
|
+
summary.label = `Switch: ${envelope.outcome}`;
|
|
176
|
+
summary.tone = envelope.ok === false ? "error" : envelope.outcome === "switched" ? "success" : "warning";
|
|
177
|
+
summary.rows = records(data.steps).map(step => `${scalar(step.name)}: ${scalar(step.effect)}`);
|
|
178
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
179
|
+
notices.push(`Effect: ${scalar(data.effect)}. File preservation, alias equivalence and Xlink effects remain unverified.`);
|
|
180
|
+
} else if ((name === "checkin" && action === "checkin" || name === "branchCreate" && action === "branch-create") && typeof envelope.outcome === "string") {
|
|
181
|
+
summary.label = `${name === "checkin" ? "Checkin" : "Branch create"}: ${envelope.outcome}`;
|
|
182
|
+
summary.tone = envelope.ok === false ? "error" : name === "checkin" ? "success" : "warning";
|
|
183
|
+
const changeset = scalar(record(data.createdChangeset).id);
|
|
184
|
+
if (changeset) summary.label += `${separator}cs:${changeset}`;
|
|
185
|
+
summary.rows = records(data.steps).map(step => `${scalar(step.name)}: ${scalar(step.effect)}`);
|
|
186
|
+
if (envelope.ok === false) notices.unshift(scalar(record(envelope.error).message));
|
|
187
|
+
if (name === "branchCreate") notices.push("Created identity and repository effects unverified; no workspace switch requested.");
|
|
188
|
+
else {
|
|
189
|
+
notices.push("Scope exhaustion, branch head, alias equivalence, merge links and Xlink effects unverified.");
|
|
190
|
+
if (data.pendingAfter === null) notices.push("Pending after not verified.");
|
|
191
|
+
if (count(data.omittedReferences)) notices.unshift(`${count(data.omittedReferences)} references omitted.`);
|
|
192
|
+
}
|
|
124
193
|
} else if (name === "mergeBranches" && typeof envelope.outcome === "string") {
|
|
125
194
|
summary.label = `Server merge: ${envelope.outcome}`;
|
|
126
|
-
summary.tone = envelope.outcome === "completed" ? "success" : "warning";
|
|
195
|
+
summary.tone = envelope.ok === false ? "error" : envelope.outcome === "completed" || envelope.outcome === "analyzed" ? "success" : "warning";
|
|
196
|
+
if (data.mode === "analyze") notices.unshift("Read-only analysis; no applying merge requested.");
|
|
197
|
+
summary.rows = Array.isArray(data.conflictPaths) ? data.conflictPaths.map(path => scalar(path)) : [];
|
|
198
|
+
if (count(record(data.counts).conflictsOmitted)) notices.unshift(`${count(record(data.counts).conflictsOmitted)} conflicts omitted.`);
|
|
127
199
|
const changeset = scalar(record(data.createdChangeset).id);
|
|
128
200
|
if (changeset) summary.label += `${separator}cs:${changeset}`;
|
|
129
201
|
if (data.mergeLinkIdentity === "unverified" || data.xlinkEffects === "unverified") notices.push("Merge-link identity and Xlink effects remain unverified.");
|
|
130
202
|
if (data.effect === "uncertain" || data.effect === "not-proven") notices.unshift("Effects not proven; inspect server state before any retry.");
|
|
131
|
-
if (data.
|
|
132
|
-
if (data.
|
|
203
|
+
if (data.serverAliasEquivalence === "unverified") notices.push("Requested and emitted server names remain separate; alias equivalence is unverified.");
|
|
204
|
+
if (data.timedOut === true || record(data.attempt).timedOut === true) notices.unshift("Command timed out; effects uncertain.");
|
|
205
|
+
if (data.aborted === true || record(data.attempt).aborted === true) notices.unshift("Command cancelled; effects uncertain.");
|
|
133
206
|
} else if (data.checkedIn === false || record(data.switchOutcome).kind === "canceled" || data.kind === "canceled" || data.strategy === "cancel-with-pending") {
|
|
134
207
|
summary.label = data.checkedIn === false ? "Checkin not performed" : "Switch cancelled";
|
|
135
208
|
summary.tone = "warning";
|
|
136
209
|
const reason = scalar(record(data.switchOutcome).reason) || scalar(data.reason);
|
|
137
210
|
if (reason) notices.unshift(reason);
|
|
138
|
-
} else if (envelope.ok === false) {
|
|
211
|
+
} else if (envelope.ok === false && !(name === "diff" && Array.isArray(data.outcomes))) {
|
|
139
212
|
summary.label = "Operation not successful";
|
|
140
213
|
summary.tone = "error";
|
|
141
214
|
} else if (name === "status" && count(record(data.summary).totalPending) !== undefined) {
|
|
@@ -147,20 +220,24 @@ function summarize(name: string, result: Result, raw: string, context?: Context)
|
|
|
147
220
|
summary.label = data.branch;
|
|
148
221
|
} else if (name === "branchExists" && /^(true|false)$/.test(raw.trim())) {
|
|
149
222
|
summary.label = raw.trim() === "true" ? "Branch found" : "Branch not found";
|
|
150
|
-
} else if (
|
|
223
|
+
} else if (name === "diff" && typeof data.status === "string") {
|
|
151
224
|
summary.label = data.status === "unchanged" ? "No differences" : data.status === "binary-different" ? "Binary content differs" : data.status === "added-empty" ? "Added file is empty" : "Text differences";
|
|
152
225
|
if (data.binary === true) notices.push("Binary comparison; no text hunks.");
|
|
153
|
-
} else if (name === "
|
|
226
|
+
} else if (name === "diff" && Array.isArray(data.outcomes)) {
|
|
154
227
|
const outcomes = records(data.outcomes);
|
|
228
|
+
const counts = record(data.counts);
|
|
155
229
|
const unavailable = outcomes.filter(item => item.status === "unavailable");
|
|
156
230
|
const skipped = outcomes.filter(item => item.status === "skipped-directory");
|
|
157
|
-
summary.label = `${outcomes.length} results${separator}${outcomes.filter(item => item.changed === true).length} changed${separator}${unavailable.length} unavailable`;
|
|
158
|
-
if (unavailable.length) summary.tone = "warning";
|
|
231
|
+
summary.label = `${outcomes.length} returned results${separator}${outcomes.filter(item => record(item.comparison).changed === true).length} changed${separator}${count(counts.failed) ?? unavailable.length} unavailable`;
|
|
232
|
+
if (unavailable.length || envelope.ok === false) summary.tone = "warning";
|
|
159
233
|
if (skipped.length) notices.push(`${skipped.length} directories skipped.`);
|
|
160
|
-
summary.rows = [...unavailable, ...outcomes.filter(item => item.status !== "unavailable" && item.status !== "skipped-directory"), ...skipped].map(item => `${scalar(item.path)}: ${scalar(item.status)}${item.error ? ` - ${scalar(item.error)}` : ""}`);
|
|
161
|
-
if (outcomes.some(item => item.truncated === true)) notices.push("Some file diffs were truncated by the tool.");
|
|
162
|
-
if (count(
|
|
163
|
-
if (count(
|
|
234
|
+
summary.rows = [...unavailable, ...outcomes.filter(item => item.status !== "unavailable" && item.status !== "skipped-directory"), ...skipped].map(item => `${scalar(item.path)}: ${scalar(item.status)}${item.error ? ` - ${scalar(record(item.error).code)}` : ""}`);
|
|
235
|
+
if (outcomes.some(item => record(record(item.comparison).excerpt).truncated === true)) notices.push("Some file diffs were truncated by the tool.");
|
|
236
|
+
if (count(counts.omitted)) notices.push(`${counts.omitted} outcomes omitted by the tool.`);
|
|
237
|
+
if (count(counts.limited)) notices.push(`${counts.limited} pending items skipped by the file limit.`);
|
|
238
|
+
if (count(counts.unattempted)) notices.push(`${counts.unattempted} selected items not attempted.`);
|
|
239
|
+
if (count(data.excludedPrivate)) notices.push(`${data.excludedPrivate} private items excluded.`);
|
|
240
|
+
if (strings(data.unmatched).length) notices.push(`${strings(data.unmatched).length} selected paths had no pending match.`);
|
|
164
241
|
} else if (name === "patch" && typeof envelope.status === "string") {
|
|
165
242
|
summary.label = `Patch ${envelope.status}${count(envelope.bytes) !== undefined ? `${separator}${envelope.bytes} bytes` : ""}`;
|
|
166
243
|
if (envelope.binaryLimited === true) notices.push("Binary content could not be fully represented.");
|
|
@@ -189,7 +266,7 @@ function summarize(name: string, result: Result, raw: string, context?: Context)
|
|
|
189
266
|
notices.unshift(...urgent.map(line => line.replace(/^#+\s*/, "")));
|
|
190
267
|
notices.push(...warnings);
|
|
191
268
|
}
|
|
192
|
-
if (data.truncated === true || data.outputTruncated === true) notices.push("Output truncated by the tool.");
|
|
269
|
+
if (data.truncated === true || data.outputTruncated === true || record(data.excerpt).truncated === true) notices.push("Output truncated by the tool.");
|
|
193
270
|
const limited = data.truncated === true || data.outputTruncated === true || envelope.truncated === true
|
|
194
271
|
|| Boolean(count(record(data.itemCount).omitted) || count(data.omittedOutcomes) || count(data.skippedByLimit))
|
|
195
272
|
|| records(data.outcomes).some(item => item.truncated === true);
|
|
@@ -234,8 +311,11 @@ export function renderPlasticResult(name: string, result: Result, options: Optio
|
|
|
234
311
|
const envelope = payload(result, raw);
|
|
235
312
|
const data = record(envelope.data);
|
|
236
313
|
if (typeof data.diff === "string") section("Diff", evidence(data.diff, theme));
|
|
314
|
+
if (typeof record(data.excerpt).text === "string") section("Diff", evidence(scalar(record(data.excerpt).text), theme));
|
|
237
315
|
for (const item of records(data.outcomes)) {
|
|
238
316
|
if (typeof item.diff === "string") section(safe(scalar(item.path)), evidence(item.diff, theme));
|
|
317
|
+
const excerpt=record(record(item.comparison).excerpt);
|
|
318
|
+
if (typeof excerpt.text === "string") section(safe(scalar(item.path)), evidence(excerpt.text, theme));
|
|
239
319
|
}
|
|
240
320
|
if (name === "patch" && typeof envelope.content === "string") section("Patch", evidence(envelope.content, theme));
|
|
241
321
|
// Keep original content, including JSON numeric lexemes and duplicate keys, as the evidence.
|
|
@@ -14,11 +14,7 @@ export type PlasticSearchCatalogEntry = {
|
|
|
14
14
|
guidance: readonly string[];
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Package-owned executable capability vocabulary. `plastic_diff` deliberately
|
|
19
|
-
* remains outside this catalog: it is registered for compatibility, but is a
|
|
20
|
-
* disabled alias rather than a safe executable diff capability.
|
|
21
|
-
*/
|
|
17
|
+
/** Canonical executable capabilities; there are no compatibility diff tools. */
|
|
22
18
|
export const PLASTIC_SEARCH_CATALOG: readonly PlasticSearchCatalogEntry[] = [
|
|
23
19
|
{ name: "plastic_status", aliases: ["status", "pending changes", "working changes", "what changed", "changed files", "list changes"], tags: ["workspace", "inspect", "changes"], guidance: ["Use machineReadable=true to list changed, added, moved, deleted, and private paths without invoking GUI-capable cm diff."] },
|
|
24
20
|
{ name: "plastic_update", aliases: ["update", "sync", "pull latest"], tags: ["workspace", "synchronize", "safe"], guidance: ["Review pending changes before updating a workspace."] },
|
|
@@ -27,13 +23,11 @@ export const PLASTIC_SEARCH_CATALOG: readonly PlasticSearchCatalogEntry[] = [
|
|
|
27
23
|
{ name: "plastic_undo", aliases: ["undo changes", "revert changes", "discard changes"], tags: ["changes", "mutation", "destructive"], guidance: ["Confirm the exact paths before discarding pending changes."] },
|
|
28
24
|
{ name: "plastic_resolveDeleteChangeConflict", aliases: ["resolve delete conflict", "deleted conflict"], tags: ["merge", "conflict", "resolution"], guidance: ["Inspect the conflict and choose whether files should remain on disk before resolving it."] },
|
|
29
25
|
{ name: "plastic_patch", aliases: ["patch", "generate patch", "review patch"], tags: ["diff", "patch", "export"], guidance: ["Inspect generated patches before sharing because they can include source, paths, or secrets."] },
|
|
30
|
-
{ name: "
|
|
31
|
-
{ name: "plastic_workspaceDiff", aliases: ["workspace diff", "pending review", "review pending changes", "review workspace changes", "diff pending files"], tags: ["diff", "workspace", "review", "inspect"], guidance: ["Use only for an intentional multi-file content review, not routine validation. Select exact paths or pass allPending=true explicitly; use plastic_status when only changed paths are needed."] },
|
|
32
|
-
{ name: "plastic_diffRevisions", aliases: ["diff revisions", "compare revisions", "revision diff"], tags: ["diff", "revision", "inspect"], guidance: ["Use this only for two explicit file-qualified revision specifications."] },
|
|
26
|
+
{ name: "plastic_diff", aliases: ["diff", "diff file", "compare file", "workspace diff", "pending review", "diff revisions", "compare revisions"], tags: ["diff", "file", "workspace", "revision", "inspect"], guidance: ["Require mode=file, revisions, or workspace. Use explicit paths or allPending=true for pending review; raw GUI cm diff remains blocked."] },
|
|
33
27
|
{ name: "plastic_branchCreate", aliases: ["create branch", "new branch"], tags: ["branch", "mutation", "workflow"], guidance: ["Create normal work branches beneath an intended parent, which may differ from the loaded branch. Use allowRootBranch only for intentional top-level branches."] },
|
|
34
28
|
{ name: "plastic_switchBranch", aliases: ["switch branch", "checkout branch", "change branch"], tags: ["branch", "workspace", "mutation"], guidance: ["Check pending changes and choose how to handle them before switching branches."] },
|
|
35
29
|
{ name: "plastic_merge", aliases: ["merge branch", "merge changes"], tags: ["merge", "conflict", "workflow"], guidance: ["Inspect merge state and resolve conflicts before checkin."] },
|
|
36
|
-
{ name: "plastic_mergeBranches", aliases: ["server merge", "workspace-free merge", "merge qualified branches"], tags: ["merge", "branch", "server", "mutation"], guidance: ["Require explicit same-repository/server qualified source and target
|
|
30
|
+
{ name: "plastic_mergeBranches", aliases: ["server merge", "workspace-free merge", "merge qualified branches", "server merge conflicts", "resolve server conflicts"], tags: ["merge", "branch", "server", "conflict", "resolutions", "mutation"], guidance: ["Require explicit same-repository/server qualified source and target. mode=analyze performs read-only remote conflict discovery without a message; apply requires a nonempty message and can submit a reviewed fileConflictsResolutionsFile. Keep resolution inputs immutable until completion. Preflight only renders the command and validates local inputs; completed output verifies a target changeset, not a merge link, exclusive target ownership, or xlink effects."] },
|
|
37
31
|
{ name: "plastic_mergeToBranch", aliases: ["merge to branch", "close branch", "merge closeout"], tags: ["merge", "branch", "closeout"], guidance: ["Plastic merges into the branch loaded in the workspace; confirm source, target, final branch, and target-branch checkin rather than assuming a Git-style fast-forward."] },
|
|
38
32
|
{ name: "plastic_finalizeMerge", aliases: ["finalize merge", "complete merge", "resolve merge"], tags: ["merge", "conflict", "finalize"], guidance: ["Use after reviewed manual conflict resolution when Plastic still needs merge metadata finalized."] },
|
|
39
33
|
{ name: "plastic_currentBranch", aliases: ["current branch", "which branch", "branch name"], tags: ["branch", "workspace", "inspect"], guidance: ["Inspect the current branch before branch, merge, or checkin operations."] },
|
|
@@ -51,9 +45,8 @@ export const PLASTIC_SEARCH_CATALOG: readonly PlasticSearchCatalogEntry[] = [
|
|
|
51
45
|
{ name: "plastic_workspaceList", aliases: ["list workspaces", "find workspace", "workspaces"], tags: ["workspace", "inspect", "query"], guidance: ["Inspect workspace paths and repository identity before using a workspace in a mutation."] },
|
|
52
46
|
] as const;
|
|
53
47
|
|
|
54
|
-
export const PLASTIC_COMPATIBILITY_TOOL_NAMES = ["plastic_diff"] as const;
|
|
55
48
|
export const PLASTIC_SEARCHABLE_TOOL_NAMES = new Set(PLASTIC_SEARCH_CATALOG.map((entry) => entry.name));
|
|
56
|
-
export const PLASTIC_TOOL_NAMES = new Set(
|
|
49
|
+
export const PLASTIC_TOOL_NAMES = new Set(PLASTIC_SEARCHABLE_TOOL_NAMES);
|
|
57
50
|
|
|
58
51
|
export type ToolSourceInfo = {
|
|
59
52
|
path: string;
|
|
@@ -168,6 +161,8 @@ function safetyPriority(entry: PlasticSearchCatalogEntry): number {
|
|
|
168
161
|
/** Search the executable catalog with deterministic scoring and safe bare-domain ordering. */
|
|
169
162
|
export function searchPlasticTools(input: PlasticToolSearchInput, descriptions: ReadonlyMap<string, string> = new Map()): PlasticToolSearchMatch[] {
|
|
170
163
|
const requestedNames = exactRequestedNames(input.toolNames);
|
|
164
|
+
const exactQuery = typeof input.query === "string" ? input.query.trim().toLowerCase() : "";
|
|
165
|
+
if (requestedNames.size === 0 && /^plastic_[a-z0-9]+$/.test(exactQuery) && !PLASTIC_SEARCH_CATALOG.some(entry => entry.name.toLowerCase() === exactQuery)) return [];
|
|
171
166
|
const terms = typeof input.query === "string" ? tokenize(input.query) : [];
|
|
172
167
|
const limit = normalizeLimit(input.limit, requestedNames);
|
|
173
168
|
const domainTerms = terms.filter((term) => SEARCH_DOMAIN_TERMS.has(term));
|
|
@@ -231,5 +226,5 @@ export function getRestoredPlasticToolNames(branchEntries: readonly unknown[], e
|
|
|
231
226
|
if (typeof name === "string" && effectiveToolNames.has(name)) restored.add(name);
|
|
232
227
|
}
|
|
233
228
|
}
|
|
234
|
-
return
|
|
229
|
+
return PLASTIC_SEARCH_CATALOG.map((entry) => entry.name).filter((name) => restored.has(name));
|
|
235
230
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AddReceipt } from "../domain/add-contract";
|
|
2
|
+
export function presentAddReceipt(dto: AddReceipt): string {
|
|
3
|
+
return dto.ok ? `Add command completed for ${dto.data.requestedOperandCount} requested operands, not a verified added-item count. Workspace identity, added items, expanded scope and preservation remain unverified.` :
|
|
4
|
+
`Add ${dto.outcome}: ${dto.error.message} Effects: ${dto.data.effect}. Do not retry automatically.`;
|
|
5
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BranchCreateReceipt } from "../domain/branch-create-contract";
|
|
2
|
+
export function presentBranchCreateReceipt(dto: BranchCreateReceipt): string {
|
|
3
|
+
const text = dto.ok
|
|
4
|
+
? "Branch create command completed. Created branch identity, repository and effects are unverified; no workspace switch was requested."
|
|
5
|
+
: `Branch create ${dto.outcome}. ${dto.error.message} Effect: ${dto.data.effect}.`;
|
|
6
|
+
return text.length <= 24000 ? text : "Branch create evidence is unavailable; inspect structuredContent and do not retry automatically.";
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BranchDeleteReceipt } from "../domain/branch-delete-contract";
|
|
2
|
+
export function presentBranchDeleteReceipt(dto:BranchDeleteReceipt):string {
|
|
3
|
+
if(dto.data.outputFormatRequested==="json")return JSON.stringify(dto);
|
|
4
|
+
if(!dto.ok)return `Branch deletion ${dto.outcome}: ${dto.error.message} Effects: ${dto.data.effect}. Do not retry automatically.`;
|
|
5
|
+
if(dto.outcome==="preflight")return `Branch-delete command-only preview (not executed): ${JSON.stringify(dto.data.intendedArgv)}. No branch existence, permissions or deletion readiness analysis.`;
|
|
6
|
+
return "Branch-delete command completed. Deleted branch identity, changeset-history effects, repository/workspace scope and rollback remain unverified.";
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CheckinReceipt } from "../domain/checkin-contract";
|
|
2
|
+
export function presentCheckinReceipt(dto: CheckinReceipt, format: "text" | "json" = "text"): string {
|
|
3
|
+
const text = format === "json" ? JSON.stringify(dto) : [
|
|
4
|
+
`## Checkin ${dto.outcome}`,
|
|
5
|
+
`- Effect: ${dto.data.effect}`,
|
|
6
|
+
...(dto.outcome === "preflight" ? [`- Would run: ${dto.data.wouldRun ? "yes" : "no"}`, `- Command argv: ${JSON.stringify(dto.data.command)}`] : []),
|
|
7
|
+
...(dto.data.createdChangeset ? [`- Observed root changeset: cs:${dto.data.createdChangeset.id}@br:${dto.data.createdChangeset.branch}@${dto.data.createdChangeset.repository}@${dto.data.createdChangeset.server}`] : []),
|
|
8
|
+
`- Steps: ${dto.data.steps.map(s => `${s.name} (${s.effect})`).join(", ") || "none"}`,
|
|
9
|
+
`- Pending after: ${dto.data.pendingAfter ? dto.data.pendingAfter.totalPending : "not verified"}`,
|
|
10
|
+
"- Requested scope exhaustion, branch head, alias equivalence, merge links and Xlink effects: unverified",
|
|
11
|
+
...(!dto.ok ? [dto.error.message] : []),
|
|
12
|
+
].join("\n");
|
|
13
|
+
return Buffer.byteLength(text, "utf8") <= 24000 ? text : JSON.stringify({ action: dto.action, outcome: dto.outcome, ok: dto.ok, note: "Presentation omitted; inspect structuredContent for the bounded receipt." });
|
|
14
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { CloseoutReceipt } from "../domain/closeout-contract";
|
|
2
|
+
export function presentCloseoutReceipt(dto:CloseoutReceipt,format?:"text"|"json"):string {if(format==="json")return JSON.stringify(dto);return [`## Merge To Branch ${dto.ok?dto.outcome==="preflight"?"Preview":"Receipt":"Paused"}`,`- Outcome: ${dto.outcome}; effect: ${dto.data.effect}`,`- Recorded stages: ${dto.data.stages.length}; target verification: ${dto.data.targetVerification}`,`- Source: ${dto.data.sourceBranch??"unobserved"}; target: ${dto.data.targetBranch??"unobserved"}`,`- Created changeset: ${dto.data.createdChangeset?`cs:${dto.data.createdChangeset.id}@br:${dto.data.createdChangeset.branch}@${dto.data.createdChangeset.repository}@${dto.data.createdChangeset.server}`:"unobserved"}`,`- Final pending count: ${dto.data.pendingAfter?.totalPending??"unobserved"}`,...(!dto.ok?[`- ${dto.error.message}`]:[]),"- Point-in-time evidence only; source links, branch head, aliases, exclusive scope and rollback are unverified. No automatic replay or compensation."].join("\n");}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { OutputFormat, TOOL_VERSION } from "./results";
|
|
2
|
+
import { TextDiffResult } from "../diff/text";
|
|
3
|
+
import { getCmVersion } from "../execution/cli-version";
|
|
4
|
+
|
|
5
|
+
export const DIFF_RESPONSE_DEFAULT_MAX_CHARS = 8_000;
|
|
6
|
+
|
|
7
|
+
export const DIFF_RESPONSE_MAX_CHARS = 20_000;
|
|
8
|
+
|
|
9
|
+
export const DIFF_RESPONSE_MIN_CHARS = 500;
|
|
10
|
+
|
|
11
|
+
export const DIFF_RESPONSE_TOTAL_MAX_CHARS = 24_000;
|
|
12
|
+
|
|
13
|
+
export const normalizeDiffResponseMaxChars = (value: unknown): number =>
|
|
14
|
+
{
|
|
15
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
16
|
+
{
|
|
17
|
+
return DIFF_RESPONSE_DEFAULT_MAX_CHARS;
|
|
18
|
+
}
|
|
19
|
+
return Math.min(DIFF_RESPONSE_MAX_CHARS, Math.max(DIFF_RESPONSE_MIN_CHARS, Math.trunc(value)));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const formatTextDiff = async (
|
|
23
|
+
action: "diffFile" | "diffRevisions",
|
|
24
|
+
format: OutputFormat | undefined,
|
|
25
|
+
comparisonKind: string,
|
|
26
|
+
unboundedResult: TextDiffResult,
|
|
27
|
+
workdir: string | undefined,
|
|
28
|
+
comparisonMetadata: Record<string, unknown> = {},
|
|
29
|
+
maxChars?: number,
|
|
30
|
+
): Promise<string> =>
|
|
31
|
+
{
|
|
32
|
+
const result = boundTextDiffResult(unboundedResult, normalizeDiffResponseMaxChars(maxChars));
|
|
33
|
+
const status = result.binary ? (result.changed ? "binary-different" : "unchanged") : (result.changed ? "changed" : (comparisonKind === "workspace-added" ? "added-empty" : "unchanged"));
|
|
34
|
+
const text = result.binary
|
|
35
|
+
? (result.changed ? "Binary content differs; a text diff is unavailable." : "No differences.")
|
|
36
|
+
: (result.changed ? result.output : (status === "added-empty" ? "Added file is empty; no text diff hunks." : "No differences."));
|
|
37
|
+
const data = {
|
|
38
|
+
comparisonKind,
|
|
39
|
+
...comparisonMetadata,
|
|
40
|
+
backend: result.backend,
|
|
41
|
+
status,
|
|
42
|
+
changed: result.changed,
|
|
43
|
+
binary: result.binary,
|
|
44
|
+
truncated: result.truncated,
|
|
45
|
+
totalChars: result.totalChars,
|
|
46
|
+
diff: result.changed && !result.binary ? result.output : undefined,
|
|
47
|
+
};
|
|
48
|
+
const warnings = result.truncated ? ["Diff output was truncated; use a narrower file or generate a review patch for the complete result."] : undefined;
|
|
49
|
+
if (format === "json")
|
|
50
|
+
{
|
|
51
|
+
return toBoundedDiffStructuredResult(action, data, workdir, warnings);
|
|
52
|
+
}
|
|
53
|
+
return text;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const boundTextDiffResult = (result: TextDiffResult, maxChars: number): TextDiffResult =>
|
|
57
|
+
{
|
|
58
|
+
if (!result.output || result.output.length <= maxChars)
|
|
59
|
+
{
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const suffix = `\n\n[Diff output truncated at the requested ${maxChars}-character response bound.]`;
|
|
64
|
+
return {
|
|
65
|
+
...result,
|
|
66
|
+
output: suffix.length >= maxChars ? suffix.slice(0, maxChars) : `${result.output.slice(0, maxChars - suffix.length)}${suffix}`,
|
|
67
|
+
truncated: true,
|
|
68
|
+
totalChars: result.totalChars,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const toBoundedDiffStructuredResult = async (
|
|
73
|
+
action: "diffFile" | "diffRevisions",
|
|
74
|
+
data: Record<string, unknown>,
|
|
75
|
+
workdir?: string,
|
|
76
|
+
warnings?: string[],
|
|
77
|
+
): Promise<string> =>
|
|
78
|
+
{
|
|
79
|
+
const wrap = (payload: Record<string, unknown>): string => `## ${action}\n\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``;
|
|
80
|
+
const basePayload = {
|
|
81
|
+
ok: true,
|
|
82
|
+
action,
|
|
83
|
+
toolVersion: TOOL_VERSION,
|
|
84
|
+
cliVersion: (await getCmVersion(workdir)).slice(0, 256),
|
|
85
|
+
};
|
|
86
|
+
const initial = wrap({ ...basePayload, data, ...(warnings?.length ? { warnings } : {}) });
|
|
87
|
+
if (initial.length <= DIFF_RESPONSE_TOTAL_MAX_CHARS)
|
|
88
|
+
{
|
|
89
|
+
return initial;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const originalDiff = typeof data.diff === "string" ? data.diff : "";
|
|
93
|
+
const responseWarning = `Diff content was further truncated after JSON escaping to keep the complete response within ${DIFF_RESPONSE_TOTAL_MAX_CHARS} characters.`;
|
|
94
|
+
const boundedWarnings = [...(warnings ?? []), responseWarning];
|
|
95
|
+
const suffix = `\n\n[Diff output truncated to keep the complete JSON response within ${DIFF_RESPONSE_TOTAL_MAX_CHARS} characters.]`;
|
|
96
|
+
const render = (prefixChars: number): string => wrap({
|
|
97
|
+
...basePayload,
|
|
98
|
+
data: {
|
|
99
|
+
...data,
|
|
100
|
+
truncated: true,
|
|
101
|
+
diff: `${originalDiff.slice(0, prefixChars)}${suffix}`,
|
|
102
|
+
},
|
|
103
|
+
warnings: boundedWarnings,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let low = 0;
|
|
107
|
+
let high = originalDiff.length;
|
|
108
|
+
let best = render(0);
|
|
109
|
+
while (low <= high)
|
|
110
|
+
{
|
|
111
|
+
const middle = Math.floor((low + high) / 2);
|
|
112
|
+
const candidate = render(middle);
|
|
113
|
+
if (candidate.length <= DIFF_RESPONSE_TOTAL_MAX_CHARS)
|
|
114
|
+
{
|
|
115
|
+
best = candidate;
|
|
116
|
+
low = middle + 1;
|
|
117
|
+
}
|
|
118
|
+
else
|
|
119
|
+
{
|
|
120
|
+
high = middle - 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return best;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const WORKSPACE_DIFF_DEFAULT_MAX_FILES = 3;
|
|
127
|
+
|
|
128
|
+
export const WORKSPACE_DIFF_MAX_FILES = 20;
|
|
129
|
+
|
|
130
|
+
export const WORKSPACE_DIFF_MAX_PATHS = 20;
|
|
131
|
+
|
|
132
|
+
export const WORKSPACE_DIFF_PATH_MAX_CHARS = 1_024;
|
|
133
|
+
|
|
134
|
+
export const WORKSPACE_DIFF_DISPLAY_PATH_MAX_CHARS = 256;
|
|
135
|
+
|
|
136
|
+
export const WORKSPACE_DIFF_ERROR_MAX_CHARS = 1_024;
|
|
137
|
+
|
|
138
|
+
export const WORKSPACE_DIFF_DEFAULT_MAX_CHARS = 3_000;
|
|
139
|
+
|
|
140
|
+
export const WORKSPACE_DIFF_PER_FILE_MAX_CHARS = 8_000;
|
|
141
|
+
|
|
142
|
+
export const WORKSPACE_DIFF_MIN_CHARS = 500;
|
|
143
|
+
// This is a response bound, not just a diff-body bound. Reserve space for
|
|
144
|
+
// framing and an omission summary so both text and JSON remain useful.
|
|
145
|
+
|
|
146
|
+
export const WORKSPACE_DIFF_TOTAL_MAX_CHARS = 20_000;
|
|
147
|
+
|
|
148
|
+
export const WORKSPACE_DIFF_CONTENT_MAX_CHARS = 16_000;
|
|
149
|
+
|
|
150
|
+
export const boundWorkspaceValue = (value: string, maxChars: number): string =>
|
|
151
|
+
{
|
|
152
|
+
if (maxChars <= 0)
|
|
153
|
+
{
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
if (value.length <= maxChars)
|
|
157
|
+
{
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
const suffix = `… [truncated; original ${value.length} characters]`;
|
|
161
|
+
return suffix.length >= maxChars ? value.slice(0, maxChars) : `${value.slice(0, maxChars - suffix.length)}${suffix}`;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const boundWorkspaceDiffResult = (result: TextDiffResult, maxChars: number): TextDiffResult =>
|
|
165
|
+
{
|
|
166
|
+
if (result.output.length <= maxChars)
|
|
167
|
+
{
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
return { ...result, output: boundWorkspaceValue(result.output, maxChars), truncated: true };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const workspacePathPreview = (paths: string[]): string[] => paths.map((path) => boundWorkspaceValue(path, WORKSPACE_DIFF_DISPLAY_PATH_MAX_CHARS));
|
|
174
|
+
|
|
175
|
+
export const appendBoundedWorkspaceText = (prefix: string, sections: string[], omitted: number): string =>
|
|
176
|
+
{
|
|
177
|
+
const omission = (count: number): string => `\n\n[${count} file outcome(s) omitted to keep this response within ${WORKSPACE_DIFF_TOTAL_MAX_CHARS} characters.]`;
|
|
178
|
+
let text = prefix;
|
|
179
|
+
let omittedCount = omitted;
|
|
180
|
+
for (let index = 0; index < sections.length; index += 1)
|
|
181
|
+
{
|
|
182
|
+
const candidate = `${text}\n\n${sections[index]}`;
|
|
183
|
+
if (candidate.length + omission(omittedCount + sections.length - index - 1).length > WORKSPACE_DIFF_TOTAL_MAX_CHARS)
|
|
184
|
+
{
|
|
185
|
+
omittedCount += sections.length - index;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
text = candidate;
|
|
189
|
+
}
|
|
190
|
+
return omittedCount > 0 ? `${text}${omission(omittedCount)}` : text;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const formatWorkspaceDiffResult = async (
|
|
194
|
+
format: OutputFormat,
|
|
195
|
+
textPrefix: string,
|
|
196
|
+
textSections: string[],
|
|
197
|
+
data: Record<string, unknown>,
|
|
198
|
+
warnings: string[],
|
|
199
|
+
outcomes: Array<Record<string, unknown>>,
|
|
200
|
+
preOmittedOutcomes: number,
|
|
201
|
+
workdir?: string,
|
|
202
|
+
): Promise<string> =>
|
|
203
|
+
{
|
|
204
|
+
if (format !== "json")
|
|
205
|
+
{
|
|
206
|
+
return appendBoundedWorkspaceText(textPrefix, textSections, preOmittedOutcomes);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cliVersion = boundWorkspaceValue(await getCmVersion(workdir), WORKSPACE_DIFF_DISPLAY_PATH_MAX_CHARS);
|
|
210
|
+
const wrap = (payload: Record<string, unknown>): string => `## workspaceDiff\n\n\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``;
|
|
211
|
+
const retained: Array<Record<string, unknown>> = [];
|
|
212
|
+
let omittedOutcomes = preOmittedOutcomes;
|
|
213
|
+
const payloadFor = (): Record<string, unknown> => ({
|
|
214
|
+
ok: true,
|
|
215
|
+
action: "workspaceDiff",
|
|
216
|
+
toolVersion: TOOL_VERSION,
|
|
217
|
+
cliVersion,
|
|
218
|
+
data: { ...data, outcomes: retained, omittedOutcomes },
|
|
219
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
for (let index = 0; index < outcomes.length; index += 1)
|
|
223
|
+
{
|
|
224
|
+
retained.push(outcomes[index]);
|
|
225
|
+
const remainingCount = outcomes.length - index - 1;
|
|
226
|
+
// Keep an omission field in the serialized payload before deciding to
|
|
227
|
+
// retain another result; JSON escaping can otherwise exceed the bound.
|
|
228
|
+
omittedOutcomes = preOmittedOutcomes + remainingCount;
|
|
229
|
+
if (wrap(payloadFor()).length > WORKSPACE_DIFF_TOTAL_MAX_CHARS)
|
|
230
|
+
{
|
|
231
|
+
retained.pop();
|
|
232
|
+
omittedOutcomes = preOmittedOutcomes + outcomes.length - index;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return wrap(payloadFor());
|
|
237
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RemovalReceipt } from "../domain/removal-contract";
|
|
2
|
+
export function presentRemovalReceipt(dto: RemovalReceipt): string {
|
|
3
|
+
if (dto.data.outputFormatRequested === "json") return `## resolve-delete-change-conflict\n\n\`\`\`json\n${JSON.stringify(dto, null, 2)}\n\`\`\``;
|
|
4
|
+
if (!dto.ok) return `Removal ${dto.outcome}: ${dto.error.message} Effects: ${dto.data.effect}. Do not retry automatically.`;
|
|
5
|
+
if (dto.outcome === "preflight") return `Removal command-only preview (not executed): ${JSON.stringify(dto.data.intendedArgv)}. Requested --nodisk: ${dto.data.keepOnDiskRequested}. No conflict or execution readiness analysis.`;
|
|
6
|
+
return `Remove command completed for ${dto.data.requestedOperandCount} requested operands, not a verified resolved-item count. Conflict resolution, disk preservation, pending state, workspace identity and affected scope remain unverified.`;
|
|
7
|
+
}
|