@astrosheep/keiyaku 2.9.8 → 2.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -182
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/control-types.js +18 -1
- package/build/agents/harness/index.js +2 -0
- package/build/agents/harness/outcome-error.js +81 -0
- package/build/agents/harness/projection.js +10 -7
- package/build/agents/harness/router.js +5 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
- package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
- package/build/agents/providers/claude-agent-sdk/session.js +19 -5
- package/build/agents/providers/codex-app-server/adapter.js +11 -6
- package/build/agents/providers/codex-app-server/events.js +67 -6
- package/build/agents/providers/codex-app-server/registration.js +11 -1
- package/build/agents/providers/opencode-sdk/adapter.js +8 -1
- package/build/agents/providers/opencode-sdk/events.js +24 -2
- package/build/agents/providers/opencode-sdk/registration.js +11 -1
- package/build/agents/providers/pi/checkpoint.js +9 -1
- package/build/agents/providers/pi/events.js +2 -0
- package/build/agents/providers/pi/registration.js +14 -1
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +3 -3
- package/build/cli/commands/akuma/catalog.js +2 -2
- package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
- package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +5 -4
- package/build/cli/commands/contract/arc/handler.js +5 -4
- package/build/cli/commands/contract/audit/handler.js +2 -2
- package/build/cli/commands/contract/bind/handler.js +12 -3
- package/build/cli/commands/contract/bind/meta.js +45 -15
- package/build/cli/commands/contract/forfeit/handler.js +9 -5
- package/build/cli/commands/contract/log/handler.js +2 -2
- package/build/cli/commands/contract/petition/handler.js +34 -7
- package/build/cli/commands/contract/renew/handler.js +7 -7
- package/build/cli/commands/contract/renew/meta.js +3 -3
- package/build/cli/commands/metadata.js +10 -6
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -0
- package/build/cli/commands/projection/history/handler.js +21 -0
- package/build/cli/commands/projection/history/meta.js +9 -0
- package/build/cli/commands/projection/shared.js +3 -4
- package/build/cli/commands/projection/status/handler.js +3 -3
- package/build/cli/commands/projection/wait/handler.js +20 -11
- package/build/cli/commands/task/add/handler.js +14 -5
- package/build/cli/commands/task/add/meta.js +2 -2
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/done/handler.js +1 -4
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/hold/handler.js +3 -0
- package/build/cli/commands/task/hold/meta.js +8 -0
- package/build/cli/commands/task/log/handler.js +1 -1
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/resume/handler.js +3 -0
- package/build/cli/commands/task/resume/meta.js +8 -0
- package/build/cli/commands/task/shared.js +39 -14
- package/build/cli/commands/task/show/handler.js +1 -1
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +1 -4
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +11 -3
- package/build/cli/commands/task/update/meta.js +3 -3
- package/build/cli/completion.js +3 -2
- package/build/cli/flags.js +51 -14
- package/build/cli/help.js +4 -1
- package/build/cli/index.js +40 -25
- package/build/cli/parse-flags.js +40 -7
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +2 -3
- package/build/cli/parse.js +40 -6
- package/build/cli/projection-address.js +12 -10
- package/build/cli/render/amendment-warning.js +8 -0
- package/build/cli/render/arc.js +6 -2
- package/build/cli/render/audit.js +400 -81
- package/build/cli/render/kanshi.js +242 -79
- package/build/cli/render/lifecycle.js +56 -0
- package/build/cli/render/misc.js +24 -28
- package/build/cli/render/petition.js +5 -0
- package/build/cli/render/projection-activity.js +65 -29
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +13 -35
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +45 -69
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +24 -1
- package/build/config/settings/schema.js +3 -1
- package/build/core/address-carrier.js +89 -0
- package/build/core/addressing.js +255 -196
- package/build/core/amend.js +115 -38
- package/build/core/amendment-criteria.js +17 -0
- package/build/core/amendment-warning.js +10 -0
- package/build/core/arc.js +184 -83
- package/build/core/audit/candidate.js +110 -9
- package/build/core/audit/coordinates.js +120 -65
- package/build/core/audit/evidence.js +19 -23
- package/build/core/audit/facade.js +34 -11
- package/build/core/audit/report.js +5 -5
- package/build/core/bind-reconciliation.js +390 -0
- package/build/core/bind-workspace.js +12 -20
- package/build/core/bind.js +242 -273
- package/build/core/call/call.js +1 -0
- package/build/core/call/context.js +46 -71
- package/build/core/call/prompt.js +6 -3
- package/build/core/context.js +49 -35
- package/build/core/contract-carrier-runtime.js +408 -0
- package/build/core/contract-carrier.js +270 -0
- package/build/core/contract-view.js +33 -27
- package/build/core/contract.js +5 -1
- package/build/core/derived-replay.js +207 -0
- package/build/core/draft.js +125 -57
- package/build/core/forfeit.js +113 -56
- package/build/core/identifier-slug.js +63 -13
- package/build/core/ids.js +4 -4
- package/build/core/lifecycle-history.js +35 -0
- package/build/core/lifecycle-recovery.js +117 -0
- package/build/core/lifecycle-runner.js +377 -0
- package/build/core/log.js +37 -11
- package/build/core/outcome-base.js +181 -0
- package/build/core/process-group.js +139 -0
- package/build/core/projection/generation/database.js +82 -7
- package/build/core/projection/generation/model.js +9 -36
- package/build/core/projection/generation/projection-generation-continuation.js +10 -22
- package/build/core/projection/generation/projection-generation-execution.js +13 -6
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +29 -8
- package/build/core/projection/projection-alias.js +23 -10
- package/build/core/projection/projection-core.js +21 -7
- package/build/core/projection/projection-execution-observer.js +91 -39
- package/build/core/projection/projection-history.js +474 -0
- package/build/core/projection/projection-runner-lock.js +12 -0
- package/build/core/projection/projection-status-observation.js +365 -0
- package/build/core/projection/projection-status.js +35 -337
- package/build/core/projection/projection-terminal-failure.js +13 -8
- package/build/core/projection/projection-wait.js +7 -2
- package/build/core/projection/tell/launch-store.js +244 -0
- package/build/core/projection/tell/model.js +3 -3
- package/build/core/projection/tell/store.js +70 -118
- package/build/core/registry.js +22 -87
- package/build/core/renew-build.js +424 -0
- package/build/core/renew-plan.js +239 -0
- package/build/core/renew-rewrite.js +77 -0
- package/build/core/renew.js +295 -520
- package/build/core/repository-ledger/accepted-fold-read.js +19 -0
- package/build/core/repository-ledger/accepted-tail.js +116 -0
- package/build/core/repository-ledger/admission.js +38 -0
- package/build/core/repository-ledger/authoritative-transaction.js +1 -0
- package/build/core/repository-ledger/claim-evidence.js +147 -0
- package/build/core/repository-ledger/codec.js +297 -0
- package/build/core/repository-ledger/current-state-store.js +853 -0
- package/build/core/repository-ledger/fold-repository.js +375 -0
- package/build/core/repository-ledger/fold.js +248 -0
- package/build/core/repository-ledger/git-object-store.js +243 -0
- package/build/core/repository-ledger/history-read.js +15 -0
- package/build/core/repository-ledger/incremental-read.js +153 -0
- package/build/core/repository-ledger/index.js +15 -0
- package/build/core/repository-ledger/inventory-plan.js +75 -0
- package/build/core/repository-ledger/inventory.js +310 -0
- package/build/core/repository-ledger/publication-admission.js +125 -0
- package/build/core/repository-ledger/publication.js +353 -0
- package/build/core/repository-ledger/read-model.js +469 -0
- package/build/core/repository-ledger/transaction-membership.js +78 -0
- package/build/core/repository-ledger/traversal.js +404 -0
- package/build/core/repository-ledger/write-transaction.js +339 -0
- package/build/core/seal.js +168 -161
- package/build/core/settlement/claim-delivery.js +42 -215
- package/build/core/settlement/claim.js +235 -153
- package/build/core/settlement/index.js +4 -4
- package/build/core/settlement/petition-claim-gates.js +52 -33
- package/build/core/settlement/petition-forfeit.js +12 -5
- package/build/core/settlement/petition-head-guard.js +8 -5
- package/build/core/settlement/petition-preview.js +68 -56
- package/build/core/settlement/petition-window.js +50 -0
- package/build/core/settlement/petition.js +252 -131
- package/build/core/settlement/queue-read-model.js +72 -95
- package/build/core/settlement/queue-seat-allocation.js +35 -126
- package/build/core/settlement/queue.js +1 -1
- package/build/core/settlement/settlement.js +172 -80
- package/build/core/settlement/verdict.js +112 -66
- package/build/core/settlement/verification-coordination.js +305 -0
- package/build/core/settlement/verification-supervisor.js +275 -0
- package/build/core/settlement/verification.js +398 -31
- package/build/core/status/board.js +125 -127
- package/build/core/status/lifecycle.js +7 -80
- package/build/core/structured-query.js +23 -0
- package/build/core/target-ref.js +1 -1
- package/build/core/task/board.js +59 -24
- package/build/core/task/commands.js +181 -33
- package/build/core/task/coordinate.js +56 -0
- package/build/core/task/document.js +25 -9
- package/build/core/task/index.js +7 -4
- package/build/core/task/query.js +17 -27
- package/build/core/task/settlement-git.js +164 -179
- package/build/core/task/settlement-policy.js +15 -11
- package/build/core/task/source-board.js +31 -13
- package/build/core/task/task-bind-preparation.js +193 -0
- package/build/core/task/task-contract.js +102 -193
- package/build/core/task/task-file-transaction.js +171 -0
- package/build/core/task/task-git-runtime.js +76 -14
- package/build/core/task/task-git-store.js +68 -169
- package/build/core/task/task-store-repository.js +13 -6
- package/build/core/task/task-worktree.js +6 -33
- package/build/core/task/task.js +1 -1
- package/build/core/task/tree.js +3 -3
- package/build/core/task/validation.js +4 -3
- package/build/core/transcripts.js +93 -1
- package/build/core/worktree-bootstrap.js +44 -36
- package/build/core/worktree-path.js +210 -77
- package/build/flow-error.js +7 -7
- package/build/generated/version.js +2 -2
- package/build/git/core.js +76 -13
- package/build/git/refs.js +168 -46
- package/build/git/streaming-batch.js +73 -13
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +3 -3
- package/skills/keiyaku-task/SKILL.md +4 -2
- package/skills/keiyaku-workflow/SKILL.md +88 -203
- package/build/core/entry.js +0 -305
- package/build/core/ledger-batch.js +0 -194
- package/build/core/ledger.js +0 -84
- package/build/core/projection/leash.js +0 -87
- package/build/core/ref-log.js +0 -119
- package/build/core/renew-session.js +0 -128
- package/build/core/status/ledger-batch.js +0 -1
- package/build/core/status/reconciliation.js +0 -137
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertHostObjectId, deriveHostGitCoordinates, readCommitObject } from "./repository-ledger/git-object-store.js";
|
|
4
|
+
import { runGitProcess, runGitProcessBytes } from "../git/core.js";
|
|
5
|
+
import { stableRepoRoot } from "./worktree-path.js";
|
|
6
|
+
const SCRATCH_VERSION = 1;
|
|
7
|
+
const SCRATCH_DIRECTORY = path.join("keiyaku", "renew-scratch");
|
|
8
|
+
const SCRATCH_STATE_FILE = "state.json";
|
|
9
|
+
const SCRATCH_FILES_DIRECTORY = "files";
|
|
10
|
+
const SCRATCH_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
11
|
+
export class RenewBuildError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "RenewBuildError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function assertScratchName(scratchName) {
|
|
20
|
+
if (!SCRATCH_NAME_PATTERN.test(scratchName)) {
|
|
21
|
+
throw new RenewBuildError("INVALID_SCRATCH_NAME", `invalid renew scratch name ${JSON.stringify(scratchName)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function scratchPaths(scratchBase, scratchName) {
|
|
25
|
+
assertScratchName(scratchName);
|
|
26
|
+
const root = path.join(scratchBase, scratchName);
|
|
27
|
+
return {
|
|
28
|
+
root,
|
|
29
|
+
state: path.join(root, SCRATCH_STATE_FILE),
|
|
30
|
+
files: path.join(root, SCRATCH_FILES_DIRECTORY),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function renewScratchBase(cwd) {
|
|
34
|
+
const commonDirectory = (await successfulGit(cwd, [
|
|
35
|
+
"rev-parse",
|
|
36
|
+
"--path-format=absolute",
|
|
37
|
+
"--git-common-dir",
|
|
38
|
+
])).trim();
|
|
39
|
+
if (!path.isAbsolute(commonDirectory)) {
|
|
40
|
+
throw new RenewBuildError("GIT_RENEW_BUILD_FAILED", `Git common directory is not absolute: ${commonDirectory}`);
|
|
41
|
+
}
|
|
42
|
+
return path.join(await fs.realpath(commonDirectory), SCRATCH_DIRECTORY);
|
|
43
|
+
}
|
|
44
|
+
function gitFailure(args, detail, cause) {
|
|
45
|
+
return new RenewBuildError("GIT_RENEW_BUILD_FAILED", `git ${args.join(" ")} failed: ${detail}`, cause ? { cause } : undefined);
|
|
46
|
+
}
|
|
47
|
+
export async function successfulGit(cwd, args, options) {
|
|
48
|
+
const result = await runGitProcess(cwd, args, options);
|
|
49
|
+
if (result.status !== 0 || result.error) {
|
|
50
|
+
throw gitFailure(args, result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`, result.error);
|
|
51
|
+
}
|
|
52
|
+
return result.stdout;
|
|
53
|
+
}
|
|
54
|
+
export async function assertTree(cwd, coordinates, tree) {
|
|
55
|
+
assertHostObjectId(coordinates, tree);
|
|
56
|
+
const result = await runGitProcess(cwd, ["cat-file", "-t", tree]);
|
|
57
|
+
if (result.status !== 0 || result.error || result.stdout.trim() !== "tree") {
|
|
58
|
+
throw new RenewBuildError("INVALID_RENEW_RESOLUTION", `renew resolution ${tree} is not a tree`, {
|
|
59
|
+
cause: result.error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseIdentity(header, label, objectId) {
|
|
64
|
+
const value = header.slice(label.length + 1);
|
|
65
|
+
const emailEnd = value.lastIndexOf("> ");
|
|
66
|
+
const emailStart = emailEnd < 0 ? -1 : value.lastIndexOf(" <", emailEnd);
|
|
67
|
+
if (emailStart <= 0 || emailEnd <= emailStart || !/^\d+ [+-]\d{4}$/.test(value.slice(emailEnd + 2))) {
|
|
68
|
+
throw new RenewBuildError("INVALID_DELIVERY_CHAIN", `commit ${objectId} has an invalid ${label} header`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: value.slice(0, emailStart),
|
|
72
|
+
email: value.slice(emailStart + 2, emailEnd),
|
|
73
|
+
date: value.slice(emailEnd + 2),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function readOriginalCommit(cwd, coordinates, objectId, expectedParent) {
|
|
77
|
+
const commit = await readCommitObject(cwd, coordinates, objectId);
|
|
78
|
+
if (commit.parents.length !== 1 || commit.parents[0] !== expectedParent) {
|
|
79
|
+
throw new RenewBuildError("INVALID_DELIVERY_CHAIN", `delivery commit ${objectId} must have exactly parent ${expectedParent}`);
|
|
80
|
+
}
|
|
81
|
+
const raw = await successfulGit(cwd, ["cat-file", "commit", objectId]);
|
|
82
|
+
const headers = raw.slice(0, raw.indexOf("\n\n")).split("\n");
|
|
83
|
+
const authorRows = headers.filter((row) => row.startsWith("author "));
|
|
84
|
+
const committerRows = headers.filter((row) => row.startsWith("committer "));
|
|
85
|
+
if (authorRows.length !== 1 || committerRows.length !== 1) {
|
|
86
|
+
throw new RenewBuildError("INVALID_DELIVERY_CHAIN", `commit ${objectId} has invalid identity headers`);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
objectId,
|
|
90
|
+
parent: expectedParent,
|
|
91
|
+
message: commit.messageBytes,
|
|
92
|
+
author: parseIdentity(authorRows[0], "author", objectId),
|
|
93
|
+
committer: parseIdentity(committerRows[0], "committer", objectId),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function validateCoordinates(input) {
|
|
97
|
+
for (const coordinate of [input.oldBase, input.oldHead, input.newBase]) {
|
|
98
|
+
assertHostObjectId(input.coordinates, coordinate);
|
|
99
|
+
await readCommitObject(input.cwd, input.coordinates, coordinate);
|
|
100
|
+
}
|
|
101
|
+
if (input.deliveryCommits.length === 0) {
|
|
102
|
+
if (input.oldHead !== input.oldBase) {
|
|
103
|
+
throw new RenewBuildError("INVALID_DELIVERY_CHAIN", "an empty delivery chain requires oldHead to equal oldBase");
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
if (input.deliveryCommits.at(-1) !== input.oldHead) {
|
|
108
|
+
throw new RenewBuildError("INVALID_DELIVERY_CHAIN", "the delivery chain must terminate at oldHead");
|
|
109
|
+
}
|
|
110
|
+
const commits = [];
|
|
111
|
+
let parent = input.oldBase;
|
|
112
|
+
for (const objectId of input.deliveryCommits) {
|
|
113
|
+
assertHostObjectId(input.coordinates, objectId);
|
|
114
|
+
const commit = await readOriginalCommit(input.cwd, input.coordinates, objectId, parent);
|
|
115
|
+
commits.push(commit);
|
|
116
|
+
parent = objectId;
|
|
117
|
+
}
|
|
118
|
+
return commits;
|
|
119
|
+
}
|
|
120
|
+
async function mergeCommitTree(input) {
|
|
121
|
+
const args = [
|
|
122
|
+
"--no-replace-objects",
|
|
123
|
+
"merge-tree",
|
|
124
|
+
"--write-tree",
|
|
125
|
+
`--merge-base=${input.original.parent}`,
|
|
126
|
+
input.currentHead,
|
|
127
|
+
input.original.objectId,
|
|
128
|
+
];
|
|
129
|
+
const result = await runGitProcess(input.cwd, args);
|
|
130
|
+
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
131
|
+
throw gitFailure(args, result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`, result.error);
|
|
132
|
+
}
|
|
133
|
+
const lines = result.stdout.split(/\r?\n/);
|
|
134
|
+
const tree = lines[0]?.trim() ?? "";
|
|
135
|
+
assertHostObjectId(input.coordinates, tree);
|
|
136
|
+
await assertTree(input.cwd, input.coordinates, tree);
|
|
137
|
+
if (result.status === 0)
|
|
138
|
+
return { status: "clean", tree };
|
|
139
|
+
const paths = Array.from(new Set(lines.flatMap((line) => {
|
|
140
|
+
const match = /^\d{6} [0-9a-f]+ [123]\t(.+)$/.exec(line);
|
|
141
|
+
return match ? [match[1]] : [];
|
|
142
|
+
})));
|
|
143
|
+
return { status: "conflict", tree, paths };
|
|
144
|
+
}
|
|
145
|
+
export async function buildCommit(cwd, coordinates, original, tree, parent) {
|
|
146
|
+
await assertTree(cwd, coordinates, tree);
|
|
147
|
+
const args = ["commit-tree", tree, "-p", parent];
|
|
148
|
+
const result = await runGitProcessBytes(cwd, args, {
|
|
149
|
+
input: original.message,
|
|
150
|
+
env: {
|
|
151
|
+
GIT_AUTHOR_NAME: original.author.name,
|
|
152
|
+
GIT_AUTHOR_EMAIL: original.author.email,
|
|
153
|
+
GIT_AUTHOR_DATE: original.author.date,
|
|
154
|
+
GIT_COMMITTER_NAME: original.committer.name,
|
|
155
|
+
GIT_COMMITTER_EMAIL: original.committer.email,
|
|
156
|
+
GIT_COMMITTER_DATE: original.committer.date,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
if (result.status !== 0 || result.error) {
|
|
160
|
+
throw gitFailure(args, result.stderr.toString("utf8").trim() || result.error?.message || `exit ${String(result.status)}`, result.error);
|
|
161
|
+
}
|
|
162
|
+
const objectId = result.stdout.toString("utf8").trim();
|
|
163
|
+
assertHostObjectId(coordinates, objectId);
|
|
164
|
+
return objectId;
|
|
165
|
+
}
|
|
166
|
+
export function interrupted(signal) {
|
|
167
|
+
if (signal?.aborted) {
|
|
168
|
+
throw new RenewBuildError("RENEW_BUILD_INTERRUPTED", "detached renew build was interrupted", {
|
|
169
|
+
cause: signal.reason instanceof Error ? signal.reason : undefined,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function materializeConflict(cwd, scratch, tree) {
|
|
174
|
+
await fs.rm(scratch.files, { recursive: true, force: true });
|
|
175
|
+
await fs.mkdir(scratch.files, { recursive: true });
|
|
176
|
+
const listingArgs = ["ls-tree", "-r", "-z", "--full-tree", tree];
|
|
177
|
+
const listing = await runGitProcessBytes(cwd, listingArgs);
|
|
178
|
+
if (listing.status !== 0 || listing.error) {
|
|
179
|
+
throw gitFailure(listingArgs, listing.stderr.toString("utf8").trim() || listing.error?.message || `exit ${String(listing.status)}`, listing.error);
|
|
180
|
+
}
|
|
181
|
+
for (const row of listing.stdout.toString("utf8").split("\0").filter(Boolean)) {
|
|
182
|
+
const match = /^(\d{6}) (blob|commit) ([0-9a-f]+)\t([\s\S]+)$/.exec(row);
|
|
183
|
+
if (!match)
|
|
184
|
+
throw new RenewBuildError("GIT_RENEW_BUILD_FAILED", `invalid ls-tree row while materializing renew scratch`);
|
|
185
|
+
const [, mode, type, objectId, relative] = match;
|
|
186
|
+
const destination = path.resolve(scratch.files, relative);
|
|
187
|
+
const withinScratch = path.relative(scratch.files, destination);
|
|
188
|
+
if (withinScratch === "" || path.isAbsolute(withinScratch) || withinScratch === ".." || withinScratch.startsWith(`..${path.sep}`)) {
|
|
189
|
+
throw new RenewBuildError("GIT_RENEW_BUILD_FAILED", `unsafe path in renew conflict tree: ${JSON.stringify(relative)}`);
|
|
190
|
+
}
|
|
191
|
+
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
192
|
+
if (type === "commit") {
|
|
193
|
+
await fs.writeFile(destination, `${objectId}\n`, "utf8");
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const blobArgs = ["cat-file", "blob", objectId];
|
|
197
|
+
const blob = await runGitProcessBytes(cwd, blobArgs);
|
|
198
|
+
if (blob.status !== 0 || blob.error) {
|
|
199
|
+
throw gitFailure(blobArgs, blob.stderr.toString("utf8").trim() || blob.error?.message || `exit ${String(blob.status)}`, blob.error);
|
|
200
|
+
}
|
|
201
|
+
if (mode === "120000") {
|
|
202
|
+
await fs.symlink(blob.stdout.toString("utf8"), destination);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
await fs.writeFile(destination, blob.stdout, { mode: mode === "100755" ? 0o755 : 0o644 });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function writeScratchState(scratch, state) {
|
|
210
|
+
const temporary = `${scratch.state}.next`;
|
|
211
|
+
await fs.rm(temporary, { force: true });
|
|
212
|
+
await fs.writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
213
|
+
await fs.rename(temporary, scratch.state);
|
|
214
|
+
}
|
|
215
|
+
export function evidenceResult(state) {
|
|
216
|
+
return {
|
|
217
|
+
observedLedgerHead: state.observedLedgerHead,
|
|
218
|
+
deliveryRef: state.deliveryRef,
|
|
219
|
+
targetRef: state.targetRef,
|
|
220
|
+
effectiveScope: [...state.effectiveScope],
|
|
221
|
+
oldBase: state.oldBase,
|
|
222
|
+
oldHead: state.oldHead,
|
|
223
|
+
newBase: state.newBase,
|
|
224
|
+
deliveryCommits: [...state.deliveryCommits],
|
|
225
|
+
evidenceMap: { ...state.evidenceMap },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function persistConflict(cwd, scratchBase, state, create) {
|
|
229
|
+
const scratch = scratchPaths(scratchBase, state.scratchName);
|
|
230
|
+
if (create) {
|
|
231
|
+
await fs.mkdir(path.dirname(scratch.root), { recursive: true });
|
|
232
|
+
try {
|
|
233
|
+
await fs.mkdir(scratch.root);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error.code === "EEXIST") {
|
|
237
|
+
throw new RenewBuildError("RENEW_SCRATCH_EXISTS", `renew scratch ${state.scratchName} already exists`, { cause: error });
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await materializeConflict(cwd, scratch, state.conflictTree);
|
|
243
|
+
await writeScratchState(scratch, state);
|
|
244
|
+
return {
|
|
245
|
+
status: "conflict",
|
|
246
|
+
...evidenceResult(state),
|
|
247
|
+
scratchName: state.scratchName,
|
|
248
|
+
scratchPath: scratch.root,
|
|
249
|
+
filesPath: scratch.files,
|
|
250
|
+
conflictCommit: state.conflictCommit,
|
|
251
|
+
conflictPaths: [...state.conflictPaths],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
export async function replayFrom(input) {
|
|
255
|
+
let currentHead = input.state.currentHead;
|
|
256
|
+
for (let index = input.state.nextIndex; index < input.commits.length; index += 1) {
|
|
257
|
+
interrupted(input.signal);
|
|
258
|
+
const original = input.commits[index];
|
|
259
|
+
const merged = await mergeCommitTree({
|
|
260
|
+
cwd: input.cwd,
|
|
261
|
+
coordinates: input.coordinates,
|
|
262
|
+
original,
|
|
263
|
+
currentHead,
|
|
264
|
+
});
|
|
265
|
+
if (merged.status === "conflict") {
|
|
266
|
+
const state = {
|
|
267
|
+
...input.state,
|
|
268
|
+
nextIndex: index,
|
|
269
|
+
currentHead,
|
|
270
|
+
conflictCommit: original.objectId,
|
|
271
|
+
conflictTree: merged.tree,
|
|
272
|
+
conflictPaths: merged.paths,
|
|
273
|
+
};
|
|
274
|
+
return await persistConflict(input.cwd, input.scratchBase, state, input.createScratch);
|
|
275
|
+
}
|
|
276
|
+
currentHead = await buildCommit(input.cwd, input.coordinates, original, merged.tree, currentHead);
|
|
277
|
+
input.state.evidenceMap[original.objectId] = currentHead;
|
|
278
|
+
input.state.currentHead = currentHead;
|
|
279
|
+
input.state.nextIndex = index + 1;
|
|
280
|
+
}
|
|
281
|
+
interrupted(input.signal);
|
|
282
|
+
return {
|
|
283
|
+
status: "clean",
|
|
284
|
+
...evidenceResult(input.state),
|
|
285
|
+
newHead: currentHead,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export async function buildDetachedRenewChain(input) {
|
|
289
|
+
assertScratchName(input.scratchName);
|
|
290
|
+
const repositoryRoot = await stableRepoRoot(input.cwd);
|
|
291
|
+
const scratchBase = await renewScratchBase(repositoryRoot);
|
|
292
|
+
const coordinates = await deriveHostGitCoordinates(repositoryRoot);
|
|
293
|
+
const commits = await validateCoordinates({ ...input, cwd: repositoryRoot, coordinates });
|
|
294
|
+
const existingScratch = scratchPaths(scratchBase, input.scratchName);
|
|
295
|
+
try {
|
|
296
|
+
await fs.lstat(existingScratch.root);
|
|
297
|
+
const state = await readScratchState(existingScratch);
|
|
298
|
+
if (state.observedLedgerHead !== input.observedLedgerHead
|
|
299
|
+
|| state.deliveryRef !== input.deliveryRef
|
|
300
|
+
|| state.targetRef !== input.targetRef
|
|
301
|
+
|| state.oldBase !== input.oldBase
|
|
302
|
+
|| state.oldHead !== input.oldHead
|
|
303
|
+
|| state.newBase !== input.newBase
|
|
304
|
+
|| JSON.stringify(state.effectiveScope) !== JSON.stringify(input.effectiveScope)
|
|
305
|
+
|| JSON.stringify(state.deliveryCommits) !== JSON.stringify(input.deliveryCommits)) {
|
|
306
|
+
throw new RenewBuildError("RENEW_SCRATCH_EXISTS", `renew scratch ${input.scratchName} belongs to different coordinates`);
|
|
307
|
+
}
|
|
308
|
+
await validateScratchProgress(repositoryRoot, coordinates, state, commits);
|
|
309
|
+
return {
|
|
310
|
+
status: "conflict",
|
|
311
|
+
...evidenceResult(state),
|
|
312
|
+
scratchName: state.scratchName,
|
|
313
|
+
scratchPath: existingScratch.root,
|
|
314
|
+
filesPath: existingScratch.files,
|
|
315
|
+
conflictCommit: state.conflictCommit,
|
|
316
|
+
conflictPaths: [...state.conflictPaths],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (error.code !== "ENOENT")
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
const state = {
|
|
324
|
+
version: SCRATCH_VERSION,
|
|
325
|
+
scratchName: input.scratchName,
|
|
326
|
+
observedLedgerHead: input.observedLedgerHead,
|
|
327
|
+
deliveryRef: input.deliveryRef,
|
|
328
|
+
targetRef: input.targetRef,
|
|
329
|
+
effectiveScope: [...input.effectiveScope],
|
|
330
|
+
oldBase: input.oldBase,
|
|
331
|
+
oldHead: input.oldHead,
|
|
332
|
+
newBase: input.newBase,
|
|
333
|
+
deliveryCommits: [...input.deliveryCommits],
|
|
334
|
+
evidenceMap: {},
|
|
335
|
+
nextIndex: 0,
|
|
336
|
+
currentHead: input.newBase,
|
|
337
|
+
conflictCommit: "",
|
|
338
|
+
conflictTree: "",
|
|
339
|
+
conflictPaths: [],
|
|
340
|
+
};
|
|
341
|
+
return await replayFrom({
|
|
342
|
+
cwd: repositoryRoot,
|
|
343
|
+
scratchBase,
|
|
344
|
+
coordinates,
|
|
345
|
+
state,
|
|
346
|
+
commits,
|
|
347
|
+
createScratch: true,
|
|
348
|
+
signal: input.signal,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function isRecord(value) {
|
|
352
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
353
|
+
}
|
|
354
|
+
export async function readScratchState(scratch) {
|
|
355
|
+
let parsed;
|
|
356
|
+
try {
|
|
357
|
+
parsed = JSON.parse(await fs.readFile(scratch.state, "utf8"));
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (error.code === "ENOENT") {
|
|
361
|
+
throw new RenewBuildError("RENEW_SCRATCH_NOT_FOUND", `renew scratch ${path.basename(scratch.root)} was not found`, { cause: error });
|
|
362
|
+
}
|
|
363
|
+
throw new RenewBuildError("INVALID_RENEW_SCRATCH", `cannot read renew scratch ${path.basename(scratch.root)}`, { cause: error });
|
|
364
|
+
}
|
|
365
|
+
if (!isRecord(parsed)
|
|
366
|
+
|| parsed.version !== SCRATCH_VERSION
|
|
367
|
+
|| parsed.scratchName !== path.basename(scratch.root)
|
|
368
|
+
|| typeof parsed.observedLedgerHead !== "string"
|
|
369
|
+
|| typeof parsed.deliveryRef !== "string"
|
|
370
|
+
|| typeof parsed.targetRef !== "string"
|
|
371
|
+
|| !Array.isArray(parsed.effectiveScope)
|
|
372
|
+
|| parsed.effectiveScope.some((value) => typeof value !== "string")
|
|
373
|
+
|| typeof parsed.oldBase !== "string"
|
|
374
|
+
|| typeof parsed.oldHead !== "string"
|
|
375
|
+
|| typeof parsed.newBase !== "string"
|
|
376
|
+
|| !Array.isArray(parsed.deliveryCommits)
|
|
377
|
+
|| parsed.deliveryCommits.some((value) => typeof value !== "string")
|
|
378
|
+
|| !isRecord(parsed.evidenceMap)
|
|
379
|
+
|| Object.values(parsed.evidenceMap).some((value) => typeof value !== "string")
|
|
380
|
+
|| !Number.isSafeInteger(parsed.nextIndex)
|
|
381
|
+
|| typeof parsed.currentHead !== "string"
|
|
382
|
+
|| typeof parsed.conflictCommit !== "string"
|
|
383
|
+
|| typeof parsed.conflictTree !== "string"
|
|
384
|
+
|| !Array.isArray(parsed.conflictPaths)
|
|
385
|
+
|| parsed.conflictPaths.some((value) => typeof value !== "string")) {
|
|
386
|
+
throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch ${path.basename(scratch.root)} has invalid state`);
|
|
387
|
+
}
|
|
388
|
+
return parsed;
|
|
389
|
+
}
|
|
390
|
+
export async function treeFromScratchFiles(cwd, coordinates, scratch, state) {
|
|
391
|
+
const index = path.join(scratch.root, "continue.index");
|
|
392
|
+
await fs.rm(index, { force: true });
|
|
393
|
+
const env = { GIT_INDEX_FILE: index, GIT_WORK_TREE: scratch.files };
|
|
394
|
+
try {
|
|
395
|
+
await successfulGit(cwd, ["read-tree", state.conflictTree], { env });
|
|
396
|
+
await successfulGit(cwd, ["add", "--all", "--", "."], { env });
|
|
397
|
+
const tree = (await successfulGit(cwd, ["write-tree"], { env })).trim();
|
|
398
|
+
await assertTree(cwd, coordinates, tree);
|
|
399
|
+
return tree;
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
await fs.rm(index, { force: true });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
export async function validateScratchProgress(cwd, coordinates, state, commits) {
|
|
406
|
+
if (state.nextIndex < 0 || state.nextIndex >= commits.length || commits[state.nextIndex]?.objectId !== state.conflictCommit) {
|
|
407
|
+
throw new RenewBuildError("INVALID_RENEW_SCRATCH", "renew scratch does not identify the stopped delivery commit");
|
|
408
|
+
}
|
|
409
|
+
let parent = state.newBase;
|
|
410
|
+
for (let index = 0; index < state.nextIndex; index += 1) {
|
|
411
|
+
const original = commits[index].objectId;
|
|
412
|
+
const rewritten = state.evidenceMap[original];
|
|
413
|
+
if (!rewritten)
|
|
414
|
+
throw new RenewBuildError("INVALID_RENEW_SCRATCH", `renew scratch is missing evidence for ${original}`);
|
|
415
|
+
const commit = await readCommitObject(cwd, coordinates, rewritten);
|
|
416
|
+
if (commit.parents.length !== 1 || commit.parents[0] !== parent) {
|
|
417
|
+
throw new RenewBuildError("INVALID_RENEW_SCRATCH", `rewritten commit ${rewritten} is not attached to its scratch predecessor`);
|
|
418
|
+
}
|
|
419
|
+
parent = rewritten;
|
|
420
|
+
}
|
|
421
|
+
if (parent !== state.currentHead || Object.keys(state.evidenceMap).length !== state.nextIndex) {
|
|
422
|
+
throw new RenewBuildError("INVALID_RENEW_SCRATCH", "renew scratch evidence does not match its stopped head");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { FlowError } from "../flow-error.js";
|
|
2
|
+
import { currentLifecycleState } from "./repository-ledger/read-model.js";
|
|
3
|
+
import { buildDetachedRenewChain, continueDetachedRenewChain, renewScratchName } from "./renew-rewrite.js";
|
|
4
|
+
import { hasStrandableProjectionGeneration } from "./projection/index.js";
|
|
5
|
+
import { runGitProcess } from "../git/core.js";
|
|
6
|
+
import { assertCleanTrackedWorkingTree } from "../git/worktree.js";
|
|
7
|
+
import { normalizeScopePattern } from "./scope.js";
|
|
8
|
+
import { captureFoldedRenewSeal } from "./seal.js";
|
|
9
|
+
function assertOperable(contractId, model) {
|
|
10
|
+
if ((model.phase !== "active" && model.phase !== "sealed") || model.petition.state !== "none") {
|
|
11
|
+
const state = currentLifecycleState(model);
|
|
12
|
+
throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot renew from state ${state.state}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function checkCancellation(signal) {
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
throw new FlowError("CARRIER_GATE_CANCELLED", "contract lifecycle operation cancelled before durable effects");
|
|
18
|
+
}
|
|
19
|
+
async function deliveryChain(cwd, oldBase, oldHead) {
|
|
20
|
+
if (oldBase === oldHead)
|
|
21
|
+
return [];
|
|
22
|
+
const result = await runGitProcess(cwd, ["rev-list", "--reverse", "--ancestry-path", `${oldBase}..${oldHead}`]);
|
|
23
|
+
if (result.status !== 0 || result.error) {
|
|
24
|
+
throw new FlowError("INTERNAL_STATE", result.stderr.trim() || result.error?.message || "cannot read renew delivery chain", { cause: result.error });
|
|
25
|
+
}
|
|
26
|
+
return result.stdout.split(/\r?\n/).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
export function scopeEntry(input) {
|
|
29
|
+
const patterns = (input.appendScope ?? []).map(normalizeScopePattern);
|
|
30
|
+
return patterns.length === 0 ? null : {
|
|
31
|
+
v: 1,
|
|
32
|
+
kind: "amend",
|
|
33
|
+
contract: input.contractId,
|
|
34
|
+
at: new Date(input.nowMs).toISOString(),
|
|
35
|
+
actor: input.actor,
|
|
36
|
+
data: { amendment: "Scope extended by lifecycle command.", scopeDelta: { add: patterns } },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function effectiveScope(model, amendment) {
|
|
40
|
+
return [...model.effectiveScope, ...(amendment?.data.scopeDelta?.add ?? [])];
|
|
41
|
+
}
|
|
42
|
+
async function gitTree(cwd, revision) {
|
|
43
|
+
const result = await runGitProcess(cwd, ["rev-parse", "--verify", `${revision}^{tree}`]);
|
|
44
|
+
return result.status === 0 && !result.error ? result.stdout.trim() : null;
|
|
45
|
+
}
|
|
46
|
+
async function acceptedRenewIsReplayable(targetRoot, entry) {
|
|
47
|
+
const [index, oldTree, newTree, worktree] = await Promise.all([
|
|
48
|
+
runGitProcess(targetRoot, ["write-tree"]),
|
|
49
|
+
gitTree(targetRoot, entry.data.oldHead),
|
|
50
|
+
gitTree(targetRoot, entry.data.newHead),
|
|
51
|
+
runGitProcess(targetRoot, ["diff-files", "--quiet"]),
|
|
52
|
+
]);
|
|
53
|
+
if (index.status !== 0 || index.error || oldTree === null || newTree === null)
|
|
54
|
+
return false;
|
|
55
|
+
const indexTree = index.stdout.trim();
|
|
56
|
+
return (indexTree === oldTree || indexTree === newTree) && worktree.status === 0 && !worktree.error;
|
|
57
|
+
}
|
|
58
|
+
async function acceptedRenewFromReplay(model, targetRoot, deliveryHead, targetHead, replay) {
|
|
59
|
+
if (replay.state !== "verb-committed")
|
|
60
|
+
return null;
|
|
61
|
+
const renewIndex = replay.transaction.entries.findIndex((entry) => entry.kind === "renew");
|
|
62
|
+
const sealIndex = replay.transaction.entries.findIndex((entry) => entry.kind === "seal");
|
|
63
|
+
const renew = renewIndex < 0 ? undefined : replay.transaction.entries[renewIndex];
|
|
64
|
+
const seal = sealIndex < 0 ? undefined : replay.transaction.entries[sealIndex];
|
|
65
|
+
if (renew?.kind !== "renew" ||
|
|
66
|
+
seal?.kind !== "seal" ||
|
|
67
|
+
seal.data.sealedBy !== "renew" ||
|
|
68
|
+
renew.data.newBase !== targetHead ||
|
|
69
|
+
renew.data.newHead !== deliveryHead ||
|
|
70
|
+
model.pins.base !== renew.data.newBase ||
|
|
71
|
+
model.pins.delivery !== renew.data.newHead ||
|
|
72
|
+
!await acceptedRenewIsReplayable(targetRoot, renew))
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
transactionObjectId: replay.transaction.transactionObjectId,
|
|
76
|
+
renewObjectId: replay.transaction.entryObjectIds[renewIndex],
|
|
77
|
+
sealObjectId: replay.transaction.entryObjectIds[sealIndex],
|
|
78
|
+
entry: structuredClone(renew),
|
|
79
|
+
seal: structuredClone(seal),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function acceptedFreshScopeFromReplay(model, replay, expected) {
|
|
83
|
+
if (replay.state !== "verb-committed" || !expected)
|
|
84
|
+
return null;
|
|
85
|
+
const accepted = replay.transaction.entries.at(-1);
|
|
86
|
+
if (accepted?.kind !== "amend")
|
|
87
|
+
return null;
|
|
88
|
+
return accepted.data.amendment === expected.data.amendment
|
|
89
|
+
&& JSON.stringify(accepted.data.scopeDelta ?? null) === JSON.stringify(expected.data.scopeDelta ?? null)
|
|
90
|
+
? { transactionObjectId: replay.transaction.transactionObjectId, entry: structuredClone(accepted) }
|
|
91
|
+
: null;
|
|
92
|
+
}
|
|
93
|
+
export async function admitRenew(session, input, replay, expected) {
|
|
94
|
+
const model = session.lifecycle;
|
|
95
|
+
assertOperable(input.contractId, model);
|
|
96
|
+
if (session.target.status !== "available")
|
|
97
|
+
throw new FlowError("INTERNAL_STATE", session.target.reason);
|
|
98
|
+
const oldHead = session.pins.delivery.head;
|
|
99
|
+
const newBase = session.pins.target.head;
|
|
100
|
+
if (!oldHead || !newBase)
|
|
101
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has missing delivery or target coordinates`);
|
|
102
|
+
if (session.pins.delivery.ref !== model.resources.deliveryRef || session.pins.target.ref !== model.pins.target.ref) {
|
|
103
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} carrier pins disagree with the lifecycle read model`);
|
|
104
|
+
}
|
|
105
|
+
const amendment = scopeEntry(input);
|
|
106
|
+
const acceptedReplay = await acceptedRenewFromReplay(model, session.target.root, oldHead, newBase, replay);
|
|
107
|
+
if (acceptedReplay) {
|
|
108
|
+
return {
|
|
109
|
+
model,
|
|
110
|
+
repository: session.repository,
|
|
111
|
+
targetRoot: session.target.root,
|
|
112
|
+
oldBase: acceptedReplay.entry.data.oldBase,
|
|
113
|
+
oldFence: acceptedReplay.seal.data.oldFence,
|
|
114
|
+
oldHead: acceptedReplay.entry.data.oldHead,
|
|
115
|
+
newBase: acceptedReplay.entry.data.newBase,
|
|
116
|
+
deliveryRef: model.resources.deliveryRef,
|
|
117
|
+
targetRef: model.pins.target.ref,
|
|
118
|
+
effectiveScope: [...model.effectiveScope],
|
|
119
|
+
amendment: null,
|
|
120
|
+
acceptedReplay,
|
|
121
|
+
acceptedScopeReplay: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const acceptedScopeReplay = acceptedFreshScopeFromReplay(model, replay, amendment);
|
|
125
|
+
if (acceptedScopeReplay) {
|
|
126
|
+
return {
|
|
127
|
+
model,
|
|
128
|
+
repository: session.repository,
|
|
129
|
+
targetRoot: session.target.root,
|
|
130
|
+
oldBase: model.pins.base,
|
|
131
|
+
oldFence: model.pins.delivery,
|
|
132
|
+
oldHead,
|
|
133
|
+
newBase,
|
|
134
|
+
deliveryRef: model.resources.deliveryRef,
|
|
135
|
+
targetRef: model.pins.target.ref,
|
|
136
|
+
effectiveScope: [...model.effectiveScope],
|
|
137
|
+
amendment: null,
|
|
138
|
+
acceptedReplay: null,
|
|
139
|
+
acceptedScopeReplay,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
await assertCleanTrackedWorkingTree(session.target.root);
|
|
143
|
+
if (await hasStrandableProjectionGeneration({ cwd: session.stableRoot, commissionId: input.contractId, deliveryWorktree: session.target.root, nowMs: input.nowMs })) {
|
|
144
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has a live projection that owns the delivery worktree`);
|
|
145
|
+
}
|
|
146
|
+
const scope = effectiveScope(model, amendment);
|
|
147
|
+
const admission = {
|
|
148
|
+
model,
|
|
149
|
+
repository: session.repository,
|
|
150
|
+
targetRoot: session.target.root,
|
|
151
|
+
oldBase: model.pins.base,
|
|
152
|
+
oldFence: model.pins.delivery,
|
|
153
|
+
oldHead,
|
|
154
|
+
newBase,
|
|
155
|
+
deliveryRef: model.resources.deliveryRef,
|
|
156
|
+
targetRef: model.pins.target.ref,
|
|
157
|
+
effectiveScope: scope,
|
|
158
|
+
amendment,
|
|
159
|
+
acceptedReplay: null,
|
|
160
|
+
acceptedScopeReplay: null,
|
|
161
|
+
};
|
|
162
|
+
if (expected && (expected.observedLedgerHead !== model.observedLedgerHead ||
|
|
163
|
+
expected.deliveryRef !== admission.deliveryRef ||
|
|
164
|
+
expected.targetRef !== admission.targetRef ||
|
|
165
|
+
expected.oldBase !== admission.oldBase ||
|
|
166
|
+
expected.oldHead !== admission.oldHead ||
|
|
167
|
+
expected.newBase !== admission.newBase ||
|
|
168
|
+
JSON.stringify(expected.effectiveScope) !== JSON.stringify(scope))) {
|
|
169
|
+
throw new FlowError("CONCURRENT_MODIFICATION", `renew scratch coordinates no longer match ${input.contractId}`);
|
|
170
|
+
}
|
|
171
|
+
return admission;
|
|
172
|
+
}
|
|
173
|
+
export async function prepareRenew(admission, input, continuation) {
|
|
174
|
+
if (admission.acceptedReplay || admission.acceptedScopeReplay)
|
|
175
|
+
return { build: null, capture: null, prefix: [], entry: null };
|
|
176
|
+
if (admission.oldBase === admission.newBase) {
|
|
177
|
+
return { build: null, capture: null, prefix: admission.amendment ? [admission.amendment] : [], entry: null };
|
|
178
|
+
}
|
|
179
|
+
const build = continuation
|
|
180
|
+
? await continueDetachedRenewChain({ cwd: input.cwd, scratchName: continuation.scratch.scratchName, resolution: continuation.resolution, signal: input.signal })
|
|
181
|
+
: await buildDetachedRenewChain({
|
|
182
|
+
cwd: input.cwd,
|
|
183
|
+
scratchName: renewScratchName(input.contractId),
|
|
184
|
+
observedLedgerHead: admission.model.observedLedgerHead,
|
|
185
|
+
deliveryRef: admission.deliveryRef,
|
|
186
|
+
targetRef: admission.targetRef,
|
|
187
|
+
effectiveScope: admission.effectiveScope,
|
|
188
|
+
oldBase: admission.oldBase,
|
|
189
|
+
oldHead: admission.oldHead,
|
|
190
|
+
newBase: admission.newBase,
|
|
191
|
+
deliveryCommits: await deliveryChain(input.cwd, admission.oldBase, admission.oldHead),
|
|
192
|
+
signal: input.signal,
|
|
193
|
+
});
|
|
194
|
+
if (build.status === "conflict")
|
|
195
|
+
return { build, capture: null, prefix: [], entry: null };
|
|
196
|
+
const capture = await captureFoldedRenewSeal({
|
|
197
|
+
cwd: input.cwd,
|
|
198
|
+
contract: admission.model.contract,
|
|
199
|
+
intent: admission.model.intent,
|
|
200
|
+
receiptSegmentOpen: admission.model.seal === null,
|
|
201
|
+
deliveryFence: admission.model.pins.delivery,
|
|
202
|
+
effectiveScope: admission.model.effectiveScope,
|
|
203
|
+
pendingScopeOverlay: admission.amendment?.data.scopeDelta?.add ?? [],
|
|
204
|
+
receiptHead: admission.oldHead,
|
|
205
|
+
contentBase: admission.newBase,
|
|
206
|
+
contentHead: build.newHead,
|
|
207
|
+
});
|
|
208
|
+
if (!capture)
|
|
209
|
+
throw new FlowError("NO_OPEN_ARC", `contract ${input.contractId} has no active folded receipt segment to renew`);
|
|
210
|
+
const { scopeViolationEvidence: _scopeViolationEvidence, ...durableCapture } = capture;
|
|
211
|
+
const seal = {
|
|
212
|
+
v: 2,
|
|
213
|
+
kind: "seal",
|
|
214
|
+
contract: input.contractId,
|
|
215
|
+
at: new Date(input.nowMs).toISOString(),
|
|
216
|
+
actor: "engine",
|
|
217
|
+
data: { ...durableCapture, newFence: build.newHead, sealedBy: "renew" },
|
|
218
|
+
};
|
|
219
|
+
const entry = {
|
|
220
|
+
v: 1,
|
|
221
|
+
kind: "renew",
|
|
222
|
+
contract: input.contractId,
|
|
223
|
+
at: new Date(input.nowMs).toISOString(),
|
|
224
|
+
actor: input.actor,
|
|
225
|
+
data: { oldBase: admission.oldBase, newBase: admission.newBase, oldHead: admission.oldHead, newHead: build.newHead },
|
|
226
|
+
};
|
|
227
|
+
const prefix = admission.amendment ? [admission.amendment, seal] : [seal];
|
|
228
|
+
return { build, capture, prefix, entry };
|
|
229
|
+
}
|
|
230
|
+
export function sameAdmission(current, pinned) {
|
|
231
|
+
return current.model.observedLedgerHead === pinned.model.observedLedgerHead &&
|
|
232
|
+
current.oldBase === pinned.oldBase && current.oldFence === pinned.oldFence &&
|
|
233
|
+
current.oldHead === pinned.oldHead && current.newBase === pinned.newBase &&
|
|
234
|
+
current.deliveryRef === pinned.deliveryRef && current.targetRef === pinned.targetRef &&
|
|
235
|
+
current.targetRoot === pinned.targetRoot &&
|
|
236
|
+
JSON.stringify(current.effectiveScope) === JSON.stringify(pinned.effectiveScope) &&
|
|
237
|
+
current.acceptedReplay?.renewObjectId === pinned.acceptedReplay?.renewObjectId &&
|
|
238
|
+
current.acceptedScopeReplay?.transactionObjectId === pinned.acceptedScopeReplay?.transactionObjectId;
|
|
239
|
+
}
|