@aefree/pi-plastic 0.7.5 → 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 +51 -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 +12 -6
- 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,68 @@
|
|
|
1
|
+
import { Type, type TProperties } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleCheckinReceipt, presentCheckinReceipt, checkinSafeValue, parseCheckinChangeset, type CheckinReceipt } from "../operations/checkin-receipt";
|
|
4
|
+
const object = <P extends TProperties>(p: P) => Type.Object(p, { additionalProperties: false });
|
|
5
|
+
const enumOf = <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 });
|
|
7
|
+
const uint = (max = Number.MAX_SAFE_INTEGER) => Type.Integer({ minimum: 0, maximum: max });
|
|
8
|
+
const nullable = <T extends ReturnType<typeof object> | ReturnType<typeof Type.Array>>(s: T) => Type.Union([s, Type.Null()]);
|
|
9
|
+
const cs = object({ id: Type.String({ pattern: "^(0|[1-9][0-9]{0,19})$" }), branch: str(), repository: str(), server: str(), mount: Type.Literal("/") });
|
|
10
|
+
const summary = object({ totalPending: uint(499), tracked: uint(499), private: uint(499), added: uint(499), changed: uint(499), moved: uint(499), deleted: uint(499), other: uint(499) });
|
|
11
|
+
const attempt = object({ state: enumOf(["not-attempted", "not-started", "started", "unknown"]), terminal: enumOf(["not-observed", "observed"]), exitCode: Type.Union([uint(2147483647), Type.Null()]), aborted: Type.Boolean(), timedOut: Type.Boolean() });
|
|
12
|
+
const capture = object({ stdoutBytes: uint(), stderrBytes: uint(), stdoutRetainedBytes: uint(65536), stderrRetainedBytes: uint(16384), truncated: Type.Boolean(), complete: Type.Boolean(), validUtf8: Type.Boolean() });
|
|
13
|
+
const step = object({ name: enumOf(["pending-before", "initial-checkin", "private-add", "private-retry", "pending-recovery", "fallback-checkin", "pending-after", "merge-diagnostic"]), operation: enumOf(["status", "checkin", "add"]), attempt, capture, effect: enumOf(["not-attempted", "uncertain", "command-completed", "changeset-created", "read-observed"]), evidence: enumOf(["unadmitted", "admitted"]), records: uint(501), summary: nullable(summary), changeset: nullable(cs) });
|
|
14
|
+
const paths = Type.Array(str(), { maxItems: 100 });
|
|
15
|
+
const reasons = Type.Array(object({ path: str(), reason: str(256) }), { maxItems: 100 });
|
|
16
|
+
const data = { sourceAdmission: Type.Literal("windows-cm11.0.16.10371-observed"), requestedPaths: paths, includedPaths: paths, fallbackPaths: paths, excludedPaths: reasons, privateAddPaths: paths, blockedPrivatePaths: reasons, itemEvents: Type.Array(Type.Union([object({ operation: enumOf(["CO", "AD", "DE", "CP"]), path: str() }), object({ operation: Type.Literal("MV"), sourcePath: str(), path: str() })]), { maxItems: 100 }), observedChangesets: Type.Array(cs, { maxItems: 100 }), command: nullable(Type.Array(str(8192), { maxItems: 120 })), steps: Type.Array(step, { maxItems: 8 }), autoEnabledApplyChanged: Type.Boolean(), usedFallbackRetry: Type.Boolean(), usedPrivateAutoAddRecovery: Type.Boolean(), pendingBefore: nullable(summary), pendingAfter: nullable(summary), wouldRun: Type.Boolean(), omittedReferences: uint(10000), scopeExhaustion: Type.Literal("unverified"), branchHead: Type.Literal("unverified"), serverAliasEquivalence: Type.Literal("unverified"), xlinkEffects: Type.Literal("unverified"), mergeLinkIdentity: Type.Literal("unverified") };
|
|
17
|
+
const header = { schemaVersion: Type.Literal(1), action: Type.Literal("checkin"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }), completeness: object({ capture: enumOf(["complete", "incomplete", "unknown"]), projection: Type.Boolean() }) };
|
|
18
|
+
const error = object({ code: str(64), stage: enumOf(["input", "pending", "checkin", "producer"]), message: str(512) });
|
|
19
|
+
export const checkinOutputSchema = Type.Unsafe<CheckinReceipt>(Type.Union([
|
|
20
|
+
object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("completed"), data: object({ ...data, effect: Type.Literal("changeset-created"), createdChangeset: cs }) }),
|
|
21
|
+
object({ ...header, ok: Type.Literal(true), outcome: Type.Literal("preflight"), data: object({ ...data, effect: Type.Literal("not-attempted"), createdChangeset: Type.Null() }) }),
|
|
22
|
+
object({ ...header, ok: Type.Literal(false), outcome: enumOf(["failed", "unsupported", "uncertain"]), error, data: object({ ...data, effect: enumOf(["not-attempted", "uncertain", "command-completed", "changeset-created"]), createdChangeset: nullable(cs) }) }),
|
|
23
|
+
]));
|
|
24
|
+
export function validateCheckinOutput(value: unknown): value is CheckinReceipt {
|
|
25
|
+
if (!Check(checkinOutputSchema, value) || Buffer.byteLength(JSON.stringify(value), "utf8") > 131072) return false;
|
|
26
|
+
const d = (value as CheckinReceipt).data, dto = value as CheckinReceipt;
|
|
27
|
+
const lists = [d.requestedPaths, d.includedPaths, d.fallbackPaths, d.excludedPaths, d.privateAddPaths, d.blockedPrivatePaths, d.itemEvents, d.observedChangesets, d.command ?? []];
|
|
28
|
+
if (lists.reduce((n, a) => n + a.length, 0) > 100 || dto.completeness.projection !== (d.omittedReferences === 0)) return false;
|
|
29
|
+
if (![...d.requestedPaths, ...d.includedPaths, ...d.fallbackPaths, ...d.privateAddPaths, ...d.excludedPaths.map(p => p.path), ...d.blockedPrivatePaths.map(p => p.path)].every(checkinSafeValue)) return false;
|
|
30
|
+
const validCs = (c: NonNullable<typeof d.createdChangeset>) => !!parseCheckinChangeset(`cs:${c.id}@br:${c.branch}@${c.repository}@${c.server} (mount:'/')`);
|
|
31
|
+
if (!d.observedChangesets.every(validCs) || d.createdChangeset && !validCs(d.createdChangeset)) return false;
|
|
32
|
+
for (const e of d.itemEvents) if (!checkinSafeValue(e.path) || e.operation === "MV" && !checkinSafeValue(e.sourcePath)) return false;
|
|
33
|
+
if (d.command && (d.command.reduce((n, a) => n + a.length, 0) > 32768 || Buffer.byteLength(JSON.stringify(d.command), "utf8") > 65536 || d.command[0] !== "cm" || d.command[1] !== "checkin")) return false;
|
|
34
|
+
const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
|
|
35
|
+
const summaryValid = (s: NonNullable<typeof d.pendingBefore>) => s.totalPending === s.tracked + s.private && s.totalPending === s.added + s.changed + s.moved + s.deleted + s.private + s.other;
|
|
36
|
+
if (d.pendingBefore && !summaryValid(d.pendingBefore) || d.pendingAfter && !summaryValid(d.pendingAfter)) return false;
|
|
37
|
+
const normal = (s: typeof d.steps[number]) => s.attempt.state === "started" && s.attempt.terminal === "observed" && s.attempt.exitCode !== null && !s.attempt.aborted && !s.attempt.timedOut && s.capture.complete;
|
|
38
|
+
for (const s of d.steps) {
|
|
39
|
+
const a = s.attempt, c = s.capture;
|
|
40
|
+
if (a.exitCode !== null && a.terminal !== "observed" || c.stdoutRetainedBytes > c.stdoutBytes || c.stderrRetainedBytes > c.stderrBytes || c.complete && (!c.validUtf8 || c.truncated || a.terminal !== "observed" || a.aborted || a.timedOut)) return false;
|
|
41
|
+
if (s.summary && (!summaryValid(s.summary) || s.operation !== "status" || s.effect !== "read-observed")) return false;
|
|
42
|
+
if (s.operation === "status" && !["not-attempted", "read-observed"].includes(s.effect)) return false;
|
|
43
|
+
if (s.operation !== "status" && s.summary) return false;
|
|
44
|
+
if (s.effect === "read-observed" && (!normal(s) || a.exitCode !== 0 || c.stderrBytes || s.evidence !== "admitted" || !s.summary)) return false;
|
|
45
|
+
if (s.effect === "command-completed" && (s.operation !== "add" || !normal(s) || a.exitCode !== 0)) return false;
|
|
46
|
+
if (s.effect === "changeset-created" && (s.operation !== "checkin" || !normal(s) || a.exitCode !== 0 || c.stderrBytes || s.evidence !== "admitted" || !s.changeset || !validCs(s.changeset))) return false;
|
|
47
|
+
if (s.changeset && s.effect !== "changeset-created") return false;
|
|
48
|
+
if (s.operation !== "status" && s.effect === "not-attempted" && !["not-attempted", "not-started"].includes(a.state)) return false;
|
|
49
|
+
}
|
|
50
|
+
const mutations = d.steps.filter(s => s.operation !== "status"), created = mutations.filter(s => s.effect === "changeset-created");
|
|
51
|
+
const expected = created.length ? "changeset-created" : mutations.some(s => s.effect === "uncertain") ? "uncertain" : mutations.some(s => s.effect === "command-completed") ? "command-completed" : "not-attempted";
|
|
52
|
+
if (d.effect !== expected || created.length > 1 || !!d.createdChangeset !== !!created.length || d.createdChangeset && !same(d.createdChangeset, created[0].changeset)) return false;
|
|
53
|
+
if (dto.outcome === "completed" && (!d.createdChangeset || !d.pendingBefore || !d.wouldRun)) return false;
|
|
54
|
+
if (dto.outcome === "preflight" && (mutations.length || !d.pendingBefore || !d.wouldRun || d.steps.length !== 1)) return false;
|
|
55
|
+
if ((dto.outcome === "failed" || dto.outcome === "unsupported") && mutations.length) return false;
|
|
56
|
+
if (d.usedFallbackRetry !== d.steps.some(s => s.name === "fallback-checkin")) return false;
|
|
57
|
+
if (d.usedPrivateAutoAddRecovery !== d.steps.some(s => s.name === "private-add" && s.effect === "command-completed")) return false;
|
|
58
|
+
if (dto.completeness.capture !== (d.steps.length ? d.steps.every(s => s.capture.complete) ? "complete" : "incomplete" : "unknown")) return false;
|
|
59
|
+
if (d.pendingBefore && !same(d.pendingBefore, d.steps.find(s => s.name === "pending-before")?.summary)) return false;
|
|
60
|
+
if (d.pendingAfter && !d.steps.some(s => ["pending-recovery", "pending-after"].includes(s.name) && same(s.summary, d.pendingAfter))) return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
export async function executeCheckinOutput(args: unknown) {
|
|
64
|
+
const dto = await assembleCheckinReceipt(args);
|
|
65
|
+
if (!validateCheckinOutput(dto)) throw new Error("The bounded checkin producer receipt could not be verified.");
|
|
66
|
+
const text = presentCheckinReceipt(dto, (args as { format?: unknown })?.format === "json" ? "json" : "text");
|
|
67
|
+
return { content: [{ type: "text" as const, text }], details: {}, structuredContent: dto, isError: !dto.ok };
|
|
68
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { switchOutputSchema, validateSwitchOutput } from "./switch-output";
|
|
5
|
+
import { updateOutputSchema, validateUpdateOutput } from "./update-output";
|
|
6
|
+
import { workspaceMergeOutputSchema, validateWorkspaceMergeOutput } from "./workspace-merge-output";
|
|
7
|
+
import { checkinOutputSchema, validateCheckinOutput } from "./checkin-output";
|
|
8
|
+
import { buildCloseoutParentArgv, assembleCloseoutReceipt, presentCloseoutReceipt, CLOSEOUT_STAGE_NAMES, CLOSEOUT_DTO_BYTES, closeoutCaptures, closeoutChildEffect, compareSwitchTarget, parseSwitchTarget, safeSwitchValue, switchPendingArgv, checkinSafeComment, checkinSafeValue, cmWhereEquals, type CloseoutReceipt, type CloseoutStage, type CloseoutRead } from "../operations/closeout-receipt";
|
|
9
|
+
const obj=(p:Record<string,any>)=>Type.Object(p,{additionalProperties:false}),enm=(v:readonly string[])=>Type.Union(v.map(x=>Type.Literal(x))),nil=(t:any)=>Type.Union([t,Type.Null()]),str=(max=4096)=>Type.String({maxLength:max}),uint=(max=Number.MAX_SAFE_INTEGER)=>Type.Integer({minimum:0,maximum:max});
|
|
10
|
+
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()});
|
|
11
|
+
const capture=obj({stdoutBytes:uint(),stderrBytes:uint(),stdoutRetainedBytes:uint(65536),stderrRetainedBytes:uint(16384),truncated:Type.Boolean(),complete:Type.Boolean(),validUtf8:Type.Boolean()});
|
|
12
|
+
const branch=obj({branch:str(),repository:str(),server:str(),basis:Type.Literal("standard-status"),scope:Type.Literal("loaded-workspace")});
|
|
13
|
+
const summary=obj(Object.fromEntries(["totalPending","tracked","private","added","changed","moved","deleted","other"].map(k=>[k,uint(20000)])));
|
|
14
|
+
const changeset=obj({id:Type.String({pattern:"^(0|[1-9][0-9]{0,19})$"}),branch:str(),repository:str(),server:str(),mount:Type.Literal("/")});
|
|
15
|
+
const read=obj({argv:Type.Array(str(16384),{minItems:1,maxItems:5}),attempt,capture:nil(capture),admission:enm(["unsupported","admitted"]),branch:nil(branch),parentPath:nil(str()),emptyParentResult:Type.Boolean(),summary:nil(summary)});
|
|
16
|
+
const stage=Type.Union([obj({name:enm(CLOSEOUT_STAGE_NAMES),kind:Type.Literal("read"),result:read}),...[["switch",switchOutputSchema],["update",updateOutputSchema],["merge",workspaceMergeOutputSchema],["checkin",checkinOutputSchema]].map(([k,s])=>obj({name:enm(CLOSEOUT_STAGE_NAMES),kind:Type.Literal(k as string),result:s}))]);
|
|
17
|
+
const requested=obj({source:nil(str()),target:nil(str()),message:nil(str()),cardRef:nil(str()),strategy:enm(["auto","source","destination"]),updateTarget:Type.Boolean(),includePrivate:Type.Boolean(),preflight:Type.Boolean(),outputFormat:enm(["text","json"]),workdir:str()});
|
|
18
|
+
const data=obj({requested:nil(requested),sourceBranch:nil(str()),targetBranch:nil(str()),targetSource:enm(["unresolved","explicit","parent-read"]),checkinMessage:nil(str(16384)),stages:Type.Array(stage,{maxItems:16}),unattemptedStages:Type.Array(enm(CLOSEOUT_STAGE_NAMES),{maxItems:14,uniqueItems:true}),unobservedChild:nil(enm(["switch","update","merge","checkin-preflight","checkin"])),createdChangeset:nil(changeset),pendingAfter:nil(summary),targetVerification:enm(["unverified","name-only","exact-qualifier-spelling"]),serverAliasEquivalence:Type.Literal("unverified"),sourceLink:Type.Literal("unverified"),branchHead:Type.Literal("unverified"),exclusiveScope:Type.Literal("unverified"),rollback:Type.Literal("not-proven"),effect:enm(["not-attempted","command-completed","uncertain","changeset-created"])});
|
|
19
|
+
const header={schemaVersion:Type.Literal(1),action:Type.Literal("merge-to-branch"),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.Boolean()}),data};
|
|
20
|
+
export const closeoutOutputSchema=Type.Unsafe<CloseoutReceipt>(Type.Union([obj({...header,ok:Type.Literal(true),outcome:enm(["preflight","completed"])}),obj({...header,ok:Type.Literal(false),outcome:enm(["blocked","failed","uncertain"]),error:obj({code:enm(["invalid_request","unsupported_source","observation_failed","target_unverified","policy_blocked","child_failed","producer_failed"]),stage:enm(["input",...CLOSEOUT_STAGE_NAMES,"producer"]),message:str(256)})})]));
|
|
21
|
+
const same=(a:unknown,b:unknown)=>JSON.stringify(a)===JSON.stringify(b);
|
|
22
|
+
const normal=(s:CloseoutRead)=>s.capture?.complete&&s.attempt.state==="started"&&s.attempt.terminal==="observed"&&s.attempt.exitCode===0&&!s.attempt.aborted&&!s.attempt.timedOut&&s.capture.stderrBytes===0;
|
|
23
|
+
function validateRead(s:CloseoutStage&{kind:"read"}):boolean {
|
|
24
|
+
const r=s.result,c=r.capture,a=r.attempt;
|
|
25
|
+
if(a.exitCode!==null&&a.terminal!=="observed")return false;
|
|
26
|
+
if(c&&(c.stdoutRetainedBytes>c.stdoutBytes||c.stderrRetainedBytes>c.stderrBytes||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;
|
|
27
|
+
if(r.admission==="admitted"&&!normal(r))return false;
|
|
28
|
+
if(r.admission==="unsupported"&&(r.branch||r.parentPath||r.summary||r.emptyParentResult))return false;
|
|
29
|
+
if(s.name.startsWith("loaded-")) {if(!same(r.argv,["status"])||r.parentPath||r.summary||r.emptyParentResult||r.admission==="admitted"&&!r.branch)return false;if(r.branch&&(!parseSwitchTarget(`${r.branch.branch}@${r.branch.repository}@${r.branch.server}`)||!safeSwitchValue(r.branch.branch)))return false;}
|
|
30
|
+
else if(s.name==="parent"){if(r.branch||r.summary||r.admission==="admitted"&&!(r.emptyParentResult?!r.parentPath:!!r.parentPath&&!!parseSwitchTarget(r.parentPath)))return false;}
|
|
31
|
+
else if(s.name==="pending-before"||s.name==="pending-final"){if(!same(r.argv,[...switchPendingArgv])||r.branch||r.parentPath||r.emptyParentResult||r.admission==="admitted"&&!r.summary)return false;if(r.summary){const p=r.summary;if(p.tracked!==p.added+p.changed+p.moved+p.deleted||p.totalPending!==p.tracked+p.private+p.other)return false;}}
|
|
32
|
+
else if(s.name==="readiness"){if(!(same(r.argv,["status"])||same(r.argv,[...switchPendingArgv]))||r.branch||r.parentPath||r.summary||r.emptyParentResult||r.admission==="admitted"&&!r.capture?.stdoutBytes)return false;}
|
|
33
|
+
else return false;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
export function validateCloseoutOutput(value:unknown):value is CloseoutReceipt {
|
|
37
|
+
if(!Check(closeoutOutputSchema,value)||Buffer.byteLength(JSON.stringify(value),"utf8")>CLOSEOUT_DTO_BYTES)return false;
|
|
38
|
+
const dto=value as CloseoutReceipt,d=dto.data,r=d.requested,stages=d.stages;
|
|
39
|
+
if(!same(d.unattemptedStages,CLOSEOUT_STAGE_NAMES.filter(n=>!stages.some(s=>s.name===n)&&d.unobservedChild!==n)))return false;
|
|
40
|
+
const caps=closeoutCaptures(stages),expectedCapture=!caps.length?"unknown":!d.unobservedChild&&stages.every(s=>s.kind!=="read"||s.result.capture!==null)&&caps.every(c=>c.complete)?"complete":"incomplete";
|
|
41
|
+
if(dto.completeness.capture!==expectedCapture||dto.completeness.projection!==stages.every(s=>s.kind==="read"||s.result.completeness.projection))return false;
|
|
42
|
+
const effects=stages.map(closeoutChildEffect),effect=d.createdChangeset?"changeset-created":d.unobservedChild||effects.includes("uncertain")?"uncertain":effects.includes("command-completed")?"command-completed":"not-attempted";
|
|
43
|
+
if(d.effect!==effect)return false;
|
|
44
|
+
if(!r)return !dto.ok&&dto.outcome==="failed"&&dto.error.code==="invalid_request"&&dto.error.stage==="input"&&!stages.length&&!d.unobservedChild&&!d.sourceBranch&&!d.targetBranch&&d.targetSource==="unresolved"&&!d.checkinMessage&&!d.createdChangeset&&!d.pendingAfter&&d.targetVerification==="unverified";
|
|
45
|
+
if(!safeSwitchValue(r.workdir)||r.source!==null&&!parseSwitchTarget(r.source)||r.target!==null&&!parseSwitchTarget(r.target)||r.message!==null&&!checkinSafeComment(r.message)||r.cardRef!==null&&!checkinSafeValue(r.cardRef)||Buffer.byteLength(JSON.stringify(r),"utf8")>32768+256)return false;
|
|
46
|
+
const parentCount=stages.filter(s=>s.name==="parent").length;if(parentCount>1||r.target!==null&&parentCount)return false;
|
|
47
|
+
const plan=["loaded-before",...(r.target===null?Array(Math.max(1,parentCount)).fill("parent"):[]),"pending-before","switch","loaded-after-switch",...(r.updateTarget?["update"]:[]),"loaded-before-merge","merge","readiness","loaded-before-checkin","checkin-preflight","checkin","loaded-final","pending-final"];
|
|
48
|
+
if(stages.some((s,i)=>s.name!==plan[i])||d.unobservedChild&&d.unobservedChild!==plan[stages.length])return false;
|
|
49
|
+
if(d.unobservedChild&&(dto.ok||dto.error.code!=="producer_failed"||dto.error.stage!==d.unobservedChild))return false;
|
|
50
|
+
const initial=stages[0]?.kind==="read"?stages[0].result.branch:null;
|
|
51
|
+
if(d.sourceBranch!==null&&(!initial||d.sourceBranch!==(r.source??initial.branch)))return false;
|
|
52
|
+
if(d.targetSource==="explicit"&&(d.targetBranch!==r.target||r.target===null)||d.targetSource==="unresolved"&&d.targetBranch!==null)return false;
|
|
53
|
+
const parent=stages.filter(s=>s.name==="parent"&&s.kind==="read").at(-1) as (CloseoutStage&{kind:"read"})|undefined;
|
|
54
|
+
if(d.targetSource==="parent-read"){if(!parent?.result.parentPath||r.target!==null||!d.sourceBranch)return false;const src=parseSwitchTarget(d.sourceBranch)!;if(d.targetBranch!==(src.repository===null?parent.result.parentPath:`${parent.result.parentPath}@${src.repository}@${src.server}`))return false;}
|
|
55
|
+
if(d.targetBranch!==null&&!parseSwitchTarget(d.targetBranch))return false;
|
|
56
|
+
if(d.checkinMessage!==null&&(!d.sourceBranch||!d.targetBranch||d.checkinMessage!==(r.message?.trim()?r.message:`Merge ${d.sourceBranch} into ${d.targetBranch}${r.cardRef?.trim()?`\n\n${r.cardRef.trim()}`:""}`)))return false;
|
|
57
|
+
let lastVerification="unverified";
|
|
58
|
+
for(const [index,s] of stages.entries()) {
|
|
59
|
+
if(s.kind==="read") {
|
|
60
|
+
if(!validateRead(s))return false;
|
|
61
|
+
if(s.name==="readiness"){const merge=stages.slice(0,index).find(x=>x.kind==="merge");if(!merge||merge.kind!=="merge"||!same(s.result.argv,merge.result.data.protocol==="apply-add-copied"?[...switchPendingArgv]:["status"]))return false;}
|
|
62
|
+
if(s.name==="parent"){if(!d.sourceBranch||!same(s.result.argv,buildCloseoutParentArgv(d.sourceBranch)))return false;const src=parseSwitchTarget(d.sourceBranch)!;if(s.result.parentPath&&(s.result.parentPath!==src.branch.slice(0,src.branch.lastIndexOf("/"))||!s.result.capture?.stdoutBytes))return false;if(s.result.emptyParentResult&&s.result.capture?.stdoutBytes!==0)return false;if(index<stages.length-1&&(s.result.emptyParentResult||!s.result.parentPath||stages[index+1].name!=="pending-before"))return false;}
|
|
63
|
+
if(index<stages.length-1&&s.result.admission!=="admitted")return false;
|
|
64
|
+
if(s.name.startsWith("loaded-")&&s.name!=="loaded-before"&&s.result.branch){if(!d.targetBranch)return false;lastVerification=compareSwitchTarget(s.result.branch,d.targetBranch);if(index<stages.length-1&&lastVerification==="unverified")return false;}
|
|
65
|
+
}else{
|
|
66
|
+
if(!d.sourceBranch||!d.targetBranch||!d.checkinMessage||!checkinSafeComment(d.checkinMessage))return false;
|
|
67
|
+
if(s.kind==="switch"){if(s.name!=="switch"||!validateSwitchOutput(s.result)||s.result.data.requestedTarget!==d.targetBranch||s.result.data.pendingPolicy!=="cancel"||s.result.data.preflight||s.result.data.workdir!==resolve(r.workdir))return false;}
|
|
68
|
+
else if(s.kind==="update"){if(s.name!=="update"||!validateUpdateOutput(s.result)||s.result.data.workingDirectory!==r.workdir)return false;}
|
|
69
|
+
else if(s.kind==="merge"){if(s.name!=="merge"||!validateWorkspaceMergeOutput(s.result)||s.result.action!=="merge"||s.result.data.requestedSource!==d.sourceBranch||s.result.data.strategy!==r.strategy||s.result.data.workingDirectory!==r.workdir||s.result.data.previewRequested!==false||s.result.data.cherrypicking||s.result.data.forced)return false;}
|
|
70
|
+
else{if(!["checkin-preflight","checkin"].includes(s.name)||!validateCheckinOutput(s.result)||s.name==="checkin-preflight"&&s.result.ok&&s.result.outcome!=="preflight"||s.name==="checkin"&&s.result.ok&&s.result.outcome!=="completed")return false;const cmd=s.result.data.command,expected=["cm","checkin",`-c=${d.checkinMessage}`,"--all",...(r.includePrivate?["--private"]:[])];if(cmd&&(!same(cmd.slice(0,expected.length),expected)||cmd[expected.length]!=="--machinereadable"))return false;}
|
|
71
|
+
if(index<stages.length-1&&!s.result.ok)return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const pending=stages.find(s=>s.name==="pending-before");if(pending&&pending.kind==="read"&&pending.result.summary&&d.targetBranch&&initial){const v=compareSwitchTarget(initial,d.targetBranch);if(!stages.some(s=>s.name.startsWith("loaded-")&&s.name!=="loaded-before"&&s.kind==="read"&&s.result.branch))lastVerification=v;if(v==="unverified"&&pending.result.summary.totalPending>0&&stages.some(s=>s.kind!=="read"))return false;}
|
|
75
|
+
if(d.targetVerification!==lastVerification)return false;
|
|
76
|
+
if(dto.ok||stages.some(s=>s.kind!=="read")){if(!initial||!d.targetBranch||!d.sourceBranch||parseSwitchTarget(d.sourceBranch)!.branch===parseSwitchTarget(d.targetBranch)!.branch)return false;for(const spec of [d.sourceBranch,d.targetBranch]){const p=parseSwitchTarget(spec)!;if(p.repository!==null&&(p.repository!==initial.repository||p.server!==initial.server))return false;}}
|
|
77
|
+
const checked=stages.find(s=>s.name==="checkin"&&s.kind==="checkin") as (CloseoutStage&{kind:"checkin"})|undefined;
|
|
78
|
+
if(!same(d.createdChangeset,checked?.result.data.createdChangeset??null))return false;
|
|
79
|
+
const final=stages.find(s=>s.name==="pending-final"&&s.kind==="read") as (CloseoutStage&{kind:"read"})|undefined;
|
|
80
|
+
if(!same(d.pendingAfter,final?.result.summary??null))return false;
|
|
81
|
+
if(r.preflight&&(stages.some(s=>s.kind!=="read"||!["loaded-before","parent","pending-before"].includes(s.name))||d.unobservedChild))return false;
|
|
82
|
+
if(dto.ok){if(dto.outcome==="preflight")return r.preflight&&!!d.sourceBranch&&!!d.targetBranch&&!!d.checkinMessage&&checkinSafeComment(d.checkinMessage)&&stages.at(-1)?.name==="pending-before"&&pending?.kind==="read"&&pending.result.admission==="admitted"&&d.effect==="not-attempted";const loaded=stages.find(s=>s.name==="loaded-final"&&s.kind==="read") as (CloseoutStage&{kind:"read"})|undefined,b=loaded?.result.branch,c=d.createdChangeset;return !r.preflight&&stages.length===plan.length&&!!c&&!!b&&c.branch===b.branch&&c.repository===b.repository&&c.server===b.server&&!!d.pendingAfter&&d.targetVerification!=="unverified"&&final?.result.admission==="admitted";}
|
|
83
|
+
if(!safeSwitchValue(dto.error.message))return false;
|
|
84
|
+
if(dto.outcome==="uncertain"?d.effect==="not-attempted":d.effect!=="not-attempted")return false;
|
|
85
|
+
return dto.error.stage==="input"&&!stages.length||dto.error.stage===stages.at(-1)?.name||dto.error.stage===d.unobservedChild;
|
|
86
|
+
}
|
|
87
|
+
export async function executeCloseoutOutput(args:unknown){const dto=await assembleCloseoutReceipt(args);if(!validateCloseoutOutput(dto))throw Error("Closeout receipt could not be validated; prior effects remain possible. No automatic retry.");return {content:[{type:"text" as const,text:presentCloseoutReceipt(dto)}],details:{},structuredContent:dto,isError:!dto.ok};}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Type, type Static, type TSchema } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleCodeReviewFindObservation, presentCodeReviewFindObservation, codeReviewFindPayload, CodeReviewFindError, validCodeReviewId, validCodeReviewQueryString, CODE_REVIEW_FIND_MAX_BYTES, type CodeReviewFindArgs } from "../operations/code-review-find";
|
|
4
|
+
import { CodeReviewFindCommandError } from "../execution/code-review-find-command";
|
|
5
|
+
import { getActiveAbortSignal } from "../execution/context";
|
|
6
|
+
const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
|
|
7
|
+
const nullable = <T extends TSchema>(schema: T) => Type.Union([schema, Type.Null()]);
|
|
8
|
+
const integer = (maximum: number) => Type.Integer({ minimum: 0, maximum });
|
|
9
|
+
const queryString = nullable(Type.String({ maxLength: 4096, pattern: "^[^\\u0000-\\u001f\\u007f-\\u009f]*$" }));
|
|
10
|
+
const query = object({ status: queryString, assignee: queryString, owner: queryString, target: queryString, titleLike: queryString, targetType: nullable(Type.Union([Type.Literal("branch"), Type.Literal("changeset")])), orderBy: nullable(Type.Union([Type.Literal("date"), Type.Literal("modifieddate"), Type.Literal("status")])), descending: nullable(Type.Boolean()), limit: nullable(Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER })) });
|
|
11
|
+
const common = { schemaVersion: Type.Literal(1), action: Type.Literal("code-review-find"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }) };
|
|
12
|
+
const dataCommon = { scope: Type.Literal("workspace_repository"), qualifierVerified: Type.Literal(false), query, limits: object({ maxItems: Type.Integer({ minimum: 1, maximum: 500 }), idDigits: Type.Literal(20), queryCodeUnits: Type.Literal(4096), compactUtf8Bytes: Type.Literal(131072) }) };
|
|
13
|
+
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")]);
|
|
14
|
+
export const codeReviewFindOutputSchema = Type.Union([
|
|
15
|
+
object({ ...common, ok: Type.Literal(true), completeness: object({ read: Type.Literal("complete"), capture: Type.Literal("complete"), projection: Type.Boolean() }), data: object({ ...dataCommon, mode: Type.Literal("ids"), basis: Type.Literal("find_review_id_utf8"), diagnostics: object({ duplicateRecords: integer(19999) }), rows: Type.Array(object({ id: Type.String({ minLength: 1, maxLength: 20, pattern: "^(?:0|[1-9][0-9]*)$" }) }), { maxItems: 500 }), countBasis: Type.Literal("observed_query_rows"), counts: object({ observed: integer(20000), returned: integer(500), omitted: integer(20000), excluded: Type.Literal(0) }) }) }),
|
|
16
|
+
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("find_review_native"), rows: Type.Null(), counts: Type.Null(), countBasis: Type.Literal("unavailable") }) }),
|
|
17
|
+
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 }) }) }),
|
|
18
|
+
]);
|
|
19
|
+
export type CodeReviewFindOutput = Static<typeof codeReviewFindOutputSchema>;
|
|
20
|
+
export type CodeReviewFindErrorCode = Extract<CodeReviewFindOutput, { ok: false }>["error"]["code"];
|
|
21
|
+
const failure = (code: CodeReviewFindErrorCode): CodeReviewFindOutput => ({ schemaVersion: 1, action: "code-review-find", provenance: { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" }, ok: false, completeness: { read: "incomplete", capture: "unknown", projection: false }, error: { code, message: `Code review find read failed (${code}); no reusable rows are available.` } });
|
|
22
|
+
export function validateCodeReviewFindOutput(value: unknown): CodeReviewFindOutput {
|
|
23
|
+
if (!Check(codeReviewFindOutputSchema, value)) return failure("invalid_producer_data");
|
|
24
|
+
const dto = value as CodeReviewFindOutput;
|
|
25
|
+
if (dto.ok) {
|
|
26
|
+
if ([dto.data.query.status, dto.data.query.assignee, dto.data.query.owner, dto.data.query.target, dto.data.query.titleLike].some(value => value !== null && !validCodeReviewQueryString(value))) return failure("invalid_producer_data");
|
|
27
|
+
if (dto.data.mode === "ids") {
|
|
28
|
+
const { rows, counts, diagnostics, limits, query } = dto.data;
|
|
29
|
+
const repeats = rows.length - new Set(rows.map(row => row.id)).size;
|
|
30
|
+
if (rows.some(row => !validCodeReviewId(row.id))
|
|
31
|
+
|| counts.returned !== rows.length || counts.observed !== counts.returned + counts.omitted || counts.returned !== Math.min(counts.observed, limits.maxItems)
|
|
32
|
+
|| (query.limit !== null && counts.observed > query.limit) || dto.completeness.projection !== (counts.omitted === 0)
|
|
33
|
+
|| diagnostics.duplicateRecords < repeats || diagnostics.duplicateRecords > repeats + counts.omitted || diagnostics.duplicateRecords > Math.max(0, counts.observed - 1)) return failure("invalid_producer_data");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (Buffer.byteLength(JSON.stringify(dto), "utf8") > CODE_REVIEW_FIND_MAX_BYTES) return failure("output_overflow");
|
|
37
|
+
return dto;
|
|
38
|
+
}
|
|
39
|
+
export async function executeCodeReviewFindOutput(args: CodeReviewFindArgs) {
|
|
40
|
+
let dto: CodeReviewFindOutput, rawResult: string | undefined;
|
|
41
|
+
try {
|
|
42
|
+
const observation = await assembleCodeReviewFindObservation(args, true);
|
|
43
|
+
if (getActiveAbortSignal()?.aborted) throw new CodeReviewFindCommandError("aborted");
|
|
44
|
+
dto = validateCodeReviewFindOutput(codeReviewFindPayload(observation));
|
|
45
|
+
if (dto.ok) rawResult = await presentCodeReviewFindObservation(observation, args);
|
|
46
|
+
if (getActiveAbortSignal()?.aborted) throw new CodeReviewFindCommandError("aborted");
|
|
47
|
+
} catch (error) { dto = failure(error instanceof CodeReviewFindError || error instanceof CodeReviewFindCommandError ? error.code : "invalid_producer_data"); }
|
|
48
|
+
dto = validateCodeReviewFindOutput(dto);
|
|
49
|
+
return { content: [{ type: "text" as const, text: dto.ok ? rawResult! : dto.error.message }], structuredContent: dto, isError: !dto.ok, details: { exportName: "codeReviewFind", rawResult: dto.ok ? rawResult : undefined, ...(args.workdir ? { workdir: args.workdir } : {}) } };
|
|
50
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Type, type Static, type TProperties } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { validDiffRevisionsSelector } from "../operations/diff-revisions";
|
|
4
|
+
import { assembleConsolidatedDiff, diffError, type DiffRequest } from "../operations/consolidated-diff";
|
|
5
|
+
const object=<P extends TProperties>(p:P)=>Type.Object(p,{additionalProperties:false});
|
|
6
|
+
const enumOf=<const V extends readonly string[]>(v:V)=>Type.Unsafe<V[number]>({anyOf:v.map(x=>({type:"string",const:x}))});
|
|
7
|
+
const string=(max=4096)=>Type.String({maxLength:max});
|
|
8
|
+
const uint=(max=4194304)=>Type.Integer({minimum:0,maximum:max});
|
|
9
|
+
const nullable=<T extends ReturnType<typeof Type.String>|ReturnType<typeof object>|ReturnType<typeof Type.Array>|ReturnType<typeof Type.Integer>|ReturnType<typeof enumOf>>(s:T)=>Type.Union([s,Type.Null()]);
|
|
10
|
+
const common={workdir:Type.Optional(Type.String({minLength:1,maxLength:4096})),format:Type.Optional(enumOf(["text","json"])),maxChars:Type.Optional(Type.Integer({minimum:500,maximum:20000}))};
|
|
11
|
+
const paths=Type.Array(Type.String({minLength:1,maxLength:1024}),{minItems:1,maxItems:20});
|
|
12
|
+
const workspace={...common,maxChars:Type.Optional(Type.Integer({minimum:500,maximum:8000})),maxFiles:Type.Optional(Type.Integer({minimum:1,maximum:20}))};
|
|
13
|
+
const variants=Type.Union([
|
|
14
|
+
object({mode:Type.Literal("file"),path:Type.String({minLength:1,maxLength:1024}),revision:Type.Optional(Type.String({minLength:1,maxLength:4096})),...common}),
|
|
15
|
+
object({mode:Type.Literal("revisions"),leftRevision:Type.String({minLength:1,maxLength:4096}),rightRevision:Type.String({minLength:1,maxLength:4096}),...common}),
|
|
16
|
+
object({mode:Type.Literal("workspace"),paths,...workspace}),
|
|
17
|
+
object({mode:Type.Literal("workspace"),allPending:Type.Literal(true),includePrivate:Type.Optional(Type.Boolean()),...workspace}),
|
|
18
|
+
]);
|
|
19
|
+
export const diffInputSchema=Type.Unsafe<DiffRequest>({...variants,type:"object"});
|
|
20
|
+
const provenance=object({source:Type.Literal("plastic"),producer:Type.Literal("@aefree/pi-plastic"),contentTrust:Type.Literal("external")});
|
|
21
|
+
const completeness=object({read:enumOf(["complete","incomplete"]),capture:enumOf(["complete","unknown"]),projection:Type.Boolean()});
|
|
22
|
+
const error=object({code:enumOf(["invalid_request","invalid_selector","workspace_unavailable","outside_workspace","base_unavailable","base_changed","malformed_output","command_failed","aborted","cleanup_failed","materialization_failed","materialization_limit","invalid_utf8","capture_incomplete","output_overflow","missing_file","unsupported_file","file_limit","file_changed","read_failed","comparison_failed","partial_comparison"]),stage:enumOf(["input","selection","status","base","left","right","comparison","cleanup","producer"]),message:string(256)});
|
|
23
|
+
const identity=object({revisionId:Type.String({pattern:"^(0|[1-9][0-9]{0,19})$"}),changeset:Type.String({pattern:"^(0|[1-9][0-9]{0,19})$"}),repository:string(),server:string()});
|
|
24
|
+
const side=object({origin:enumOf(["historical","local-snapshot","synthetic-empty"]),selector:nullable(string()),path:nullable(string()),identity:nullable(identity),bytes:uint(),binary:Type.Boolean()});
|
|
25
|
+
const excerpt=object({countBasis:Type.Literal("normalized_diff_utf16"),text:string(20000),observedChars:uint(1060000),sourceChars:uint(20000),returnedChars:uint(20000),omittedChars:uint(1060000),markerChars:uint(59),truncated:Type.Boolean()});
|
|
26
|
+
const comparison=object({atomicSnapshot:Type.Literal(false),kind:enumOf(["revision-to-revision","explicit-revision-to-local","loaded-base-to-local"]),pendingKind:nullable(enumOf(["changed","added","deleted","moved","private","checkedout","copied","replaced"])),left:side,right:side,status:enumOf(["changed","unchanged","binary-different","added-empty"]),changed:Type.Boolean(),binary:Type.Boolean(),comparisonBasis:enumOf(["byte_equality","diff_u"]),hunkCount:uint(),capture:object({diffStdoutBytes:nullable(uint(1048576)),diffExitCode:nullable(uint(1))}),excerpt:nullable(excerpt)});
|
|
27
|
+
const outcome=Type.Union([object({path:string(),sourcePath:nullable(string()),kind:string(32),status:Type.Literal("compared"),comparison}),object({path:string(),sourcePath:nullable(string()),kind:string(32),status:Type.Literal("skipped-directory")}),object({path:string(),sourcePath:nullable(string()),kind:string(32),status:Type.Literal("unavailable"),error})]);
|
|
28
|
+
const collection=object({scope:enumOf(["selected","all-pending"]),requestedPaths:nullable(paths),includePrivate:Type.Boolean(),excludedPrivate:uint(20000),counts:object({parsed:uint(20000),eligible:uint(20000),selected:uint(20),attempted:uint(20),completed:uint(20),failed:uint(20),skipped:uint(20),unattempted:uint(20),limited:uint(20000),returned:uint(20),omitted:uint(20)}),unmatched:Type.Array(string(1024),{maxItems:20}),outcomes:Type.Array(outcome,{maxItems:20})});
|
|
29
|
+
const header={schemaVersion:Type.Literal(1),action:Type.Literal("diff"),provenance,completeness};
|
|
30
|
+
export const diffOutputSchema=Type.Union([
|
|
31
|
+
object({...header,mode:enumOf(["file","revisions"]),ok:Type.Literal(true),data:comparison}),
|
|
32
|
+
object({...header,mode:Type.Literal("workspace"),ok:Type.Literal(true),data:collection}),
|
|
33
|
+
object({...header,mode:Type.Literal("workspace"),ok:Type.Literal(false),data:collection,error}),
|
|
34
|
+
object({...header,mode:Type.Union([enumOf(["file","revisions","workspace"]),Type.Null()]),ok:Type.Literal(false),error}),
|
|
35
|
+
]);
|
|
36
|
+
export type DiffOutput=Static<typeof diffOutputSchema>;
|
|
37
|
+
export function validateDiffOutput(value:unknown):value is DiffOutput {
|
|
38
|
+
if(!Check(diffOutputSchema,value)||Buffer.byteLength(JSON.stringify(value),"utf8")>131072)return false;
|
|
39
|
+
const p=value as DiffOutput;
|
|
40
|
+
const check=(c:Static<typeof comparison>)=>{
|
|
41
|
+
if(c.kind==="revision-to-revision"&&(c.pendingKind!==null||c.left.origin!=="historical"||c.right.origin!=="historical"||c.left.identity!==null||c.right.identity!==null))return false;
|
|
42
|
+
if(c.kind==="explicit-revision-to-local"&&(c.pendingKind!==null||c.left.origin!=="historical"||c.left.identity!==null||c.right.origin!=="local-snapshot"))return false;
|
|
43
|
+
if(c.kind==="loaded-base-to-local"&&(["added","private"].includes(c.pendingKind??"")?c.left.origin!=="synthetic-empty":c.left.origin!=="historical"||c.left.identity===null))return false;
|
|
44
|
+
for(const s of [c.left,c.right]){if(s.origin==="historical"&&(!s.selector||!validDiffRevisionsSelector(s.selector))||s.origin!=="historical"&&(s.selector!==null||s.identity!==null)||s.origin==="synthetic-empty"&&(s.bytes!==0||s.binary))return false;}
|
|
45
|
+
const e=c.excerpt;if(e&&(e.text.length!==e.returnedChars||e.returnedChars!==e.sourceChars+e.markerChars||e.observedChars!==e.sourceChars+e.omittedChars||e.truncated!==(e.omittedChars>0)||e.markerChars!==(e.truncated?59:0)))return false;
|
|
46
|
+
if(c.binary!==(c.left.binary||c.right.binary)||c.comparisonBasis!==(c.binary?"byte_equality":"diff_u")||(c.excerpt===null)!==c.binary)return false;
|
|
47
|
+
if(c.status==="changed"&&(!c.changed||c.binary)||c.status==="binary-different"&&(!c.binary||!c.changed)||c.status==="unchanged"&&c.changed||c.status==="added-empty"&&(c.changed||c.pendingKind!=="added"||c.right.bytes))return false;
|
|
48
|
+
return c.binary?c.hunkCount===0&&c.capture.diffExitCode===null&&c.capture.diffStdoutBytes===null:c.capture.diffExitCode===(c.changed?1:0)&&c.hunkCount>0===c.changed;
|
|
49
|
+
};
|
|
50
|
+
if("data"in p){if(p.mode==="workspace"){const c=p.data.counts;if(c.returned!==p.data.outcomes.length||c.returned+c.omitted!==c.selected-c.unattempted||c.completed+c.failed!==c.attempted||c.attempted+c.skipped+c.unattempted!==c.selected||c.selected+c.limited!==c.eligible||c.eligible>c.parsed||p.ok!==(c.failed===0&&c.unattempted===0)||!p.data.outcomes.every(o=>o.status!=="compared"||check(o.comparison)))return false;}else if(!check(p.data)||(p.mode==="revisions")!==(p.data.kind==="revision-to-revision")||p.completeness.read!=="complete"||p.completeness.capture!=="complete"||p.completeness.projection!==!p.data.excerpt?.truncated)return false;}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
export async function executeDiffOutput(args:unknown){
|
|
54
|
+
let payload:unknown;
|
|
55
|
+
try{payload=await assembleConsolidatedDiff(args);}catch(e){const mode=(args as {mode?:unknown})?.mode;payload={schemaVersion:1,action:"diff",mode:["file","revisions","workspace"].includes(String(mode))?mode:null,provenance:{source:"plastic",producer:"@aefree/pi-plastic",contentTrust:"external"},ok:false,completeness:{read:"incomplete",capture:"unknown",projection:false},error:diffError(e)};}
|
|
56
|
+
if(!validateDiffOutput(payload))payload={schemaVersion:1,action:"diff",mode:null,provenance:{source:"plastic",producer:"@aefree/pi-plastic",contentTrust:"external"},ok:false,completeness:{read:"incomplete",capture:"unknown",projection:false},error:{code:"comparison_failed",stage:"producer",message:"The diff producer envelope could not be verified."}};
|
|
57
|
+
const dto=payload as DiffOutput;
|
|
58
|
+
const json=(args as {format?:unknown})?.format==="json";
|
|
59
|
+
let text=JSON.stringify(dto);
|
|
60
|
+
if(!json){
|
|
61
|
+
if("data"in dto)text=dto.mode==="workspace"?"## Workspace Diff\n"+(dto.ok?"":"Partial failure: "+dto.error.code+"\n")+JSON.stringify(dto.data.counts)+"\nPrivate excluded: "+dto.data.excludedPrivate+"; unmatched paths: "+dto.data.unmatched.length+"\n"+dto.data.outcomes.map(o=>o.path+": "+o.status+(o.status==="compared"?"\n"+(o.comparison.excerpt?.text??o.comparison.status):o.status==="unavailable"?" ("+o.error.code+")":"")).join("\n\n"):"## Diff\n"+dto.data.status+"\n"+(dto.data.excerpt?.text??"");
|
|
62
|
+
else text=dto.error.message;
|
|
63
|
+
}
|
|
64
|
+
if(text.length>24000)text=json?JSON.stringify({action:"diff",mode:dto.mode,ok:dto.ok,note:"Presentation omitted; use structuredContent for the bounded producer observation."}):text.slice(0,23920)+"\n[Presentation truncated; inspect the structured projection/completeness fields.]";
|
|
65
|
+
return {content:[{type:"text" as const,text}],details:{},structuredContent:dto,isError:!dto.ok};
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Type, type Static, type TSchema } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleDiffRevisionsObservation, presentDiffRevisions, diffRevisionsPayload, validDiffRevisionsSelector, DiffRevisionsError, DIFF_REVISIONS_FILE_BYTES, DIFF_REVISIONS_MAX_BYTES, DIFF_REVISIONS_EXCERPT_MARKER, type DiffRevisionsArgs, type DiffRevisionsErrorCode, type DiffRevisionsStage } from "../operations/diff-revisions";
|
|
4
|
+
import { getActiveAbortSignal } from "../execution/context";
|
|
5
|
+
const object = <T extends Record<string, TSchema>>(fields: T) => Type.Object(fields, { additionalProperties: false });
|
|
6
|
+
const integer = (max: number) => Type.Integer({ minimum: 0, maximum: max });
|
|
7
|
+
const nullable = <T extends TSchema>(schema: T) => Type.Union([schema, Type.Null()]);
|
|
8
|
+
const common = { schemaVersion: Type.Literal(1), action: Type.Literal("diff-revisions"), provenance: object({ source: Type.Literal("plastic"), producer: Type.Literal("@aefree/pi-plastic"), contentTrust: Type.Literal("external") }) };
|
|
9
|
+
const side = object({ selector: Type.String({ minLength: 1, maxLength: 4096 }), kind: Type.Union([Type.Literal("file-qualified"), Type.Literal("global-revision")]), resolvedIdentity: Type.Null(), bytes: integer(DIFF_REVISIONS_FILE_BYTES), binary: Type.Boolean() });
|
|
10
|
+
export const diffRevisionsOutputSchema = Type.Union([
|
|
11
|
+
object({ ...common, ok: Type.Literal(true), completeness: object({ read: Type.Literal("complete"), capture: Type.Literal("complete"), projection: Type.Boolean() }), data: object({
|
|
12
|
+
scope: Type.Literal("explicit_selector_pair"), qualifierVerified: Type.Literal(false), resolvedIdentities: Type.Null(), comparisonKind: Type.Literal("revision-to-revision"), left: side, right: side,
|
|
13
|
+
status: Type.Union([Type.Literal("changed"),Type.Literal("unchanged"),Type.Literal("binary-different")]), changed: Type.Boolean(), binary: Type.Boolean(), comparisonBasis: Type.Union([Type.Literal("byte_equality"),Type.Literal("diff_u")]),
|
|
14
|
+
hunkCount: integer(1048576), capture: object({ diffStdoutBytes: nullable(integer(1048576)), diffExitCode: nullable(integer(1)) }),
|
|
15
|
+
limits: object({ maxChars: Type.Integer({ minimum: 500, maximum: 20000 }), selectorCodeUnits: Type.Literal(4096), materializedBytesPerSide: Type.Literal(4194304), compactUtf8Bytes: Type.Literal(131072) }),
|
|
16
|
+
excerpt: nullable(object({ countBasis: Type.Literal("normalized_diff_utf16"), text: Type.String({ maxLength: 20000 }), observedChars: integer(1060000), sourceChars: integer(20000), returnedChars: integer(20000), omittedChars: integer(1060000), markerChars: integer(100), truncated: Type.Boolean() })),
|
|
17
|
+
}) }),
|
|
18
|
+
object({ ...common, ok: Type.Literal(false), completeness: object({ read: Type.Literal("incomplete"), capture: Type.Literal("unknown"), projection: Type.Literal(false) }), error: object({ code: Type.Union([Type.Literal("invalid_selector"),Type.Literal("invalid_producer_data"),Type.Literal("materialization_failed"),Type.Literal("materialization_limit"),Type.Literal("cleanup_failed"),Type.Literal("invalid_utf8"),Type.Literal("malformed_output"),Type.Literal("command_failed"),Type.Literal("aborted"),Type.Literal("capture_incomplete"),Type.Literal("output_overflow")]), stage: Type.Union([Type.Literal("input"),Type.Literal("left"),Type.Literal("right"),Type.Literal("comparison"),Type.Literal("cleanup"),Type.Literal("producer")]), message: Type.String({ minLength: 1, maxLength: 256 }) }) }),
|
|
19
|
+
]);
|
|
20
|
+
export type DiffRevisionsOutput = Static<typeof diffRevisionsOutputSchema>;
|
|
21
|
+
const failure = (code: DiffRevisionsErrorCode, stage: DiffRevisionsStage): DiffRevisionsOutput => ({ ...{ schemaVersion: 1, action: "diff-revisions", provenance: { source: "plastic", producer: "@aefree/pi-plastic", contentTrust: "external" } } as const, ok: false, completeness: { read: "incomplete", capture: "unknown", projection: false }, error: { code, stage, message: "Historical revision comparison failed (" + code + ", " + stage + "); no verified comparison is available." } });
|
|
22
|
+
export function validateDiffRevisionsOutput(value: unknown): DiffRevisionsOutput {
|
|
23
|
+
if (!Check(diffRevisionsOutputSchema,value)) return failure("invalid_producer_data","producer");
|
|
24
|
+
const dto=value as DiffRevisionsOutput;
|
|
25
|
+
if(dto.ok){
|
|
26
|
+
const d=dto.data;
|
|
27
|
+
if([d.left,d.right].some(s=>!validDiffRevisionsSelector(s.selector)||s.kind!==(s.selector.includes("#")?"file-qualified":"global-revision"))
|
|
28
|
+
|| d.binary!==(d.left.binary||d.right.binary) || d.status!==(d.changed?d.binary?"binary-different":"changed":"unchanged")) return failure("invalid_producer_data","producer");
|
|
29
|
+
if(d.binary){
|
|
30
|
+
if(d.comparisonBasis!=="byte_equality"||d.excerpt!==null||d.hunkCount!==0||d.capture.diffStdoutBytes!==null||d.capture.diffExitCode!==null||!dto.completeness.projection) return failure("invalid_producer_data","producer");
|
|
31
|
+
}else{
|
|
32
|
+
const e=d.excerpt;
|
|
33
|
+
if(d.comparisonBasis!=="diff_u"||e===null||d.capture.diffStdoutBytes===null||d.capture.diffExitCode!==(d.changed?1:0)
|
|
34
|
+
||e.returnedChars!==e.text.length||e.returnedChars> d.limits.maxChars||e.returnedChars!==e.sourceChars+e.markerChars
|
|
35
|
+
||e.observedChars!==e.sourceChars+e.omittedChars||e.truncated!==(e.omittedChars>0)||dto.completeness.projection===e.truncated
|
|
36
|
+
|| /[\uD800-\uDFFF]/u.test(e.text)
|
|
37
|
+
|| (e.truncated ? !e.text.endsWith(DIFF_REVISIONS_EXCERPT_MARKER)||e.markerChars!==DIFF_REVISIONS_EXCERPT_MARKER.length : e.markerChars!==0)
|
|
38
|
+
|| (d.changed ? d.hunkCount<1||e.observedChars<1||d.capture.diffStdoutBytes<1 : d.hunkCount!==0||e.observedChars!==0||d.capture.diffStdoutBytes!==0)) return failure("invalid_producer_data","producer");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if(Buffer.byteLength(JSON.stringify(dto),"utf8")>DIFF_REVISIONS_MAX_BYTES)return failure("output_overflow","producer");
|
|
42
|
+
return dto;
|
|
43
|
+
}
|
|
44
|
+
export async function executeDiffRevisionsOutput(args: DiffRevisionsArgs){
|
|
45
|
+
let dto: DiffRevisionsOutput, rawResult: string | undefined;
|
|
46
|
+
try{
|
|
47
|
+
const observation=await assembleDiffRevisionsObservation(args);
|
|
48
|
+
if(getActiveAbortSignal()?.aborted)throw new DiffRevisionsError("aborted","comparison");
|
|
49
|
+
dto=validateDiffRevisionsOutput(diffRevisionsPayload(observation));
|
|
50
|
+
if(dto.ok) rawResult=await presentDiffRevisions(observation,args);
|
|
51
|
+
if(getActiveAbortSignal()?.aborted)throw new DiffRevisionsError("aborted","comparison");
|
|
52
|
+
}catch(error){dto=failure(error instanceof DiffRevisionsError?error.code:"invalid_producer_data",error instanceof DiffRevisionsError?error.stage:"producer");}
|
|
53
|
+
dto=validateDiffRevisionsOutput(dto);
|
|
54
|
+
return { content:[{type:"text" as const,text:dto.ok?rawResult!:dto.error.message}], structuredContent:dto, isError:!dto.ok, details:{exportName:"diffRevisions",rawResult:dto.ok?rawResult:undefined,...(args.workdir?{workdir:args.workdir}:{})} };
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {Type} from "typebox";import {Check} from "typebox/value";
|
|
2
|
+
import {assembleObjectDeleteReceipt,emptyObjectDeleteData,objectDeleteFailure,objectDeleteArgv,objectDeletePrefix,presentObjectDeleteReceipt,validObjectDeleteOperands,validObjectDeleteText as safeBranchDeleteText,type ObjectDeleteAction,type ObjectDeleteReceipt} from "../operations/object-delete-receipt";
|
|
3
|
+
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=(n=Number.MAX_SAFE_INTEGER)=>Type.Integer({minimum:0,maximum:n}),text=Type.String({minLength:1,maxLength:4096});
|
|
4
|
+
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()}),capture=obj({stdoutBytes:uint(),stderrBytes:uint(),stdoutRetainedBytes:uint(65536),stderrRetainedBytes:uint(16384),truncated:Type.Boolean(),complete:Type.Boolean(),validUtf8:Type.Boolean()});
|
|
5
|
+
function schema<A extends ObjectDeleteAction>(action:A){const request=obj({operands:Type.Array(text,{minItems:1,maxItems:action==="shelveset-delete"?1:256}),repository:action==="shelveset-delete"?Type.Null():nil(text),workdir:text,preflight:Type.Boolean(),format:enm(["text","json"])}),data={requested:nil(request),intendedArgv:Type.Array(Type.String({minLength:1,maxLength:8192}),{minItems:2,maxItems:259}),attempt,capture:nil(capture),observedDeletedObjects:Type.Null(),observedRepositoryIdentity:Type.Null(),scope:Type.Literal("requested-only"),rollback:Type.Literal("not-proven")},header={schemaVersion:Type.Literal(1),action:Type.Literal(action),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)})},error=obj({code:enm(["invalid_request","launch_failed","aborted","uncertain","producer_failed"]),message:Type.String({minLength:1,maxLength:256})});return Type.Unsafe<ObjectDeleteReceipt & {action:A;data:{requested:(ObjectDeleteReceipt["data"]["requested"] & {repository:A extends "shelveset-delete"?null:string|null})|null}}>(Type.Union([obj({...header,ok:Type.Literal(true),outcome:Type.Literal("preflight"),data:obj({...data,effect:Type.Literal("not-attempted")})}),obj({...header,ok:Type.Literal(true),outcome:Type.Literal("command-completed"),data:obj({...data,effect:Type.Literal("not-proven")})}),obj({...header,ok:Type.Literal(false),outcome:Type.Literal("failed"),data:obj({...data,effect:Type.Literal("not-attempted")}),error}),obj({...header,ok:Type.Literal(false),outcome:Type.Literal("uncertain"),data:obj({...data,effect:Type.Literal("uncertain")}),error})]));}
|
|
6
|
+
export const shelvesetDeleteOutputSchema=schema("shelveset-delete"),codeReviewDeleteOutputSchema=schema("code-review-delete");
|
|
7
|
+
export function validateObjectDeleteOutput(v:unknown):v is ObjectDeleteReceipt{if(!v||typeof v!=="object")return false;const dto=v as ObjectDeleteReceipt;if(!["shelveset-delete","code-review-delete"].includes(dto.action)||!Check(dto.action==="shelveset-delete"?shelvesetDeleteOutputSchema:codeReviewDeleteOutputSchema,v)||Buffer.byteLength(JSON.stringify(v),"utf8")>131072)return false;const d=dto.data,r=d.requested,a=d.attempt,c=d.capture;
|
|
8
|
+
if(r===null){if(JSON.stringify(d.intendedArgv)!==JSON.stringify(objectDeletePrefix(dto.action))||c||a.terminal!=="not-observed"||a.exitCode!==null||a.aborted||a.timedOut||!["not-attempted","unknown"].includes(a.state))return false;}else{if(!validObjectDeleteOperands(dto.action,r.operands)||!safeBranchDeleteText(r.workdir)||r.repository!==null&&!safeBranchDeleteText(r.repository)||Buffer.byteLength(JSON.stringify(r),"utf8")>32768||JSON.stringify(d.intendedArgv)!==JSON.stringify(objectDeleteArgv(dto.action,r)))return false;}
|
|
9
|
+
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;
|
|
10
|
+
if(c){if(!r||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;if(a.state==="not-attempted"&&(!a.aborted||c.stdoutBytes||c.stderrBytes||c.validUtf8||c.complete))return false;}
|
|
11
|
+
if(dto.completeness.capture!==(c?c.complete?"complete":"incomplete":"unknown"))return false;
|
|
12
|
+
if(r?.preflight===true&&(!dto.ok||dto.outcome!=="preflight"||c||a.state!=="not-attempted"||a.terminal!=="not-observed"||a.exitCode!==null||a.aborted||a.timedOut))return false;
|
|
13
|
+
if(dto.ok&&dto.outcome==="preflight"&&!r?.preflight)return false;if(dto.ok&&dto.outcome==="command-completed"&&(!r||r.preflight||!c?.complete||a.state!=="started"||a.exitCode!==0))return false;
|
|
14
|
+
if(!dto.ok){if(c?.complete&&a.exitCode===0||dto.error.code==="uncertain"&&(dto.outcome!=="uncertain"||d.effect!=="uncertain"||!["started","unknown"].includes(a.state)))return false;if(!safeBranchDeleteText(dto.error.message)||dto.outcome==="uncertain"&&!["started","unknown"].includes(a.state)||dto.outcome==="failed"&&!["not-attempted","not-started"].includes(a.state))return false;if(dto.error.code==="invalid_request"&&(r!==null||c||a.state!=="not-attempted"||a.aborted)||dto.error.code==="aborted"&&!a.aborted||dto.error.code==="launch_failed"&&!["not-attempted","not-started"].includes(a.state))return false;}return true;}
|
|
15
|
+
export async function executeObjectDeleteOutput(action:ObjectDeleteAction,args:unknown){let dto=await assembleObjectDeleteReceipt(action,args);const valid:boolean=validateObjectDeleteOutput(dto)&&dto.action===action;if(!valid){const possible=["started","unknown"].includes(dto.data.attempt.state),d=emptyObjectDeleteData(action);if(possible){d.attempt.state="unknown";d.effect="uncertain";}dto=objectDeleteFailure(action,d,"producer_failed","Object-delete receipt validation failed; effects remain possible, no automatic retry.");if(!validateObjectDeleteOutput(dto))throw Error("Safe object-delete receipt unavailable.");}return {content:[{type:"text" as const,text:presentObjectDeleteReceipt(dto)}],details:{},structuredContent:dto,isError:!dto.ok};}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { assembleObjectWriteReceipt, emptyObjectWriteData, objectWriteFailure, objectWriteArgv, objectWritePrefix, parseObjectWriteIdentity, presentObjectWriteReceipt, validObjectWriteRequest, validWriteText, OBJECT_WRITE_ACTIONS, type ObjectWriteAction, type ObjectWriteReceipt } from "../operations/object-write-receipt";
|
|
4
|
+
const obj=(p:Record<string,any>)=>Type.Object(p,{additionalProperties:false}),enm=(v:readonly 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}),text=Type.String({minLength:1,maxLength:4096}),optionalText=nil(Type.String({maxLength:4096}));
|
|
5
|
+
const common={workdir:text,preflight:Type.Boolean(),output:enm(["text","json"])},paths=nil(Type.Array(text,{maxItems:256}));
|
|
6
|
+
const requests={
|
|
7
|
+
"shelveset-create":obj({...common,paths,comment:optionalText,commentsFile:optionalText,all:Type.Boolean(),dependencies:Type.Boolean(),summaryFormat:Type.Boolean()}),
|
|
8
|
+
"shelveset-apply":obj({...common,shelveset:text,changePaths:paths,preview:Type.Boolean(),dontCheckout:Type.Boolean(),comparisonMethod:nil(enm(["ignoreeol","ignorewhitespaces","ignoreeolandwhitespaces","recognizeall"]))}),
|
|
9
|
+
"code-review-create":obj({...common,target:text,title:text,status:optionalText,assignee:optionalText,repository:optionalText,format:optionalText}),
|
|
10
|
+
"code-review-update":obj({...common,id:text,status:optionalText,assignee:optionalText,repository:optionalText}),
|
|
11
|
+
};
|
|
12
|
+
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()}),capture=obj({stdoutBytes:uint(),stderrBytes:uint(),stdoutRetainedBytes:uint(65536),stderrRetainedBytes:uint(16384),truncated:Type.Boolean(),complete:Type.Boolean(),validUtf8:Type.Boolean()});
|
|
13
|
+
function schema<A extends ObjectWriteAction>(action:A) {
|
|
14
|
+
const data=obj({requested:nil(requests[action]),intendedArgv:Type.Array(Type.String({minLength:1,maxLength:8192}),{minItems:1,maxItems:263}),attempt,capture:nil(capture),commandOutput:nil(obj({kind:Type.Literal("opaque-command-output"),text:Type.String({maxLength:65536})})),observedCreatedIdentity:nil(obj({kind:enm(["shelveset","code-review"]),value:text,basis:enm(["default-scalar","summary-scalar"])})),observedAppliedItems:Type.Null(),observedUpdatedState:Type.Null(),observedRepositoryIdentity:Type.Null(),scope:Type.Literal("requested-only"),rollback:Type.Literal("not-proven"),effect:enm(["not-attempted","not-proven","identity-emitted","uncertain"])});
|
|
15
|
+
const header={schemaVersion:Type.Literal(1),action:Type.Literal(action),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};
|
|
16
|
+
return Type.Unsafe<ObjectWriteReceipt<A>>(Type.Union([obj({...header,ok:Type.Literal(true),outcome:enm(["preflight","command-completed"])}),obj({...header,ok:Type.Literal(false),outcome:enm(["failed","uncertain"]),error:obj({code:enm(["invalid_request","editor_configured","launch_failed","aborted","uncertain","producer_failed"]),message:Type.String({minLength:1,maxLength:256})})})]));
|
|
17
|
+
}
|
|
18
|
+
export const shelvesetCreateOutputSchema=schema("shelveset-create"),shelvesetApplyOutputSchema=schema("shelveset-apply"),codeReviewCreateOutputSchema=schema("code-review-create"),codeReviewUpdateOutputSchema=schema("code-review-update");
|
|
19
|
+
export const objectWriteSchemas={"shelveset-create":shelvesetCreateOutputSchema,"shelveset-apply":shelvesetApplyOutputSchema,"code-review-create":codeReviewCreateOutputSchema,"code-review-update":codeReviewUpdateOutputSchema};
|
|
20
|
+
export function validateObjectWriteOutput(v:unknown):v is ObjectWriteReceipt {
|
|
21
|
+
if(!v||typeof v!=="object")return false;const dto=v as ObjectWriteReceipt;
|
|
22
|
+
if(!OBJECT_WRITE_ACTIONS.includes(dto.action)||!Check(objectWriteSchemas[dto.action],v)||Buffer.byteLength(JSON.stringify(v),"utf8")>524288)return false;
|
|
23
|
+
const d=dto.data,r=d.requested,a=d.attempt,c=d.capture,o=d.commandOutput,id=d.observedCreatedIdentity;
|
|
24
|
+
if(dto.completeness.capture!==(c?c.complete?"complete":"incomplete":"unknown"))return false;
|
|
25
|
+
if(r===null) {if(JSON.stringify(d.intendedArgv)!==JSON.stringify(objectWritePrefix(dto.action))||c||o||id||a.terminal!=="not-observed"||a.exitCode!==null||a.aborted||a.timedOut||!["not-attempted","unknown"].includes(a.state)||dto.ok)return false;}
|
|
26
|
+
else if(!validObjectWriteRequest(dto.action,r)||JSON.stringify(d.intendedArgv)!==JSON.stringify(objectWriteArgv(dto.action,r)))return false;
|
|
27
|
+
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;
|
|
28
|
+
if(c) {
|
|
29
|
+
if(!r||c.stdoutRetainedBytes>c.stdoutBytes||c.stderrRetainedBytes>c.stderrBytes)return false;
|
|
30
|
+
if(c.truncated!==(c.stdoutBytes>65536||c.stderrBytes>16384))return false;
|
|
31
|
+
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;
|
|
32
|
+
if(a.state==="not-attempted"&&(!a.aborted||c.stdoutBytes||c.stderrBytes||c.validUtf8||c.complete))return false;
|
|
33
|
+
}
|
|
34
|
+
const completed=!!c?.complete&&a.state==="started"&&a.exitCode===0;
|
|
35
|
+
if(o!==null) {if(!r||!completed||/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(o.text)||Buffer.byteLength(o.text,"utf8")!==c!.stdoutBytes||JSON.stringify(id)!==JSON.stringify(parseObjectWriteIdentity(dto.action,r,o.text)))return false;}
|
|
36
|
+
else if(id!==null||completed)return false;
|
|
37
|
+
if(d.effect!==(id?"identity-emitted":dto.ok&&dto.outcome==="command-completed"?"not-proven":["started","unknown"].includes(a.state)?"uncertain":"not-attempted"))return false;
|
|
38
|
+
if(r?.preflight)return dto.ok&&dto.outcome==="preflight"&&a.state==="not-attempted"&&a.terminal==="not-observed"&&a.exitCode===null&&!a.aborted&&!a.timedOut&&!c&&!o&&!id;
|
|
39
|
+
if(dto.ok)return dto.outcome==="command-completed"&&r!==null&&!r.preflight&&completed&&c!.stderrBytes===0;
|
|
40
|
+
if(dto.outcome!==(["started","unknown"].includes(a.state)?"uncertain":"failed")||!validWriteText(dto.error.message))return false;
|
|
41
|
+
if(completed&&c!.stderrBytes===0)return false;
|
|
42
|
+
if(dto.error.code==="invalid_request"&&(r!==null||c||a.state!=="not-attempted"||a.aborted)||dto.error.code==="aborted"&&!a.aborted||dto.error.code==="launch_failed"&&!["not-attempted","not-started"].includes(a.state)||dto.error.code==="uncertain"&&!['started','unknown'].includes(a.state))return false;
|
|
43
|
+
if(dto.error.code==="editor_configured"&&(dto.action!=="shelveset-create"||r===null||(r as any).comment!==null||(r as any).commentsFile!==null||a.state!=="not-attempted"||c||o||id))return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export async function executeObjectWriteOutput(a:ObjectWriteAction,args:unknown) {
|
|
47
|
+
let dto=await assembleObjectWriteReceipt(a,args);
|
|
48
|
+
if(!validateObjectWriteOutput(dto)||dto.action!==a) {
|
|
49
|
+
const d=emptyObjectWriteData(a);d.attempt.state="unknown";d.effect="uncertain";
|
|
50
|
+
dto=objectWriteFailure(a,d,"producer_failed","Object-write receipt invalid; earlier creation/application/update effects may exist. No automatic retry or verification.");
|
|
51
|
+
if(!validateObjectWriteOutput(dto))throw Error("Safe object-write receipt unavailable; earlier effects may exist.");
|
|
52
|
+
}
|
|
53
|
+
return {content:[{type:"text" as const,text:presentObjectWriteReceipt(dto)}],details:{},structuredContent:dto,isError:!dto.ok};
|
|
54
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { assemblePatchReceipt, emptyPatchData, patchFailure, patchReceiptArgv, presentPatchReceipt, validPatchRequest, PATCH_FILE_MAX_BYTES, PATCH_RECEIPT_MAX_BYTES, type PatchReceipt } from "../operations/patch-receipt";
|
|
6
|
+
const obj=(p:Record<string,any>)=>Type.Object(p,{additionalProperties:false});
|
|
7
|
+
const 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}),text=Type.String({minLength:1,maxLength:8192});
|
|
8
|
+
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()});
|
|
9
|
+
const capture=obj({stdoutBytes:uint(),stderrBytes:uint(),stdoutRetainedBytes:uint(65536),stderrRetainedBytes:uint(16384),truncated:Type.Boolean(),complete:Type.Boolean(),validUtf8:Type.Boolean()});
|
|
10
|
+
const artifact=obj({bytes:uint(PATCH_FILE_MAX_BYTES),sha256:Type.String({pattern:"^[a-f0-9]{64}$"}),utf8:Type.Boolean(),empty:Type.Boolean(),binaryLimited:Type.Boolean(),totalChars:nil(uint(PATCH_FILE_MAX_BYTES)),truncated:Type.Boolean(),content:nil(Type.String({maxLength:60500}))});
|
|
11
|
+
const request=obj({source:text,destination:nil(text),output:nil(text),toolPath:nil(text),clean:Type.Boolean(),integration:Type.Boolean(),workdir:text,preflight:Type.Boolean(),format:enm(["text","json"])});
|
|
12
|
+
const data=obj({requested:nil(request),resolved:nil(obj({source:text,destination:nil(text),toolPath:text,output:nil(text)})),intendedArgv:Type.Array(text,{minItems:1,maxItems:7}),attempt,capture:nil(capture),artifact:nil(artifact),publication:enm(["not-requested","not-published","published","unknown"]),cleanup:enm(["not-created","completed","failed","retained","unknown"]),retainedStagingDirectory:nil(text),effect:enm(["not-attempted","unverified","artifact-observed","published"]),workspaceEffects:Type.Literal("unverified"),moveRepresentation:Type.Literal("backend-determined")});
|
|
13
|
+
const header={schemaVersion:Type.Literal(1),action:Type.Literal("patch"),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};
|
|
14
|
+
export const patchOutputSchema=Type.Unsafe<PatchReceipt>(Type.Union([obj({...header,ok:Type.Literal(true),outcome:enm(["preflight","generated","empty"])}),obj({...header,ok:Type.Literal(false),outcome:enm(["failed","uncertain"]),error:obj({code:enm(["invalid_request","setup_failed","command_failed","uncertain","backend_incompatible","artifact_failed","publication_failed","cleanup_failed","producer_failed"]),message:Type.String({minLength:1,maxLength:256})})})]));
|
|
15
|
+
const safe=(s:string)=>s.trim().length>0&&!/[\u0000-\u001f\u007f-\u009f]/.test(s);
|
|
16
|
+
function resolvedSelector(requested:string,observed:string) {const s=requested.trim();return observed===s||/^br:\/.+/i.test(s)&&!s.includes("@")&&observed.startsWith(s+"@")&&observed.slice(s.length+1).split("@").every(p=>p.trim().length>0&&p===p.trim());}
|
|
17
|
+
export function validatePatchOutput(v:unknown):v is PatchReceipt {
|
|
18
|
+
if(!Check(patchOutputSchema,v)||Buffer.byteLength(JSON.stringify(v),"utf8")>PATCH_RECEIPT_MAX_BYTES)return false;
|
|
19
|
+
const dto=v as PatchReceipt,d=dto.data,r=d.requested,z=d.resolved,a=d.attempt,c=d.capture,f=d.artifact;
|
|
20
|
+
if(dto.completeness.capture!==(c?c.complete?"complete":"incomplete":"unknown"))return false;
|
|
21
|
+
if(r===null) {
|
|
22
|
+
if(z||c||f||JSON.stringify(d.intendedArgv)!=='["patch"]'||d.retainedStagingDirectory!==null||a.terminal!=="not-observed"||a.exitCode!==null||a.aborted||a.timedOut)return false;
|
|
23
|
+
if(a.state==="not-attempted")return !dto.ok&&dto.outcome==="failed"&&dto.error.code==="invalid_request"&&d.effect==="not-attempted"&&d.cleanup==="not-created"&&d.publication==="not-requested";
|
|
24
|
+
return !dto.ok&&dto.outcome==="uncertain"&&dto.error.code==="producer_failed"&&a.state==="unknown"&&d.effect==="unverified"&&d.cleanup==="unknown"&&d.publication==="unknown";
|
|
25
|
+
}
|
|
26
|
+
if(!validPatchRequest(r))return false;
|
|
27
|
+
if(z) {
|
|
28
|
+
if(!Object.values(z).every(s=>s===null||safe(s))||!resolvedSelector(r.source,z.source)||(r.destination===null?z.destination!==null:z.destination===null||!resolvedSelector(r.destination,z.destination))||JSON.stringify(d.intendedArgv)!==JSON.stringify(patchReceiptArgv(r,z)))return false;
|
|
29
|
+
if(r.toolPath!==null&&z.toolPath!==r.toolPath.trim()||r.output===null&&z.output!==null||r.output!==null&&z.output!==(isAbsolute(r.output)?r.output:resolve(r.workdir,r.output)))return false;
|
|
30
|
+
const tails=[r.source,r.destination].map((s,i)=>s!==null&&/^br:\/.+/i.test(s.trim())&&!s.includes("@")?(i===0?z.source:z.destination)!.slice(s.trim().length):null).filter(s=>s!==null);
|
|
31
|
+
if(tails.some(t=>t!==tails[0]))return false;
|
|
32
|
+
} else if(c||f||JSON.stringify(d.intendedArgv)!=='["patch"]'||a.state!=="not-attempted"||d.cleanup!=="not-created")return false;
|
|
33
|
+
if(a.exitCode!==null&&a.terminal!=="observed"||["not-attempted","not-started"].includes(a.state)&&(a.exitCode!==null||a.terminal!=="not-observed")||a.state==="not-attempted"&&a.timedOut)return false;
|
|
34
|
+
if(c) {
|
|
35
|
+
if(!z||c.stdoutRetainedBytes>c.stdoutBytes||c.stderrRetainedBytes>c.stderrBytes)return false;
|
|
36
|
+
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;
|
|
37
|
+
}
|
|
38
|
+
if(dto.completeness.capture!==(c?c.complete?"complete":"incomplete":"unknown"))return false;
|
|
39
|
+
if((d.retainedStagingDirectory!==null)!==["failed","retained"].includes(d.cleanup)||["failed","retained"].includes(d.cleanup)&&!safe(d.retainedStagingDirectory!))return false;
|
|
40
|
+
if(d.cleanup==="retained"&&(a.terminal==="observed"&&!a.aborted&&!a.timedOut||!["started","unknown"].includes(a.state)))return false;
|
|
41
|
+
if(f) {
|
|
42
|
+
if(f.utf8&&(f.totalChars===null||f.totalChars>f.bytes||f.bytes>3*f.totalChars))return false;
|
|
43
|
+
if(!c?.complete||a.state!=="started"||a.exitCode!==0||c.stderrBytes!==0||f.empty!==(f.bytes===0)||!f.utf8&&!f.binaryLimited||!f.utf8&&(f.content!==null||f.totalChars!==null||f.truncated)||f.utf8&&f.totalChars===null)return false;
|
|
44
|
+
if(r.output!==null&&(f.content!==null||f.truncated)||r.output===null&&f.utf8&&f.content===null)return false;
|
|
45
|
+
if(r.output===null&&f.utf8) {
|
|
46
|
+
if(f.truncated!==(f.totalChars!>60000))return false;
|
|
47
|
+
if(!f.truncated&&(f.totalChars!==f.content!.length||f.bytes!==Buffer.byteLength(f.content!,"utf8")||f.sha256!==createHash("sha256").update(f.content!).digest("hex")))return false;
|
|
48
|
+
if(f.truncated&&f.content!.length<=60000)return false;
|
|
49
|
+
}
|
|
50
|
+
if(f.empty&&(!f.utf8||f.totalChars!==0||f.sha256!==createHash("sha256").update("").digest("hex")))return false;
|
|
51
|
+
}
|
|
52
|
+
if(d.publication==="published"&&(!f||f.empty||r.output===null||d.effect!=="published")||d.publication!=="published"&&d.effect==="published")return false;
|
|
53
|
+
if(d.publication!=="published"&&d.publication!==(r.output===null?"not-requested":"not-published"))return false;
|
|
54
|
+
if(d.effect!==(d.publication==="published"?"published":f?"artifact-observed":["started","unknown"].includes(a.state)?"unverified":"not-attempted"))return false;
|
|
55
|
+
if(r.preflight)return !c&&!f&&a.state==="not-attempted"&&!a.aborted&&d.cleanup==="not-created"&&(dto.ok?dto.outcome==="preflight"&&!!z:dto.outcome==="failed"&&dto.error.code==="setup_failed");
|
|
56
|
+
if(dto.ok)return !!z&&!!f&&d.cleanup==="completed"&&(dto.outcome==="empty"?f.empty:dto.outcome==="generated"&&!f.empty)&&(r.output===null||d.publication==="published");
|
|
57
|
+
if(dto.outcome!==(["started","unknown"].includes(a.state)&&!f?"uncertain":"failed"))return false;
|
|
58
|
+
if(dto.error.code==="invalid_request"||dto.error.code==="setup_failed"&&(a.state!=="not-attempted"||c||f)||dto.error.code==="uncertain"&&!["started","unknown"].includes(a.state)||["publication_failed","cleanup_failed"].includes(dto.error.code)&&!f)return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
export async function executePatchOutput(args:unknown) {
|
|
62
|
+
let dto=await assemblePatchReceipt(args);
|
|
63
|
+
if(!validatePatchOutput(dto)) {
|
|
64
|
+
const d=emptyPatchData();d.attempt.state="unknown";d.effect="unverified";d.publication="unknown";d.cleanup="unknown";
|
|
65
|
+
dto=patchFailure(d,"producer_failed","Patch receipt invalid; generation/publication and staging effects may already exist. No automatic retry or cleanup.");
|
|
66
|
+
if(!validatePatchOutput(dto))throw new Error("Safe patch receipt unavailable; earlier effects may exist.");
|
|
67
|
+
}
|
|
68
|
+
return {content:[{type:"text" as const,text:presentPatchReceipt(dto)}],details:{},structuredContent:dto,isError:!dto.ok};
|
|
69
|
+
}
|