@astrosheep/keiyaku 2.9.4 → 2.9.6
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/call-terms.js +1 -0
- package/build/agents/harness/execution-handle.js +1 -0
- package/build/agents/harness/outcome.js +31 -5
- package/build/agents/harness/projection.js +151 -78
- 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 +43 -10
- package/build/agents/providers/codex-app-server/events.js +23 -0
- 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/events.js +4 -3
- package/build/agents/providers/pi/registration.js +3 -1
- package/build/agents/types.js +2 -0
- package/build/cli/commands/akuma/akuma/meta.js +1 -1
- package/build/cli/commands/akuma/list/meta-ls.js +1 -1
- package/build/cli/commands/akuma/view/meta.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/contract/arc/handler.js +1 -1
- package/build/cli/commands/contract/arc/meta.js +4 -4
- package/build/cli/commands/contract/audit/handler.js +16 -0
- package/build/cli/commands/contract/audit/meta.js +16 -0
- package/build/cli/commands/contract/bind/meta.js +20 -11
- package/build/cli/commands/contract/catalog.js +2 -0
- package/build/cli/commands/contract/forfeit/meta.js +1 -1
- package/build/cli/commands/contract/log/meta.js +1 -1
- package/build/cli/commands/contract/petition/handler.js +1 -2
- package/build/cli/commands/contract/petition/meta.js +6 -6
- package/build/cli/commands/contract/renew/handler.js +2 -2
- package/build/cli/commands/contract/renew/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/call/meta.js +4 -1
- package/build/cli/commands/projection/kill/meta.js +1 -1
- package/build/cli/commands/projection/revive/meta.js +4 -1
- package/build/cli/commands/projection/status/meta.js +1 -1
- package/build/cli/commands/projection/tell/handler.js +1 -1
- package/build/cli/commands/projection/tell/meta.js +4 -1
- package/build/cli/commands/projection/wait/meta.js +1 -1
- package/build/cli/commands/skills/install/meta.js +1 -1
- package/build/cli/commands/skills/skills/meta.js +1 -1
- package/build/cli/commands/system/completion/meta.js +1 -1
- package/build/cli/commands/system/dump-env/meta.js +1 -1
- package/build/cli/commands/system/guide/meta.js +1 -1
- package/build/cli/commands/task/add/handler.js +2 -2
- package/build/cli/commands/task/add/meta.js +4 -1
- package/build/cli/commands/task/doctor/meta.js +1 -1
- 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/ls/handler.js +10 -4
- package/build/cli/commands/task/ls/meta.js +3 -3
- 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/meta.js +1 -1
- package/build/cli/commands/task/task/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +2 -3
- package/build/cli/commands/task/update/meta.js +4 -1
- package/build/cli/completion.js +9 -2
- package/build/cli/flags.js +19 -3
- package/build/cli/help.js +3 -3
- package/build/cli/index.js +59 -75
- package/build/cli/output-stream-error-policy.js +19 -5
- package/build/cli/parse-flags.js +19 -2
- package/build/cli/parse-metadata.js +1 -7
- package/build/cli/parse-selectors.js +1 -0
- package/build/cli/parse.js +16 -7
- package/build/cli/projection-address.js +6 -1
- package/build/cli/render/audit.js +121 -0
- package/build/cli/render/errors.js +18 -1
- package/build/cli/render/format.js +5 -2
- package/build/cli/render/misc.js +3 -1
- package/build/cli/render/projection-activity.js +12 -6
- package/build/cli/render/status.js +28 -18
- package/build/cli/render/success-response.js +6 -5
- package/build/cli/render/task.js +12 -43
- package/build/cli/render/terminal-failure.js +7 -2
- package/build/cli/render/wait.js +36 -8
- package/build/config/env-keys.js +2 -0
- package/build/config/env.js +3 -0
- package/build/core/amend.js +20 -94
- package/build/core/arc.js +14 -6
- 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 -0
- package/build/core/bind.js +1 -1
- package/build/core/call/execution.js +1 -0
- package/build/core/contract-view.js +2 -3
- package/build/core/draft.js +33 -20
- package/build/core/entry.js +3 -4
- package/build/core/markdown/lex.js +2 -1
- package/build/core/projection/generation/database.js +41 -16
- package/build/core/projection/generation/model.js +22 -2
- package/build/core/projection/generation/projection-generation-execution.js +20 -7
- package/build/core/projection/generation/projection-generation-launcher.js +4 -4
- package/build/core/projection/generation/projection-generation-runner.js +1 -0
- package/build/core/projection/generation/store.js +1 -1
- package/build/core/projection/generation/transitions.js +19 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-execution-observer.js +35 -21
- package/build/core/projection/projection-life-observer.js +16 -2
- package/build/core/projection/projection-status.js +67 -22
- package/build/core/projection/projection-terminal-failure.js +2 -0
- package/build/core/projection/projection-wait.js +6 -9
- package/build/core/projection/projection-wake.js +30 -6
- package/build/core/renew.js +41 -22
- package/build/core/scope.js +129 -2
- package/build/core/seal.js +36 -15
- package/build/core/settlement/petition-claim-gates.js +4 -33
- package/build/core/settlement/petition-preview.js +1 -1
- package/build/core/settlement/petition.js +10 -3
- package/build/core/settlement/settlement.js +3 -10
- package/build/core/settlement/verification.js +59 -0
- package/build/core/status/board.js +48 -28
- package/build/core/stored-agent-event.js +3 -2
- package/build/core/task/board.js +1 -0
- package/build/core/task/commands.js +20 -12
- package/build/core/task/document.js +5 -0
- package/build/core/task/index.js +1 -1
- package/build/core/task/model.js +8 -1
- package/build/core/task/query.js +214 -0
- package/build/core/task/settlement-git.js +115 -16
- package/build/core/task/settlement-policy.js +17 -9
- package/build/core/task/source-board.js +1 -0
- package/build/core/task/task-contract.js +125 -51
- package/build/core/task/task-store-repository.js +1 -0
- package/build/core/task/task.js +2 -0
- package/build/flow-error.js +2 -4
- package/build/generated/version.js +2 -2
- package/build/git/diff/parsers.js +23 -13
- package/build/git/diff/structured.js +127 -0
- package/build/index.js +7 -7
- package/build/telemetry/logger.js +4 -2
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +2 -2
- package/skills/keiyaku-akuma/SKILL.md +17 -6
- package/skills/keiyaku-task/SKILL.md +16 -7
- package/skills/keiyaku-workflow/SKILL.md +48 -13
package/build/core/amend.js
CHANGED
|
@@ -5,91 +5,33 @@ import { refreshContractView } from "./contract-view.js";
|
|
|
5
5
|
import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
|
|
6
6
|
import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
|
|
7
7
|
import { resolveDefaultBranch } from "./status/drift.js";
|
|
8
|
-
import {
|
|
8
|
+
import { markdownSourceSections, normalizeScopePattern, validateScopeFragmentLines } from "./scope.js";
|
|
9
9
|
import { normalizeSectionTitle } from "./markdown/titles.js";
|
|
10
10
|
import { resolveContractDeliveryCwd } from "./worktree-path.js";
|
|
11
11
|
function amendScopeHeading(name) {
|
|
12
12
|
return `## Scope ${name[0].toUpperCase()}${name.slice(1)}`;
|
|
13
13
|
}
|
|
14
|
-
function sourceLines(markdown) {
|
|
15
|
-
const lines = [];
|
|
16
|
-
let start = 0;
|
|
17
|
-
while (start < markdown.length) {
|
|
18
|
-
const newline = markdown.indexOf("\n", start);
|
|
19
|
-
const end = newline === -1 ? markdown.length : newline;
|
|
20
|
-
const next = newline === -1 ? markdown.length : newline + 1;
|
|
21
|
-
const raw = markdown.slice(start, end);
|
|
22
|
-
lines.push({ text: raw.endsWith("\r") ? raw.slice(0, -1) : raw, start, end, next });
|
|
23
|
-
start = next;
|
|
24
|
-
}
|
|
25
|
-
return lines;
|
|
26
|
-
}
|
|
27
|
-
function amendHeading(line) {
|
|
28
|
-
const match = line.match(/^ {0,3}(#{1,2})(?!#)[ \t]+(.+)$/);
|
|
29
|
-
if (!match)
|
|
30
|
-
return null;
|
|
31
|
-
const level = match[1].length;
|
|
32
|
-
const title = normalizeSectionTitle(match[2]);
|
|
33
|
-
if (level !== 2)
|
|
34
|
-
return { level };
|
|
35
|
-
if (title === "scope add")
|
|
36
|
-
return { level, name: "add" };
|
|
37
|
-
return { level };
|
|
38
|
-
}
|
|
39
|
-
function amendFence(line) {
|
|
40
|
-
const match = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
41
|
-
if (!match)
|
|
42
|
-
return null;
|
|
43
|
-
const marker = match[1];
|
|
44
|
-
return { delimiter: marker[0], length: marker.length };
|
|
45
|
-
}
|
|
46
14
|
function parseAmendScopeSections(markdown) {
|
|
47
|
-
const lines = sourceLines(markdown);
|
|
48
15
|
const sections = {};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let frontMatterEnd = -1;
|
|
52
|
-
if (lines[0]?.text.trim() === "---") {
|
|
53
|
-
for (let index = 1; index < lines.length; index += 1) {
|
|
54
|
-
if (lines[index].text.trim() === "---") {
|
|
55
|
-
frontMatterEnd = index;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const finishActive = (end) => {
|
|
61
|
-
if (!active)
|
|
62
|
-
return;
|
|
63
|
-
sections[active.name] = { ...active, end };
|
|
64
|
-
active = undefined;
|
|
65
|
-
};
|
|
66
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
67
|
-
const line = lines[index];
|
|
68
|
-
if (index <= frontMatterEnd)
|
|
69
|
-
continue;
|
|
70
|
-
const marker = amendFence(line.text);
|
|
71
|
-
if (fence) {
|
|
72
|
-
if (marker && marker.delimiter === fence.delimiter && marker.length >= fence.length) {
|
|
73
|
-
fence = undefined;
|
|
74
|
-
}
|
|
16
|
+
for (const section of markdownSourceSections(markdown)) {
|
|
17
|
+
if (section.level !== 2)
|
|
75
18
|
continue;
|
|
19
|
+
const title = normalizeSectionTitle(section.title);
|
|
20
|
+
if (title === "scope add") {
|
|
21
|
+
throw new FlowError("INVALID_KEIYAKU_DRAFT", "Scope Add is obsolete; use ## Scope Append with a fenced gitignore-subset block");
|
|
76
22
|
}
|
|
77
|
-
if (
|
|
78
|
-
fence = marker;
|
|
23
|
+
if (title !== "scope append")
|
|
79
24
|
continue;
|
|
25
|
+
if (sections.append) {
|
|
26
|
+
throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input may contain ${amendScopeHeading("append")} at most once`);
|
|
80
27
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (sections[heading.name]) {
|
|
88
|
-
throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input may contain ${amendScopeHeading(heading.name)} at most once`);
|
|
89
|
-
}
|
|
90
|
-
active = { name: heading.name, start: line.start, contentStart: line.next };
|
|
28
|
+
sections.append = {
|
|
29
|
+
name: "append",
|
|
30
|
+
start: section.start,
|
|
31
|
+
contentStart: section.contentStart,
|
|
32
|
+
end: section.end,
|
|
33
|
+
};
|
|
91
34
|
}
|
|
92
|
-
finishActive(markdown.length);
|
|
93
35
|
let prose = markdown;
|
|
94
36
|
for (const section of Object.values(sections).sort((left, right) => right.start - left.start)) {
|
|
95
37
|
prose = `${prose.slice(0, section.start)}${prose.slice(section.end)}`;
|
|
@@ -97,30 +39,14 @@ function parseAmendScopeSections(markdown) {
|
|
|
97
39
|
return { sections, prose };
|
|
98
40
|
}
|
|
99
41
|
function scopePatterns(markdown, section) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
.filter((line) => !isBlankScopePatternLine(line))
|
|
104
|
-
.map((pattern) => {
|
|
105
|
-
try {
|
|
106
|
-
// Do not trim() here: gitignore treats trailing `\ ` as a significant space.
|
|
107
|
-
return normalizeScopePattern(pattern);
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
if (error instanceof ScopePatternError) {
|
|
111
|
-
throw new FlowError("INVALID_KEIYAKU_DRAFT", error.message);
|
|
112
|
-
}
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
42
|
+
return validateScopeFragmentLines(markdown.slice(section.contentStart, section.end).split(/\r?\n/), {
|
|
43
|
+
heading: amendScopeHeading(section.name),
|
|
44
|
+
emptyMessage: `amend input has an empty ${amendScopeHeading(section.name)} section`,
|
|
115
45
|
});
|
|
116
|
-
if (patterns.length === 0) {
|
|
117
|
-
throw new FlowError("INVALID_KEIYAKU_DRAFT", `amend input has an empty ${amendScopeHeading(section.name)} section`);
|
|
118
|
-
}
|
|
119
|
-
return patterns;
|
|
120
46
|
}
|
|
121
47
|
export function buildAmendInput(markdown, cwd, contractId, contractAddressSource) {
|
|
122
48
|
const { sections, prose } = parseAmendScopeSections(markdown);
|
|
123
|
-
const
|
|
49
|
+
const append = sections.append ? scopePatterns(markdown, sections.append) : [];
|
|
124
50
|
if (!prose.trim()) {
|
|
125
51
|
throw new FlowError("EMPTY_PARAM", "amendment cannot be empty");
|
|
126
52
|
}
|
|
@@ -129,7 +55,7 @@ export function buildAmendInput(markdown, cwd, contractId, contractAddressSource
|
|
|
129
55
|
...(contractId ? { contractId } : {}),
|
|
130
56
|
...(contractAddressSource ? { contractAddressSource } : {}),
|
|
131
57
|
amendment: prose,
|
|
132
|
-
...(sections.
|
|
58
|
+
...(sections.append ? { scopeDelta: { add: append } } : {}),
|
|
133
59
|
};
|
|
134
60
|
}
|
|
135
61
|
function requireAmendment(value) {
|
package/build/core/arc.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseCommandMarkdown, requireMarkdownTextSection, requireMarkdownTitle,
|
|
|
9
9
|
import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
|
|
10
10
|
import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
|
|
11
11
|
import { refreshContractView } from "./contract-view.js";
|
|
12
|
-
import { findOpenArc, getMaxArc, sealCurrentArc } from "./seal.js";
|
|
12
|
+
import { appendScopeTerms, findOpenArc, getMaxArc, sealCurrentArc } from "./seal.js";
|
|
13
13
|
import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
|
|
14
14
|
import { resolveDefaultBranch } from "./status/drift.js";
|
|
15
15
|
import { persistResponseHistory } from "./transcripts.js";
|
|
@@ -61,6 +61,14 @@ export async function openArc(input) {
|
|
|
61
61
|
throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
|
|
62
62
|
}
|
|
63
63
|
assertArcOpenable(input.contractId, ledger.entries);
|
|
64
|
+
await appendScopeTerms({
|
|
65
|
+
cwd: input.cwd,
|
|
66
|
+
renderCwd,
|
|
67
|
+
contractId: input.contractId,
|
|
68
|
+
patterns: input.appendScope,
|
|
69
|
+
actor: input.actor,
|
|
70
|
+
nowMs: input.nowMs,
|
|
71
|
+
});
|
|
64
72
|
// Seal capture needs delivery HEAD; ledger append may use hub or delivery (shared refs).
|
|
65
73
|
const sealed = await sealCurrentArc({
|
|
66
74
|
cwd: renderCwd,
|
|
@@ -68,7 +76,6 @@ export async function openArc(input) {
|
|
|
68
76
|
contractId: input.contractId,
|
|
69
77
|
sealedBy: "arc",
|
|
70
78
|
nowMs: input.nowMs,
|
|
71
|
-
waivePaths: input.waivePaths,
|
|
72
79
|
});
|
|
73
80
|
const at = new Date(input.nowMs).toISOString();
|
|
74
81
|
let arcNumber = 0;
|
|
@@ -93,7 +100,7 @@ export async function openArc(input) {
|
|
|
93
100
|
await refreshContractView({ ledgerCwd: input.cwd, renderCwd, contractId: input.contractId });
|
|
94
101
|
return { contractId: input.contractId, arc: arcNumber, ledgerHead: result.head, sealed };
|
|
95
102
|
}
|
|
96
|
-
export function buildArcInput(content, cwd,
|
|
103
|
+
export function buildArcInput(content, cwd, appendScope = [], contractId, contractAddressSource) {
|
|
97
104
|
const parts = parseCommandMarkdown(content);
|
|
98
105
|
return {
|
|
99
106
|
cwd,
|
|
@@ -102,7 +109,7 @@ export function buildArcInput(content, cwd, waivePaths = [], contractId, contrac
|
|
|
102
109
|
title: requireMarkdownTitle(parts),
|
|
103
110
|
objective: requireMarkdownTextSection(parts, "Objective"),
|
|
104
111
|
brief: requireMarkdownTextSection(parts, "Brief"),
|
|
105
|
-
|
|
112
|
+
appendScope,
|
|
106
113
|
};
|
|
107
114
|
}
|
|
108
115
|
async function readBranchLabels(cwd) {
|
|
@@ -126,16 +133,17 @@ export async function arcKeiyaku(input) {
|
|
|
126
133
|
const title = normalizeInlineTitle(requireText("title", input.title));
|
|
127
134
|
const objective = requireText("objective", input.objective);
|
|
128
135
|
const brief = requireText("brief", input.brief);
|
|
136
|
+
const nowMs = Date.now();
|
|
129
137
|
const opened = await openArc({
|
|
130
138
|
cwd: input.cwd,
|
|
131
139
|
renderCwd,
|
|
132
140
|
contractId,
|
|
133
141
|
actor: ACTOR_IDENTITY,
|
|
134
|
-
nowMs
|
|
142
|
+
nowMs,
|
|
135
143
|
title,
|
|
136
144
|
objective,
|
|
137
145
|
brief,
|
|
138
|
-
|
|
146
|
+
appendScope: input.appendScope,
|
|
139
147
|
});
|
|
140
148
|
const { currentBranch, baseBranch } = await readBranchLabels(renderCwd);
|
|
141
149
|
const summary = opened.sealed
|
|
@@ -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
|
+
}
|