@akagilnc/pi-workflow-roles 0.1.3749 → 0.1.3771
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/CLAUDE.md +4 -0
- package/README.md +3 -2
- package/README.zh-CN.md +3 -2
- package/dist/acp-host/production-host.js +1389 -751
- package/dist/collector-config.js +0 -1
- package/dist/collector-github.js +199 -2
- package/dist/collector-identity.js +128 -41
- package/dist/collector-ledger.js +38 -9
- package/dist/collector-receipt.js +19 -7
- package/dist/collector-role.js +330 -369
- package/dist/collector-target.js +169 -0
- package/dist/collector-tool-schemas.js +51 -14
- package/dist/package-contracts/collector-output.js +32 -0
- package/dist/package-contracts/terminating-infrastructure.js +13 -12
- package/dist/pi/role-turn-host.js +1 -2
- package/dist/public-cli/github-remote.js +45 -0
- package/dist/public-cli/invocation.js +53 -54
- package/dist/public-cli/main.js +615 -148
- package/dist/public-cli/option-definitions.js +6 -4
- package/dist/public-cli/run-lifecycle.js +3 -3
- package/dist/public-cli/settlement.js +83 -6
- package/dist/role-runtime.js +137 -7
- package/dist/submission-correctable-error.js +24 -0
- package/extensions/role-runtime.ts +0 -1
- package/package.json +1 -1
- package/souls/coder.md +11 -6
- package/souls/fixer.md +10 -9
- package/src/acp-host/role-envelope.ts +8 -22
- package/src/collector-config.ts +0 -1
- package/src/collector-github.ts +236 -2
- package/src/collector-identity.ts +148 -40
- package/src/collector-ledger.ts +48 -10
- package/src/collector-receipt.ts +33 -14
- package/src/collector-role.ts +376 -450
- package/src/collector-target.ts +207 -0
- package/src/collector-tool-schemas.ts +62 -15
- package/src/host-contracts.ts +2 -1
- package/src/package-contracts/collector-output.ts +72 -0
- package/src/package-contracts/terminating-infrastructure.ts +24 -13
- package/src/pi/role-turn-host.ts +1 -2
- package/src/public-cli/cli.ts +10 -1
- package/src/public-cli/collector-run.ts +3 -2
- package/src/public-cli/github-remote.ts +45 -0
- package/src/public-cli/invocation.ts +60 -59
- package/src/public-cli/option-definitions.ts +6 -4
- package/src/public-cli/run-lifecycle.ts +2 -2
- package/src/public-cli/settlement.ts +82 -6
- package/src/role-runtime.ts +166 -13
- package/src/submission-correctable-error.ts +38 -0
package/dist/collector-config.js
CHANGED
|
@@ -3,7 +3,6 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
export const COLLECTOR_HOST = "github.com";
|
|
4
4
|
export const COLLECTOR_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
5
5
|
export const COLLECTOR_REPO_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
|
|
6
|
-
export const COLLECTOR_FIXED_KICKOFF = "采集目标已受理,本局开始。";
|
|
7
6
|
function fail(message, cause) {
|
|
8
7
|
throw new Error(message, cause === undefined ? undefined : { cause });
|
|
9
8
|
}
|
package/dist/collector-github.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { parseCollectorPrNumber } from "./collector-config.js";
|
|
3
4
|
function isRecord(value) {
|
|
4
5
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
6
|
}
|
|
@@ -36,6 +37,196 @@ function parseJson(text, label) {
|
|
|
36
37
|
throw new Error(`GitHub ${label} returned malformed JSON`, { cause: error });
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Shared PR-list payload parse for admission target lookup (#676 D1/D7).
|
|
42
|
+
* Every entry must carry a positive safe-integer number — malformed items fail
|
|
43
|
+
* the response (do not skip then claim uniqueness).
|
|
44
|
+
*/
|
|
45
|
+
export function parsePullRequestNumberList(raw, label) {
|
|
46
|
+
if (!Array.isArray(raw)) {
|
|
47
|
+
throw new Error(`GitHub ${label} payload is not a list`);
|
|
48
|
+
}
|
|
49
|
+
const numbers = [];
|
|
50
|
+
for (const item of raw) {
|
|
51
|
+
if (!isRecord(item)) {
|
|
52
|
+
throw new Error(`GitHub ${label} payload contains a non-object pull request entry`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
numbers.push(parseCollectorPrNumber(item["number"]));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new Error(`GitHub ${label} payload contains an invalid pull request number`, {
|
|
59
|
+
cause: error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return numbers;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Online PR association by head owner:ref (state=all). Caller supplies the real
|
|
67
|
+
* head owner from branch context — never assume base repository owner is the fork head.
|
|
68
|
+
* Transport/HTTP/JSON failures throw with true cause (not target-ambiguity wash).
|
|
69
|
+
*/
|
|
70
|
+
export async function listPullRequestNumbersByHead(runner, input) {
|
|
71
|
+
const head = `${input.headOwner}:${input.headRef}`;
|
|
72
|
+
const path = `/repos/${input.owner}/${input.repo}/pulls?head=${encodeURIComponent(head)}&state=all&per_page=100`;
|
|
73
|
+
const response = await runner(["api", "--hostname", "github.com", "--include", "-X", "GET", path], input.signal === undefined ? {} : { signal: input.signal });
|
|
74
|
+
if (response.status < 200 || response.status >= 300) {
|
|
75
|
+
throw new Error(`GitHub ${path} failed with HTTP ${response.status}`, {
|
|
76
|
+
cause: {
|
|
77
|
+
endpoint: path,
|
|
78
|
+
status: response.status,
|
|
79
|
+
headers: response.headers,
|
|
80
|
+
body: response.bodyText,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return parsePullRequestNumberList(parseJson(response.bodyText, path), path);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Online PR association for a commit SHA (fork-safe; works when the commit is on the PR).
|
|
88
|
+
* Transport/HTTP/JSON failures throw with true cause.
|
|
89
|
+
*/
|
|
90
|
+
export async function listPullRequestNumbersByCommit(runner, input) {
|
|
91
|
+
const path = `/repos/${input.owner}/${input.repo}/commits/${encodeURIComponent(input.commitSha)}/pulls`;
|
|
92
|
+
const response = await runner(["api", "--hostname", "github.com", "--include", "-X", "GET", path], input.signal === undefined ? {} : { signal: input.signal });
|
|
93
|
+
if (response.status < 200 || response.status >= 300) {
|
|
94
|
+
throw new Error(`GitHub ${path} failed with HTTP ${response.status}`, {
|
|
95
|
+
cause: {
|
|
96
|
+
endpoint: path,
|
|
97
|
+
status: response.status,
|
|
98
|
+
headers: response.headers,
|
|
99
|
+
body: response.bodyText,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return parsePullRequestNumberList(parseJson(response.bodyText, path), path);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Online association for a structured ticket number (#676 D1):
|
|
107
|
+
* - Number is itself a pull request → that PR.
|
|
108
|
+
* - Number is an issue → PRs linked via timeline cross-reference / closed-by.
|
|
109
|
+
* Transport/HTTP/JSON failures throw with true cause.
|
|
110
|
+
* 404 / empty association → [].
|
|
111
|
+
*/
|
|
112
|
+
export async function listPullRequestNumbersByTicket(runner, input) {
|
|
113
|
+
const issuePath = `/repos/${input.owner}/${input.repo}/issues/${input.ticketNumber}`;
|
|
114
|
+
const issueResponse = await runner(["api", "--hostname", "github.com", "--include", "-X", "GET", issuePath], input.signal === undefined ? {} : { signal: input.signal });
|
|
115
|
+
if (issueResponse.status === 404)
|
|
116
|
+
return [];
|
|
117
|
+
if (issueResponse.status < 200 || issueResponse.status >= 300) {
|
|
118
|
+
throw new Error(`GitHub ${issuePath} failed with HTTP ${issueResponse.status}`, {
|
|
119
|
+
cause: {
|
|
120
|
+
endpoint: issuePath,
|
|
121
|
+
status: issueResponse.status,
|
|
122
|
+
headers: issueResponse.headers,
|
|
123
|
+
body: issueResponse.bodyText,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const issueRaw = parseJson(issueResponse.bodyText, issuePath);
|
|
128
|
+
if (!isRecord(issueRaw)) {
|
|
129
|
+
throw new Error(`GitHub ${issuePath} payload is not an object`);
|
|
130
|
+
}
|
|
131
|
+
// Issues endpoint returns PRs too — own-key pull_request means the number is the PR.
|
|
132
|
+
if (Object.hasOwn(issueRaw, "pull_request")) {
|
|
133
|
+
return [parseCollectorPrNumber(issueRaw["number"] ?? input.ticketNumber)];
|
|
134
|
+
}
|
|
135
|
+
// Linked PRs: GraphQL closed-by + cross-referenced PR sources (existing gh runner seam).
|
|
136
|
+
const query = `query($owner: String!, $repo: String!, $number: Int!) {
|
|
137
|
+
repository(owner: $owner, name: $repo) {
|
|
138
|
+
issue(number: $number) {
|
|
139
|
+
closedByPullRequestsReferences(first: 50) { nodes { number } }
|
|
140
|
+
timelineItems(first: 100, itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]) {
|
|
141
|
+
nodes {
|
|
142
|
+
__typename
|
|
143
|
+
... on CrossReferencedEvent {
|
|
144
|
+
source { ... on PullRequest { number } }
|
|
145
|
+
}
|
|
146
|
+
... on ConnectedEvent {
|
|
147
|
+
subject { ... on PullRequest { number } }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}`;
|
|
154
|
+
const args = [
|
|
155
|
+
"api",
|
|
156
|
+
"graphql",
|
|
157
|
+
"--hostname",
|
|
158
|
+
"github.com",
|
|
159
|
+
"--include",
|
|
160
|
+
"-f",
|
|
161
|
+
`query=${query}`,
|
|
162
|
+
"-f",
|
|
163
|
+
`owner=${input.owner}`,
|
|
164
|
+
"-f",
|
|
165
|
+
`repo=${input.repo}`,
|
|
166
|
+
"-F",
|
|
167
|
+
`number=${input.ticketNumber}`,
|
|
168
|
+
];
|
|
169
|
+
const gqlResponse = await runner(args, input.signal === undefined ? {} : { signal: input.signal });
|
|
170
|
+
if (gqlResponse.status < 200 || gqlResponse.status >= 300) {
|
|
171
|
+
throw new Error(`GitHub GraphQL issue→PR failed with HTTP ${gqlResponse.status}`, {
|
|
172
|
+
cause: {
|
|
173
|
+
endpoint: "graphql",
|
|
174
|
+
status: gqlResponse.status,
|
|
175
|
+
headers: gqlResponse.headers,
|
|
176
|
+
body: gqlResponse.bodyText,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
let payload;
|
|
181
|
+
try {
|
|
182
|
+
payload = JSON.parse(gqlResponse.bodyText);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
throw new Error("GitHub GraphQL issue→PR returned malformed JSON", { cause: error });
|
|
186
|
+
}
|
|
187
|
+
if (!isRecord(payload)) {
|
|
188
|
+
throw new Error("GitHub GraphQL issue→PR payload is not an object");
|
|
189
|
+
}
|
|
190
|
+
if (payload.errors !== undefined) {
|
|
191
|
+
throw new Error(`GitHub GraphQL issue→PR errors: ${JSON.stringify(payload.errors).slice(0, 600)}`, {
|
|
192
|
+
cause: { body: gqlResponse.bodyText, errors: payload.errors },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const data = payload.data;
|
|
196
|
+
if (!isRecord(data))
|
|
197
|
+
return [];
|
|
198
|
+
const repository = data["repository"];
|
|
199
|
+
if (!isRecord(repository))
|
|
200
|
+
return [];
|
|
201
|
+
const issue = repository["issue"];
|
|
202
|
+
if (!isRecord(issue))
|
|
203
|
+
return [];
|
|
204
|
+
const numbers = [];
|
|
205
|
+
const closedBy = issue["closedByPullRequestsReferences"];
|
|
206
|
+
if (isRecord(closedBy) && Array.isArray(closedBy["nodes"])) {
|
|
207
|
+
for (const node of closedBy["nodes"]) {
|
|
208
|
+
if (isRecord(node) && typeof node["number"] === "number") {
|
|
209
|
+
numbers.push(parseCollectorPrNumber(node["number"]));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const timeline = issue["timelineItems"];
|
|
214
|
+
if (isRecord(timeline) && Array.isArray(timeline["nodes"])) {
|
|
215
|
+
for (const node of timeline["nodes"]) {
|
|
216
|
+
if (!isRecord(node))
|
|
217
|
+
continue;
|
|
218
|
+
const source = node["source"];
|
|
219
|
+
if (isRecord(source) && typeof source["number"] === "number") {
|
|
220
|
+
numbers.push(parseCollectorPrNumber(source["number"]));
|
|
221
|
+
}
|
|
222
|
+
const subject = node["subject"];
|
|
223
|
+
if (isRecord(subject) && typeof subject["number"] === "number") {
|
|
224
|
+
numbers.push(parseCollectorPrNumber(subject["number"]));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [...new Set(numbers)];
|
|
229
|
+
}
|
|
39
230
|
let commentFailureEvidence = 0;
|
|
40
231
|
function commentFailureCause(error) {
|
|
41
232
|
return {
|
|
@@ -84,13 +275,19 @@ export function normalizePullRequest(raw) {
|
|
|
84
275
|
throw new Error("GitHub pull request payload missing head.sha");
|
|
85
276
|
}
|
|
86
277
|
const number = requireNumber(raw["number"], "number");
|
|
87
|
-
|
|
278
|
+
// GitHub REST: merged PRs keep state="closed" and set merged/merged_at. Project the
|
|
279
|
+
// real merge fact so receipt/settlement can distinguish merged from merely closed (#676 D6).
|
|
280
|
+
const mergedFlag = raw["merged"] === true
|
|
281
|
+
|| (typeof raw["merged_at"] === "string" && raw["merged_at"].length > 0);
|
|
282
|
+
const rawState = requireString(raw["state"], "state").toUpperCase();
|
|
283
|
+
// After toUpperCase the only OPEN spelling is "OPEN"; keep MERGED vs CLOSED distinction.
|
|
284
|
+
const state = mergedFlag ? "MERGED" : rawState;
|
|
88
285
|
const htmlUrl = typeof raw["html_url"] === "string"
|
|
89
286
|
? raw["html_url"]
|
|
90
287
|
: `https://github.com/unknown/unknown/pull/${number}`;
|
|
91
288
|
return {
|
|
92
289
|
number,
|
|
93
|
-
state
|
|
290
|
+
state,
|
|
94
291
|
headOid: head["sha"],
|
|
95
292
|
...(typeof raw["updated_at"] === "string" ? { updatedAt: raw["updated_at"] } : {}),
|
|
96
293
|
url: htmlUrl,
|
|
@@ -70,9 +70,9 @@ export class CollectorUnknownEvidenceError extends CorrectableSubmissionError {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Evidence-binding failure for a finding pointer that resolves to the wrong kind
|
|
74
|
+
* of stored record, lacks a GitHub locator, or has no identity group — not a
|
|
75
|
+
* free-shape gate on the submission envelope.
|
|
76
76
|
*/
|
|
77
77
|
export class CollectorFindingsValidationError extends CorrectableSubmissionError {
|
|
78
78
|
constructor(message) {
|
|
@@ -81,71 +81,158 @@ export class CollectorFindingsValidationError extends CorrectableSubmissionError
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* #
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
* #676 D6: non-OPEN targets keep collected materials and must not fire new review
|
|
85
|
+
* requests. Bounce the request as correctable so the seat can still seal output.
|
|
86
|
+
*/
|
|
87
|
+
export class CollectorNonOpenRequestError extends CorrectableSubmissionError {
|
|
88
|
+
constructor(prState) {
|
|
89
|
+
super(`通进司请求要求 OPEN 状态的 PR 快照;当前为 ${prState},不再触发新评审,请直接交回已有材料`);
|
|
90
|
+
this.name = "CollectorNonOpenRequestError";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function candidateRecord(candidate) {
|
|
94
|
+
if (candidate === undefined || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
/** Canonical top-level keys the collector output projection understands. */
|
|
100
|
+
const COLLECTOR_OUTPUT_CANONICAL_KEYS = new Set([
|
|
101
|
+
"findings",
|
|
102
|
+
"unfinishedReasons",
|
|
103
|
+
"infrastructureFailure",
|
|
104
|
+
]);
|
|
105
|
+
/** Non-canonical own keys mean content was present but not under a projectable key. */
|
|
106
|
+
function hasNonCanonicalOwnKeys(record) {
|
|
107
|
+
for (const key of Object.keys(record)) {
|
|
108
|
+
if (!COLLECTOR_OUTPUT_CANONICAL_KEYS.has(key))
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* #641 chain① / #676: turn model-submitted finding pointer refs into receipt findings.
|
|
115
|
+
* Binds resolvable evidence references only — no pure shape rejection of the
|
|
116
|
+
* candidate envelope. Unknown refs, wrong kinds, missing GitHub locator, and
|
|
117
|
+
* missing identity group stay as binding failures. Unreadable findings content
|
|
118
|
+
* is not washed into "zero findings": projection facts record the gap so
|
|
119
|
+
* downstream can open the session 正本 (第 0 条 / #676 C).
|
|
89
120
|
*/
|
|
90
121
|
export function enrichCollectorFindings(input) {
|
|
91
|
-
|
|
92
|
-
if (candidate
|
|
93
|
-
return;
|
|
94
|
-
|
|
95
|
-
|
|
122
|
+
// Candidate present but not a record — content existed, none projected (#676 C).
|
|
123
|
+
if (input.candidate !== undefined && input.candidate !== null && candidateRecord(input.candidate) === undefined) {
|
|
124
|
+
return { findingsSource: "unreadable", findingsProjectedCount: 0, findingsUnprojected: true };
|
|
125
|
+
}
|
|
126
|
+
const record = candidateRecord(input.candidate);
|
|
127
|
+
if (record === undefined) {
|
|
128
|
+
return { findingsSource: "absent", findingsProjectedCount: 0, findingsUnprojected: false };
|
|
129
|
+
}
|
|
130
|
+
if (!Object.hasOwn(record, "findings")) {
|
|
131
|
+
// Non-canonical top-level content is not "absent" — record the projection gap (#676 C).
|
|
132
|
+
if (hasNonCanonicalOwnKeys(record)) {
|
|
133
|
+
return { findingsSource: "unreadable", findingsProjectedCount: 0, findingsUnprojected: true };
|
|
134
|
+
}
|
|
135
|
+
return { findingsSource: "absent", findingsProjectedCount: 0, findingsUnprojected: false };
|
|
96
136
|
}
|
|
97
|
-
const rawFindings =
|
|
98
|
-
if (rawFindings === undefined)
|
|
99
|
-
return;
|
|
137
|
+
const rawFindings = record["findings"];
|
|
100
138
|
if (!Array.isArray(rawFindings)) {
|
|
101
|
-
|
|
139
|
+
// Key present but not an array — content exists, none projected. No shape bounce.
|
|
140
|
+
return { findingsSource: "unreadable", findingsProjectedCount: 0, findingsUnprojected: true };
|
|
102
141
|
}
|
|
103
|
-
const byEvidenceId = new Map(input.records.map((
|
|
142
|
+
const byEvidenceId = new Map(input.records.map((evidence) => [evidence.evidenceId, evidence]));
|
|
143
|
+
let projected = 0;
|
|
144
|
+
let unprojected = false;
|
|
104
145
|
for (const raw of rawFindings) {
|
|
105
146
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
106
|
-
|
|
147
|
+
unprojected = true;
|
|
148
|
+
continue;
|
|
107
149
|
}
|
|
108
|
-
const
|
|
150
|
+
const item = raw;
|
|
151
|
+
const evidenceId = item.evidenceId;
|
|
109
152
|
if (typeof evidenceId !== "string" || evidenceId.length === 0) {
|
|
110
|
-
|
|
153
|
+
// Present item without a bindable pointer — keep gap fact, do not fabricate.
|
|
154
|
+
unprojected = true;
|
|
155
|
+
continue;
|
|
111
156
|
}
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
157
|
+
const evidence = byEvidenceId.get(evidenceId);
|
|
158
|
+
if (evidence === undefined) {
|
|
114
159
|
throw new CollectorUnknownEvidenceError(evidenceId);
|
|
115
160
|
}
|
|
116
|
-
if (
|
|
117
|
-
throw new CollectorFindingsValidationError(`通进司 finding 指针指向不可承 finding 的证据种类 ${
|
|
161
|
+
if (evidence.kind !== "review" && evidence.kind !== "issue_comment" && evidence.kind !== "review_comment") {
|
|
162
|
+
throw new CollectorFindingsValidationError(`通进司 finding 指针指向不可承 finding 的证据种类 ${evidence.kind}`);
|
|
118
163
|
}
|
|
119
|
-
if (
|
|
164
|
+
if (evidence.githubId === undefined) {
|
|
120
165
|
throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 缺少 GitHub id`);
|
|
121
166
|
}
|
|
122
|
-
const
|
|
123
|
-
if (category !== undefined && (typeof category !== "string" || category.trim().length === 0)) {
|
|
124
|
-
throw new CollectorFindingsValidationError("通进司 finding category 必须为非空字符串");
|
|
125
|
-
}
|
|
126
|
-
const identity = record.machineIdentity ?? null;
|
|
167
|
+
const identity = evidence.machineIdentity ?? null;
|
|
127
168
|
const group = input.groups.find((candidateGroup) => identityKey(candidateGroup.identity) === identityKey(identity));
|
|
128
169
|
if (group === undefined) {
|
|
129
170
|
throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 无归属身份组`);
|
|
130
171
|
}
|
|
172
|
+
// Bound finding keeps pointer; non-string category/summary are unprojected field gaps
|
|
173
|
+
// (pointer success ≠ full content projection) — #676 C / 3939511832.
|
|
174
|
+
const category = item.category;
|
|
175
|
+
const summary = item.summary;
|
|
176
|
+
if (Object.hasOwn(item, "category") && typeof category !== "string")
|
|
177
|
+
unprojected = true;
|
|
178
|
+
if (Object.hasOwn(item, "summary") && typeof summary !== "string")
|
|
179
|
+
unprojected = true;
|
|
131
180
|
group.findings.push({
|
|
132
181
|
identity,
|
|
133
182
|
source: {
|
|
134
|
-
kind:
|
|
135
|
-
id:
|
|
136
|
-
evidenceId:
|
|
137
|
-
headRelation: headRelationFor(
|
|
183
|
+
kind: evidence.kind,
|
|
184
|
+
id: evidence.githubId,
|
|
185
|
+
evidenceId: evidence.evidenceId,
|
|
186
|
+
headRelation: headRelationFor(evidence, input.targetHead),
|
|
138
187
|
},
|
|
139
|
-
...(category ===
|
|
188
|
+
...(typeof category === "string" ? { category } : {}),
|
|
189
|
+
...(typeof summary === "string" ? { summary } : {}),
|
|
140
190
|
pointer: {
|
|
141
191
|
repository: input.repository,
|
|
142
192
|
prNumber: input.prNumber,
|
|
143
|
-
commentId:
|
|
144
|
-
...(
|
|
145
|
-
...(
|
|
146
|
-
kind:
|
|
147
|
-
authoritativeTime:
|
|
193
|
+
commentId: evidence.githubId,
|
|
194
|
+
...(evidence.htmlUrl === undefined ? {} : { htmlUrl: evidence.htmlUrl }),
|
|
195
|
+
...(evidence.authorLogin === undefined ? {} : { authorLogin: evidence.authorLogin }),
|
|
196
|
+
kind: evidence.kind,
|
|
197
|
+
authoritativeTime: evidence.authoritativeTime ?? null,
|
|
198
|
+
...(evidence.commitOid === undefined ? {} : { commitOid: evidence.commitOid }),
|
|
148
199
|
},
|
|
149
200
|
});
|
|
201
|
+
projected += 1;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
findingsSource: "array",
|
|
205
|
+
findingsProjectedCount: projected,
|
|
206
|
+
findingsUnprojected: unprojected,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* #676 D6/C: optional unfinished reasons from the model submission.
|
|
211
|
+
* Project readable strings; record when original content could not fully project
|
|
212
|
+
* (do not wash unreadable unfinishedReasons into "none").
|
|
213
|
+
*/
|
|
214
|
+
export function extractCollectorUnfinishedReasons(candidate) {
|
|
215
|
+
if (candidate !== undefined && candidate !== null && candidateRecord(candidate) === undefined) {
|
|
216
|
+
return { reasons: undefined, source: "unreadable", unprojected: true };
|
|
217
|
+
}
|
|
218
|
+
const record = candidateRecord(candidate);
|
|
219
|
+
if (record === undefined) {
|
|
220
|
+
return { reasons: undefined, source: "absent", unprojected: false };
|
|
221
|
+
}
|
|
222
|
+
if (!Object.hasOwn(record, "unfinishedReasons")) {
|
|
223
|
+
// unfinishedReasons absent is fine when only findings (or nothing) present.
|
|
224
|
+
// Non-canonical keys alone are recorded on the findings projection path.
|
|
225
|
+
return { reasons: undefined, source: "absent", unprojected: false };
|
|
226
|
+
}
|
|
227
|
+
const raw = record["unfinishedReasons"];
|
|
228
|
+
if (!Array.isArray(raw)) {
|
|
229
|
+
return { reasons: undefined, source: "unreadable", unprojected: true };
|
|
150
230
|
}
|
|
231
|
+
const reasons = raw.filter((item) => typeof item === "string");
|
|
232
|
+
const unprojected = reasons.length !== raw.length;
|
|
233
|
+
return {
|
|
234
|
+
reasons: reasons.length > 0 ? reasons : undefined,
|
|
235
|
+
source: "array",
|
|
236
|
+
unprojected,
|
|
237
|
+
};
|
|
151
238
|
}
|
package/dist/collector-ledger.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import Value from "typebox/value";
|
|
2
2
|
import { applyEvidenceVersionHistory, assignWindowRelations, COLLECTOR_ELIGIBILITY_MS, measureNormalizedBytes, normalizeAuthenticatedUserEvidence, normalizeIssueCommentEvidence, normalizePullRequestEvidence, normalizePullRequestReactionEvidence, normalizeReviewCommentEvidence, normalizeReviewEvidence, sha256Text, } from "./collector-evidence.js";
|
|
3
3
|
import { buildCollectorRequestBody, } from "./collector-github.js";
|
|
4
|
+
import { CollectorNonOpenRequestError } from "./collector-identity.js";
|
|
4
5
|
import { collectorObserveArgsSchema, collectorOutputArgsSchema, collectorReadArgsSchema, collectorRequestArgsSchema, collectorWaitArgsSchema, } from "./collector-tool-schemas.js";
|
|
5
6
|
import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.js";
|
|
6
7
|
export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
|
|
7
8
|
export const COLLECTOR_READ_TOOL = "ak_collector_read";
|
|
8
9
|
export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
|
|
9
10
|
export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
|
|
11
|
+
/** #676 A: role-decided target bind — business tool, ledger-booked. */
|
|
12
|
+
export const COLLECTOR_BIND_TARGET_TOOL = "ak_collector_bind_target";
|
|
10
13
|
export { COLLECTOR_OUTPUT_TOOL };
|
|
11
14
|
export const COLLECTOR_OPERATIONAL_TOOLS = [
|
|
15
|
+
COLLECTOR_BIND_TARGET_TOOL,
|
|
12
16
|
COLLECTOR_OBSERVE_TOOL,
|
|
13
17
|
COLLECTOR_READ_TOOL,
|
|
14
18
|
COLLECTOR_REQUEST_TOOL,
|
|
@@ -129,10 +133,16 @@ export function createCollectorLedger(config, options) {
|
|
|
129
133
|
return Math.max(0, deadlineMono - monoNowOrThrow(clock));
|
|
130
134
|
};
|
|
131
135
|
const prIdentity = (pr) => `${pr.state}|${pr.headOid}|${pr.updatedAt ?? ""}`;
|
|
136
|
+
const requireBoundPr = () => {
|
|
137
|
+
if (config.prNumber === undefined) {
|
|
138
|
+
throw new Error("Collector PR target is unbound; call ak_collector_bind_target with the role-decided issue/PR or pass --pr");
|
|
139
|
+
}
|
|
140
|
+
return config.prNumber;
|
|
141
|
+
};
|
|
132
142
|
const fetchObserveSurfaces = async (transport, observedAt, signal) => {
|
|
133
143
|
const owner = config.repository.owner;
|
|
134
144
|
const repo = config.repository.repo;
|
|
135
|
-
const prNumber =
|
|
145
|
+
const prNumber = requireBoundPr();
|
|
136
146
|
const signalOpt = signal === undefined ? {} : { signal };
|
|
137
147
|
const user = await transport.getAuthenticatedUser(signalOpt);
|
|
138
148
|
const prInitial = await transport.getPullRequest({
|
|
@@ -433,6 +443,23 @@ export function createCollectorLedger(config, options) {
|
|
|
433
443
|
assertNotFatal();
|
|
434
444
|
outputCandidate = true;
|
|
435
445
|
},
|
|
446
|
+
bindTarget(prNumber) {
|
|
447
|
+
assertNotFatal();
|
|
448
|
+
if (outputCandidate || pendingOutputCallId !== undefined) {
|
|
449
|
+
throw new Error("通进司已产出输出候选,本局不再受理目标绑定");
|
|
450
|
+
}
|
|
451
|
+
if (!Number.isSafeInteger(prNumber) || prNumber < 1) {
|
|
452
|
+
throw new Error("Collector bind target requires a positive safe-integer PR number");
|
|
453
|
+
}
|
|
454
|
+
if (config.prNumber !== undefined && config.prNumber !== prNumber) {
|
|
455
|
+
throw new Error(`Collector target already bound to PR ${config.prNumber}; cannot rebind to ${prNumber}`);
|
|
456
|
+
}
|
|
457
|
+
config.prNumber = prNumber;
|
|
458
|
+
appendJournal("ak-collector-target-bound", {
|
|
459
|
+
prNumber,
|
|
460
|
+
repository: config.repository.canonical,
|
|
461
|
+
});
|
|
462
|
+
},
|
|
436
463
|
beginOperational(toolName, toolCallId) {
|
|
437
464
|
assertNotFatal();
|
|
438
465
|
if (toolName !== COLLECTOR_OUTPUT_TOOL &&
|
|
@@ -581,7 +608,7 @@ export function createCollectorLedger(config, options) {
|
|
|
581
608
|
completedMono,
|
|
582
609
|
host: "github.com",
|
|
583
610
|
repository: config.repository.canonical,
|
|
584
|
-
prNumber:
|
|
611
|
+
prNumber: requireBoundPr(),
|
|
585
612
|
prState: pr.state,
|
|
586
613
|
headOid: pr.headOid,
|
|
587
614
|
complete: true,
|
|
@@ -620,10 +647,6 @@ export function createCollectorLedger(config, options) {
|
|
|
620
647
|
if (activationTime === undefined || deadlineTime === undefined) {
|
|
621
648
|
throw latchFatal("通进司请求需要激活");
|
|
622
649
|
}
|
|
623
|
-
if (pastCutoff(clock)) {
|
|
624
|
-
finalObservationRequired = true;
|
|
625
|
-
throw latchFatal("通进司请求不在资格截止前");
|
|
626
|
-
}
|
|
627
650
|
if (ledger.unresolvedTransportFailure) {
|
|
628
651
|
throw latchFatal("通进司请求时存在未恢复的传输失败");
|
|
629
652
|
}
|
|
@@ -639,7 +662,12 @@ export function createCollectorLedger(config, options) {
|
|
|
639
662
|
throw new Error("通进司请求要求最新完整快照");
|
|
640
663
|
}
|
|
641
664
|
if (snapshot.prState !== "OPEN") {
|
|
642
|
-
|
|
665
|
+
// #676 D6: non-OPEN keeps materials; bounce before cutoff fatal so post-deadline materials still seal.
|
|
666
|
+
throw new CollectorNonOpenRequestError(snapshot.prState);
|
|
667
|
+
}
|
|
668
|
+
if (pastCutoff(clock)) {
|
|
669
|
+
finalObservationRequired = true;
|
|
670
|
+
throw latchFatal("通进司请求不在资格截止前");
|
|
643
671
|
}
|
|
644
672
|
const { body, marker } = buildCollectorRequestBody({
|
|
645
673
|
configuredBody: request.requestBody,
|
|
@@ -657,9 +685,10 @@ export function createCollectorLedger(config, options) {
|
|
|
657
685
|
if (existingMarker) {
|
|
658
686
|
throw new Error(`通进司在此 HEAD 已有同 marker 的已认证请求 "${input.requestId}"`);
|
|
659
687
|
}
|
|
688
|
+
const boundPr = requireBoundPr();
|
|
660
689
|
const attemptKey = [
|
|
661
690
|
config.repository.canonical,
|
|
662
|
-
String(
|
|
691
|
+
String(boundPr),
|
|
663
692
|
snapshot.headOid,
|
|
664
693
|
request.id,
|
|
665
694
|
].join("|");
|
|
@@ -686,7 +715,7 @@ export function createCollectorLedger(config, options) {
|
|
|
686
715
|
const result = await transport.createIssueComment({
|
|
687
716
|
owner: config.repository.owner,
|
|
688
717
|
repo: config.repository.repo,
|
|
689
|
-
prNumber:
|
|
718
|
+
prNumber: boundPr,
|
|
690
719
|
body,
|
|
691
720
|
...(signal === undefined ? {} : { signal }),
|
|
692
721
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { COLLECTOR_HOST } from "./collector-config.js";
|
|
2
|
-
import { enrichCollectorFindings, extractCollectorEvidenceIdentityGroups, } from "./collector-identity.js";
|
|
3
|
-
import { validateAcceptedCollectorReceipt } from "./package-contracts/collector-output.js";
|
|
2
|
+
import { enrichCollectorFindings, extractCollectorEvidenceIdentityGroups, extractCollectorUnfinishedReasons, } from "./collector-identity.js";
|
|
3
|
+
import { validateAcceptedCollectorReceipt, } from "./package-contracts/collector-output.js";
|
|
4
4
|
export { validateAcceptedCollectorReceipt };
|
|
5
5
|
function fail(message) { throw new Error(message); }
|
|
6
6
|
/** Runtime output assembles findings pointers from the model submission into the typed receipt. */
|
|
@@ -12,6 +12,9 @@ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
|
|
|
12
12
|
fail("Collector output requires a complete final snapshot");
|
|
13
13
|
if (ledger.activationTime === undefined || ledger.deadlineTime === undefined)
|
|
14
14
|
fail("Collector output requires activation timeline");
|
|
15
|
+
if (ledger.config.prNumber === undefined) {
|
|
16
|
+
fail("Collector output requires a bound PR target; call ak_collector_bind_target first or pass --pr");
|
|
17
|
+
}
|
|
15
18
|
if (clock !== undefined)
|
|
16
19
|
ledger.assertOutputObservationLaw(clock);
|
|
17
20
|
else if (ledger.observedGeneration !== ledger.mutationGeneration ||
|
|
@@ -21,8 +24,7 @@ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
|
|
|
21
24
|
const finalSnapshot = ledger.getSnapshot(ledger.latestCompleteSnapshotId);
|
|
22
25
|
if (finalSnapshot === undefined || !finalSnapshot.complete)
|
|
23
26
|
fail("Collector final snapshot is incomplete");
|
|
24
|
-
|
|
25
|
-
fail("Collector final snapshot PR state is not OPEN");
|
|
27
|
+
// #676 D6: closed/merged still deliver collected materials; status is a fact on the receipt.
|
|
26
28
|
const evidenceRecords = [...ledger.allEvidence()];
|
|
27
29
|
const snapshots = [...ledger.allSnapshots()];
|
|
28
30
|
const evidenceIndex = new Map(evidenceRecords.map((record) => [record.evidenceId, record]));
|
|
@@ -44,7 +46,7 @@ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
|
|
|
44
46
|
const groups = extractCollectorEvidenceIdentityGroups(evidenceRecords, finalSnapshot.headOid);
|
|
45
47
|
// #641 chain①: the collector LLM submits findings as pointer refs; the runtime
|
|
46
48
|
// enriches each with the machine locator from the same stored record.
|
|
47
|
-
enrichCollectorFindings({
|
|
49
|
+
const findingsProjection = enrichCollectorFindings({
|
|
48
50
|
candidate: candidateRaw,
|
|
49
51
|
records: evidenceRecords,
|
|
50
52
|
groups,
|
|
@@ -53,8 +55,6 @@ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
|
|
|
53
55
|
prNumber: ledger.config.prNumber,
|
|
54
56
|
});
|
|
55
57
|
for (const group of groups) {
|
|
56
|
-
if (group.attendance !== true)
|
|
57
|
-
fail("Collector group lacks attendance");
|
|
58
58
|
for (const material of group.materials) {
|
|
59
59
|
if (material.evidenceId === undefined || !evidenceIndex.has(material.evidenceId))
|
|
60
60
|
fail("Collector material lacks a receipt-local evidence ref");
|
|
@@ -64,10 +64,20 @@ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
|
|
|
64
64
|
fail("Collector finding lacks a receipt-local evidence ref");
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
const unfinished = extractCollectorUnfinishedReasons(candidateRaw);
|
|
68
|
+
const submissionProjection = {
|
|
69
|
+
findingsSource: findingsProjection.findingsSource,
|
|
70
|
+
findingsProjectedCount: findingsProjection.findingsProjectedCount,
|
|
71
|
+
findingsUnprojected: findingsProjection.findingsUnprojected,
|
|
72
|
+
unfinishedReasonsSource: unfinished.source,
|
|
73
|
+
unfinishedReasonsProjectedCount: unfinished.reasons?.length ?? 0,
|
|
74
|
+
unfinishedReasonsUnprojected: unfinished.unprojected,
|
|
75
|
+
};
|
|
67
76
|
return {
|
|
68
77
|
host: COLLECTOR_HOST,
|
|
69
78
|
repository: ledger.config.repository.canonical,
|
|
70
79
|
prNumber: ledger.config.prNumber,
|
|
80
|
+
prState: finalSnapshot.prState,
|
|
71
81
|
manifestDigest: ledger.config.manifest.digest,
|
|
72
82
|
activationTime: ledger.activationTime.toISOString(),
|
|
73
83
|
deadlineTime: ledger.deadlineTime.toISOString(),
|
|
@@ -75,6 +85,8 @@ export function buildCollectorReceipt(ledger, candidateRaw, clock) {
|
|
|
75
85
|
finalSnapshotId: finalSnapshot.snapshotId,
|
|
76
86
|
targetHead: finalSnapshot.headOid,
|
|
77
87
|
groups,
|
|
88
|
+
...(unfinished.reasons === undefined ? {} : { unfinishedReasons: unfinished.reasons }),
|
|
89
|
+
submissionProjection,
|
|
78
90
|
requestAttempts: [...ledger.requestAttempts()],
|
|
79
91
|
snapshots,
|
|
80
92
|
evidenceRecords: evidenceRecords.map(toReceiptEvidenceRecord),
|