@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Type, type Static, type TSchema } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleStatusObservation, presentStatusObservation, type StatusObservationOptions, type StatusObservation, StatusOptionError, StatusXmlError } from "../operations/status";
|
|
4
|
+
import { StatusCommandError } from "../execution/status-command";
|
|
5
|
+
|
|
6
|
+
export const STATUS_OUTPUT_MAX_BYTES = 131072;
|
|
7
|
+
const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
|
|
8
|
+
const count = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
9
|
+
const baseSummary = { totalPending: count, added: count, changed: count, moved: count, deleted: count, other: count };
|
|
10
|
+
const completeness = object({
|
|
11
|
+
read: Type.Union([Type.Literal("complete"), Type.Literal("incomplete"), Type.Literal("unknown")]),
|
|
12
|
+
capture: Type.Union([Type.Literal("complete"), Type.Literal("unknown")]),
|
|
13
|
+
projection: Type.Boolean(),
|
|
14
|
+
});
|
|
15
|
+
const common = {
|
|
16
|
+
schemaVersion: Type.Literal(1), action: Type.Literal("status"),
|
|
17
|
+
provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }),
|
|
18
|
+
completeness,
|
|
19
|
+
};
|
|
20
|
+
// Reject pre-decoding Windows substitution as well as UTF-8 replacement loss.
|
|
21
|
+
const statusPathPattern = process.platform === "win32" ? "^[^\\uFFFD?]*$" : "^[^\\uFFFD]*$";
|
|
22
|
+
const item = object({
|
|
23
|
+
statusCode: Type.String({ minLength: 1, maxLength: 64 }),
|
|
24
|
+
kind: Type.Union([Type.Literal("added"), Type.Literal("changed"), Type.Literal("moved"), Type.Literal("deleted"), Type.Literal("private"), Type.Literal("other")]),
|
|
25
|
+
path: Type.String({ minLength: 1, maxLength: 4096, pattern: statusPathPattern }), isDirectory: Type.Boolean(),
|
|
26
|
+
revisionId: Type.Optional(Type.String({ minLength: 1, maxLength: 128, pattern: "^[0-9]+$" })),
|
|
27
|
+
sourcePath: Type.Optional(Type.String({ minLength: 1, maxLength: 4096, pattern: statusPathPattern })),
|
|
28
|
+
});
|
|
29
|
+
const xmlCount = Type.Integer({ minimum: 0, maximum: 20000 });
|
|
30
|
+
const xmlSummary = { totalPending: xmlCount, added: xmlCount, changed: xmlCount, moved: xmlCount, deleted: xmlCount, other: Type.Literal(0), private: xmlCount, tracked: xmlCount };
|
|
31
|
+
const xmlPathPattern = `^(?:/|[A-Za-z]:\\\\|\\\\\\\\)[^\\u0000-\\u001f${process.platform === "win32" ? "?" : ""}]*$`;
|
|
32
|
+
const xmlItem = object({
|
|
33
|
+
statusCode: Type.Union([Type.Literal("CH"), Type.Literal("AD"), Type.Literal("DE"), Type.Literal("LD"), Type.Literal("MV"), Type.Literal("PR")]),
|
|
34
|
+
kind: Type.Union([Type.Literal("added"), Type.Literal("changed"), Type.Literal("moved"), Type.Literal("deleted"), Type.Literal("private")]),
|
|
35
|
+
path: Type.String({ minLength: 1, maxLength: 4096, pattern: xmlPathPattern }), isDirectory: Type.Boolean(),
|
|
36
|
+
sourcePath: Type.Optional(Type.String({ minLength: 1, maxLength: 4096, pattern: xmlPathPattern })),
|
|
37
|
+
});
|
|
38
|
+
export const statusOutputSchema = Type.Union([
|
|
39
|
+
object({ ...common, schemaVersion: Type.Literal(2), ok: Type.Literal(true), data: object({
|
|
40
|
+
mode: Type.Literal("xml"), requestedShort: Type.Literal(false), summaryBasis: Type.Literal("parsed_records"),
|
|
41
|
+
source: object({ transport: Type.Literal("status_xml"), encoding: Type.Literal("utf-8"), pathBasis: Type.Literal("absolute"), scope: Type.Literal("workspace"), baseRevisionAvailability: Type.Literal("unavailable") }),
|
|
42
|
+
items: Type.Array(xmlItem, { maxItems: 500 }),
|
|
43
|
+
itemCount: object({ parsed: xmlCount, returned: Type.Integer({ minimum: 0, maximum: 500 }), omitted: xmlCount, excluded: Type.Literal(0), overCap: xmlCount }),
|
|
44
|
+
summary: object(xmlSummary),
|
|
45
|
+
parse: object({ records: xmlCount, unsupportedRecords: Type.Literal(0), malformedRecords: Type.Literal(0) }),
|
|
46
|
+
}) }),
|
|
47
|
+
object({ ...common, schemaVersion: Type.Literal(2), ok: Type.Literal(false), error: object({
|
|
48
|
+
code: Type.Union([Type.Literal("command_failed"), Type.Literal("aborted"), Type.Literal("capture_incomplete"), Type.Literal("invalid_producer_data"), Type.Literal("output_overflow"), Type.Literal("unsupported_source"), Type.Literal("malformed_output")]),
|
|
49
|
+
message: Type.String({ maxLength: 256 }),
|
|
50
|
+
}) }),
|
|
51
|
+
object({ ...common, ok: Type.Literal(true), data: object({
|
|
52
|
+
mode: Type.Literal("machine"), requestedShort: Type.Boolean(),
|
|
53
|
+
summaryBasis: Type.Literal("parsed_records"),
|
|
54
|
+
items: Type.Array(item, { maxItems: 500 }),
|
|
55
|
+
itemCount: object({ parsed: count, returned: count, omitted: count, excluded: count, overCap: count }),
|
|
56
|
+
summary: object({ ...baseSummary, private: count, tracked: count }),
|
|
57
|
+
parse: object({ valid: count, blank: count, header: count, unsupported: count, malformed: count, ambiguousLegacyMove: count }),
|
|
58
|
+
}) }),
|
|
59
|
+
object({ ...common, ok: Type.Literal(true), data: object({
|
|
60
|
+
mode: Type.Literal("standard"), requestedShort: Type.Boolean(),
|
|
61
|
+
summaryBasis: Type.Literal("short_output_classification"), summary: object(baseSummary),
|
|
62
|
+
mergeHints: object({ basis: Type.Literal("text_hints"), hasPendingMergeLinks: Type.Boolean(), hasMergeInProgress: Type.Boolean(), pendingMergeLinkCount: count, mergeInProgressHintCount: count }),
|
|
63
|
+
}) }),
|
|
64
|
+
object({ ...common, ok: Type.Literal(false), error: object({
|
|
65
|
+
code: Type.Union([Type.Literal("command_failed"), Type.Literal("aborted"), Type.Literal("capture_incomplete"), Type.Literal("invalid_producer_data"), Type.Literal("output_overflow")]),
|
|
66
|
+
message: Type.String({ maxLength: 256 }),
|
|
67
|
+
}) }),
|
|
68
|
+
]);
|
|
69
|
+
export type StatusOutput = Static<typeof statusOutputSchema>;
|
|
70
|
+
const provenance = { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" } as const;
|
|
71
|
+
const envelope = { schemaVersion: 1, action: "status", provenance } as const;
|
|
72
|
+
type StatusErrorCode = Extract<StatusOutput, { ok: false; schemaVersion: 1 }>["error"]["code"];
|
|
73
|
+
const errorOutput = (code: StatusErrorCode, message: string, schemaVersion: 1 | 2 = 1): StatusOutput => ({ ...envelope, schemaVersion, ok: false, completeness: { read: "incomplete", capture: "unknown", projection: false }, error: { code, message } });
|
|
74
|
+
|
|
75
|
+
export function validateStatusOutput(value: unknown, schemaVersion: 1 | 2 = 1): StatusOutput {
|
|
76
|
+
if (!Check(statusOutputSchema, value)) return errorOutput("invalid_producer_data", "Status producer data did not match the output contract.", schemaVersion);
|
|
77
|
+
const checked = value as StatusOutput;
|
|
78
|
+
if (checked.ok && checked.data.mode === "xml") {
|
|
79
|
+
const data = checked.data;
|
|
80
|
+
const { parsed, returned, omitted, excluded, overCap } = data.itemCount;
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
const kinds = { CH: "changed", AD: "added", DE: "deleted", LD: "deleted", MV: "moved", PR: "private" };
|
|
83
|
+
const invalidRows = data.items.some(row => {
|
|
84
|
+
const unsafe = (path: string) => /[\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(path);
|
|
85
|
+
const invalid = unsafe(row.path) || (row.sourcePath !== undefined && unsafe(row.sourcePath)) || row.kind !== kinds[row.statusCode] || (row.kind === "moved") !== Boolean(row.sourcePath) || row.sourcePath === row.path || seen.has(row.path);
|
|
86
|
+
seen.add(row.path); return invalid;
|
|
87
|
+
});
|
|
88
|
+
if (data.summary.added + data.summary.changed + data.summary.deleted + data.summary.moved + data.summary.private + data.summary.other !== parsed || data.summary.other !== 0 || data.summary.tracked !== parsed - data.summary.private || invalidRows || returned !== data.items.length || parsed !== data.parse.records || parsed !== data.summary.totalPending || omitted !== parsed - returned || excluded !== 0 || overCap !== omitted || checked.completeness.read !== "complete" || checked.completeness.capture !== "complete" || checked.completeness.projection !== (returned === parsed)) return errorOutput("invalid_producer_data", "Status producer data did not match the output contract.", 2);
|
|
89
|
+
}
|
|
90
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > STATUS_OUTPUT_MAX_BYTES) return errorOutput("output_overflow", "Status output exceeded the compact UTF-8 byte limit.", schemaVersion);
|
|
91
|
+
return value as StatusOutput;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function projectStatusOutput(observation: StatusObservation, maxItems = 100): StatusOutput {
|
|
95
|
+
if (observation.kind === "xml") {
|
|
96
|
+
const items = observation.items.slice(0, maxItems);
|
|
97
|
+
const parsed = observation.items.length;
|
|
98
|
+
const summary = observation.summary;
|
|
99
|
+
return validateStatusOutput({ ...envelope, schemaVersion: 2, ok: true,
|
|
100
|
+
completeness: { read: "complete", capture: "complete", projection: items.length === parsed },
|
|
101
|
+
data: { mode: "xml", requestedShort: false, summaryBasis: "parsed_records",
|
|
102
|
+
source: { transport: "status_xml", encoding: "utf-8", pathBasis: "absolute", scope: "workspace", baseRevisionAvailability: "unavailable" },
|
|
103
|
+
items, itemCount: { parsed, returned: items.length, omitted: parsed - items.length, excluded: 0, overCap: parsed - items.length }, summary,
|
|
104
|
+
parse: { records: parsed, unsupportedRecords: 0, malformedRecords: 0 } },
|
|
105
|
+
}, 2);
|
|
106
|
+
}
|
|
107
|
+
if (observation.kind === "machine") {
|
|
108
|
+
const { diagnostics: parse, pendingItems } = observation;
|
|
109
|
+
const eligible = pendingItems.map(row => ({
|
|
110
|
+
statusCode: row.statusCode, kind: row.kind, path: row.workspacePath, isDirectory: row.isDirectory,
|
|
111
|
+
...(row.revisionId !== undefined ? { revisionId: row.revisionId } : {}),
|
|
112
|
+
...(row.sourceWorkspacePath !== undefined ? { sourcePath: row.sourceWorkspacePath } : {}),
|
|
113
|
+
})).filter(row => Check(item, row));
|
|
114
|
+
const items = eligible.slice(0, maxItems);
|
|
115
|
+
const excluded = pendingItems.length - eligible.length;
|
|
116
|
+
const summary = { totalPending: pendingItems.length, added: 0, changed: 0, moved: 0, deleted: 0, private: 0, other: 0, tracked: 0 };
|
|
117
|
+
for (const row of pendingItems) summary[row.kind]++;
|
|
118
|
+
summary.tracked = summary.totalPending - summary.private;
|
|
119
|
+
const parseComplete = parse.unsupported + parse.malformed + parse.ambiguousLegacyMove === 0;
|
|
120
|
+
return validateStatusOutput({ ...envelope, ok: true,
|
|
121
|
+
completeness: { read: !parseComplete ? "incomplete" : observation.capture === "complete" ? "complete" : "unknown", capture: observation.capture, projection: items.length === pendingItems.length },
|
|
122
|
+
data: { mode: "machine", requestedShort: observation.requestedShort, summaryBasis: "parsed_records", items, itemCount: { parsed: pendingItems.length, returned: items.length, omitted: pendingItems.length - items.length, excluded, overCap: eligible.length - items.length }, summary, parse },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return validateStatusOutput({ ...envelope, ok: true,
|
|
126
|
+
completeness: { read: "unknown", capture: observation.capture, projection: true },
|
|
127
|
+
data: { mode: "standard", requestedShort: observation.usedShortFlag, summaryBasis: "short_output_classification", summary: observation.summary,
|
|
128
|
+
mergeHints: { basis: "text_hints", hasPendingMergeLinks: observation.mergeState.hasPendingMergeLinks, hasMergeInProgress: observation.mergeState.hasMergeInProgress, pendingMergeLinkCount: observation.mergeState.pendingMergeLinks.length, mergeInProgressHintCount: observation.mergeState.mergeInProgressHints.length },
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function executeStatusOutput(args: StatusObservationOptions & { format?: "text" | "json"; maxItems?: number; includeRaw?: boolean }) {
|
|
134
|
+
let dto: StatusOutput;
|
|
135
|
+
let rawResult: string | undefined;
|
|
136
|
+
try {
|
|
137
|
+
const observation = await assembleStatusObservation(args);
|
|
138
|
+
dto = projectStatusOutput(observation, args.maxItems);
|
|
139
|
+
if (dto.ok) rawResult = await presentStatusObservation(observation, args);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const version = args.source === "xml" ? 2 : 1;
|
|
142
|
+
dto = validateStatusOutput(error instanceof StatusXmlError ? ({ ...envelope, schemaVersion: 2, ok: false, completeness: { read: "incomplete", capture: "complete", projection: false }, error: { code: error.code, message: error.message } }) : error instanceof StatusCommandError ? errorOutput(error.code, error.message, version) : error instanceof StatusOptionError ? errorOutput("command_failed", error.message, version) : errorOutput("invalid_producer_data", "Status producer failed to produce valid data.", version), version);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: "text" as const, text: dto.ok ? dto.data.mode !== "standard" ? `Status: ${dto.data.itemCount.parsed} parsed records; ${dto.data.itemCount.returned} returned. Read: ${dto.completeness.read}.` : `Status: ${dto.data.summary.totalPending} classified short-output lines. Read: unknown.` : dto.error.message }],
|
|
146
|
+
structuredContent: dto, isError: !dto.ok,
|
|
147
|
+
details: { exportName: "status", rawResult, ...(args.workdir && (dto.ok || args.source !== "xml") ? { workdir: args.workdir } : {}) },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Type, type TProperties } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleSwitchReceipt, presentSwitchReceipt, safeSwitchValue, parseSwitchTarget, compareSwitchTarget, type SwitchReceipt, type SwitchStep, type SwitchSummary, type SwitchBranchObservation } from "../operations/switch-receipt";
|
|
4
|
+
const object = <P extends TProperties>(p:P) => Type.Object(p,{additionalProperties:false});
|
|
5
|
+
const enumeration = <const V extends readonly string[]>(v:V) => Type.Unsafe<V[number]>({anyOf:v.map(value=>({type:"string",const:value}))});
|
|
6
|
+
const str = (max=4096) => Type.String({maxLength:max,minLength:1});
|
|
7
|
+
const uint = (max=Number.MAX_SAFE_INTEGER) => Type.Integer({minimum:0,maximum:max});
|
|
8
|
+
const nullable = (s:ReturnType<typeof Type.Object>|ReturnType<typeof Type.String>) => Type.Union([s,Type.Null()]);
|
|
9
|
+
const attempt = object({state:enumeration(["not-attempted","not-started","started","unknown"]),terminal:enumeration(["not-observed","observed"]),exitCode:Type.Union([uint(2147483647),Type.Null()]),aborted:Type.Boolean(),timedOut:Type.Boolean()});
|
|
10
|
+
const capture = object({stdoutBytes:uint(),stderrBytes:uint(),stdoutRetainedBytes:uint(1048576),stderrRetainedBytes:uint(65536),truncated:Type.Boolean(),complete:Type.Boolean(),validUtf8:Type.Boolean()});
|
|
11
|
+
const branch = object({branch:str(),repository:str(),server:str(),basis:Type.Literal("standard-status"),scope:Type.Literal("loaded-workspace")});
|
|
12
|
+
const summary = object({totalPending:uint(19999),tracked:uint(19999),private:uint(19999),added:uint(19999),changed:uint(19999),moved:uint(19999),deleted:uint(19999),other:uint(19999)});
|
|
13
|
+
const names=["branch-before","pending-before","shelve","pending-recovery","switch","branch-after"] as const;
|
|
14
|
+
const step=object({name:enumeration(names),argv:Type.Array(str(8192),{minItems:1,maxItems:4}),attempt,capture,evidence:enumeration(["unadmitted","admitted","terminal-only","eligible-no-changes"]),effect:enumeration(["read-observed","not-attempted","command-completed","uncertain"]),branch:nullable(branch),summary:nullable(summary),records:uint(20001)});
|
|
15
|
+
const mutations=Type.Array(enumeration(["shelve","switch"]),{maxItems:2});
|
|
16
|
+
const data=object({requestedTarget:nullable(str()),workdir:nullable(str()),pendingPolicy:enumeration(["shelve","bring","cancel"]),defaultedPolicy:Type.Boolean(),preflight:Type.Boolean(),strategy:enumeration(["unresolved","already-on-target-branch","silent-noinput","direct-switch-private-only","blocked-bring-tracked-pending","cancel-with-pending","shelve-then-switch-noinput"]),wouldRun:Type.Boolean(),plannedMutations:mutations,unattemptedMutations:mutations,steps:Type.Array(step,{maxItems:6}),branchBefore:nullable(branch),branchAfter:nullable(branch),pendingBefore:nullable(summary),pendingRecovery:nullable(summary),observedShelveset:Type.Null(),usedNoChangesShelveRecovery:Type.Boolean(),targetVerification:enumeration(["unverified","name-only","exact-qualifier-spelling"]),serverAliasEquivalence:Type.Literal("unverified"),filePreservation:Type.Literal("unverified"),xlinkEffects:Type.Literal("unverified"),effect:enumeration(["not-attempted","command-completed","uncertain"])});
|
|
17
|
+
const base={schemaVersion:Type.Literal(1),action:Type.Literal("switch-branch"),provenance:object({source:Type.Literal("plastic"),producer:Type.Literal("@aefree/pi-plastic"),contentTrust:Type.Literal("external")}),sourceAdmission:Type.Literal("windows-cm11.0.16.10371-observed"),completeness:object({read:enumeration(["complete","incomplete","unknown"]),capture:enumeration(["complete","incomplete","unknown"]),projection:Type.Literal(true)}),data};
|
|
18
|
+
const error=object({code:enumeration(["invalid_request","unsupported_source","observation_failed","policy_canceled","policy_blocked","mutation_failed","target_unverified","producer_failed"]),stage:enumeration(["input",...names,"policy","producer"]),message:str(256)});
|
|
19
|
+
export const switchOutputSchema=Type.Unsafe<SwitchReceipt>(Type.Union([object({...base,ok:Type.Literal(true),outcome:enumeration(["preflight","already-loaded","switched"])}),object({...base,ok:Type.Literal(false),outcome:enumeration(["canceled","blocked","failed","uncertain","command-completed-unverified"]),error})]));
|
|
20
|
+
const same=(a:unknown,b:unknown)=>JSON.stringify(a)===JSON.stringify(b);
|
|
21
|
+
const validSummary=(s:SwitchSummary)=>s.totalPending===s.tracked+s.private&&s.totalPending===s.added+s.changed+s.moved+s.deleted+s.other+s.private;
|
|
22
|
+
const validBranch=(b:SwitchBranchObservation)=>safeSwitchValue(`${b.branch}@${b.repository}@${b.server}`)&&!!parseSwitchTarget(`${b.branch}@${b.repository}@${b.server}`);
|
|
23
|
+
const normal=(s:SwitchStep)=>s.capture.complete&&s.attempt.state==="started"&&s.attempt.terminal==="observed"&&!s.attempt.aborted&&!s.attempt.timedOut;
|
|
24
|
+
export function validateSwitchOutput(value:unknown):value is SwitchReceipt {
|
|
25
|
+
if(!Check(switchOutputSchema,value)||Buffer.byteLength(JSON.stringify(value),"utf8")>131072)return false;
|
|
26
|
+
const dto=value as SwitchReceipt,d=dto.data;
|
|
27
|
+
if(!dto.ok&&!safeSwitchValue(dto.error.message))return false;
|
|
28
|
+
if(d.requestedTarget!==null&&!parseSwitchTarget(d.requestedTarget)||d.workdir!==null&&!safeSwitchValue(d.workdir))return false;
|
|
29
|
+
if(d.steps.length&&(!d.requestedTarget||!d.workdir)||d.defaultedPolicy&&d.pendingPolicy!=="cancel")return false;
|
|
30
|
+
let prior=-1;
|
|
31
|
+
for(const s of d.steps){
|
|
32
|
+
const i=names.indexOf(s.name);if(i<=prior)return false;prior=i;
|
|
33
|
+
const mut=s.name==="shelve"||s.name==="switch",pending=s.name==="pending-before"||s.name==="pending-recovery",a=s.attempt,c=s.capture;
|
|
34
|
+
const expected=s.name==="shelve"?["shelveset","create","--all",`-c=Auto-shelve before switch to ${d.requestedTarget}`]:s.name==="switch"?["switch","--silent","--noinput",d.requestedTarget]:pending?["status","--machinereadable","--includeRevId","--fieldseparator=\x1f"]:["status"];
|
|
35
|
+
if(!same(s.argv,expected)||c.stdoutRetainedBytes>(pending?1048576:65536)||c.stderrRetainedBytes>(pending?65536:16384)||c.stdoutRetainedBytes>c.stdoutBytes||c.stderrRetainedBytes>c.stderrBytes)return false;
|
|
36
|
+
if(a.exitCode!==null&&(a.terminal!=="observed"||a.state!=="started"&&a.state!=="unknown")||a.state==="not-attempted"&&(a.terminal!=="not-observed"||a.timedOut)||a.state==="not-started"&&a.exitCode!==null)return false;
|
|
37
|
+
if(c.complete&&(!c.validUtf8||c.truncated||c.stdoutRetainedBytes!==c.stdoutBytes||c.stderrRetainedBytes!==c.stderrBytes||a.state!=="started"||a.terminal!=="observed"||a.exitCode===null||a.aborted||a.timedOut))return false;
|
|
38
|
+
if(s.branch&&!validBranch(s.branch)||s.summary&&!validSummary(s.summary))return false;
|
|
39
|
+
if(s.evidence==="admitted") { if(mut||!normal(s)||a.exitCode!==0||c.stderrBytes||s.effect!=="read-observed"||(pending?!s.summary||!!s.branch||s.records!==s.summary.totalPending+1:!s.branch||!!s.summary||s.records!==1))return false; }
|
|
40
|
+
else if(s.branch||s.summary||s.effect==="read-observed")return false;
|
|
41
|
+
if(mut){
|
|
42
|
+
if(s.records||!d.plannedMutations.includes(s.name as "shelve"|"switch"))return false;
|
|
43
|
+
const completed=normal(s)&&a.exitCode===0&&!c.stderrBytes;
|
|
44
|
+
if(s.effect!== (completed?"command-completed":["not-attempted","not-started"].includes(a.state)?"not-attempted":"uncertain"))return false;
|
|
45
|
+
if(s.evidence==="terminal-only"&&!completed||completed&&s.evidence!=="terminal-only")return false;
|
|
46
|
+
if(s.evidence==="eligible-no-changes"&&(s.name!=="shelve"||!normal(s)||a.exitCode!==1||c.stdoutBytes!==0||c.stderrBytes===0))return false;
|
|
47
|
+
}else if(!["admitted","unadmitted"].includes(s.evidence)||s.evidence==="unadmitted"&&s.effect!=="not-attempted")return false;
|
|
48
|
+
}
|
|
49
|
+
if(d.steps.length&&d.steps[0].name!=="branch-before"||d.steps.length>1&&(d.steps[0].evidence!=="admitted"||d.steps[1].name!=="pending-before"))return false;
|
|
50
|
+
const find=(name:SwitchStep["name"])=>d.steps.find(s=>s.name===name);
|
|
51
|
+
if(!same(d.branchBefore,find("branch-before")?.branch??null)||!same(d.branchAfter,find("branch-after")?.branch??null)||!same(d.pendingBefore,find("pending-before")?.summary??null)||!same(d.pendingRecovery,find("pending-recovery")?.summary??null))return false;
|
|
52
|
+
if(find("branch-after")&&!find("switch")||find("branch-after")&&find("switch")?.effect!=="command-completed")return false;
|
|
53
|
+
if(find("shelve")&&d.strategy!=="shelve-then-switch-noinput"||find("pending-recovery")&&find("shelve")?.evidence!=="eligible-no-changes")return false;
|
|
54
|
+
if(find("switch")&&find("shelve")&&find("shelve")?.effect!=="command-completed"&&(!d.usedNoChangesShelveRecovery||!d.pendingRecovery||d.pendingRecovery.tracked!==0))return false;
|
|
55
|
+
if(d.usedNoChangesShelveRecovery!==!!(find("pending-recovery")?.evidence==="admitted"&&d.pendingRecovery?.tracked===0))return false;
|
|
56
|
+
const ms=d.steps.filter(s=>s.name==="shelve"||s.name==="switch");
|
|
57
|
+
const effect=ms.some(s=>s.effect==="uncertain")?"uncertain":ms.some(s=>s.effect==="command-completed")?"command-completed":"not-attempted";
|
|
58
|
+
if(d.effect!==effect||!same(d.unattemptedMutations,d.plannedMutations.filter(n=>!ms.some(s=>s.name===n&&s.attempt.state!=="not-attempted"))))return false;
|
|
59
|
+
if(d.wouldRun!==!!d.plannedMutations.length||!same(d.plannedMutations,d.strategy==="shelve-then-switch-noinput"?["shelve","switch"]:["silent-noinput","direct-switch-private-only"].includes(d.strategy)?["switch"]:[]))return false;
|
|
60
|
+
if(d.pendingBefore){
|
|
61
|
+
const p=d.pendingBefore,match=!!d.branchBefore&&compareSwitchTarget(d.branchBefore,d.requestedTarget!)!=="unverified";
|
|
62
|
+
const expected=match?"already-on-target-branch":!p.totalPending?"silent-noinput":p.private>0&&p.tracked===0&&(d.pendingPolicy!=="cancel"||d.defaultedPolicy)?"direct-switch-private-only":d.pendingPolicy==="bring"&&p.tracked>0?"blocked-bring-tracked-pending":d.pendingPolicy==="cancel"?"cancel-with-pending":"shelve-then-switch-noinput";
|
|
63
|
+
if(d.strategy!==expected)return false;
|
|
64
|
+
}else if(d.strategy!=="unresolved")return false;
|
|
65
|
+
const observation=d.strategy==="already-on-target-branch"?d.branchBefore:d.branchAfter;
|
|
66
|
+
if(d.targetVerification!==(observation&&d.requestedTarget?compareSwitchTarget(observation,d.requestedTarget):"unverified"))return false;
|
|
67
|
+
if(dto.outcome==="preflight"&&(!d.preflight||!d.pendingBefore||ms.length)||dto.outcome==="already-loaded"&&(d.preflight||d.strategy!=="already-on-target-branch"||ms.length||d.targetVerification==="unverified"))return false;
|
|
68
|
+
if(dto.outcome==="switched"&&(d.preflight||!d.branchAfter||find("switch")?.effect!=="command-completed"||d.targetVerification==="unverified"||d.usedNoChangesShelveRecovery||d.effect!=="command-completed"))return false;
|
|
69
|
+
if(dto.outcome==="canceled"&&(d.preflight||d.strategy!=="cancel-with-pending"||ms.length)||dto.outcome==="blocked"&&(d.preflight||d.strategy!=="blocked-bring-tracked-pending"||ms.length))return false;
|
|
70
|
+
if(dto.outcome==="command-completed-unverified"&&(find("switch")?.effect!=="command-completed"||d.targetVerification!=="unverified"))return false;
|
|
71
|
+
if(dto.ok&&dto.outcome!=="preflight"&&d.preflight)return false;
|
|
72
|
+
const reads=d.steps.filter(s=>s.name!=="shelve"&&s.name!=="switch");
|
|
73
|
+
if(dto.completeness.capture!==(d.steps.length?d.steps.every(s=>s.capture.complete)?"complete":"incomplete":"unknown")||dto.completeness.read!==(reads.length?reads.every(s=>s.evidence==="admitted")?"complete":"incomplete":"unknown"))return false;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
export async function executeSwitchOutput(args:unknown){
|
|
77
|
+
const dto=await assembleSwitchReceipt(args);
|
|
78
|
+
if(!validateSwitchOutput(dto))throw Error("Bounded switch receipt could not be validated. Earlier effects may exist; do not retry automatically.");
|
|
79
|
+
return {content:[{type:"text" as const,text:presentSwitchReceipt(dto,(args as SwitchReceipt["data"] & {format?:string})?.format==="json"?"json":"text")}],details:{},structuredContent:dto,isError:!dto.ok};
|
|
80
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { status } from "../operations/status";
|
|
2
|
+
import { update, add, undo, resolveDeleteChangeConflict, workspaceList } from "../operations/workspace";
|
|
3
|
+
import { checkin } from "../operations/checkin";
|
|
4
|
+
import { diff } from "../operations/diff";
|
|
5
|
+
import { patch } from "../operations/patch";
|
|
6
|
+
import { branchCreate, currentBranch, branchList, branchExists, branchDelete } from "../operations/branches";
|
|
7
|
+
import { switchBranch } from "../operations/switch";
|
|
8
|
+
import { merge, finalizeMerge } from "../operations/merge";
|
|
9
|
+
import { mergeBranches } from "../operations/server-merge";
|
|
10
|
+
import { mergeToBranch } from "../operations/closeout";
|
|
11
|
+
import { shelvesetCreate, shelvesetApply, shelvesetDelete, shelvesetList } from "../operations/shelvesets";
|
|
12
|
+
import { codeReviewCreate, codeReviewUpdate, codeReviewDelete, codeReviewFind } from "../operations/reviews";
|
|
13
|
+
import { PLASTIC_TOOL_NAMES } from "../plastic-tool-loading";
|
|
14
|
+
|
|
15
|
+
type CoreTool = {
|
|
16
|
+
description?: string;
|
|
17
|
+
args?: Record<string, unknown>;
|
|
18
|
+
execute: (args: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const PLASTIC_TOOL_REGISTRY = {
|
|
22
|
+
status,
|
|
23
|
+
update,
|
|
24
|
+
add,
|
|
25
|
+
checkin,
|
|
26
|
+
undo,
|
|
27
|
+
resolveDeleteChangeConflict,
|
|
28
|
+
diff,
|
|
29
|
+
patch,
|
|
30
|
+
branchCreate,
|
|
31
|
+
switchBranch,
|
|
32
|
+
merge,
|
|
33
|
+
mergeBranches,
|
|
34
|
+
mergeToBranch,
|
|
35
|
+
finalizeMerge,
|
|
36
|
+
currentBranch,
|
|
37
|
+
branchList,
|
|
38
|
+
branchExists,
|
|
39
|
+
branchDelete,
|
|
40
|
+
shelvesetCreate,
|
|
41
|
+
shelvesetApply,
|
|
42
|
+
shelvesetDelete,
|
|
43
|
+
shelvesetList,
|
|
44
|
+
codeReviewCreate,
|
|
45
|
+
codeReviewUpdate,
|
|
46
|
+
codeReviewDelete,
|
|
47
|
+
codeReviewFind,
|
|
48
|
+
workspaceList,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type PlasticExportName = keyof typeof PLASTIC_TOOL_REGISTRY;
|
|
52
|
+
|
|
53
|
+
export function toToolName(exportName: string): string {
|
|
54
|
+
return `plastic_${exportName}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Preserve registration order while verifying the loading vocabulary has no drift. */
|
|
58
|
+
export function getRegisteredPlasticExportNames(): PlasticExportName[] {
|
|
59
|
+
const names = Object.keys(PLASTIC_TOOL_REGISTRY) as PlasticExportName[];
|
|
60
|
+
if (names.length !== PLASTIC_TOOL_NAMES.size || names.some((name) => !PLASTIC_TOOL_NAMES.has(toToolName(name)))) {
|
|
61
|
+
throw new Error("Plastic tool registry does not match the loading catalogue.");
|
|
62
|
+
}
|
|
63
|
+
return names;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getCoreTool(exportName: PlasticExportName): CoreTool {
|
|
67
|
+
const candidate = PLASTIC_TOOL_REGISTRY[exportName] as unknown as CoreTool;
|
|
68
|
+
if (!candidate || typeof candidate.execute !== "function") {
|
|
69
|
+
throw new Error(`Missing Plastic core tool export '${exportName}'.`);
|
|
70
|
+
}
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleUndoReceipt, emptyUndoData, undoFailure, presentUndoReceipt, safeUndoText, UNDO_COMMAND, validUndoOperands, type UndoReceipt } from "../operations/undo-receipt";
|
|
4
|
+
const object = (p: Record<string, any>) => Type.Object(p, { additionalProperties: false });
|
|
5
|
+
const enumeration = (values: readonly string[]) => Type.Union(values.map(value => Type.Literal(value)));
|
|
6
|
+
const uint = (max = Number.MAX_SAFE_INTEGER) => Type.Integer({ minimum: 0, maximum: max });
|
|
7
|
+
const attempt = object({ state: enumeration(["not-attempted", "not-started", "started", "unknown"]), terminal: enumeration(["not-observed", "observed"]), exitCode: Type.Union([uint(2147483647), Type.Null()]), aborted: Type.Boolean(), timedOut: Type.Boolean() });
|
|
8
|
+
const capture = object({ stdoutBytes: uint(), stderrBytes: uint(), stdoutRetainedBytes: uint(65536), stderrRetainedBytes: uint(16384), truncated: Type.Boolean(), complete: Type.Boolean(), validUtf8: Type.Boolean() });
|
|
9
|
+
const data = { intendedArgv: Type.Array(Type.String({ minLength: 1, maxLength: 4096 }), { minItems: 1, maxItems: 257 }), requestedOperandCount: Type.Union([uint(256), Type.Null()]), workingDirectory: Type.Union([Type.String({ minLength: 1, maxLength: 4096 }), Type.Null()]), attempt, capture: Type.Union([capture, Type.Null()]), observedWorkspaceIdentity: Type.Null(), observedUndoneItems: Type.Null(), verification: Type.Literal("unverified") };
|
|
10
|
+
const header = { schemaVersion: Type.Literal(1), action: Type.Literal("undo"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }), completeness: object({ capture: enumeration(["complete", "incomplete", "unknown"]), projection: Type.Literal(true) }) };
|
|
11
|
+
const error = object({ code: enumeration(["invalid_request", "launch_failed", "aborted", "uncertain", "producer_failed"]), message: Type.String({ minLength: 1, maxLength: 256 }) });
|
|
12
|
+
export const undoOutputSchema = Type.Unsafe<UndoReceipt>(Type.Union([
|
|
13
|
+
object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("command-completed"), data: object({ ...data, effect: Type.Literal("not-proven") }) }),
|
|
14
|
+
object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("failed"), data: object({ ...data, effect: Type.Literal("not-attempted") }), error }),
|
|
15
|
+
object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("uncertain"), data: object({ ...data, effect: Type.Literal("uncertain") }), error }),
|
|
16
|
+
]));
|
|
17
|
+
export function validateUndoOutput(value: unknown): value is UndoReceipt {
|
|
18
|
+
if (!Check(undoOutputSchema, value) || Buffer.byteLength(JSON.stringify(value), "utf8") > 131072) return false;
|
|
19
|
+
const dto = value as UndoReceipt, d = dto.data, a = d.attempt, c = d.capture;
|
|
20
|
+
if (d.intendedArgv[0] !== UNDO_COMMAND) return false;
|
|
21
|
+
if (d.requestedOperandCount === null) {
|
|
22
|
+
if (d.intendedArgv.length !== 1 || d.workingDirectory !== null || c || !["not-attempted", "unknown"].includes(a.state)) return false;
|
|
23
|
+
} else if (!validUndoOperands(d.intendedArgv.slice(1)) || d.requestedOperandCount !== d.intendedArgv.length - 1 || !d.workingDirectory) return false;
|
|
24
|
+
if (d.workingDirectory !== null && !safeUndoText(d.workingDirectory)) return false;
|
|
25
|
+
if (!dto.ok && !safeUndoText(dto.error.message)) return false;
|
|
26
|
+
if (a.exitCode !== null && a.terminal !== "observed" || a.state === "not-attempted" && (a.terminal !== "not-observed" || a.exitCode !== null || a.timedOut) || a.state === "not-started" && (a.terminal !== "not-observed" || a.exitCode !== null)) return false;
|
|
27
|
+
if (c) {
|
|
28
|
+
if (!d.workingDirectory || c.stdoutRetainedBytes > c.stdoutBytes || c.stderrRetainedBytes > c.stderrBytes) return false;
|
|
29
|
+
if (c.complete && (!c.validUtf8 || c.truncated || c.stdoutBytes !== c.stdoutRetainedBytes || c.stderrBytes !== c.stderrRetainedBytes || a.state !== "started" || a.terminal !== "observed" || a.exitCode === null || a.aborted || a.timedOut)) return false;
|
|
30
|
+
if (a.state === "not-attempted" && (!a.aborted || c.stdoutBytes || c.stderrBytes || c.validUtf8 || c.complete)) return false;
|
|
31
|
+
}
|
|
32
|
+
if (dto.completeness.capture !== (c ? c.complete ? "complete" : "incomplete" : "unknown")) return false;
|
|
33
|
+
if (dto.ok && (d.requestedOperandCount === null || !d.workingDirectory || !c?.complete || a.state !== "started" || a.terminal !== "observed" || a.exitCode !== 0 || a.aborted || a.timedOut)) return false;
|
|
34
|
+
if (!dto.ok && dto.outcome === "uncertain" && !["started", "unknown"].includes(a.state)) return false;
|
|
35
|
+
if (!dto.ok && dto.outcome === "failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
|
|
36
|
+
if (!dto.ok && dto.error.code === "invalid_request" && (d.workingDirectory !== null || c || a.state !== "not-attempted" || a.aborted)) return false;
|
|
37
|
+
if (!dto.ok && dto.error.code === "aborted" && !a.aborted) return false;
|
|
38
|
+
if (!dto.ok && dto.error.code === "launch_failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
export async function executeUndoOutput(args: unknown) {
|
|
42
|
+
let dto = await assembleUndoReceipt(args);
|
|
43
|
+
const valid: boolean = validateUndoOutput(dto);
|
|
44
|
+
if (!valid) {
|
|
45
|
+
const possibleStart = !["not-attempted", "not-started"].includes(dto.data.attempt.state);
|
|
46
|
+
const data = emptyUndoData();
|
|
47
|
+
if (possibleStart) { data.attempt.state = "unknown"; data.effect = "uncertain"; }
|
|
48
|
+
dto = undoFailure(data, "producer_failed", "Undo receipt validation failed; do not retry automatically.");
|
|
49
|
+
if (!validateUndoOutput(dto)) throw new Error("Safe undo receipt could not be constructed.");
|
|
50
|
+
}
|
|
51
|
+
return { content: [{ type: "text" as const, text: presentUndoReceipt(dto) }], details: {}, structuredContent: dto, isError: !dto.ok };
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleUpdateReceipt, emptyUpdateData, updateFailure, presentUpdateReceipt, safeUpdateText, UPDATE_ARGV, type UpdateReceipt } from "../operations/update-receipt";
|
|
4
|
+
const object = (p: Record<string, any>) => Type.Object(p, { additionalProperties: false });
|
|
5
|
+
const enumeration = (values: readonly string[]) => Type.Union(values.map(value => Type.Literal(value)));
|
|
6
|
+
const uint = (max = Number.MAX_SAFE_INTEGER) => Type.Integer({ minimum: 0, maximum: max });
|
|
7
|
+
const attempt = object({ state: enumeration(["not-attempted", "not-started", "started", "unknown"]), terminal: enumeration(["not-observed", "observed"]), exitCode: Type.Union([uint(2147483647), Type.Null()]), aborted: Type.Boolean(), timedOut: Type.Boolean() });
|
|
8
|
+
const capture = object({ stdoutBytes: uint(), stderrBytes: uint(), stdoutRetainedBytes: uint(65536), stderrRetainedBytes: uint(16384), truncated: Type.Boolean(), complete: Type.Boolean(), validUtf8: Type.Boolean() });
|
|
9
|
+
const data = { intendedArgv: Type.Tuple(UPDATE_ARGV.map(value => Type.Literal(value))), workingDirectory: Type.Union([Type.String({ minLength: 1, maxLength: 4096 }), Type.Null()]), attempt, capture: Type.Union([capture, Type.Null()]), observedWorkspaceIdentity: Type.Null(), observedChanges: Type.Null(), verification: Type.Literal("unverified") };
|
|
10
|
+
const header = { schemaVersion: Type.Literal(1), action: Type.Literal("update"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }), completeness: object({ capture: enumeration(["complete", "incomplete", "unknown"]), projection: Type.Literal(true) }) };
|
|
11
|
+
const error = object({ code: enumeration(["invalid_request", "launch_failed", "aborted", "uncertain", "producer_failed"]), message: Type.String({ minLength: 1, maxLength: 256 }) });
|
|
12
|
+
export const updateOutputSchema = Type.Unsafe<UpdateReceipt>(Type.Union([
|
|
13
|
+
object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("command-completed"), data: object({ ...data, effect: Type.Literal("not-proven") }) }),
|
|
14
|
+
object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("failed"), data: object({ ...data, effect: Type.Literal("not-attempted") }), error }),
|
|
15
|
+
object({ ...header, ok: Type.Literal(false), outcome: Type.Literal("uncertain"), data: object({ ...data, effect: Type.Literal("uncertain") }), error }),
|
|
16
|
+
]));
|
|
17
|
+
export function validateUpdateOutput(value: unknown): value is UpdateReceipt {
|
|
18
|
+
if (!Check(updateOutputSchema, value) || Buffer.byteLength(JSON.stringify(value), "utf8") > 131072) return false;
|
|
19
|
+
const dto = value as UpdateReceipt, d = dto.data, a = d.attempt, c = d.capture;
|
|
20
|
+
if (d.workingDirectory !== null && !safeUpdateText(d.workingDirectory)) return false;
|
|
21
|
+
if (!dto.ok && !safeUpdateText(dto.error.message)) return false;
|
|
22
|
+
if (a.exitCode !== null && a.terminal !== "observed" || a.state === "not-attempted" && (a.terminal !== "not-observed" || a.exitCode !== null || a.timedOut) || a.state === "not-started" && (a.terminal !== "not-observed" || a.exitCode !== null)) return false;
|
|
23
|
+
if (c) {
|
|
24
|
+
if (!d.workingDirectory || c.stdoutRetainedBytes > c.stdoutBytes || c.stderrRetainedBytes > c.stderrBytes) return false;
|
|
25
|
+
if (c.complete && (!c.validUtf8 || c.truncated || c.stdoutBytes !== c.stdoutRetainedBytes || c.stderrBytes !== c.stderrRetainedBytes || a.state !== "started" || a.terminal !== "observed" || a.exitCode === null || a.aborted || a.timedOut)) return false;
|
|
26
|
+
if (a.state === "not-attempted" && (!a.aborted || c.stdoutBytes || c.stderrBytes || c.validUtf8 || c.complete)) return false;
|
|
27
|
+
}
|
|
28
|
+
if (dto.completeness.capture !== (c ? c.complete ? "complete" : "incomplete" : "unknown")) return false;
|
|
29
|
+
if (dto.ok && (!d.workingDirectory || !c?.complete || a.state !== "started" || a.terminal !== "observed" || a.exitCode !== 0 || a.aborted || a.timedOut)) return false;
|
|
30
|
+
if (!dto.ok && dto.outcome === "uncertain" && !["started", "unknown"].includes(a.state)) return false;
|
|
31
|
+
if (!dto.ok && dto.outcome === "failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
|
|
32
|
+
if (!dto.ok && dto.error.code === "invalid_request" && (d.workingDirectory !== null || c || a.state !== "not-attempted" || a.aborted)) return false;
|
|
33
|
+
if (!dto.ok && dto.error.code === "aborted" && !a.aborted) return false;
|
|
34
|
+
if (!dto.ok && dto.error.code === "launch_failed" && !["not-attempted", "not-started"].includes(a.state)) return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export async function executeUpdateOutput(args: unknown) {
|
|
38
|
+
let dto = await assembleUpdateReceipt(args);
|
|
39
|
+
const valid: boolean = validateUpdateOutput(dto);
|
|
40
|
+
if (!valid) {
|
|
41
|
+
const possibleStart = !["not-attempted", "not-started"].includes(dto.data.attempt.state);
|
|
42
|
+
const data = emptyUpdateData();
|
|
43
|
+
if (possibleStart) { data.attempt.state = "unknown"; data.effect = "uncertain"; }
|
|
44
|
+
dto = updateFailure(data, "producer_failed", "Update receipt validation failed; do not retry automatically.");
|
|
45
|
+
if (!validateUpdateOutput(dto)) throw new Error("Safe update receipt could not be constructed.");
|
|
46
|
+
}
|
|
47
|
+
return { content: [{ type: "text" as const, text: presentUpdateReceipt(dto) }], details: {}, structuredContent: dto, isError: !dto.ok };
|
|
48
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Type, type Static, type TSchema } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleWorkspaceListObservation, presentWorkspaceListObservation, workspaceListPayload, WorkspaceListError, validWorkspaceName, validWorkspacePath, validWorkspaceGuid, workspaceCapability, WORKSPACE_LIST_MAX_BYTES, type WorkspaceListArgs } from "../operations/workspace-list";
|
|
4
|
+
import { WorkspaceListCommandError } from "../execution/workspace-list-command";
|
|
5
|
+
import { getActiveAbortSignal } from "../execution/context";
|
|
6
|
+
const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
|
|
7
|
+
const integer = (maximum: number) => Type.Integer({ minimum: 0, maximum });
|
|
8
|
+
const identity = Type.String({ minLength: 1, maxLength: 4096, pattern: "^[\\x20-\\x3e\\x40-\\x7e]+$" });
|
|
9
|
+
const common = { schemaVersion: Type.Literal(1), action: Type.Literal("workspace-list"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }) };
|
|
10
|
+
const dataCommon = { scope: Type.Literal("local_client_inventory"), limits: object({ maxItems: Type.Integer({ minimum: 1, maximum: 500 }), identityCodeUnits: Type.Literal(4096), compactUtf8Bytes: Type.Literal(131072) }) };
|
|
11
|
+
const capability = object({ identities: Type.Literal(workspaceCapability.identities), prerequisite: Type.Literal(workspaceCapability.prerequisite), originalVerification: Type.Literal(workspaceCapability.originalVerification), formatterLoss: Type.Literal(workspaceCapability.formatterLoss) });
|
|
12
|
+
const errorCode = Type.Union([Type.Literal("command_failed"), Type.Literal("aborted"), Type.Literal("capture_incomplete"), Type.Literal("malformed_output"), Type.Literal("invalid_identity"), Type.Literal("unsupported_query"), Type.Literal("invalid_producer_data"), Type.Literal("output_overflow")]);
|
|
13
|
+
export const workspaceListOutputSchema = Type.Union([
|
|
14
|
+
object({ ...common, ok: Type.Literal(true), completeness: object({ read: Type.Literal("complete_under_ascii_prerequisite"), capture: Type.Literal("complete"), projection: Type.Boolean() }), data: object({ ...dataCommon, mode: Type.Literal("fields"), basis: Type.Literal("workspace_list_tab_fields"), capability, pathBasis: Type.Literal("producer_absolute"), diagnostics: object({ duplicateRecords: integer(19999) }), rows: Type.Array(object({ name: identity, path: identity, guid: Type.String({ pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" }) }), { maxItems: 500 }), countBasis: Type.Literal("observed_response_rows"), counts: object({ observed: integer(20000), returned: integer(500), omitted: integer(20000), excluded: Type.Literal(0) }) }) }),
|
|
15
|
+
object({ ...common, ok: Type.Literal(true), completeness: object({ read: Type.Literal("unknown"), capture: Type.Literal("unknown"), projection: Type.Literal(false) }), data: object({ ...dataCommon, mode: Type.Literal("native"), basis: Type.Literal("workspace_list_native"), capability: Type.Literal("noncanonical"), pathBasis: Type.Literal("unavailable"), rows: Type.Null(), counts: Type.Null(), countBasis: Type.Literal("unavailable") }) }),
|
|
16
|
+
object({ ...common, ok: Type.Literal(false), completeness: object({ read: Type.Literal("incomplete"), capture: Type.Literal("unknown"), projection: Type.Literal(false) }), error: object({ code: errorCode, message: Type.String({ minLength: 1, maxLength: 256 }) }) }),
|
|
17
|
+
]);
|
|
18
|
+
export type WorkspaceListOutput = Static<typeof workspaceListOutputSchema>;
|
|
19
|
+
export type WorkspaceListErrorCode = Extract<WorkspaceListOutput, { ok: false }>["error"]["code"];
|
|
20
|
+
const failure = (code: WorkspaceListErrorCode): WorkspaceListOutput => ({ schemaVersion: 1, action: "workspace-list", provenance: { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" }, ok: false, completeness: { read: "incomplete", capture: "unknown", projection: false }, error: { code, message: `Workspace list read failed (${code}); no reusable rows are available.` } });
|
|
21
|
+
export function validateWorkspaceListOutput(value: unknown): WorkspaceListOutput {
|
|
22
|
+
if (!Check(workspaceListOutputSchema, value)) return failure("invalid_producer_data");
|
|
23
|
+
const dto = value as WorkspaceListOutput;
|
|
24
|
+
if (dto.ok && dto.data.mode === "fields") {
|
|
25
|
+
const { rows, counts, limits, diagnostics } = dto.data;
|
|
26
|
+
const guids = new Map<string, string>(), names = new Map<string, string>(), paths = new Map<string, string>();
|
|
27
|
+
let returnedDuplicates = 0, conflict = false;
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const value = JSON.stringify(row);
|
|
30
|
+
const prior = [guids.get(row.guid.toLowerCase()), names.get(row.name), paths.get(row.path)].filter(value => value !== undefined);
|
|
31
|
+
if (prior.some(previous => previous !== value)) conflict = true;
|
|
32
|
+
if (prior.length) returnedDuplicates++;
|
|
33
|
+
guids.set(row.guid.toLowerCase(), value); names.set(row.name, value); paths.set(row.path, value);
|
|
34
|
+
}
|
|
35
|
+
if (rows.some(row => !validWorkspaceName(row.name) || !validWorkspacePath(row.path) || !validWorkspaceGuid(row.guid))
|
|
36
|
+
|| conflict || diagnostics.duplicateRecords < returnedDuplicates || diagnostics.duplicateRecords > returnedDuplicates + counts.omitted || diagnostics.duplicateRecords > Math.max(0, counts.observed - 1)
|
|
37
|
+
|| counts.returned !== rows.length || counts.observed !== counts.returned + counts.omitted || counts.returned !== Math.min(counts.observed, limits.maxItems) || dto.completeness.projection !== (counts.omitted === 0)) return failure("invalid_producer_data");
|
|
38
|
+
}
|
|
39
|
+
if (Buffer.byteLength(JSON.stringify(dto), "utf8") > WORKSPACE_LIST_MAX_BYTES) return failure("output_overflow");
|
|
40
|
+
return dto;
|
|
41
|
+
}
|
|
42
|
+
export async function executeWorkspaceListOutput(args: WorkspaceListArgs) {
|
|
43
|
+
let dto: WorkspaceListOutput, rawResult: string | undefined;
|
|
44
|
+
try {
|
|
45
|
+
const observation = await assembleWorkspaceListObservation(args, true);
|
|
46
|
+
if (getActiveAbortSignal()?.aborted) throw new WorkspaceListCommandError("aborted");
|
|
47
|
+
dto = validateWorkspaceListOutput(workspaceListPayload(observation));
|
|
48
|
+
if (dto.ok) rawResult = await presentWorkspaceListObservation(observation, args);
|
|
49
|
+
if (getActiveAbortSignal()?.aborted) throw new WorkspaceListCommandError("aborted");
|
|
50
|
+
} catch (error) { dto = failure(error instanceof WorkspaceListError || error instanceof WorkspaceListCommandError ? error.code : "invalid_producer_data"); }
|
|
51
|
+
dto = validateWorkspaceListOutput(dto);
|
|
52
|
+
return { content: [{ type: "text" as const, text: dto.ok ? rawResult! : dto.error.message }], structuredContent: dto, isError: !dto.ok, details: { exportName: "workspaceList", rawResult: dto.ok ? rawResult : undefined, ...(args.workdir ? { workdir: args.workdir } : {}) } };
|
|
53
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleWorkspaceMergeReceipt, emptyWorkspaceMergeData, workspaceMergeFailure, presentWorkspaceMergeReceipt, buildWorkspaceMergeArgv, safeMergeText, type MergeAction, type WorkspaceMergeReceipt } from "../operations/workspace-merge-receipt";
|
|
4
|
+
const obj=(p:Record<string,any>)=>Type.Object(p,{additionalProperties:false}),enm=(v:string[])=>Type.Union(v.map(x=>Type.Literal(x))),nil=(s:any)=>Type.Union([s,Type.Null()]),uint=(max=Number.MAX_SAFE_INTEGER)=>Type.Integer({minimum:0,maximum:max});
|
|
5
|
+
const attempt=obj({state:enm(["not-attempted","not-started","started","unknown"]),terminal:enm(["not-observed","observed"]),exitCode:nil(uint(2147483647)),aborted:Type.Boolean(),timedOut:Type.Boolean()});
|
|
6
|
+
const capture=obj({stdoutBytes:uint(),stderrBytes:uint(),stdoutRetainedBytes:uint(65536),stderrRetainedBytes:uint(16384),truncated:Type.Boolean(),complete:Type.Boolean(),validUtf8:Type.Boolean()});
|
|
7
|
+
const stage=obj({attempt,capture:nil(capture),admission:enm(["not-observed","unsupported","admitted"])});
|
|
8
|
+
const text=Type.String({minLength:1,maxLength:4096});
|
|
9
|
+
const data=obj({intendedArgv:Type.Array(text,{minItems:1,maxItems:13}),workingDirectory:nil(text),requestedSource:nil(text),strategy:nil(enm(["auto","source","destination"])),cherrypicking:nil(Type.Boolean()),forced:nil(Type.Boolean()),previewRequested:nil(Type.Boolean()),outputFormatRequested:nil(enm(["text","json"])),apply:stage,shortStatus:stage,fullStatus:stage,pendingReadMode:enm(["short","copied-machine"]),commandCompleted:Type.Boolean(),observedWorkspaceIdentity:Type.Null(),observedSourceIdentity:Type.Null(),observedFinalizedMetadata:Type.Null(),observedPreservation:Type.Null(),observedPendingItems:Type.Null(),fileConflictCount:nil(uint(256)),protocol:enm(["not-observed","already-connected","file-conflict","apply-add","apply-add-copied","unsupported"]),checkinReadiness:enm(["unknown","blocked","supported-no-unresolved-signals"]),verification:Type.Literal("unverified"),effect:enm(["not-attempted","not-proven","uncertain"])});
|
|
10
|
+
export const workspaceMergeOutputSchema=Type.Unsafe<WorkspaceMergeReceipt>(Type.Union([
|
|
11
|
+
obj({schemaVersion:Type.Literal(1),action:enm(["merge","finalize-merge"]),provenance:obj({source:Type.Literal("plastic"),producer:Type.Literal("@aefree/pi-plastic"),contentTrust:Type.Literal("external")}),completeness:obj({capture:enm(["complete","incomplete","unknown"]),projection:Type.Literal(true)}),data,ok:Type.Literal(true),outcome:enm(["preflight","command-completed"])}),
|
|
12
|
+
obj({schemaVersion:Type.Literal(1),action:enm(["merge","finalize-merge"]),provenance:obj({source:Type.Literal("plastic"),producer:Type.Literal("@aefree/pi-plastic"),contentTrust:Type.Literal("external")}),completeness:obj({capture:enm(["complete","incomplete","unknown"]),projection:Type.Literal(true)}),data,ok:Type.Literal(false),outcome:enm(["failed","uncertain","blocked"]),error:obj({code:enm(["invalid_request","launch_failed","aborted","uncertain","unsupported_readiness","conflict","producer_failed"]),message:Type.String({minLength:1,maxLength:256})})})
|
|
13
|
+
]));
|
|
14
|
+
export function validateWorkspaceMergeOutput(v:unknown):v is WorkspaceMergeReceipt{
|
|
15
|
+
if(!Check(workspaceMergeOutputSchema,v)||Buffer.byteLength(JSON.stringify(v),"utf8")>131072)return false;
|
|
16
|
+
const dto=v as WorkspaceMergeReceipt,d=dto.data,stages=[d.apply,d.shortStatus,d.fullStatus];
|
|
17
|
+
if(d.requestedSource===null){if(d.intendedArgv.length!==1||d.intendedArgv[0]!=="merge"||[d.workingDirectory,d.strategy,d.cherrypicking,d.forced,d.previewRequested,d.outputFormatRequested].some(x=>x!==null))return false;
|
|
18
|
+
if(d.commandCompleted||d.protocol!=="not-observed"||d.fileConflictCount!==null||d.checkinReadiness!=="unknown"||d.apply.capture!==null||d.apply.attempt.terminal!=="not-observed"||d.apply.attempt.exitCode!==null||d.apply.attempt.aborted||d.apply.attempt.timedOut||!["not-attempted","unknown"].includes(d.apply.attempt.state)||d.apply.admission!==(d.apply.attempt.state==="unknown"?"unsupported":"not-observed")||stages.slice(1).some(s=>s.admission!=="not-observed"))return false;}
|
|
19
|
+
else{
|
|
20
|
+
if(!safeMergeText(d.requestedSource)||d.requestedSource.trimStart().startsWith("-")||!safeMergeText(d.workingDirectory)||d.strategy===null||typeof d.cherrypicking!=="boolean"||typeof d.forced!=="boolean"||typeof d.previewRequested!=="boolean"||d.outputFormatRequested===null)return false;
|
|
21
|
+
const match=/^--startlineseparator=(__WM_[a-f0-9]{24}__)S__$/.exec(d.intendedArgv[5]??"");if(!match||JSON.stringify(d.intendedArgv)!==JSON.stringify(buildWorkspaceMergeArgv(d.requestedSource,d.strategy,d.cherrypicking,d.forced,match[1])))return false;
|
|
22
|
+
if(dto.action==="finalize-merge"&&(d.strategy==="auto"||d.cherrypicking||d.forced))return false;
|
|
23
|
+
}
|
|
24
|
+
for(const s of stages){const a=s.attempt,c=s.capture;
|
|
25
|
+
if(a.exitCode!==null&&a.terminal!=="observed"||["not-attempted","not-started"].includes(a.state)&&(a.terminal!=="not-observed"||a.exitCode!==null)||a.state==="not-attempted"&&a.timedOut)return false;
|
|
26
|
+
if(c){if(c.stdoutRetainedBytes>c.stdoutBytes||c.stderrRetainedBytes>c.stderrBytes)return false;if(c.complete&&(!c.validUtf8||c.truncated||c.stdoutBytes!==c.stdoutRetainedBytes||c.stderrBytes!==c.stderrRetainedBytes||a.state!=="started"||a.terminal!=="observed"||a.exitCode===null||a.aborted||a.timedOut))return false;}
|
|
27
|
+
if(s.admission==="admitted"&&(!c?.complete||a.state!=="started"))return false;
|
|
28
|
+
if(s.admission==="not-observed"&&(c||a.state!=="not-attempted"||a.aborted||a.timedOut))return false;
|
|
29
|
+
}
|
|
30
|
+
const captured=stages.filter(s=>s.capture);if(dto.completeness.capture!==(!captured.length?"unknown":captured.every(s=>s.capture!.complete)?"complete":"incomplete"))return false;
|
|
31
|
+
if(d.commandCompleted!==Boolean(d.apply.capture?.complete&&d.apply.attempt.state==="started"&&d.apply.attempt.exitCode===0))return false;
|
|
32
|
+
if(d.shortStatus.admission==="admitted"&&((d.pendingReadMode==="short"?d.shortStatus.capture?.stdoutBytes!==0:!d.shortStatus.capture?.stdoutBytes)||d.shortStatus.capture?.stderrBytes!==0)||d.fullStatus.admission==="admitted"&&(!d.fullStatus.capture?.stdoutBytes||d.fullStatus.capture?.stderrBytes!==0))return false;
|
|
33
|
+
if(d.pendingReadMode==="copied-machine"&&d.protocol!=="apply-add-copied"||d.protocol==="apply-add-copied"&&d.pendingReadMode!=="copied-machine")return false;
|
|
34
|
+
const later=stages.slice(1).some(s=>s.admission!=="not-observed");if(later&&(!d.commandCompleted||!["already-connected","apply-add","apply-add-copied"].includes(d.protocol)))return false;
|
|
35
|
+
if(d.fullStatus.admission!=="not-observed"&&(!d.shortStatus.capture?.complete||d.shortStatus.attempt.exitCode!==0))return false;
|
|
36
|
+
if(d.protocol==="not-observed"&&(d.apply.admission==="admitted"||d.fileConflictCount!==null)||d.protocol==="unsupported"&&(d.fileConflictCount!==null||d.apply.admission!=="unsupported"))return false;
|
|
37
|
+
if(["already-connected","apply-add","apply-add-copied","file-conflict"].includes(d.protocol)&&d.apply.admission!=="admitted")return false;
|
|
38
|
+
if(d.protocol==="file-conflict"?(d.fileConflictCount===null||d.fileConflictCount===0||d.checkinReadiness!=="blocked"):d.checkinReadiness==="blocked")return false;
|
|
39
|
+
if(["already-connected","apply-add","apply-add-copied"].includes(d.protocol)&&d.fileConflictCount!==0)return false;
|
|
40
|
+
if(d.checkinReadiness==="supported-no-unresolved-signals"&&(!d.commandCompleted||!stages.every(s=>s.admission==="admitted"&&s.capture?.complete&&s.attempt.exitCode===0&&s.capture.stderrBytes===0)))return false;
|
|
41
|
+
if(d.previewRequested===true&&(!dto.ok||dto.outcome!=="preflight"||stages.some(s=>s.admission!=="not-observed")||d.effect!=="not-attempted"||d.commandCompleted||d.checkinReadiness!=="unknown"))return false;
|
|
42
|
+
if(dto.ok){if(dto.outcome==="preflight"&&d.previewRequested!==true)return false;if(dto.outcome==="command-completed"&&(d.previewRequested!==false||d.effect!=="not-proven"||d.checkinReadiness!=="supported-no-unresolved-signals"))return false;}
|
|
43
|
+
else{if(!safeMergeText(dto.error.message))return false;if(dto.outcome==="failed"&&(d.effect!=="not-attempted"||!["not-attempted","not-started"].includes(d.apply.attempt.state)))return false;if(dto.outcome!=="failed"&&(d.effect!=="uncertain"||!["started","unknown"].includes(d.apply.attempt.state)))return false;if(dto.outcome==="blocked"&&d.checkinReadiness!=="blocked"||dto.outcome==="uncertain"&&d.checkinReadiness!=="unknown")return false;if(dto.error.code==="invalid_request"&&(d.requestedSource!==null||stages.some(s=>s.admission!=="not-observed")))return false;}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export async function executeWorkspaceMergeOutput(action:MergeAction,args:unknown){let dto=await assembleWorkspaceMergeReceipt(action,args);const valid:boolean=validateWorkspaceMergeOutput(dto);if(!valid){const possible=["started","unknown"].includes(dto.data.apply.attempt.state),d=emptyWorkspaceMergeData();if(possible){d.apply.attempt.state="unknown";d.apply.admission="unsupported";d.effect="uncertain";}dto=workspaceMergeFailure(action,d,"producer_failed","Workspace merge receipt validation failed; effects remain possible, no automatic retry.");if(!validateWorkspaceMergeOutput(dto))throw new Error("Safe workspace merge receipt unavailable.");}return {content:[{type:"text" as const,text:presentWorkspaceMergeReceipt(dto)}],details:{},structuredContent:dto,isError:!dto.ok};}
|