@akagilnc/pi-workflow-roles 0.1.3758 → 0.1.3783

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.
Files changed (63) hide show
  1. package/README.md +3 -2
  2. package/README.zh-CN.md +3 -2
  3. package/dist/acp-host/production-host.js +1735 -948
  4. package/dist/collector-config.js +0 -1
  5. package/dist/collector-github.js +199 -2
  6. package/dist/collector-identity.js +128 -41
  7. package/dist/collector-ledger.js +38 -9
  8. package/dist/collector-receipt.js +19 -7
  9. package/dist/collector-role.js +330 -369
  10. package/dist/collector-target.js +169 -0
  11. package/dist/collector-tool-schemas.js +51 -14
  12. package/dist/headless-host/description.js +77 -0
  13. package/dist/headless-host/mcp-relay.mjs +119 -0
  14. package/dist/headless-host/production-host.js +26590 -0
  15. package/dist/host-descriptions.js +44 -3
  16. package/dist/package-contracts/collector-output.js +32 -0
  17. package/dist/package-contracts/terminating-infrastructure.js +13 -12
  18. package/dist/pi/role-turn-host.js +1 -2
  19. package/dist/public-cli/github-remote.js +45 -0
  20. package/dist/public-cli/invocation.js +53 -54
  21. package/dist/public-cli/load-production-external-host.js +19 -0
  22. package/dist/public-cli/load-production-headless-host.js +36 -0
  23. package/dist/public-cli/main.js +857 -310
  24. package/dist/public-cli/option-definitions.js +6 -4
  25. package/dist/public-cli/run-lifecycle.js +3 -3
  26. package/dist/public-cli/settlement.js +83 -6
  27. package/dist/public-role-summons.js +40 -5
  28. package/dist/role-runtime.js +137 -7
  29. package/dist/submission-correctable-error.js +24 -0
  30. package/extensions/role-runtime.ts +0 -1
  31. package/package.json +1 -1
  32. package/scripts/build-package.mjs +28 -0
  33. package/src/acp-host/role-envelope.ts +146 -103
  34. package/src/acp-host/role-turn-host.ts +12 -0
  35. package/src/collector-config.ts +0 -1
  36. package/src/collector-github.ts +236 -2
  37. package/src/collector-identity.ts +148 -40
  38. package/src/collector-ledger.ts +48 -10
  39. package/src/collector-receipt.ts +33 -14
  40. package/src/collector-role.ts +376 -450
  41. package/src/collector-target.ts +207 -0
  42. package/src/collector-tool-schemas.ts +62 -15
  43. package/src/headless-host/description.ts +123 -0
  44. package/src/headless-host/production-host.ts +75 -0
  45. package/src/headless-host/role-turn-host.ts +424 -0
  46. package/src/host-contracts.ts +2 -1
  47. package/src/host-descriptions.ts +53 -6
  48. package/src/package-contracts/collector-output.ts +72 -0
  49. package/src/package-contracts/terminating-infrastructure.ts +24 -13
  50. package/src/pi/role-turn-host.ts +1 -2
  51. package/src/public-cli/cli.ts +12 -3
  52. package/src/public-cli/collector-run.ts +3 -2
  53. package/src/public-cli/github-remote.ts +45 -0
  54. package/src/public-cli/invocation.ts +60 -59
  55. package/src/public-cli/load-production-external-host.ts +29 -0
  56. package/src/public-cli/load-production-headless-host.ts +48 -0
  57. package/src/public-cli/main.ts +5 -0
  58. package/src/public-cli/option-definitions.ts +6 -4
  59. package/src/public-cli/run-lifecycle.ts +2 -2
  60. package/src/public-cli/settlement.ts +82 -6
  61. package/src/public-role-summons.ts +62 -9
  62. package/src/role-runtime.ts +166 -13
  63. package/src/submission-correctable-error.ts +38 -0
@@ -2,8 +2,11 @@ import type {
2
2
  GitHubMachineIdentity,
3
3
  } from "./collector-github.ts";
4
4
  import type { CollectorEvidenceRecord, HeadRelation } from "./collector-evidence.ts";
5
+ import type { CollectorSubmissionProjection } from "./package-contracts/collector-output.ts";
5
6
  import { CorrectableSubmissionError } from "./submission-correctable-error.ts";
6
7
 
8
+ export type { CollectorSubmissionProjection };
9
+
7
10
  export type CollectorMaterialRef = {
8
11
  kind: "review" | "issue_comment" | "review_comment" | "reaction";
9
12
  id: number;
@@ -21,7 +24,10 @@ export type CollectorMaterialRef = {
21
24
  export type CollectorFinding = {
22
25
  identity: GitHubMachineIdentity | null;
23
26
  source: CollectorMaterialRef;
27
+ /** Short classification label; not the finding summary. */
24
28
  category?: string;
29
+ /** Finding summary for the caller; not a body transcription. */
30
+ summary?: string;
25
31
  pointer: {
26
32
  repository: string;
27
33
  prNumber: number;
@@ -30,6 +36,8 @@ export type CollectorFinding = {
30
36
  authorLogin?: string;
31
37
  kind: CollectorMaterialRef["kind"];
32
38
  authoritativeTime?: string | null;
39
+ /** Corresponding commit when the evidence carries one. */
40
+ commitOid?: string | null;
33
41
  };
34
42
  };
35
43
 
@@ -121,9 +129,9 @@ export class CollectorUnknownEvidenceError extends CorrectableSubmissionError {
121
129
  }
122
130
 
123
131
  /**
124
- * #641 chain①: any malformed findings submission is a model misuse the model
125
- * can correct and resubmit — a branded correctable rejection on every supported
126
- * engine, never a round infrastructure failure.
132
+ * Evidence-binding failure for a finding pointer that resolves to the wrong kind
133
+ * of stored record, lacks a GitHub locator, or has no identity group — not a
134
+ * free-shape gate on the submission envelope.
127
135
  */
128
136
  export class CollectorFindingsValidationError extends CorrectableSubmissionError {
129
137
  constructor(message: string) {
@@ -133,11 +141,45 @@ export class CollectorFindingsValidationError extends CorrectableSubmissionError
133
141
  }
134
142
 
135
143
  /**
136
- * #641 chain①: turn model-submitted finding pointer refs into receipt findings.
137
- * Each pointer must resolve to a stored text-bearing evidence record; the
138
- * machine locator is enriched from the same record so receipt and volume agree
139
- * (指针可解析、开卷相符) by construction. Throws branded correctable
140
- * (non-fatal, model-visible) errors for unresolvable or mis-typed findings.
144
+ * #676 D6: non-OPEN targets keep collected materials and must not fire new review
145
+ * requests. Bounce the request as correctable so the seat can still seal output.
146
+ */
147
+ export class CollectorNonOpenRequestError extends CorrectableSubmissionError {
148
+ constructor(prState: string) {
149
+ super(`通进司请求要求 OPEN 状态的 PR 快照;当前为 ${prState},不再触发新评审,请直接交回已有材料`);
150
+ this.name = "CollectorNonOpenRequestError";
151
+ }
152
+ }
153
+
154
+ function candidateRecord(candidate: unknown): Record<string, unknown> | undefined {
155
+ if (candidate === undefined || candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
156
+ return undefined;
157
+ }
158
+ return candidate as Record<string, unknown>;
159
+ }
160
+
161
+ /** Canonical top-level keys the collector output projection understands. */
162
+ const COLLECTOR_OUTPUT_CANONICAL_KEYS = new Set([
163
+ "findings",
164
+ "unfinishedReasons",
165
+ "infrastructureFailure",
166
+ ]);
167
+
168
+ /** Non-canonical own keys mean content was present but not under a projectable key. */
169
+ function hasNonCanonicalOwnKeys(record: Record<string, unknown>): boolean {
170
+ for (const key of Object.keys(record)) {
171
+ if (!COLLECTOR_OUTPUT_CANONICAL_KEYS.has(key)) return true;
172
+ }
173
+ return false;
174
+ }
175
+
176
+ /**
177
+ * #641 chain① / #676: turn model-submitted finding pointer refs into receipt findings.
178
+ * Binds resolvable evidence references only — no pure shape rejection of the
179
+ * candidate envelope. Unknown refs, wrong kinds, missing GitHub locator, and
180
+ * missing identity group stay as binding failures. Unreadable findings content
181
+ * is not washed into "zero findings": projection facts record the gap so
182
+ * downstream can open the session 正本 (第 0 条 / #676 C).
141
183
  */
142
184
  export function enrichCollectorFindings(input: {
143
185
  candidate: unknown;
@@ -146,63 +188,129 @@ export function enrichCollectorFindings(input: {
146
188
  targetHead: string;
147
189
  repository: string;
148
190
  prNumber: number;
149
- }): void {
150
- const candidate = input.candidate;
151
- if (candidate === undefined || candidate === null) return;
152
- if (typeof candidate !== "object" || Array.isArray(candidate)) {
153
- throw new CollectorFindingsValidationError("通进司交件参数必须为对象");
191
+ }): {
192
+ findingsSource: CollectorSubmissionProjection["findingsSource"];
193
+ findingsProjectedCount: number;
194
+ findingsUnprojected: boolean;
195
+ } {
196
+ // Candidate present but not a record — content existed, none projected (#676 C).
197
+ if (input.candidate !== undefined && input.candidate !== null && candidateRecord(input.candidate) === undefined) {
198
+ return { findingsSource: "unreadable", findingsProjectedCount: 0, findingsUnprojected: true };
199
+ }
200
+ const record = candidateRecord(input.candidate);
201
+ if (record === undefined) {
202
+ return { findingsSource: "absent", findingsProjectedCount: 0, findingsUnprojected: false };
154
203
  }
155
- const rawFindings = (candidate as { findings?: unknown }).findings;
156
- if (rawFindings === undefined) return;
204
+ if (!Object.hasOwn(record, "findings")) {
205
+ // Non-canonical top-level content is not "absent" — record the projection gap (#676 C).
206
+ if (hasNonCanonicalOwnKeys(record)) {
207
+ return { findingsSource: "unreadable", findingsProjectedCount: 0, findingsUnprojected: true };
208
+ }
209
+ return { findingsSource: "absent", findingsProjectedCount: 0, findingsUnprojected: false };
210
+ }
211
+ const rawFindings = record["findings"];
157
212
  if (!Array.isArray(rawFindings)) {
158
- throw new CollectorFindingsValidationError("通进司 findings 必须为数组");
213
+ // Key present but not an array — content exists, none projected. No shape bounce.
214
+ return { findingsSource: "unreadable", findingsProjectedCount: 0, findingsUnprojected: true };
159
215
  }
160
- const byEvidenceId = new Map(input.records.map((record) => [record.evidenceId, record]));
216
+
217
+ const byEvidenceId = new Map(input.records.map((evidence) => [evidence.evidenceId, evidence]));
218
+ let projected = 0;
219
+ let unprojected = false;
161
220
  for (const raw of rawFindings) {
162
221
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
163
- throw new CollectorFindingsValidationError("通进司 finding 必须为对象");
222
+ unprojected = true;
223
+ continue;
164
224
  }
165
- const evidenceId = (raw as { evidenceId?: unknown }).evidenceId;
225
+ const item = raw as Record<string, unknown>;
226
+ const evidenceId = item.evidenceId;
166
227
  if (typeof evidenceId !== "string" || evidenceId.length === 0) {
167
- throw new CollectorFindingsValidationError("通进司 finding 缺少可解析的 evidenceId 指针");
228
+ // Present item without a bindable pointer — keep gap fact, do not fabricate.
229
+ unprojected = true;
230
+ continue;
168
231
  }
169
- const record = byEvidenceId.get(evidenceId);
170
- if (record === undefined) {
232
+ const evidence = byEvidenceId.get(evidenceId);
233
+ if (evidence === undefined) {
171
234
  throw new CollectorUnknownEvidenceError(evidenceId);
172
235
  }
173
- if (record.kind !== "review" && record.kind !== "issue_comment" && record.kind !== "review_comment") {
174
- throw new CollectorFindingsValidationError(`通进司 finding 指针指向不可承 finding 的证据种类 ${record.kind}`);
236
+ if (evidence.kind !== "review" && evidence.kind !== "issue_comment" && evidence.kind !== "review_comment") {
237
+ throw new CollectorFindingsValidationError(`通进司 finding 指针指向不可承 finding 的证据种类 ${evidence.kind}`);
175
238
  }
176
- if (record.githubId === undefined) {
239
+ if (evidence.githubId === undefined) {
177
240
  throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 缺少 GitHub id`);
178
241
  }
179
- const category = (raw as { category?: unknown }).category;
180
- if (category !== undefined && (typeof category !== "string" || category.trim().length === 0)) {
181
- throw new CollectorFindingsValidationError("通进司 finding category 必须为非空字符串");
182
- }
183
- const identity = record.machineIdentity ?? null;
242
+ const identity = evidence.machineIdentity ?? null;
184
243
  const group = input.groups.find((candidateGroup) => identityKey(candidateGroup.identity) === identityKey(identity));
185
244
  if (group === undefined) {
186
245
  throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 无归属身份组`);
187
246
  }
247
+ // Bound finding keeps pointer; non-string category/summary are unprojected field gaps
248
+ // (pointer success ≠ full content projection) — #676 C / 3939511832.
249
+ const category = item.category;
250
+ const summary = item.summary;
251
+ if (Object.hasOwn(item, "category") && typeof category !== "string") unprojected = true;
252
+ if (Object.hasOwn(item, "summary") && typeof summary !== "string") unprojected = true;
188
253
  group.findings.push({
189
254
  identity,
190
255
  source: {
191
- kind: record.kind,
192
- id: record.githubId,
193
- evidenceId: record.evidenceId,
194
- headRelation: headRelationFor(record, input.targetHead),
256
+ kind: evidence.kind,
257
+ id: evidence.githubId,
258
+ evidenceId: evidence.evidenceId,
259
+ headRelation: headRelationFor(evidence, input.targetHead),
195
260
  },
196
- ...(category === undefined ? {} : { category: category.trim() }),
261
+ ...(typeof category === "string" ? { category } : {}),
262
+ ...(typeof summary === "string" ? { summary } : {}),
197
263
  pointer: {
198
264
  repository: input.repository,
199
265
  prNumber: input.prNumber,
200
- commentId: record.githubId,
201
- ...(record.htmlUrl === undefined ? {} : { htmlUrl: record.htmlUrl }),
202
- ...(record.authorLogin === undefined ? {} : { authorLogin: record.authorLogin }),
203
- kind: record.kind,
204
- authoritativeTime: record.authoritativeTime ?? null,
266
+ commentId: evidence.githubId,
267
+ ...(evidence.htmlUrl === undefined ? {} : { htmlUrl: evidence.htmlUrl }),
268
+ ...(evidence.authorLogin === undefined ? {} : { authorLogin: evidence.authorLogin }),
269
+ kind: evidence.kind,
270
+ authoritativeTime: evidence.authoritativeTime ?? null,
271
+ ...(evidence.commitOid === undefined ? {} : { commitOid: evidence.commitOid }),
205
272
  },
206
273
  });
274
+ projected += 1;
275
+ }
276
+ return {
277
+ findingsSource: "array",
278
+ findingsProjectedCount: projected,
279
+ findingsUnprojected: unprojected,
280
+ };
281
+ }
282
+
283
+ /**
284
+ * #676 D6/C: optional unfinished reasons from the model submission.
285
+ * Project readable strings; record when original content could not fully project
286
+ * (do not wash unreadable unfinishedReasons into "none").
287
+ */
288
+ export function extractCollectorUnfinishedReasons(candidate: unknown): {
289
+ reasons: string[] | undefined;
290
+ source: CollectorSubmissionProjection["unfinishedReasonsSource"];
291
+ unprojected: boolean;
292
+ } {
293
+ if (candidate !== undefined && candidate !== null && candidateRecord(candidate) === undefined) {
294
+ return { reasons: undefined, source: "unreadable", unprojected: true };
295
+ }
296
+ const record = candidateRecord(candidate);
297
+ if (record === undefined) {
298
+ return { reasons: undefined, source: "absent", unprojected: false };
207
299
  }
300
+ if (!Object.hasOwn(record, "unfinishedReasons")) {
301
+ // unfinishedReasons absent is fine when only findings (or nothing) present.
302
+ // Non-canonical keys alone are recorded on the findings projection path.
303
+ return { reasons: undefined, source: "absent", unprojected: false };
304
+ }
305
+ const raw = record["unfinishedReasons"];
306
+ if (!Array.isArray(raw)) {
307
+ return { reasons: undefined, source: "unreadable", unprojected: true };
308
+ }
309
+ const reasons = raw.filter((item): item is string => typeof item === "string");
310
+ const unprojected = reasons.length !== raw.length;
311
+ return {
312
+ reasons: reasons.length > 0 ? reasons : undefined,
313
+ source: "array",
314
+ unprojected,
315
+ };
208
316
  }
@@ -25,6 +25,7 @@ import {
25
25
  type GitHubPageDiagnostics,
26
26
  type GitHubPullRequest,
27
27
  } from "./collector-github.ts";
28
+ import { CollectorNonOpenRequestError } from "./collector-identity.ts";
28
29
  import {
29
30
  collectorObserveArgsSchema,
30
31
  collectorOutputArgsSchema,
@@ -38,9 +39,12 @@ export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
38
39
  export const COLLECTOR_READ_TOOL = "ak_collector_read";
39
40
  export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
40
41
  export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
42
+ /** #676 A: role-decided target bind — business tool, ledger-booked. */
43
+ export const COLLECTOR_BIND_TARGET_TOOL = "ak_collector_bind_target";
41
44
  export { COLLECTOR_OUTPUT_TOOL };
42
45
 
43
46
  export const COLLECTOR_OPERATIONAL_TOOLS = [
47
+ COLLECTOR_BIND_TARGET_TOOL,
44
48
  COLLECTOR_OBSERVE_TOOL,
45
49
  COLLECTOR_READ_TOOL,
46
50
  COLLECTOR_REQUEST_TOOL,
@@ -157,7 +161,8 @@ export type CollectorTransportFailure = {
157
161
 
158
162
  export type CollectorConfigState = {
159
163
  repository: CollectorRepository;
160
- prNumber: number;
164
+ /** Bound PR target; undefined until explicit flag, admission bind, or role bind-target tool. */
165
+ prNumber: number | undefined;
161
166
  manifest: CollectorManifest;
162
167
  };
163
168
 
@@ -195,6 +200,8 @@ export type CollectorLedger = {
195
200
  assertNotFatal(): void;
196
201
  recordActivation(clock: CollectorClock): void;
197
202
  recordOutputCandidate(): void;
203
+ /** #676 A: role-decided unique PR bind (business fact on the ledger). */
204
+ bindTarget(prNumber: number): void;
198
205
  beginOperational(toolName: string, toolCallId: string): void;
199
206
  completeOperational(toolCallId: string): void;
200
207
  noteCutoffObserved(): void;
@@ -326,6 +333,15 @@ export function createCollectorLedger(
326
333
  const prIdentity = (pr: GitHubPullRequest): string =>
327
334
  `${pr.state}|${pr.headOid}|${pr.updatedAt ?? ""}`;
328
335
 
336
+ const requireBoundPr = (): number => {
337
+ if (config.prNumber === undefined) {
338
+ throw new Error(
339
+ "Collector PR target is unbound; call ak_collector_bind_target with the role-decided issue/PR or pass --pr",
340
+ );
341
+ }
342
+ return config.prNumber;
343
+ };
344
+
329
345
  const fetchObserveSurfaces = async (
330
346
  transport: CollectorGitHubTransport,
331
347
  observedAt: string,
@@ -333,7 +349,7 @@ export function createCollectorLedger(
333
349
  ) => {
334
350
  const owner = config.repository.owner;
335
351
  const repo = config.repository.repo;
336
- const prNumber = config.prNumber;
352
+ const prNumber = requireBoundPr();
337
353
  const signalOpt = signal === undefined ? {} : { signal };
338
354
  const user = await transport.getAuthenticatedUser(signalOpt);
339
355
  const prInitial = await transport.getPullRequest({
@@ -653,6 +669,26 @@ export function createCollectorLedger(
653
669
  outputCandidate = true;
654
670
  },
655
671
 
672
+ bindTarget(prNumber) {
673
+ assertNotFatal();
674
+ if (outputCandidate || pendingOutputCallId !== undefined) {
675
+ throw new Error("通进司已产出输出候选,本局不再受理目标绑定");
676
+ }
677
+ if (!Number.isSafeInteger(prNumber) || prNumber < 1) {
678
+ throw new Error("Collector bind target requires a positive safe-integer PR number");
679
+ }
680
+ if (config.prNumber !== undefined && config.prNumber !== prNumber) {
681
+ throw new Error(
682
+ `Collector target already bound to PR ${config.prNumber}; cannot rebind to ${prNumber}`,
683
+ );
684
+ }
685
+ config.prNumber = prNumber;
686
+ appendJournal("ak-collector-target-bound", {
687
+ prNumber,
688
+ repository: config.repository.canonical,
689
+ });
690
+ },
691
+
656
692
  beginOperational(toolName, toolCallId) {
657
693
  assertNotFatal();
658
694
  if (
@@ -833,7 +869,7 @@ export function createCollectorLedger(
833
869
  completedMono,
834
870
  host: "github.com",
835
871
  repository: config.repository.canonical,
836
- prNumber: config.prNumber,
872
+ prNumber: requireBoundPr(),
837
873
  prState: pr.state,
838
874
  headOid: pr.headOid,
839
875
  complete: true,
@@ -875,10 +911,6 @@ export function createCollectorLedger(
875
911
  if (activationTime === undefined || deadlineTime === undefined) {
876
912
  throw latchFatal("通进司请求需要激活");
877
913
  }
878
- if (pastCutoff(clock)) {
879
- finalObservationRequired = true;
880
- throw latchFatal("通进司请求不在资格截止前");
881
- }
882
914
  if (ledger.unresolvedTransportFailure) {
883
915
  throw latchFatal("通进司请求时存在未恢复的传输失败");
884
916
  }
@@ -896,7 +928,12 @@ export function createCollectorLedger(
896
928
  throw new Error("通进司请求要求最新完整快照");
897
929
  }
898
930
  if (snapshot.prState !== "OPEN") {
899
- throw latchFatal("通进司请求要求 OPEN 状态的 PR 快照");
931
+ // #676 D6: non-OPEN keeps materials; bounce before cutoff fatal so post-deadline materials still seal.
932
+ throw new CollectorNonOpenRequestError(snapshot.prState);
933
+ }
934
+ if (pastCutoff(clock)) {
935
+ finalObservationRequired = true;
936
+ throw latchFatal("通进司请求不在资格截止前");
900
937
  }
901
938
 
902
939
  const { body, marker } = buildCollectorRequestBody({
@@ -918,9 +955,10 @@ export function createCollectorLedger(
918
955
  );
919
956
  }
920
957
 
958
+ const boundPr = requireBoundPr();
921
959
  const attemptKey = [
922
960
  config.repository.canonical,
923
- String(config.prNumber),
961
+ String(boundPr),
924
962
  snapshot.headOid,
925
963
  request.id,
926
964
  ].join("|");
@@ -951,7 +989,7 @@ export function createCollectorLedger(
951
989
  const result = await transport.createIssueComment({
952
990
  owner: config.repository.owner,
953
991
  repo: config.repository.repo,
954
- prNumber: config.prNumber,
992
+ prNumber: boundPr,
955
993
  body,
956
994
  ...(signal === undefined ? {} : { signal }),
957
995
  });
@@ -4,11 +4,18 @@ import type { CollectorLedger, CollectorRequestAttempt } from "./collector-ledge
4
4
  import {
5
5
  enrichCollectorFindings,
6
6
  extractCollectorEvidenceIdentityGroups,
7
+ extractCollectorUnfinishedReasons,
7
8
  type ExtractedCollectorIdentityGroup,
8
9
  } from "./collector-identity.ts";
9
- import { validateAcceptedCollectorReceipt, type CollectorEvidenceRecord as ReceiptEvidenceRecord } from "./package-contracts/collector-output.ts";
10
+ import {
11
+ validateAcceptedCollectorReceipt,
12
+ type CollectorEvidenceRecord as ReceiptEvidenceRecord,
13
+ type CollectorReceipt as PackageCollectorReceipt,
14
+ type CollectorSubmissionProjection,
15
+ } from "./package-contracts/collector-output.ts";
10
16
 
11
17
  export { validateAcceptedCollectorReceipt };
18
+ export type { CollectorSubmissionProjection };
12
19
 
13
20
  /**
14
21
  * #641 chain① receipt-local evidence projection: machine identity, integrity
@@ -17,17 +24,15 @@ export { validateAcceptedCollectorReceipt };
17
24
  */
18
25
  export type CollectorReceiptEvidenceRecord = ReceiptEvidenceRecord;
19
26
 
20
- export type CollectorReceipt = {
21
- host: "github.com";
22
- repository: string;
23
- prNumber: number;
24
- manifestDigest: string;
25
- activationTime: string;
26
- deadlineTime: string;
27
- finalObservationTime: string;
28
- finalSnapshotId: string;
29
- targetHead: string;
27
+ /**
28
+ * #676 C: single authority for receipt shape is package-contracts/collector-output.
29
+ * Production builder always emits the full projection object and concrete prState;
30
+ * package leaf keeps those optional so old volumes stay readable.
31
+ */
32
+ export type CollectorReceipt = PackageCollectorReceipt & {
33
+ prState: string;
30
34
  groups: ExtractedCollectorIdentityGroup[];
35
+ submissionProjection: CollectorSubmissionProjection;
31
36
  requestAttempts: CollectorRequestAttempt[];
32
37
  snapshots: CollectorSnapshot[];
33
38
  evidenceRecords: CollectorReceiptEvidenceRecord[];
@@ -45,6 +50,9 @@ export function buildCollectorReceipt(
45
50
  if (ledger.unresolvedTransportFailure) fail("Collector cannot output while a transport failure is unrecovered");
46
51
  if (ledger.latestCompleteSnapshotId === undefined) fail("Collector output requires a complete final snapshot");
47
52
  if (ledger.activationTime === undefined || ledger.deadlineTime === undefined) fail("Collector output requires activation timeline");
53
+ if (ledger.config.prNumber === undefined) {
54
+ fail("Collector output requires a bound PR target; call ak_collector_bind_target first or pass --pr");
55
+ }
48
56
 
49
57
  if (clock !== undefined) ledger.assertOutputObservationLaw(clock);
50
58
  else if (ledger.observedGeneration !== ledger.mutationGeneration ||
@@ -54,7 +62,7 @@ export function buildCollectorReceipt(
54
62
 
55
63
  const finalSnapshot = ledger.getSnapshot(ledger.latestCompleteSnapshotId);
56
64
  if (finalSnapshot === undefined || !finalSnapshot.complete) fail("Collector final snapshot is incomplete");
57
- if (finalSnapshot.prState !== "OPEN") fail("Collector final snapshot PR state is not OPEN");
65
+ // #676 D6: closed/merged still deliver collected materials; status is a fact on the receipt.
58
66
 
59
67
  const evidenceRecords: CollectorEvidenceRecord[] = [...ledger.allEvidence()];
60
68
  const snapshots = [...ledger.allSnapshots()];
@@ -71,7 +79,7 @@ export function buildCollectorReceipt(
71
79
  const groups = extractCollectorEvidenceIdentityGroups(evidenceRecords, finalSnapshot.headOid);
72
80
  // #641 chain①: the collector LLM submits findings as pointer refs; the runtime
73
81
  // enriches each with the machine locator from the same stored record.
74
- enrichCollectorFindings({
82
+ const findingsProjection = enrichCollectorFindings({
75
83
  candidate: candidateRaw,
76
84
  records: evidenceRecords,
77
85
  groups,
@@ -80,7 +88,6 @@ export function buildCollectorReceipt(
80
88
  prNumber: ledger.config.prNumber,
81
89
  });
82
90
  for (const group of groups) {
83
- if (group.attendance !== true) fail("Collector group lacks attendance");
84
91
  for (const material of group.materials) {
85
92
  if (material.evidenceId === undefined || !evidenceIndex.has(material.evidenceId)) fail("Collector material lacks a receipt-local evidence ref");
86
93
  }
@@ -88,11 +95,21 @@ export function buildCollectorReceipt(
88
95
  if (finding.source.evidenceId === undefined || !evidenceIndex.has(finding.source.evidenceId)) fail("Collector finding lacks a receipt-local evidence ref");
89
96
  }
90
97
  }
98
+ const unfinished = extractCollectorUnfinishedReasons(candidateRaw);
99
+ const submissionProjection: CollectorSubmissionProjection = {
100
+ findingsSource: findingsProjection.findingsSource,
101
+ findingsProjectedCount: findingsProjection.findingsProjectedCount,
102
+ findingsUnprojected: findingsProjection.findingsUnprojected,
103
+ unfinishedReasonsSource: unfinished.source,
104
+ unfinishedReasonsProjectedCount: unfinished.reasons?.length ?? 0,
105
+ unfinishedReasonsUnprojected: unfinished.unprojected,
106
+ };
91
107
 
92
108
  return {
93
109
  host: COLLECTOR_HOST,
94
110
  repository: ledger.config.repository.canonical,
95
111
  prNumber: ledger.config.prNumber,
112
+ prState: finalSnapshot.prState,
96
113
  manifestDigest: ledger.config.manifest.digest,
97
114
  activationTime: ledger.activationTime.toISOString(),
98
115
  deadlineTime: ledger.deadlineTime.toISOString(),
@@ -100,6 +117,8 @@ export function buildCollectorReceipt(
100
117
  finalSnapshotId: finalSnapshot.snapshotId,
101
118
  targetHead: finalSnapshot.headOid,
102
119
  groups,
120
+ ...(unfinished.reasons === undefined ? {} : { unfinishedReasons: unfinished.reasons }),
121
+ submissionProjection,
103
122
  requestAttempts: [...ledger.requestAttempts()],
104
123
  snapshots,
105
124
  evidenceRecords: evidenceRecords.map(toReceiptEvidenceRecord),