balladeer 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/anchoring.js +2 -2
- package/dist/anchors-schema.d.ts +3 -1
- package/dist/anchors-schema.js +5 -3
- package/dist/commands/judge.d.ts +50 -1
- package/dist/commands/judge.js +768 -110
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +24 -1
- package/dist/currency.js +9 -3
- package/dist/guidance-hook.mjs +13 -3
- package/dist/headless-agent.d.ts +2 -0
- package/dist/headless-agent.js +5 -1
- package/dist/judge-brief.d.ts +11 -5
- package/dist/judge-brief.js +26 -10
- package/dist/judge-evidence.d.ts +87 -0
- package/dist/judge-evidence.js +393 -0
- package/dist/judge-hook.d.ts +14 -1
- package/dist/judge-hook.js +34 -5
- package/dist/judge-review.d.ts +10 -0
- package/dist/judge-review.js +36 -0
- package/dist/judge-said.d.ts +1 -0
- package/dist/judge-said.js +3 -0
- package/dist/promise-meaning.d.ts +4 -0
- package/dist/promise-review-client.d.ts +84 -0
- package/dist/promise-review-client.js +357 -0
- package/dist/push-operation.d.ts +61 -0
- package/dist/push-operation.js +472 -0
- package/dist/read-only-review.d.ts +2 -0
- package/dist/read-only-review.js +2 -0
- package/dist/review-backstop.d.ts +20 -0
- package/dist/review-backstop.js +54 -0
- package/dist/review-reporting.d.ts +32 -0
- package/dist/review-reporting.js +319 -0
- package/dist/review-selection.d.ts +33 -0
- package/dist/review-selection.js +64 -0
- package/dist/review-settings.d.ts +6 -0
- package/dist/review-settings.js +9 -0
- package/dist/self-update.d.ts +9 -0
- package/dist/self-update.js +35 -1
- package/dist/user-scope.d.ts +1 -10
- package/dist/user-scope.js +12 -7
- package/dist/wire.d.ts +3 -3
- package/dist/wire.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type PromiseAnchors } from "./anchors-schema.js";
|
|
2
|
+
import type { AnchorsClient } from "./anchors-client.js";
|
|
3
|
+
import { connectionAgent, type PromiseDocument, type PromiseReader } from "./promise-meaning.js";
|
|
4
|
+
import type { StoredAgent } from "./store.js";
|
|
5
|
+
import { type ReviewSettings } from "./review-settings.js";
|
|
6
|
+
export type ReviewSubject = Readonly<{
|
|
7
|
+
kind: "agreed";
|
|
8
|
+
promiseId: string;
|
|
9
|
+
revisionId: string;
|
|
10
|
+
}> | Readonly<{
|
|
11
|
+
kind: "recorded";
|
|
12
|
+
proposalId: string;
|
|
13
|
+
candidateRevision: number;
|
|
14
|
+
candidateEventId: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const reviewSubjectId: (s: ReviewSubject) => string;
|
|
17
|
+
export type ReviewTarget = PromiseDocument & Readonly<{
|
|
18
|
+
subject: ReviewSubject;
|
|
19
|
+
standing: "recorded" | "agreed";
|
|
20
|
+
anchors: PromiseAnchors | null;
|
|
21
|
+
openFindings?: readonly Readonly<{
|
|
22
|
+
reportId: string;
|
|
23
|
+
sourceSha: string;
|
|
24
|
+
baseSha: string;
|
|
25
|
+
destinationRef: string;
|
|
26
|
+
destinationDigest?: string;
|
|
27
|
+
}>[];
|
|
28
|
+
}>;
|
|
29
|
+
export type ReviewCatalog = Readonly<{
|
|
30
|
+
ok: true;
|
|
31
|
+
targets: readonly ReviewTarget[];
|
|
32
|
+
reviewSettings?: ReviewSettings;
|
|
33
|
+
}> | Readonly<{
|
|
34
|
+
ok: false;
|
|
35
|
+
reason: string;
|
|
36
|
+
unsupported?: boolean;
|
|
37
|
+
reviewSettings?: ReviewSettings;
|
|
38
|
+
}>;
|
|
39
|
+
export type ReviewReceipt = Readonly<{
|
|
40
|
+
schemaVersion: "balladeer-review-receipt/v1";
|
|
41
|
+
subject: ReviewSubject;
|
|
42
|
+
semanticDigest: string;
|
|
43
|
+
baseSha: string;
|
|
44
|
+
sourceSha: string;
|
|
45
|
+
destinationRef: string;
|
|
46
|
+
destinationDigest?: string;
|
|
47
|
+
verdict: "kept" | "broken" | "could_not_tell";
|
|
48
|
+
reproductionDigest?: string;
|
|
49
|
+
briefRevision: string;
|
|
50
|
+
client: "codex" | "claude";
|
|
51
|
+
resolvesReportIds?: readonly string[];
|
|
52
|
+
ancestryVerified?: true;
|
|
53
|
+
}>;
|
|
54
|
+
export type ReviewsClient = Readonly<{
|
|
55
|
+
list(): Promise<ReviewCatalog>;
|
|
56
|
+
record(receipt: ReviewReceipt, options?: {
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
ok: true;
|
|
61
|
+
reportId: string;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: string;
|
|
65
|
+
}>;
|
|
66
|
+
recordAnchors(target: ReviewTarget, anchors: PromiseAnchors): ReturnType<AnchorsClient["record"]>;
|
|
67
|
+
}>;
|
|
68
|
+
/** Read current meaning once per push. Customer code and test output never enter this client. */
|
|
69
|
+
export declare function reviewsClientFor(agent: StoredAgent, options?: {
|
|
70
|
+
fetchImpl?: typeof fetch;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}): ReviewsClient;
|
|
73
|
+
export declare function connectionReviewsClient(input: Parameters<typeof connectionAgent>[0]): {
|
|
74
|
+
ok: true;
|
|
75
|
+
client: ReviewsClient;
|
|
76
|
+
} | {
|
|
77
|
+
ok: false;
|
|
78
|
+
reason: string;
|
|
79
|
+
};
|
|
80
|
+
/** Adapt the existing local anchor builder without rereading each meaning through MCP. */
|
|
81
|
+
export declare function catalogReaders(client: ReviewsClient, targets: readonly ReviewTarget[]): {
|
|
82
|
+
reader: PromiseReader;
|
|
83
|
+
anchors: AnchorsClient;
|
|
84
|
+
};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { checkPromiseAnchors } from "./anchors-schema.js";
|
|
2
|
+
import { noteServerVersion } from "./currency.js";
|
|
3
|
+
import { agentEndpoint } from "./guidance.js";
|
|
4
|
+
import { connectionAgent, normalizePromise, } from "./promise-meaning.js";
|
|
5
|
+
import { CLIENT_HEADER, CLIENT_HEADER_VALUE } from "./wire.js";
|
|
6
|
+
import { readReviewSettings } from "./review-settings.js";
|
|
7
|
+
export const reviewSubjectId = (s) => s.kind === "agreed" ? s.promiseId : s.proposalId;
|
|
8
|
+
const ID = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
9
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
10
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const PATH = "/api/promise-reviews";
|
|
12
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
13
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14
|
+
const REF = /^refs\/heads\/(?!.*(?:\.\.|@\{|\/\/|[\x00-\x20\x7f~^:?*\[\\]))(?!.*(?:\.lock|\.)$)[A-Za-z0-9_./-]+$/;
|
|
15
|
+
function exactKeys(value, allowed) {
|
|
16
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
17
|
+
}
|
|
18
|
+
/** Runtime checks precede upload: a server refusal cannot undo leaked customer material. */
|
|
19
|
+
function receiptSafe(receipt) {
|
|
20
|
+
const subjectKeys = receipt.subject?.kind === "agreed"
|
|
21
|
+
? ["kind", "promiseId", "revisionId"]
|
|
22
|
+
: ["kind", "proposalId", "candidateRevision", "candidateEventId"];
|
|
23
|
+
return (exactKeys(receipt, [
|
|
24
|
+
"schemaVersion",
|
|
25
|
+
"subject",
|
|
26
|
+
"semanticDigest",
|
|
27
|
+
"baseSha",
|
|
28
|
+
"sourceSha",
|
|
29
|
+
"destinationRef",
|
|
30
|
+
"destinationDigest",
|
|
31
|
+
"verdict",
|
|
32
|
+
"reproductionDigest",
|
|
33
|
+
"briefRevision",
|
|
34
|
+
"client",
|
|
35
|
+
"resolvesReportIds",
|
|
36
|
+
"ancestryVerified",
|
|
37
|
+
]) &&
|
|
38
|
+
receipt.subject !== null &&
|
|
39
|
+
typeof receipt.subject === "object" &&
|
|
40
|
+
exactKeys(receipt.subject, subjectKeys) &&
|
|
41
|
+
subject(receipt.subject) !== undefined &&
|
|
42
|
+
receipt.schemaVersion === "balladeer-review-receipt/v1" &&
|
|
43
|
+
DIGEST.test(receipt.semanticDigest) &&
|
|
44
|
+
SHA.test(receipt.baseSha) &&
|
|
45
|
+
SHA.test(receipt.sourceSha) &&
|
|
46
|
+
receipt.baseSha !== receipt.sourceSha &&
|
|
47
|
+
receipt.destinationRef.length <= 255 &&
|
|
48
|
+
REF.test(receipt.destinationRef) &&
|
|
49
|
+
(receipt.destinationDigest === undefined || DIGEST.test(receipt.destinationDigest)) &&
|
|
50
|
+
["kept", "broken", "could_not_tell"].includes(receipt.verdict) &&
|
|
51
|
+
(receipt.reproductionDigest === undefined || DIGEST.test(receipt.reproductionDigest)) &&
|
|
52
|
+
(receipt.verdict !== "broken" || receipt.reproductionDigest !== undefined) &&
|
|
53
|
+
/^[a-z][a-z0-9-]{0,47}\/v[1-9][0-9]{0,5}$/.test(receipt.briefRevision) &&
|
|
54
|
+
["claude", "codex"].includes(receipt.client) &&
|
|
55
|
+
(receipt.ancestryVerified === undefined || receipt.ancestryVerified === true) &&
|
|
56
|
+
(receipt.resolvesReportIds === undefined ||
|
|
57
|
+
(Array.isArray(receipt.resolvesReportIds) &&
|
|
58
|
+
receipt.resolvesReportIds.length <= 100 &&
|
|
59
|
+
new Set(receipt.resolvesReportIds).size === receipt.resolvesReportIds.length &&
|
|
60
|
+
receipt.resolvesReportIds.every((id) => UUID.test(id)) &&
|
|
61
|
+
receipt.verdict === "kept" &&
|
|
62
|
+
receipt.ancestryVerified === true &&
|
|
63
|
+
receipt.destinationDigest !== undefined)));
|
|
64
|
+
}
|
|
65
|
+
function subject(value) {
|
|
66
|
+
if (value === null || typeof value !== "object")
|
|
67
|
+
return undefined;
|
|
68
|
+
const s = value;
|
|
69
|
+
const id = (v) => typeof v === "string" && ID.test(v);
|
|
70
|
+
if (s.kind === "agreed" && id(s.promiseId) && id(s.revisionId))
|
|
71
|
+
return { kind: "agreed", promiseId: s.promiseId, revisionId: s.revisionId };
|
|
72
|
+
if (s.kind === "recorded" &&
|
|
73
|
+
id(s.proposalId) &&
|
|
74
|
+
id(s.candidateEventId) &&
|
|
75
|
+
typeof s.candidateRevision === "number" &&
|
|
76
|
+
Number.isSafeInteger(s.candidateRevision) &&
|
|
77
|
+
s.candidateRevision > 0)
|
|
78
|
+
return {
|
|
79
|
+
kind: "recorded",
|
|
80
|
+
proposalId: s.proposalId,
|
|
81
|
+
candidateRevision: s.candidateRevision,
|
|
82
|
+
candidateEventId: s.candidateEventId,
|
|
83
|
+
};
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Read current meaning once per push. Customer code and test output never enter this client. */
|
|
87
|
+
export function reviewsClientFor(agent, options = {}) {
|
|
88
|
+
const send = async (body, cursor, timeoutMs = options.timeoutMs ?? 10_000, parentSignal) => {
|
|
89
|
+
const abort = new AbortController();
|
|
90
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
91
|
+
try {
|
|
92
|
+
const url = agentEndpoint(agent, PATH);
|
|
93
|
+
if (cursor !== undefined)
|
|
94
|
+
url.searchParams.set("cursor", cursor);
|
|
95
|
+
const response = await (options.fetchImpl ?? fetch)(url, {
|
|
96
|
+
method: body === undefined ? "GET" : "POST",
|
|
97
|
+
redirect: "manual",
|
|
98
|
+
signal: parentSignal === undefined ? abort.signal : AbortSignal.any([abort.signal, parentSignal]),
|
|
99
|
+
headers: {
|
|
100
|
+
authorization: `Bearer ${agent.token}`,
|
|
101
|
+
[CLIENT_HEADER]: CLIENT_HEADER_VALUE,
|
|
102
|
+
accept: "application/json",
|
|
103
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
104
|
+
},
|
|
105
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
106
|
+
});
|
|
107
|
+
noteServerVersion(response.headers);
|
|
108
|
+
const parts = [];
|
|
109
|
+
let size = 0;
|
|
110
|
+
const reader = response.body?.getReader();
|
|
111
|
+
try {
|
|
112
|
+
if (reader)
|
|
113
|
+
for (;;) {
|
|
114
|
+
const part = await reader.read();
|
|
115
|
+
if (part.done)
|
|
116
|
+
break;
|
|
117
|
+
size += part.value.byteLength;
|
|
118
|
+
if (size > MAX_BYTES)
|
|
119
|
+
throw new Error("oversized");
|
|
120
|
+
parts.push(part.value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await reader?.cancel().catch(() => undefined);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
status: response.status,
|
|
128
|
+
body: JSON.parse(Buffer.concat(parts).toString("utf8")),
|
|
129
|
+
bytes: size,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return {
|
|
134
|
+
error: abort.signal.aborted || parentSignal?.aborted
|
|
135
|
+
? "Balladeer's review catalog timed out"
|
|
136
|
+
: "Balladeer's review response could not be read",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
async list() {
|
|
145
|
+
const targets = [];
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
const cursors = new Set();
|
|
148
|
+
let cursor;
|
|
149
|
+
let bytes = 0;
|
|
150
|
+
let reviewSettings;
|
|
151
|
+
const started = Date.now();
|
|
152
|
+
let deadline = started + (options.timeoutMs ?? 10_000);
|
|
153
|
+
const failed = (reason, unsupported = false) => ({
|
|
154
|
+
ok: false,
|
|
155
|
+
reason,
|
|
156
|
+
...(unsupported ? { unsupported: true } : {}),
|
|
157
|
+
...(reviewSettings === undefined ? {} : { reviewSettings }),
|
|
158
|
+
});
|
|
159
|
+
for (let page = 0; page < 100; page++) {
|
|
160
|
+
if (Date.now() >= deadline)
|
|
161
|
+
return failed("Balladeer’s complete review catalog timed out");
|
|
162
|
+
const reply = await send(undefined, cursor, Math.max(1, deadline - Date.now()));
|
|
163
|
+
if ("error" in reply)
|
|
164
|
+
return failed(reply.error);
|
|
165
|
+
bytes += reply.bytes;
|
|
166
|
+
if (bytes > MAX_BYTES)
|
|
167
|
+
return failed("Balladeer’s complete review catalog exceeded the local size limit");
|
|
168
|
+
if (reply.status !== 200)
|
|
169
|
+
return failed(`Balladeer could not provide the complete review catalog (${reply.status})`, reply.status === 404 || reply.status === 405);
|
|
170
|
+
const body = reply.body;
|
|
171
|
+
if (body?.schemaVersion !== "balladeer-promise-reviews/v1" || !Array.isArray(body.targets))
|
|
172
|
+
return failed("Balladeer's review catalog is invalid");
|
|
173
|
+
// One snapshot for the whole operation, even if settings change during pagination.
|
|
174
|
+
if (page === 0) {
|
|
175
|
+
reviewSettings = readReviewSettings(body.reviewSettings);
|
|
176
|
+
if (reviewSettings !== undefined)
|
|
177
|
+
deadline = Math.min(deadline, started + reviewSettings.budgetSeconds * 1000);
|
|
178
|
+
}
|
|
179
|
+
for (const raw of body.targets) {
|
|
180
|
+
const row = raw;
|
|
181
|
+
const s = subject(row?.subject);
|
|
182
|
+
if (!s ||
|
|
183
|
+
row?.standing !== s.kind ||
|
|
184
|
+
typeof row.semanticDigest !== "string" ||
|
|
185
|
+
!DIGEST.test(row.semanticDigest))
|
|
186
|
+
return failed("Balladeer's catalog contains an invalid current meaning");
|
|
187
|
+
const id = reviewSubjectId(s);
|
|
188
|
+
const normalized = normalizePromise({ ...row, promiseId: id });
|
|
189
|
+
if (!normalized.ok || seen.has(id))
|
|
190
|
+
return failed("Balladeer's catalog contains an unreadable or duplicate meaning");
|
|
191
|
+
seen.add(id);
|
|
192
|
+
let anchors = null;
|
|
193
|
+
if (row.anchors !== null && row.anchors !== undefined) {
|
|
194
|
+
const checked = checkPromiseAnchors(row.anchors, { allowRecordedId: true });
|
|
195
|
+
if (checked.ok &&
|
|
196
|
+
checked.value.promiseId === id &&
|
|
197
|
+
checked.value.semanticDigest === row.semanticDigest)
|
|
198
|
+
anchors = checked.value;
|
|
199
|
+
}
|
|
200
|
+
const openFindings = Array.isArray(row.openFindings)
|
|
201
|
+
? row.openFindings.flatMap((item) => {
|
|
202
|
+
const finding = item;
|
|
203
|
+
return finding !== null &&
|
|
204
|
+
typeof finding?.reportId === "string" &&
|
|
205
|
+
UUID.test(finding.reportId) &&
|
|
206
|
+
typeof finding.sourceSha === "string" &&
|
|
207
|
+
SHA.test(finding.sourceSha) &&
|
|
208
|
+
typeof finding.baseSha === "string" &&
|
|
209
|
+
SHA.test(finding.baseSha) &&
|
|
210
|
+
typeof finding.destinationRef === "string" &&
|
|
211
|
+
REF.test(finding.destinationRef) &&
|
|
212
|
+
(finding.destinationDigest === undefined ||
|
|
213
|
+
(typeof finding.destinationDigest === "string" &&
|
|
214
|
+
DIGEST.test(finding.destinationDigest)))
|
|
215
|
+
? [
|
|
216
|
+
{
|
|
217
|
+
reportId: finding.reportId,
|
|
218
|
+
sourceSha: finding.sourceSha,
|
|
219
|
+
baseSha: finding.baseSha,
|
|
220
|
+
destinationRef: finding.destinationRef,
|
|
221
|
+
...(typeof finding.destinationDigest === "string"
|
|
222
|
+
? { destinationDigest: finding.destinationDigest }
|
|
223
|
+
: {}),
|
|
224
|
+
},
|
|
225
|
+
]
|
|
226
|
+
: [];
|
|
227
|
+
})
|
|
228
|
+
: [];
|
|
229
|
+
targets.push({
|
|
230
|
+
...normalized.promise,
|
|
231
|
+
subject: s,
|
|
232
|
+
standing: s.kind,
|
|
233
|
+
anchors,
|
|
234
|
+
openFindings,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (body.nextCursor === undefined || body.nextCursor === null)
|
|
238
|
+
return { ok: true, targets, ...(reviewSettings === undefined ? {} : { reviewSettings }) };
|
|
239
|
+
if (typeof body.nextCursor !== "string" ||
|
|
240
|
+
body.nextCursor.length > 2048 ||
|
|
241
|
+
cursors.has(body.nextCursor))
|
|
242
|
+
return failed("Balladeer's catalog pagination did not complete");
|
|
243
|
+
cursors.add(body.nextCursor);
|
|
244
|
+
cursor = body.nextCursor;
|
|
245
|
+
}
|
|
246
|
+
return failed("Balladeer's catalog exceeded the review limit");
|
|
247
|
+
},
|
|
248
|
+
async record(receipt, sendOptions) {
|
|
249
|
+
// Validate the serialized representation too: inherited toJSON/getters
|
|
250
|
+
// must not add unbounded material between validation and upload.
|
|
251
|
+
try {
|
|
252
|
+
const serialized = JSON.stringify(receipt);
|
|
253
|
+
if (serialized.length > 16_384)
|
|
254
|
+
throw new Error("large receipt");
|
|
255
|
+
receipt = JSON.parse(serialized);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return { ok: false, reason: "The receipt failed the local data boundary and was not sent" };
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
if (!receiptSafe(receipt))
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
reason: "The receipt failed the local data boundary and was not sent",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { ok: false, reason: "The receipt failed the local data boundary and was not sent" };
|
|
269
|
+
}
|
|
270
|
+
const reply = await send(receipt, undefined, sendOptions?.timeoutMs, sendOptions?.signal);
|
|
271
|
+
if ("error" in reply)
|
|
272
|
+
return { ok: false, reason: reply.error };
|
|
273
|
+
const body = reply.body;
|
|
274
|
+
if ((reply.status !== 200 && reply.status !== 201) ||
|
|
275
|
+
body?.recorded !== true ||
|
|
276
|
+
typeof body.reportId !== "string")
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
reason: `Balladeer did not record the review receipt (${reply.status})`,
|
|
280
|
+
};
|
|
281
|
+
return { ok: true, reportId: body.reportId };
|
|
282
|
+
},
|
|
283
|
+
async recordAnchors(target, anchors) {
|
|
284
|
+
let boundedSubject;
|
|
285
|
+
try {
|
|
286
|
+
const serialized = JSON.stringify(anchors);
|
|
287
|
+
if (serialized.length > 64 * 1024)
|
|
288
|
+
throw new Error("large anchors");
|
|
289
|
+
anchors = JSON.parse(serialized);
|
|
290
|
+
boundedSubject = JSON.parse(JSON.stringify(target.subject));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return { ok: false, reason: "The anchors failed the local data boundary" };
|
|
294
|
+
}
|
|
295
|
+
const currentSubject = subject(boundedSubject);
|
|
296
|
+
if (currentSubject === undefined ||
|
|
297
|
+
!exactKeys(boundedSubject, currentSubject.kind === "agreed"
|
|
298
|
+
? ["kind", "promiseId", "revisionId"]
|
|
299
|
+
: ["kind", "proposalId", "candidateRevision", "candidateEventId"]))
|
|
300
|
+
return { ok: false, reason: "The subject failed the local data boundary" };
|
|
301
|
+
const checked = checkPromiseAnchors(anchors, { allowRecordedId: true });
|
|
302
|
+
if (!checked.ok ||
|
|
303
|
+
anchors.promiseId !== target.promiseId ||
|
|
304
|
+
anchors.semanticDigest !== target.semanticDigest)
|
|
305
|
+
return { ok: false, reason: "The anchors do not match this current meaning" };
|
|
306
|
+
const reply = await send({
|
|
307
|
+
schemaVersion: "balladeer-review-anchors/v1",
|
|
308
|
+
subject: currentSubject,
|
|
309
|
+
semanticDigest: target.semanticDigest,
|
|
310
|
+
anchors,
|
|
311
|
+
});
|
|
312
|
+
if ("error" in reply)
|
|
313
|
+
return { ok: false, reason: reply.error };
|
|
314
|
+
const body = reply.body;
|
|
315
|
+
return (reply.status === 200 || reply.status === 201) && body?.recorded === true
|
|
316
|
+
? { ok: true, alreadyRecorded: body.alreadyRecorded === true }
|
|
317
|
+
: { ok: false, reason: `Balladeer did not record the anchors (${reply.status})` };
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
export function connectionReviewsClient(input) {
|
|
322
|
+
const connection = connectionAgent(input);
|
|
323
|
+
return connection.ok ? { ok: true, client: reviewsClientFor(connection.agent) } : connection;
|
|
324
|
+
}
|
|
325
|
+
/** Adapt the existing local anchor builder without rereading each meaning through MCP. */
|
|
326
|
+
export function catalogReaders(client, targets) {
|
|
327
|
+
const byId = new Map(targets.map((target) => [target.promiseId, target]));
|
|
328
|
+
return {
|
|
329
|
+
reader: {
|
|
330
|
+
async get(id) {
|
|
331
|
+
const promise = byId.get(id);
|
|
332
|
+
return promise
|
|
333
|
+
? { ok: true, promise }
|
|
334
|
+
: { ok: false, reason: "No current review target by that id" };
|
|
335
|
+
},
|
|
336
|
+
async list(options) {
|
|
337
|
+
const ids = [...byId.keys()];
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
ids: options?.max === undefined ? ids : ids.slice(0, options.max),
|
|
341
|
+
...(options?.max !== undefined && ids.length > options.max ? { more: true } : {}),
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
anchors: {
|
|
346
|
+
async list() {
|
|
347
|
+
return { ok: true, promises: targets, refused: 0 };
|
|
348
|
+
},
|
|
349
|
+
async record(anchors) {
|
|
350
|
+
const target = byId.get(anchors.promiseId);
|
|
351
|
+
return target
|
|
352
|
+
? client.recordAnchors(target, anchors)
|
|
353
|
+
: { ok: false, reason: "The target is no longer current" };
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type HookCommandResult } from "./judge-hook.js";
|
|
2
|
+
/** Local metadata only. Neither shell commands nor tool outputs are persisted. */
|
|
3
|
+
export declare const PUSH_OPERATION_SCHEMA = "balladeer-push-operation/v1";
|
|
4
|
+
export declare const PUSH_OPERATION_TTL_MS: number;
|
|
5
|
+
export type PushTarget = Readonly<{
|
|
6
|
+
source: string;
|
|
7
|
+
sourceRef: string;
|
|
8
|
+
head: string | null;
|
|
9
|
+
destination: string;
|
|
10
|
+
/** False for an explicit short destination whose namespace Git resolves remotely. */
|
|
11
|
+
destinationExact?: boolean;
|
|
12
|
+
/** Previously observed local tracking tip, never proof of this push's outcome. */
|
|
13
|
+
base?: string;
|
|
14
|
+
baseSource?: "local-tracking" | "push-output" | "new-branch-comparison";
|
|
15
|
+
}>;
|
|
16
|
+
export type PushOperation = Readonly<{
|
|
17
|
+
schemaVersion: typeof PUSH_OPERATION_SCHEMA;
|
|
18
|
+
root: string;
|
|
19
|
+
commandDigest: string;
|
|
20
|
+
identityDigest: string;
|
|
21
|
+
capturedAt: number;
|
|
22
|
+
remote: string;
|
|
23
|
+
remoteDigest: string;
|
|
24
|
+
targets: readonly PushTarget[];
|
|
25
|
+
}>;
|
|
26
|
+
export type PushOperationOptions = Readonly<{
|
|
27
|
+
root: string;
|
|
28
|
+
command: string;
|
|
29
|
+
/** The shell's starting folder, when git -C or cd names another checkout. */
|
|
30
|
+
cwd?: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
toolUseId?: string;
|
|
33
|
+
environment?: NodeJS.ProcessEnv;
|
|
34
|
+
now?: Date;
|
|
35
|
+
}>;
|
|
36
|
+
export type PushCapture = Readonly<{
|
|
37
|
+
status: "captured";
|
|
38
|
+
operation: PushOperation;
|
|
39
|
+
}> | Readonly<{
|
|
40
|
+
status: "unknown";
|
|
41
|
+
reason: string;
|
|
42
|
+
}>;
|
|
43
|
+
export type ReconciledPushTarget = PushTarget & Readonly<{
|
|
44
|
+
status: "pushed" | "deleted" | "unchanged" | "rejected" | "unknown";
|
|
45
|
+
}>;
|
|
46
|
+
export type PushConsumption = Readonly<{
|
|
47
|
+
status: "reconciled";
|
|
48
|
+
operation: PushOperation;
|
|
49
|
+
targets: readonly ReconciledPushTarget[];
|
|
50
|
+
}> | Readonly<{
|
|
51
|
+
status: "unknown";
|
|
52
|
+
reason: string;
|
|
53
|
+
}>;
|
|
54
|
+
/** Hash the repository destination, never a login, credential, query or fragment. */
|
|
55
|
+
export declare function remoteDestinationDigest(reference: string, root: string): string | undefined;
|
|
56
|
+
/** No remote calls: capture source commits and destinations before the observed command runs. */
|
|
57
|
+
export declare function capturePushOperation(options: PushOperationOptions): PushCapture;
|
|
58
|
+
/** Consume once. Existing remote-tracking state is never an outcome signal. */
|
|
59
|
+
export declare function consumePushOperation(options: PushOperationOptions & Readonly<{
|
|
60
|
+
result?: HookCommandResult;
|
|
61
|
+
}>): PushConsumption;
|