@astrosheep/keiyaku 2.9.5 → 2.9.7
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/build/.tsbuildinfo +1 -1
- package/build/agents/harness/outcome.js +20 -4
- package/build/agents/harness/projection.js +8 -7
- package/build/agents/harness/router.js +2 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
- package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
- package/build/agents/providers/codex-app-server/adapter.js +11 -10
- package/build/agents/providers/codex-app-server/registration.js +2 -1
- package/build/agents/providers/opencode-sdk/adapter.js +7 -0
- package/build/agents/providers/opencode-sdk/registration.js +2 -1
- package/build/agents/providers/pi/adapter.js +10 -0
- package/build/agents/providers/pi/registration.js +3 -1
- package/build/cli/commands/contract/bind/handler.js +10 -4
- package/build/cli/commands/contract/bind/meta.js +6 -6
- package/build/cli/commands/contract/petition/handler.js +16 -3
- package/build/cli/commands/contract/petition/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/tell/handler.js +10 -2
- package/build/cli/commands/projection/tell/meta.js +3 -3
- package/build/cli/commands/task/catalog.js +2 -0
- package/build/cli/commands/task/log/handler.js +12 -0
- package/build/cli/commands/task/log/meta.js +8 -0
- package/build/cli/completion.js +9 -2
- package/build/cli/flags.js +11 -2
- package/build/cli/index.js +10 -6
- package/build/cli/parse-flags.js +6 -0
- package/build/cli/parse-metadata.js +1 -1
- package/build/cli/render/errors.js +18 -1
- package/build/cli/render/format.js +5 -2
- package/build/cli/render/line-width.js +33 -0
- package/build/cli/render/path-prefix-compaction.js +88 -0
- package/build/cli/render/petition.js +4 -0
- package/build/cli/render/projection-activity.js +93 -20
- package/build/cli/render/shared.js +34 -12
- package/build/cli/render/success-response.js +2 -0
- package/build/cli/render/tool-presentation.js +3 -3
- package/build/cli/render/wait.js +68 -48
- package/build/cli/subagent-guard.js +3 -0
- package/build/cli/types.js +1 -1
- package/build/core/audit/candidate.js +48 -0
- package/build/core/audit/coordinates.js +180 -0
- package/build/core/audit/evidence.js +158 -0
- package/build/core/audit/facade.js +42 -0
- package/build/core/audit/readiness.js +4 -0
- package/build/core/audit/report.js +38 -0
- package/build/core/audit.js +1 -650
- package/build/core/bind.js +111 -6
- package/build/core/draft.js +1 -1
- package/build/core/projection/generation/database.js +21 -2
- package/build/core/projection/generation/model.js +24 -3
- package/build/core/projection/generation/projection-generation-continuation.js +75 -10
- package/build/core/projection/generation/projection-generation-execution.js +4 -3
- package/build/core/projection/generation/projection-generation-runner.js +96 -53
- package/build/core/projection/generation/projection-generation-runtime.js +19 -0
- package/build/core/projection/generation/store.js +9 -0
- package/build/core/projection/generation/transitions.js +105 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-core.js +1 -1
- package/build/core/projection/projection-kill.js +31 -0
- package/build/core/projection/projection-life-protocol.js +10 -0
- package/build/core/projection/projection-wait.js +53 -15
- package/build/core/projection/projection-wake.js +35 -3
- package/build/core/projection/tell/database.js +18 -0
- package/build/core/projection/tell/model.js +1 -0
- package/build/core/projection/tell/store.js +102 -55
- package/build/core/registry.js +82 -69
- package/build/core/scope.js +9 -9
- package/build/core/task/index.js +2 -2
- package/build/core/task/query.js +65 -23
- package/build/core/task/task-contract.js +18 -0
- package/build/core/task/task-git-store.js +50 -0
- package/build/generated/version.js +2 -2
- package/package.json +1 -1
- package/skills/keiyaku-akuma/SKILL.md +10 -0
- package/skills/keiyaku-workflow/SKILL.md +82 -7
package/build/cli/render/wait.js
CHANGED
|
@@ -3,6 +3,27 @@ import { displayColumns, resolveLineColumns, truncateColumns } from "./line-widt
|
|
|
3
3
|
import { formatTellCommand, projectionRejoinHintLines, textResponse } from "./shared.js";
|
|
4
4
|
import { renderTerminalFailureFace } from "./terminal-failure.js";
|
|
5
5
|
const WAIT_HEADER_MAX_COLUMNS = 60;
|
|
6
|
+
const WAIT_BODY_MAX_ROWS = 10;
|
|
7
|
+
const WAIT_ROW_PRIORITY = {
|
|
8
|
+
"end-anchor": 100,
|
|
9
|
+
"storage-diagnostic": 90,
|
|
10
|
+
diagnostic: 90,
|
|
11
|
+
tell: 80,
|
|
12
|
+
summary: 70,
|
|
13
|
+
activity: 60,
|
|
14
|
+
plan: 55,
|
|
15
|
+
liveness: 50,
|
|
16
|
+
silence: 65,
|
|
17
|
+
file: 20,
|
|
18
|
+
};
|
|
19
|
+
/** One owner admits every wait-body row, then restores presentation order. */
|
|
20
|
+
export function selectWaitBodyRows(rows, limit = WAIT_BODY_MAX_ROWS) {
|
|
21
|
+
const selected = rows
|
|
22
|
+
.slice()
|
|
23
|
+
.sort((left, right) => WAIT_ROW_PRIORITY[right.kind] - WAIT_ROW_PRIORITY[left.kind] || left.order - right.order)
|
|
24
|
+
.slice(0, limit);
|
|
25
|
+
return selected.sort((left, right) => left.order - right.order);
|
|
26
|
+
}
|
|
6
27
|
function shortAddress(projectionId) {
|
|
7
28
|
return /\/([0-9a-f]{8})$/.exec(projectionId)?.[1] ?? projectionId;
|
|
8
29
|
}
|
|
@@ -26,29 +47,10 @@ function isTerminalPhase(phase) {
|
|
|
26
47
|
return ["done", "failed", "killed", "dismissed"].includes(phase);
|
|
27
48
|
}
|
|
28
49
|
function waitStateText(snapshot) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return `pulse ${pulse} · breathing ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
34
|
-
}
|
|
35
|
-
case "minting":
|
|
36
|
-
return `starting · lived ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
37
|
-
case "startup-timeout":
|
|
38
|
-
return `startup timed out · lived ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
39
|
-
case "unknown":
|
|
40
|
-
return `state unknown · observed ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
41
|
-
case "done":
|
|
42
|
-
return `returned · lived ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
43
|
-
case "failed":
|
|
44
|
-
return `dead · lived ${formatTimelineDuration(snapshot.durationMs)}`;
|
|
45
|
-
case "killed":
|
|
46
|
-
return "killed";
|
|
47
|
-
case "dismissed":
|
|
48
|
-
return "dismissed";
|
|
49
|
-
case "lost":
|
|
50
|
-
return `no pulse ${snapshot.activityAgeMs === undefined ? "unknown" : formatTimelineDuration(snapshot.activityAgeMs)}`;
|
|
51
|
-
}
|
|
50
|
+
const idle = snapshot.activityAgeMs === undefined
|
|
51
|
+
? formatTimelineDuration(snapshot.durationMs)
|
|
52
|
+
: formatTimelineDuration(snapshot.activityAgeMs);
|
|
53
|
+
return `up ${formatTimelineDuration(snapshot.durationMs)} · idle ${idle}`;
|
|
52
54
|
}
|
|
53
55
|
function projectionIdentityParts(projectionId) {
|
|
54
56
|
const match = /^(.*)\/([0-9a-f]{8})$/.exec(projectionId);
|
|
@@ -56,8 +58,7 @@ function projectionIdentityParts(projectionId) {
|
|
|
56
58
|
}
|
|
57
59
|
function waitHeader(projectionId, akuma, snapshot, outputColumns) {
|
|
58
60
|
const maxColumns = Math.min(WAIT_HEADER_MAX_COLUMNS, outputColumns);
|
|
59
|
-
const
|
|
60
|
-
const state = `${waitStateText(snapshot)}${tellSuffix}`;
|
|
61
|
+
const state = waitStateText(snapshot);
|
|
61
62
|
const identity = projectionIdentityParts(projectionId);
|
|
62
63
|
// Slug often mirrors the Akuma name; print once when they are the same fact.
|
|
63
64
|
const description = identity.slug && akuma && identity.slug === akuma
|
|
@@ -69,6 +70,14 @@ function waitHeader(projectionId, akuma, snapshot, outputColumns) {
|
|
|
69
70
|
const dashCount = Math.max(1, maxColumns - displayColumns(`${prefix} ${state}`));
|
|
70
71
|
return `${prefix} ${"─".repeat(dashCount)} ${state}`;
|
|
71
72
|
}
|
|
73
|
+
function pendingTellLines(snapshot, maxColumns) {
|
|
74
|
+
return snapshot.tells.window.map((tell) => {
|
|
75
|
+
const age = Math.max(0, snapshot.observedAtMs - Date.parse(tell.createdAt));
|
|
76
|
+
const fixed = `${"".padStart(6)}│ ${"tell".padEnd(7)} `;
|
|
77
|
+
const suffix = ` · undelivered ${formatTimelineDuration(Number.isFinite(age) ? age : 0)}`;
|
|
78
|
+
return `${fixed}${truncateColumns(`“${tell.text}”`, Math.max(0, maxColumns - displayColumns(fixed) - displayColumns(suffix)))}${suffix}`;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
72
81
|
function ledgerHint(projectionId, snapshot) {
|
|
73
82
|
const file = snapshot.eventLedgerFile ?? "events.jsonl";
|
|
74
83
|
return `· ledger: .keiyaku/projection/${projectionId}/${file}`;
|
|
@@ -136,7 +145,11 @@ function unknownResponse(projectionId, akuma, snapshot, maxColumns) {
|
|
|
136
145
|
...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
|
|
137
146
|
maxColumns,
|
|
138
147
|
});
|
|
139
|
-
const historicalLines = rendered.lines.map((line) =>
|
|
148
|
+
const historicalLines = selectWaitBodyRows(rendered.lines.map((line, index) => ({
|
|
149
|
+
kind: rendered.rowKinds[index] ?? "activity",
|
|
150
|
+
line,
|
|
151
|
+
order: index,
|
|
152
|
+
}))).map((row) => row.line.replace(/[┆├]/, "│"));
|
|
140
153
|
return {
|
|
141
154
|
...textResponse([
|
|
142
155
|
`? ${akuma} · projection: ${projectionId} · unknown`,
|
|
@@ -186,7 +199,7 @@ export function buildWaitResponse(projectionId, result, options = {}) {
|
|
|
186
199
|
&& (result.terminalSnapshot || isTerminalPhase(result.snapshot.phase))
|
|
187
200
|
? { ...result.snapshot, phase: "active" }
|
|
188
201
|
: result.snapshot;
|
|
189
|
-
const maxColumns = options.maxColumns ?? resolveLineColumns();
|
|
202
|
+
const maxColumns = Math.min(120, options.maxColumns ?? resolveLineColumns());
|
|
190
203
|
if (result.state === "terminal" && result.verdict.state === "killed") {
|
|
191
204
|
return {
|
|
192
205
|
...textResponse(`× killed ${projectionId}`),
|
|
@@ -203,6 +216,14 @@ export function buildWaitResponse(projectionId, result, options = {}) {
|
|
|
203
216
|
if (state === "returned") {
|
|
204
217
|
return returnedResponse(projectionId, result);
|
|
205
218
|
}
|
|
219
|
+
const tells = pendingTellLines(snapshot, maxColumns);
|
|
220
|
+
const summary = ["running", "stalled", "unknown"].includes(state)
|
|
221
|
+
? renderProjectionActivitySummary(snapshot.activity, {
|
|
222
|
+
state,
|
|
223
|
+
...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
|
|
224
|
+
maxColumns,
|
|
225
|
+
})
|
|
226
|
+
: undefined;
|
|
206
227
|
const rendered = renderProjectionActivity(snapshot.activity, {
|
|
207
228
|
anchorMs: snapshot.observedAtMs,
|
|
208
229
|
state,
|
|
@@ -210,28 +231,27 @@ export function buildWaitResponse(projectionId, result, options = {}) {
|
|
|
210
231
|
...(snapshot.workspaceRoot ? { workspaceRoot: snapshot.workspaceRoot } : {}),
|
|
211
232
|
maxColumns,
|
|
212
233
|
});
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
...rendered.lines,
|
|
218
|
-
]
|
|
219
|
-
.map((line) => truncateColumns(line, maxColumns));
|
|
220
|
-
if (rendered.lines.length === 0 && state === "running") {
|
|
221
|
-
lines.push(" not a word out of it yet — pulse is steady");
|
|
234
|
+
let order = 0;
|
|
235
|
+
const bodyPlan = [];
|
|
236
|
+
for (const diagnostic of snapshot.eventDiagnostics ?? []) {
|
|
237
|
+
bodyPlan.push({ kind: "storage-diagnostic", line: `‼ ${diagnostic.kind} · ${diagnostic.detail}`, order: order++ });
|
|
222
238
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
rendered.lines.forEach((line, index) => {
|
|
240
|
+
bodyPlan.push({ kind: rendered.rowKinds[index] ?? "activity", line, order: order++ });
|
|
241
|
+
});
|
|
242
|
+
for (const line of tells)
|
|
243
|
+
bodyPlan.push({ kind: "tell", line, order: order++ });
|
|
244
|
+
if (rendered.lines.length === 0 && tells.length === 0 && state === "running") {
|
|
245
|
+
bodyPlan.push({ kind: "liveness", line: " not a word out of it yet — idle", order: order++ });
|
|
246
|
+
}
|
|
247
|
+
if (!rendered.hasEndAnchor && summary) {
|
|
248
|
+
bodyPlan.push({ kind: "summary", line: summary, order: order++ });
|
|
249
|
+
}
|
|
250
|
+
const bodyLines = selectWaitBodyRows(bodyPlan).map((row) => truncateColumns(row.line, maxColumns));
|
|
251
|
+
const lines = [waitHeader(projectionId, result.pact.akuma, snapshot, maxColumns), ...bodyLines];
|
|
252
|
+
const pendingTellCount = snapshot.tells.totals.undelivered;
|
|
253
|
+
if (pendingTellCount > 0) {
|
|
254
|
+
lines.push(`${pendingTellCount} tell${pendingTellCount === 1 ? "" : "s"} undelivered`);
|
|
235
255
|
}
|
|
236
256
|
const nextActions = state === "running" && options.runningActions === false
|
|
237
257
|
? []
|
|
@@ -11,6 +11,9 @@ function isSubagentSafeCommand(args) {
|
|
|
11
11
|
const subcommand = args[1];
|
|
12
12
|
return subcommand === "list" || subcommand === "show" || SUBAGENT_SAFE_FLAGS.has(subcommand ?? "");
|
|
13
13
|
}
|
|
14
|
+
if (head === "task") {
|
|
15
|
+
return args[1] === "log" || SUBAGENT_SAFE_FLAGS.has(args[1] ?? "");
|
|
16
|
+
}
|
|
14
17
|
return ((head !== undefined && SUBAGENT_SAFE_COMMANDS.has(head)) ||
|
|
15
18
|
(args.length === 1 && head !== undefined && SUBAGENT_SAFE_FLAGS.has(head)));
|
|
16
19
|
}
|
package/build/cli/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TASK_SUBCOMMANDS = ["add", "show", "ls", "start", "stop", "done", "drop", "update", "doctor"];
|
|
1
|
+
export const TASK_SUBCOMMANDS = ["add", "show", "ls", "log", "start", "stop", "done", "drop", "update", "doctor"];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { FlowError, asMessage } from "../../flow-error.js";
|
|
5
|
+
import { createGit, wrapGitError } from "../../git/core.js";
|
|
6
|
+
import { readStructuredDiff } from "../../git/diff/structured.js";
|
|
7
|
+
import { rewriteRenewBranch } from "../renew.js";
|
|
8
|
+
async function resolveObject(cwd, revision, kind) {
|
|
9
|
+
try {
|
|
10
|
+
return (await createGit(cwd).raw(["rev-parse", "--verify", `${revision}^{${kind}}`])).trim();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw wrapGitError(`rev-parse ${revision}^{${kind}}`, error, cwd);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function createAuditCandidate(input) {
|
|
17
|
+
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "keiyaku-audit-"));
|
|
18
|
+
const git = createGit(input.root);
|
|
19
|
+
try {
|
|
20
|
+
await git.raw(["clone", "--quiet", "--no-hardlinks", "--no-checkout", input.root, cwd]);
|
|
21
|
+
const candidateGit = createGit(cwd);
|
|
22
|
+
await candidateGit.raw(["config", "user.name", "Keiyaku Audit"]);
|
|
23
|
+
await candidateGit.raw(["config", "user.email", "audit@keiyaku.invalid"]);
|
|
24
|
+
await candidateGit.raw(["checkout", "--quiet", "-B", `keiyaku/${input.contractId}`, input.frozen.coordinates.delivery.head]);
|
|
25
|
+
if (input.frozen.base === input.frozen.coordinates.target.head)
|
|
26
|
+
return { cwd, rehearsal: { status: "fresh" }, candidate: { base: input.frozen.coordinates.target.head, head: input.frozen.coordinates.delivery.head, tree: input.frozen.coordinates.delivery.tree, rewritten: false } };
|
|
27
|
+
try {
|
|
28
|
+
await rewriteRenewBranch({ cwd, contractId: input.contractId, oldHead: input.frozen.coordinates.delivery.head, recoveryRef: "refs/keiyaku/audit/disposable", newBase: input.frozen.coordinates.target.head, oldBase: input.frozen.base });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof FlowError && error.code === "CLOSE_MERGE_CONFLICT" && error.facts?.kind === "renew_conflict")
|
|
32
|
+
return { cwd, rehearsal: { status: "conflict", paths: [...error.facts.conflictPaths], pathCapture: error.facts.conflictPathCapture.status === "captured" ? "captured" : "failed", ...(error.facts.conflictPathCapture.status === "failed" ? { diagnostic: error.facts.conflictPathCapture.diagnostic } : {}) }, candidate: null };
|
|
33
|
+
return { cwd, rehearsal: { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) } }, candidate: null };
|
|
34
|
+
}
|
|
35
|
+
const [head, tree] = await Promise.all([resolveObject(cwd, "HEAD", "commit"), resolveObject(cwd, "HEAD", "tree")]);
|
|
36
|
+
return { cwd, rehearsal: { status: "clean", oldBase: input.frozen.base, newBase: input.frozen.coordinates.target.head, oldHead: input.frozen.coordinates.delivery.head }, candidate: { base: input.frozen.coordinates.target.head, head, tree, rewritten: true } };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return { cwd, rehearsal: { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) } }, candidate: null };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function disposeAuditCandidate(candidate) { await fs.rm(candidate.cwd, { recursive: true, force: true }); }
|
|
43
|
+
export async function readAuditCandidateDiff(candidateResult) {
|
|
44
|
+
const candidate = candidateResult.candidate;
|
|
45
|
+
if (!candidate)
|
|
46
|
+
return null;
|
|
47
|
+
return readStructuredDiff(candidateResult.cwd, candidate.base, candidate.head, { excludePaths: [".keiyaku/KEIYAKU.md"] });
|
|
48
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { FlowError, asMessage } from "../../flow-error.js";
|
|
5
|
+
import { createGit, wrapGitError } from "../../git/core.js";
|
|
6
|
+
import { readLedger } from "../ledger.js";
|
|
7
|
+
import { hasStrandableProjectionGeneration } from "../projection/index.js";
|
|
8
|
+
import { readRenewRecoverySession } from "../renew-session.js";
|
|
9
|
+
import { effectiveScope, findOpenArc, getFence, getMaxArc, receiptsAfterFence } from "../seal.js";
|
|
10
|
+
import { queueMembership, readQueueReadModel } from "../settlement/queue.js";
|
|
11
|
+
import { markdownSourceSections } from "../scope.js";
|
|
12
|
+
import { normalizeSectionTitle } from "../markdown/titles.js";
|
|
13
|
+
import { commissionTarget, latestBase } from "../status/drift.js";
|
|
14
|
+
import { deriveContractState, isTerminalState } from "../status/lifecycle.js";
|
|
15
|
+
import { resolveContractDeliveryCwd } from "../worktree-path.js";
|
|
16
|
+
import { KEIYAKU_FILE } from "../../keiyaku.js";
|
|
17
|
+
async function resolveObject(cwd, revision, kind) {
|
|
18
|
+
const git = createGit(cwd);
|
|
19
|
+
try {
|
|
20
|
+
return (await git.raw(["rev-parse", "--verify", `${revision}^{${kind}}`])).trim();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw wrapGitError(`rev-parse ${revision}^{${kind}}`, error, cwd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function appendedScopeTerms(entries) {
|
|
27
|
+
return entries.flatMap((entry) => entry.kind === "amend" && entry.data.scopeDelta ? entry.data.scopeDelta.add : []);
|
|
28
|
+
}
|
|
29
|
+
export function effectiveCriteria(entries) {
|
|
30
|
+
const bind = entries.find((entry) => entry.kind === "bind");
|
|
31
|
+
if (!bind)
|
|
32
|
+
return [];
|
|
33
|
+
const criteria = [...bind.data.checks];
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (entry.kind !== "amend")
|
|
36
|
+
continue;
|
|
37
|
+
for (const section of markdownSourceSections(entry.data.amendment)) {
|
|
38
|
+
if (section.level !== 2 || normalizeSectionTitle(section.title) !== "checks")
|
|
39
|
+
continue;
|
|
40
|
+
for (const line of entry.data.amendment.slice(section.contentStart, section.end).split(/\r?\n/)) {
|
|
41
|
+
const match = line.match(/^\s*-\s+(.+)$/);
|
|
42
|
+
if (match)
|
|
43
|
+
criteria.push(match[1].trim());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return criteria;
|
|
48
|
+
}
|
|
49
|
+
async function pinLedgerCoordinates(input) {
|
|
50
|
+
const ledger = await readLedger(input.root, input.contractId);
|
|
51
|
+
if (!ledger)
|
|
52
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
|
|
53
|
+
const bind = ledger.entries.find((entry) => entry.kind === "bind");
|
|
54
|
+
const target = commissionTarget(ledger.entries);
|
|
55
|
+
const base = latestBase(ledger.entries);
|
|
56
|
+
if (!bind || !target || !base)
|
|
57
|
+
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} is missing pinned audit coordinates`);
|
|
58
|
+
const [deliveryHead, deliveryTree, targetHead, targetTree] = await Promise.all([
|
|
59
|
+
resolveObject(input.root, input.deliveryRef, "commit"), resolveObject(input.root, input.deliveryRef, "tree"),
|
|
60
|
+
resolveObject(input.root, target.ref, "commit"), resolveObject(input.root, target.ref, "tree"),
|
|
61
|
+
]);
|
|
62
|
+
return {
|
|
63
|
+
criteria: effectiveCriteria(ledger.entries), scope: effectiveScope(ledger.entries), appendScope: appendedScopeTerms(ledger.entries), base, entries: ledger.entries,
|
|
64
|
+
coordinates: { ledger: { head: ledger.head }, delivery: { ref: input.deliveryRef, head: deliveryHead, tree: deliveryTree }, target: { ref: target.ref, head: targetHead, tree: targetTree } },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function deliveryResidue(cwd) {
|
|
68
|
+
const fields = (await createGit(cwd).raw(["status", "--porcelain=v1", "-z", "--untracked-files=all"])).split("\0");
|
|
69
|
+
const staged = new Set();
|
|
70
|
+
const unstaged = new Set();
|
|
71
|
+
const untracked = new Set();
|
|
72
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
73
|
+
const field = fields[index];
|
|
74
|
+
if (!field)
|
|
75
|
+
continue;
|
|
76
|
+
const xy = field.slice(0, 2);
|
|
77
|
+
const filePath = field.slice(3);
|
|
78
|
+
if (filePath === KEIYAKU_FILE || filePath.startsWith(".keiyaku/recovery/")) {
|
|
79
|
+
if (xy[0] === "R" || xy[0] === "C")
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (xy === "??")
|
|
84
|
+
untracked.add(filePath);
|
|
85
|
+
else {
|
|
86
|
+
if (xy[0] !== " ")
|
|
87
|
+
staged.add(filePath);
|
|
88
|
+
if (xy[1] !== " ")
|
|
89
|
+
unstaged.add(filePath);
|
|
90
|
+
if (xy[0] === "R" || xy[0] === "C")
|
|
91
|
+
index += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const tracked = new Set([...staged, ...unstaged]);
|
|
95
|
+
return { tracked: [...tracked], staged: [...staged], unstaged: [...unstaged], untracked: [...untracked] };
|
|
96
|
+
}
|
|
97
|
+
function residueKey(residue) {
|
|
98
|
+
return JSON.stringify({ tracked: [...residue.tracked].sort(), staged: [...residue.staged].sort(), unstaged: [...residue.unstaged].sort(), untracked: [...residue.untracked].sort() });
|
|
99
|
+
}
|
|
100
|
+
async function residueFingerprint(cwd, residue) {
|
|
101
|
+
const git = createGit(cwd);
|
|
102
|
+
const digest = createHash("sha256").update(residueKey(residue));
|
|
103
|
+
const tracked = [...residue.tracked].sort();
|
|
104
|
+
if (tracked.length > 0)
|
|
105
|
+
digest.update(await git.raw(["diff", "--no-color", "--no-ext-diff", "--binary", "HEAD", "--", ...tracked]));
|
|
106
|
+
for (const pathname of [...residue.untracked].sort()) {
|
|
107
|
+
const absolute = path.join(cwd, pathname);
|
|
108
|
+
const stat = await fs.lstat(absolute);
|
|
109
|
+
digest.update(pathname).update("\0").update(String(stat.mode)).update("\0");
|
|
110
|
+
if (stat.isSymbolicLink())
|
|
111
|
+
digest.update(await fs.readlink(absolute));
|
|
112
|
+
else
|
|
113
|
+
digest.update(await fs.readFile(absolute));
|
|
114
|
+
digest.update("\0");
|
|
115
|
+
}
|
|
116
|
+
return digest.digest("hex");
|
|
117
|
+
}
|
|
118
|
+
async function deliveryCoordinate(cwd) {
|
|
119
|
+
const [head, tree, residue] = await Promise.all([resolveObject(cwd, "HEAD", "commit"), resolveObject(cwd, "HEAD", "tree"), deliveryResidue(cwd)]);
|
|
120
|
+
return { head, tree, residue, residueFingerprint: await residueFingerprint(cwd, residue) };
|
|
121
|
+
}
|
|
122
|
+
async function readReadiness(input) {
|
|
123
|
+
const bind = input.frozen.entries.find((entry) => entry.kind === "bind");
|
|
124
|
+
const lifecycle = deriveContractState(input.frozen.entries);
|
|
125
|
+
const [residue, looseCommits, session, queue, liveProjection, dependencies] = await Promise.all([
|
|
126
|
+
deliveryResidue(input.deliveryCwd), receiptsAfterFence(input.root, input.frozen.entries, input.frozen.coordinates.delivery.head), readRenewRecoverySession(input.root, input.contractId), readQueueReadModel(input.root),
|
|
127
|
+
hasStrandableProjectionGeneration({ cwd: input.root, commissionId: input.contractId, deliveryWorktree: input.deliveryCwd, nowMs: Date.now() }),
|
|
128
|
+
Promise.all((bind.data.after ?? []).map(async (id) => { const ledger = await readLedger(input.root, id); const state = ledger ? deriveContractState(ledger.entries) : { state: "missing" }; return { id, satisfied: ledger !== null && isTerminalState(state), state: state.state }; })),
|
|
129
|
+
]);
|
|
130
|
+
const membership = queueMembership(queue, input.contractId);
|
|
131
|
+
return { deliveryResidue: residue, arc: { open: findOpenArc(input.frozen.entries)?.data.arc ?? (getMaxArc(input.frozen.entries) === 0 ? 0 : null), max: getMaxArc(input.frozen.entries), fence: getFence(input.frozen.entries), looseCommits }, renewSession: { open: session !== null }, rewriteGuard: { liveProjection }, dependencies, petition: { state: lifecycle.state, queue: membership ? { position: membership.position, length: membership.queueLength, head: membership.queueHead, diagnostics: membership.diagnostics.map((item) => item.kind) } : null } };
|
|
132
|
+
}
|
|
133
|
+
export async function pinAuditSnapshot(input) {
|
|
134
|
+
const contractId = input.address.binding.commissionId;
|
|
135
|
+
const root = input.address.repo.stableRoot;
|
|
136
|
+
const deliveryRef = `refs/heads/keiyaku/${contractId}`;
|
|
137
|
+
return pinLedgerCoordinates({ root, contractId, deliveryRef });
|
|
138
|
+
}
|
|
139
|
+
export function initialCoordinateRace(input) {
|
|
140
|
+
return { which: "delivery-worktree", pinned: input.frozen.coordinates.delivery, observed: input.observed };
|
|
141
|
+
}
|
|
142
|
+
export async function pinAuditCoordinates(input) {
|
|
143
|
+
const contractId = input.address.binding.commissionId;
|
|
144
|
+
const root = input.address.repo.stableRoot;
|
|
145
|
+
const frozen = await pinAuditSnapshot({ address: input.address });
|
|
146
|
+
const deliveryCwd = await resolveContractDeliveryCwd(input.cwd, contractId);
|
|
147
|
+
const coordinate = await deliveryCoordinate(deliveryCwd);
|
|
148
|
+
if (coordinate.head !== frozen.coordinates.delivery.head || coordinate.tree !== frozen.coordinates.delivery.tree)
|
|
149
|
+
throw new Error(`registered delivery worktree ${coordinate.head}/${coordinate.tree} does not match pinned delivery ref ${frozen.coordinates.delivery.head}/${frozen.coordinates.delivery.tree}`);
|
|
150
|
+
const readiness = await readReadiness({ root, deliveryCwd, contractId, frozen });
|
|
151
|
+
return { ...frozen, deliveryCwd, deliveryCoordinate: coordinate, readiness };
|
|
152
|
+
}
|
|
153
|
+
export async function rereadCoordinateRace(input) {
|
|
154
|
+
const contractId = input.address.binding.commissionId;
|
|
155
|
+
const root = input.address.repo.stableRoot;
|
|
156
|
+
const deliveryRef = `refs/heads/keiyaku/${contractId}`;
|
|
157
|
+
let after;
|
|
158
|
+
try {
|
|
159
|
+
after = await pinLedgerCoordinates({ root, contractId, deliveryRef });
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return { which: "ledger", pinned: input.frozen.coordinates.ledger, observed: asMessage(error) };
|
|
163
|
+
}
|
|
164
|
+
if (after.coordinates.ledger.head !== input.frozen.coordinates.ledger.head)
|
|
165
|
+
return { which: "ledger", pinned: input.frozen.coordinates.ledger, observed: after.coordinates.ledger };
|
|
166
|
+
if (after.coordinates.delivery.head !== input.frozen.coordinates.delivery.head || after.coordinates.delivery.tree !== input.frozen.coordinates.delivery.tree)
|
|
167
|
+
return { which: "delivery", pinned: input.frozen.coordinates.delivery, observed: after.coordinates.delivery };
|
|
168
|
+
if (after.coordinates.target.head !== input.frozen.coordinates.target.head || after.coordinates.target.tree !== input.frozen.coordinates.target.tree)
|
|
169
|
+
return { which: "target", pinned: input.frozen.coordinates.target, observed: after.coordinates.target };
|
|
170
|
+
try {
|
|
171
|
+
const deliveryCwd = await resolveContractDeliveryCwd(input.cwd, contractId);
|
|
172
|
+
const observed = await deliveryCoordinate(deliveryCwd);
|
|
173
|
+
if (path.resolve(deliveryCwd) !== path.resolve(input.frozen.deliveryCwd) || observed.head !== input.frozen.deliveryCoordinate.head || observed.tree !== input.frozen.deliveryCoordinate.tree || observed.residueFingerprint !== input.frozen.deliveryCoordinate.residueFingerprint)
|
|
174
|
+
return { which: "delivery-worktree", pinned: { cwd: input.frozen.deliveryCwd, ...input.frozen.deliveryCoordinate }, observed: { cwd: deliveryCwd, ...observed } };
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
return { which: "delivery-worktree", pinned: { cwd: input.frozen.deliveryCwd, ...input.frozen.deliveryCoordinate }, observed: asMessage(error) };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { FlowError, asMessage } from "../../flow-error.js";
|
|
2
|
+
import { scopePathEvidence } from "../scope.js";
|
|
3
|
+
import { verificationExecutor, resolveVerificationCommands, runVerificationCommand } from "../settlement/verification.js";
|
|
4
|
+
function importLine(line) {
|
|
5
|
+
return /^(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+\s*=\s*require\s*\(|[^=]+\brequire\s*\(|[^=]+\bimport\s*\()/.test(line.content.trim());
|
|
6
|
+
}
|
|
7
|
+
function identifierRename(before, after) {
|
|
8
|
+
const left = before.match(/[A-Za-z_$][\w$]*/g) ?? [];
|
|
9
|
+
const right = after.match(/[A-Za-z_$][\w$]*/g) ?? [];
|
|
10
|
+
if (left.length !== right.length)
|
|
11
|
+
return null;
|
|
12
|
+
const differences = left.flatMap((value, index) => value === right[index] ? [] : [{ from: value, to: right[index] }]);
|
|
13
|
+
if (differences.length !== 1)
|
|
14
|
+
return null;
|
|
15
|
+
const rename = differences[0];
|
|
16
|
+
const expression = new RegExp(`\\b${rename.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g");
|
|
17
|
+
return before.replace(expression, rename.to) === after ? rename : null;
|
|
18
|
+
}
|
|
19
|
+
function explainFile(file, matcher) {
|
|
20
|
+
const explanations = [];
|
|
21
|
+
const explained = new Set();
|
|
22
|
+
if (file.status === "R" && file.oldPath)
|
|
23
|
+
explanations.push({ kind: "file-rename", from: file.oldPath, to: file.path, similarity: file.similarity ?? 0 });
|
|
24
|
+
for (const hunk of file.hunks) {
|
|
25
|
+
const changed = hunk.lines.filter((line) => line.kind !== "context");
|
|
26
|
+
for (const line of changed) {
|
|
27
|
+
if (!importLine(line))
|
|
28
|
+
continue;
|
|
29
|
+
explanations.push({ kind: "import", hunk: hunk.index, oldLine: line.oldLine, newLine: line.newLine });
|
|
30
|
+
explained.add(`${hunk.index}:${line.kind}:${line.oldLine}:${line.newLine}`);
|
|
31
|
+
}
|
|
32
|
+
const deletions = changed.filter((line) => line.kind === "deletion" && !importLine(line));
|
|
33
|
+
const additions = changed.filter((line) => line.kind === "addition" && !importLine(line));
|
|
34
|
+
if (deletions.length === additions.length)
|
|
35
|
+
deletions.forEach((deletion, index) => {
|
|
36
|
+
const addition = additions[index];
|
|
37
|
+
const rename = identifierRename(deletion.content, addition.content);
|
|
38
|
+
if (!rename)
|
|
39
|
+
return;
|
|
40
|
+
explanations.push({ kind: "symbol-rename", ...rename, hunk: hunk.index, oldLine: deletion.oldLine, newLine: addition.newLine });
|
|
41
|
+
explained.add(`${hunk.index}:${deletion.kind}:${deletion.oldLine}:${deletion.newLine}`);
|
|
42
|
+
explained.add(`${hunk.index}:${addition.kind}:${addition.oldLine}:${addition.newLine}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const residue = file.binary ? [{ hunk: -1, oldLine: null, newLine: null, kind: "binary", content: "binary content changed" }] : [];
|
|
46
|
+
for (const hunk of file.hunks)
|
|
47
|
+
for (const line of hunk.lines) {
|
|
48
|
+
if (line.kind === "context")
|
|
49
|
+
continue;
|
|
50
|
+
const key = `${hunk.index}:${line.kind}:${line.oldLine}:${line.newLine}`;
|
|
51
|
+
if (!explained.has(key))
|
|
52
|
+
residue.push({ hunk: hunk.index, oldLine: line.oldLine, newLine: line.newLine, kind: line.kind, content: line.content });
|
|
53
|
+
}
|
|
54
|
+
return { path: file.path, ...(file.oldPath ? { oldPath: file.oldPath } : {}), matcher, explanations, residue, class: matcher.matched ? "in-scope" : residue.length === 0 && explanations.length > 0 ? "downgraded" : "violated" };
|
|
55
|
+
}
|
|
56
|
+
export async function evaluateScope(input) {
|
|
57
|
+
const matchers = await scopePathEvidence(input.cwd, input.frozen.scope, input.diff.files.map((file) => file.path));
|
|
58
|
+
return input.diff.files.map((file, index) => explainFile(file, matchers[index]));
|
|
59
|
+
}
|
|
60
|
+
function rawHunk(file, hunk) {
|
|
61
|
+
return [`${file.status} ${file.oldPath ? `${file.oldPath} -> ` : ""}${file.path} · +${file.insertions} -${file.deletions}`, hunk.header, ...hunk.lines.map((line) => `${line.kind === "addition" ? "+" : line.kind === "deletion" ? "-" : " "}${line.content}`)].join("\n");
|
|
62
|
+
}
|
|
63
|
+
function rawFile(file) { return `${file.status} ${file.oldPath ? `${file.oldPath} -> ` : ""}${file.path} · +${file.insertions} -${file.deletions}`; }
|
|
64
|
+
function moduleFor(pathname) { const segments = pathname.split("/"); return segments.length > 1 ? segments.slice(0, -1).join("/") : "."; }
|
|
65
|
+
function byteLength(content) { return new TextEncoder().encode(content).byteLength; }
|
|
66
|
+
function isAuthorityBoundary(pathname) { return /(^|\/)(auth(?:entication|orization)?|schema|schemas|migrations?|api|config|ci|\.github|permissions?|concurrency)(\/|$)|(^|\/)(package|tsconfig|vite|webpack|eslint|prettier)\.(json|ya?ml|[cm]?[jt]s)$/.test(pathname); }
|
|
67
|
+
function isGeneratedOrLowPriority(pathname, file) { return file.binary || /(^|\/)(vendor|generated|dist|build)(\/|$)|(^|\/)([^/]+\.)?(lock|min)\.(json|ya?ml|js|css)$|(?:^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/.test(pathname); }
|
|
68
|
+
function isSource(pathname) { return /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|rb|php|cs|swift)$/.test(pathname) && !/(^|\/)(test|tests|__tests__|spec)(\/|\.)/.test(pathname); }
|
|
69
|
+
function hasRelatedTest(file, all) { const stem = file.path.replace(/\.[^.]+$/, "").replace(/\/index$/, ""); return all.some((candidate) => candidate.path !== file.path && /(test|tests|__tests__|spec)/.test(candidate.path) && candidate.path.replace(/\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|rb|php|cs|swift)$/, "").includes(stem.split("/").at(-1) ?? "")); }
|
|
70
|
+
function portfolioTier(file, scope, all) {
|
|
71
|
+
if (scope.class === "violated")
|
|
72
|
+
return { tier: 0, mandatory: true };
|
|
73
|
+
if (isAuthorityBoundary(file.path))
|
|
74
|
+
return { tier: 1, mandatory: true };
|
|
75
|
+
if (/(^|\/)(package\.json|workspace|pnpm-workspace|turbo|nx)\b/.test(file.path))
|
|
76
|
+
return { tier: 2, mandatory: false };
|
|
77
|
+
if (isGeneratedOrLowPriority(file.path, file))
|
|
78
|
+
return { tier: 6, mandatory: false };
|
|
79
|
+
if (isSource(file.path) && !hasRelatedTest(file, all))
|
|
80
|
+
return { tier: 3, mandatory: false };
|
|
81
|
+
if (file.status === "R" || scope.class === "downgraded")
|
|
82
|
+
return { tier: 5, mandatory: false };
|
|
83
|
+
return { tier: 4, mandatory: false };
|
|
84
|
+
}
|
|
85
|
+
function stablePortfolioOrder(left, right) { if (left.tier !== right.tier)
|
|
86
|
+
return left.tier - right.tier; const ratio = (1000 - right.tier * 100) / Math.max(right.bytes, 1) - (1000 - left.tier * 100) / Math.max(left.bytes, 1); return ratio || left.path.localeCompare(right.path) || (left.hunk ?? -1) - (right.hunk ?? -1); }
|
|
87
|
+
function retrieval(from, to, pathname, hunk) { return { kind: "git-coordinate", from, to, argv: ["git", "diff", "--no-color", "--no-ext-diff", "--unified=3", `${from}..${to}`, "--", pathname], path: pathname, hunk }; }
|
|
88
|
+
export function presentDiff(input) {
|
|
89
|
+
const { diff, scope, budget } = input;
|
|
90
|
+
const candidates = [];
|
|
91
|
+
for (let fileIndex = 0; fileIndex < diff.files.length; fileIndex += 1) {
|
|
92
|
+
const file = diff.files[fileIndex];
|
|
93
|
+
const policy = portfolioTier(file, scope[fileIndex], diff.files);
|
|
94
|
+
const chunks = file.hunks.length === 0 ? [{ hunk: null, content: rawFile(file) }] : file.hunks.map((hunk) => ({ hunk: hunk.index, content: rawHunk(file, hunk) }));
|
|
95
|
+
for (const chunk of chunks) {
|
|
96
|
+
const bytes = byteLength(chunk.content);
|
|
97
|
+
candidates.push({ path: file.path, hunk: chunk.hunk, content: chunk.content, bytes, cost: bytes, tier: policy.tier, mandatory: policy.mandatory, module: moduleFor(file.path) });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const selected = [];
|
|
101
|
+
const selectedKeys = new Set();
|
|
102
|
+
let used = 0;
|
|
103
|
+
const keyFor = (candidate) => `${candidate.path}\0${candidate.hunk ?? "file"}`;
|
|
104
|
+
const take = (candidate) => { const key = keyFor(candidate); if (selectedKeys.has(key))
|
|
105
|
+
return; selectedKeys.add(key); selected.push(candidate); used += candidate.cost; };
|
|
106
|
+
for (const candidate of [...candidates].filter((item) => item.mandatory).sort(stablePortfolioOrder))
|
|
107
|
+
if (used + candidate.cost <= budget)
|
|
108
|
+
take(candidate);
|
|
109
|
+
for (const module of [...new Set(candidates.map((candidate) => candidate.module))].sort()) {
|
|
110
|
+
const representative = candidates.filter((candidate) => candidate.module === module && !selectedKeys.has(keyFor(candidate))).sort(stablePortfolioOrder)[0];
|
|
111
|
+
if (representative && used + representative.cost <= budget)
|
|
112
|
+
take(representative);
|
|
113
|
+
}
|
|
114
|
+
for (const candidate of [...candidates].sort(stablePortfolioOrder))
|
|
115
|
+
if (!selectedKeys.has(keyFor(candidate)) && used + candidate.cost <= budget)
|
|
116
|
+
take(candidate);
|
|
117
|
+
const omissions = candidates.filter((candidate) => !selectedKeys.has(keyFor(candidate))).sort((left, right) => left.path.localeCompare(right.path) || (left.hunk ?? -1) - (right.hunk ?? -1)).map((candidate) => ({ path: candidate.path, hunk: candidate.hunk, bytes: candidate.bytes, reason: candidate.hunk === null ? "no-raw-hunks" : used + candidate.cost > budget ? "budget" : "not-selected", retrieval: retrieval(input.retrievalCoordinates.from, input.retrievalCoordinates.to, candidate.path, candidate.hunk) }));
|
|
118
|
+
return { budget: { limit: budget, used }, selected: selected.sort((left, right) => left.path.localeCompare(right.path) || (left.hunk ?? -1) - (right.hunk ?? -1)), omissions };
|
|
119
|
+
}
|
|
120
|
+
function fileFacts(diff, scope) { return diff.files.map((file, index) => { const row = scope[index]; return { path: file.path, ...(file.oldPath ? { oldPath: file.oldPath } : {}), status: file.status, stats: { insertions: file.insertions, deletions: file.deletions, binary: file.binary }, matcher: row.matcher, explanations: row.explanations, residue: row.residue, class: row.class }; }); }
|
|
121
|
+
export async function evaluateVerification(cwd, head) {
|
|
122
|
+
let commands;
|
|
123
|
+
try {
|
|
124
|
+
commands = await resolveVerificationCommands(cwd);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) }, results: [] };
|
|
128
|
+
}
|
|
129
|
+
if (commands.length === 0)
|
|
130
|
+
return { status: "not_configured", results: [] };
|
|
131
|
+
const results = [];
|
|
132
|
+
for (const command of commands) {
|
|
133
|
+
const argv = [command.command, ...command.args];
|
|
134
|
+
const known = verificationExecutor(command);
|
|
135
|
+
if ("cause" in known) {
|
|
136
|
+
results.push({ status: "unverifiable", label: command.label, command: command.command, argv, head, cause: known.cause });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const execution = await runVerificationCommand(cwd, command);
|
|
141
|
+
results.push({ status: "executed", label: command.label, executor: known.executor, body: known.body, command: command.command, argv, head, exitCode: execution.code, durationMs: execution.durationMs, stdout: execution.stdout, stderr: execution.stderr });
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return { status: "unknown", cause: { code: error instanceof FlowError ? error.code : "INTERNAL_STATE", message: asMessage(error) }, results: [] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { status: "evaluated", results };
|
|
148
|
+
}
|
|
149
|
+
export async function collectEvidence(input) {
|
|
150
|
+
const candidate = input.candidateResult.candidate;
|
|
151
|
+
if (!candidate)
|
|
152
|
+
return null;
|
|
153
|
+
if (!input.diff)
|
|
154
|
+
return null;
|
|
155
|
+
const diff = input.diff;
|
|
156
|
+
const scope = await evaluateScope({ cwd: input.candidateResult.cwd, frozen: input.frozen, diff });
|
|
157
|
+
return { diff, scope, presentation: presentDiff({ diff, scope, budget: input.diffBudgetBytes, retrievalCoordinates: { from: input.frozen.base, to: input.frozen.coordinates.delivery.head } }), files: fileFacts(diff, scope), verification: await evaluateVerification(input.candidateResult.cwd, candidate.head), candidate };
|
|
158
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createAuditCandidate, disposeAuditCandidate, readAuditCandidateDiff } from "./candidate.js";
|
|
2
|
+
import { initialCoordinateRace, pinAuditCoordinates, pinAuditSnapshot, rereadCoordinateRace } from "./coordinates.js";
|
|
3
|
+
import { collectEvidence } from "./evidence.js";
|
|
4
|
+
import { collectFindings, emptyRaceReport } from "./report.js";
|
|
5
|
+
import { readinessFromFacts } from "./readiness.js";
|
|
6
|
+
export async function auditContract(input) {
|
|
7
|
+
const contractId = input.address.binding.commissionId;
|
|
8
|
+
let frozen;
|
|
9
|
+
try {
|
|
10
|
+
frozen = await pinAuditCoordinates({ cwd: input.cwd, address: input.address });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const fallback = await pinAuditSnapshot({ address: input.address }).catch(() => null);
|
|
14
|
+
if (!fallback)
|
|
15
|
+
throw error;
|
|
16
|
+
return emptyRaceReport({ contractId, coordinates: fallback.coordinates, terms: { criteria: fallback.criteria, scope: fallback.scope, appendScope: fallback.appendScope }, detail: error instanceof Error ? error.message : String(error), race: initialCoordinateRace({ frozen: fallback, observed: error instanceof Error ? error.message : String(error) }) });
|
|
17
|
+
}
|
|
18
|
+
const candidateResult = await createAuditCandidate({ root: input.address.repo.stableRoot, contractId, frozen });
|
|
19
|
+
let evidence = null;
|
|
20
|
+
let verification;
|
|
21
|
+
try {
|
|
22
|
+
const diff = await readAuditCandidateDiff(candidateResult);
|
|
23
|
+
evidence = await collectEvidence({ candidateResult, frozen, diff, diffBudgetBytes: input.diffBudgetBytes ?? 32_000 });
|
|
24
|
+
verification = evidence?.verification ?? { status: "not_evaluated", reason: candidateResult.rehearsal.status === "conflict" ? "renew_conflict" : "renew_unknown", results: [] };
|
|
25
|
+
await input.afterEvidence?.();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await disposeAuditCandidate(candidateResult);
|
|
29
|
+
}
|
|
30
|
+
const race = await rereadCoordinateRace({ cwd: input.cwd, address: input.address, frozen });
|
|
31
|
+
if (race) {
|
|
32
|
+
const detail = typeof race.observed === "string"
|
|
33
|
+
? `final coordinate reread failed: ${race.observed}`
|
|
34
|
+
: race.which === "delivery-worktree"
|
|
35
|
+
? "delivery worktree bytes or HEAD changed"
|
|
36
|
+
: `audit coordinates changed while evidence was collected: ${race.which}`;
|
|
37
|
+
return emptyRaceReport({ contractId, coordinates: frozen.coordinates, terms: { criteria: frozen.criteria, scope: frozen.scope, appendScope: frozen.appendScope }, detail, race });
|
|
38
|
+
}
|
|
39
|
+
const readiness = readinessFromFacts(frozen.readiness);
|
|
40
|
+
const findings = collectFindings({ contractId, deliveryCwd: frozen.deliveryCwd, rehearsal: candidateResult.rehearsal, scope: evidence?.scope ?? null, verification, readiness });
|
|
41
|
+
return { contract: contractId, coordinates: frozen.coordinates, terms: { criteria: frozen.criteria, scope: frozen.scope, appendScope: frozen.appendScope }, rehearsal: candidateResult.rehearsal, candidate: evidence ? (evidence.candidate.rewritten ? { kind: "renew-rehearsal", base: evidence.candidate.base, head: evidence.candidate.head, tree: evidence.candidate.tree, deliveryHead: frozen.coordinates.delivery.head, targetHead: frozen.coordinates.target.head } : { kind: "delivery", base: evidence.candidate.base, head: evidence.candidate.head, tree: evidence.candidate.tree }) : null, diff: evidence?.diff ?? null, presentation: evidence?.presentation ?? null, files: evidence?.files ?? null, scope: evidence?.scope ?? null, verification, readiness, findings, counts: { files: evidence?.files.length ?? 0, selected: evidence?.presentation.selected.length ?? 0, omitted: evidence?.presentation.omissions.length ?? 0, inScope: evidence?.scope.filter((row) => row.class === "in-scope").length ?? 0, downgraded: evidence?.scope.filter((row) => row.class === "downgraded").length ?? 0, violated: evidence?.scope.filter((row) => row.class === "violated").length ?? 0, findings: findings.length } };
|
|
42
|
+
}
|