@akagilnc/pi-workflow-roles 0.1.3236 → 0.1.3256

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.
@@ -8,17 +8,21 @@
8
8
 
9
9
  取舍位次:过测试 → 显意图 → 无重复 → 最少元素。
10
10
 
11
+ 尺子锚定仓内已接受的现状:与紧邻已并入代码同款的写法,不单独判新增行为缺陷;要判就按类判全仓。
12
+
11
13
  ## 测试对行为负责
12
14
 
13
- 测试从真实入口进入,沿真实行为执行,在外部可见结果上断言。每个独立行为契约保留一条最短可信的主干测试;边界和失败场景放在最低但仍能真实证明行为的层级。
15
+ 测试只保**调用输入正确**的最短确定性案:从真实入口进入,在外部可见的结构化结果上断言,秒级完成。涉真实宿主、真实 LLM、真实并发、真实网络的行为,不写测试;以真跑为证。
16
+
17
+ 修复的证明是**变异真跑**:装回旧逻辑,真实入口秒级报红;恢复后绿。命令与输出原文记入回执。不为证明修复造测试;拆掉的证明性测试不得换形再造。
14
18
 
15
- 测试向 spec 负责,不向实现妥协。不得靠放松断言、删除失败路径或 mock 被测行为使测试变绿。缺陷修复应能证明修复前失败、修复后通过。
19
+ 测试向 spec 负责,不向实现妥协:不得靠放松断言、删除失败路径或 mock 被测行为使测试变绿。
16
20
 
17
21
  盯生成物一律禁止:不得对自由文本、LLM 产出内容、图片像素等任何生成产物建立机械依赖——锁措辞、锁句式、锁模板、数条数、埋哨兵、比像素,都算。契约断言只落在结构化字段上;生成物只可特征化观察,不可约束。
18
22
 
19
23
  已有测试能表达真实行为的,修改或复用,不另建平行测试。只锁定内部结构、非契约文本、重复结论、失效行为或单纯覆盖率的测试,应当删除。
20
24
 
21
- 生产代码和测试代码都计算复杂度。新增任何机制或测试,都必须证明它提供了现有体系没有的必要价值。
25
+ 生产代码和测试代码都计算复杂度。新增任何测试,都必须证明它提供了现有体系没有的必要价值。
22
26
 
23
27
  ## 测试分级
24
28
 
@@ -1,29 +1,36 @@
1
- import {
2
- type GitHubIssueComment,
3
- type GitHubMachineIdentity,
4
- type GitHubPullRequestReaction,
5
- type GitHubReview,
6
- type GitHubReviewComment,
1
+ import type {
2
+ GitHubMachineIdentity,
7
3
  } from "./collector-github.ts";
8
4
  import type { CollectorEvidenceRecord, HeadRelation } from "./collector-evidence.ts";
9
-
10
- export type GitHubIdentityMaterial = GitHubReview | GitHubIssueComment | GitHubReviewComment | GitHubPullRequestReaction;
5
+ import { CorrectableSubmissionError } from "./submission-correctable-error.ts";
11
6
 
12
7
  export type CollectorMaterialRef = {
13
8
  kind: "review" | "issue_comment" | "review_comment" | "reaction";
14
9
  id: number;
15
- /** Original review body, retained as uninterpreted material. */
16
- body?: string;
17
10
  /** Receipt-local immutable source reference. */
18
11
  evidenceId?: string;
19
12
  headRelation?: HeadRelation | "unbound";
20
13
  };
21
14
 
15
+ /**
16
+ * #641 chain①: a receipt finding is a model-classified unit (splitting stays
17
+ * with the collector LLM) whose source pointer resolves against the stored
18
+ * evidence and whose machine locator (repo/PR/comment id/url/author/kind/时间)
19
+ * is enriched by the runtime from the same record — never transcribed bodies.
20
+ */
22
21
  export type CollectorFinding = {
23
- identity: GitHubMachineIdentity;
22
+ identity: GitHubMachineIdentity | null;
24
23
  source: CollectorMaterialRef;
25
- category: "inline";
26
- body: string;
24
+ category?: string;
25
+ pointer: {
26
+ repository: string;
27
+ prNumber: number;
28
+ commentId: number;
29
+ htmlUrl?: string;
30
+ authorLogin?: string;
31
+ kind: CollectorMaterialRef["kind"];
32
+ authoritativeTime?: string | null;
33
+ };
27
34
  };
28
35
 
29
36
  export type CollectorIdentityGroup = {
@@ -35,12 +42,10 @@ export type CollectorIdentityGroup = {
35
42
  materials: CollectorMaterialRef[];
36
43
  };
37
44
 
38
- function materialKind(material: GitHubIdentityMaterial): CollectorIdentityGroup["materials"][number]["kind"] {
39
- if ("pullRequestReviewId" in material) return "review_comment";
40
- if ("state" in material) return "review";
41
- if ("content" in material) return "reaction";
42
- return "issue_comment";
43
- }
45
+ export type ExtractedCollectorIdentityGroup = CollectorIdentityGroup & {
46
+ attendance: true;
47
+ findings: CollectorFinding[];
48
+ };
44
49
 
45
50
  function identityKey(identity: GitHubMachineIdentity | null): string {
46
51
  if (identity === null) return "unassigned";
@@ -61,60 +66,10 @@ function mergeMachineIdentity(
61
66
  return observed.userType < current.userType ? observed : current;
62
67
  }
63
68
 
64
- /** Group observed GitHub materials only by API machine identity fields. */
65
- export function groupGitHubMaterialsByIdentity(
66
- materials: readonly GitHubIdentityMaterial[],
67
- ): CollectorIdentityGroup[] {
68
- const groups = new Map<string, CollectorIdentityGroup>();
69
- for (const material of materials) {
70
- const identity = material.machineIdentity ?? null;
71
- const key = identityKey(identity);
72
- let group = groups.get(key);
73
- if (group === undefined) {
74
- group = {
75
- identity,
76
- ...(material.userLogin === null ? {} : { displayLogin: material.userLogin }),
77
- materials: [],
78
- };
79
- groups.set(key, group);
80
- } else {
81
- group.identity = mergeMachineIdentity(group.identity, identity);
82
- }
83
- const kind = materialKind(material);
84
- group.materials.push({
85
- kind,
86
- id: material.id,
87
- ...(kind === "review" && "body" in material ? { body: material.body } : {}),
88
- });
89
- }
90
- return [...groups.values()];
91
- }
92
-
93
- const CODEX_USER_ID = 199175422;
94
- const CODERABBIT_USER_ID = 136622811;
95
- export type ExtractedCollectorIdentityGroup = CollectorIdentityGroup & {
96
- attendance: true;
97
- findings: CollectorFinding[];
98
- };
99
-
100
- /** Extract attendance and provider findings without using login/display text as identity. */
101
- export function extractGitHubIdentityGroups(materials: readonly GitHubIdentityMaterial[]): ExtractedCollectorIdentityGroup[] {
102
- const groups = groupGitHubMaterialsByIdentity(materials);
103
- const byKey = new Map(groups.map((group) => [identityKey(group.identity), group]));
104
- for (const group of groups) {
105
- group.attendance = true;
106
- group.findings = [];
107
- }
108
- for (const material of materials) {
109
- const identity = material.machineIdentity ?? null;
110
- const group = byKey.get(identityKey(identity))!;
111
- if (identity === null) continue;
112
- const source = { kind: materialKind(material), id: material.id };
113
- if (source.kind === "review_comment" && "body" in material && (identity.userId === CODEX_USER_ID || identity.userId === CODERABBIT_USER_ID)) {
114
- group.findings!.push({ identity, source, category: "inline", body: material.body });
115
- }
116
- }
117
- return groups as ExtractedCollectorIdentityGroup[];
69
+ function headRelationFor(record: CollectorEvidenceRecord, targetHead: string): HeadRelation | "unbound" {
70
+ return record.commitOid === undefined || record.commitOid === null
71
+ ? "unbound"
72
+ : record.commitOid === targetHead ? "current" : "prior";
118
73
  }
119
74
 
120
75
  /** Receipt adapter consuming the typed facts retained by transport normalization. */
@@ -131,11 +86,8 @@ export function extractCollectorEvidenceIdentityGroups(
131
86
  const source: CollectorMaterialRef = {
132
87
  kind,
133
88
  id: record.githubId,
134
- ...(kind === "review" && record.body !== undefined ? { body: record.body } : {}),
135
89
  evidenceId: record.evidenceId,
136
- headRelation: record.commitOid === undefined || record.commitOid === null
137
- ? "unbound"
138
- : record.commitOid === targetHead ? "current" : "prior",
90
+ headRelation: headRelationFor(record, targetHead),
139
91
  };
140
92
  const key = identityKey(identity);
141
93
  let group = groups.get(key);
@@ -152,10 +104,105 @@ export function extractCollectorEvidenceIdentityGroups(
152
104
  group.identity = mergeMachineIdentity(group.identity, identity);
153
105
  }
154
106
  group.materials.push(source);
155
- if (identity === null || record.body === undefined) continue;
156
- if (kind === "review_comment" && (identity.userId === CODEX_USER_ID || identity.userId === CODERABBIT_USER_ID)) {
157
- group.findings.push({ identity, source: { ...source }, category: "inline", body: record.body });
158
- }
159
107
  }
160
108
  return [...groups.values()];
161
109
  }
110
+
111
+ /**
112
+ * #641 chain①: pointer-open failures are model misuse, not host failures. The
113
+ * seat rejects them as correctable so the model can retry with a stored
114
+ * evidenceId — on Pi and Grok/ACP alike (第 0 条: 模型提交方式可纠正).
115
+ */
116
+ export class CollectorUnknownEvidenceError extends CorrectableSubmissionError {
117
+ constructor(evidenceId: string) {
118
+ super(`未在本局已观测材料中找到 evidenceId ${evidenceId};请用 observe 返回的指针重试。`);
119
+ this.name = "CollectorUnknownEvidenceError";
120
+ }
121
+ }
122
+
123
+ /**
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.
127
+ */
128
+ export class CollectorFindingsValidationError extends CorrectableSubmissionError {
129
+ constructor(message: string) {
130
+ super(message);
131
+ this.name = "CollectorFindingsValidationError";
132
+ }
133
+ }
134
+
135
+ /**
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.
141
+ */
142
+ export function enrichCollectorFindings(input: {
143
+ candidate: unknown;
144
+ records: readonly CollectorEvidenceRecord[];
145
+ groups: readonly ExtractedCollectorIdentityGroup[];
146
+ targetHead: string;
147
+ repository: string;
148
+ 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("通进司交件参数必须为对象");
154
+ }
155
+ const rawFindings = (candidate as { findings?: unknown }).findings;
156
+ if (rawFindings === undefined) return;
157
+ if (!Array.isArray(rawFindings)) {
158
+ throw new CollectorFindingsValidationError("通进司 findings 必须为数组");
159
+ }
160
+ const byEvidenceId = new Map(input.records.map((record) => [record.evidenceId, record]));
161
+ for (const raw of rawFindings) {
162
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
163
+ throw new CollectorFindingsValidationError("通进司 finding 必须为对象");
164
+ }
165
+ const evidenceId = (raw as { evidenceId?: unknown }).evidenceId;
166
+ if (typeof evidenceId !== "string" || evidenceId.length === 0) {
167
+ throw new CollectorFindingsValidationError("通进司 finding 缺少可解析的 evidenceId 指针");
168
+ }
169
+ const record = byEvidenceId.get(evidenceId);
170
+ if (record === undefined) {
171
+ throw new CollectorUnknownEvidenceError(evidenceId);
172
+ }
173
+ if (record.kind !== "review" && record.kind !== "issue_comment" && record.kind !== "review_comment") {
174
+ throw new CollectorFindingsValidationError(`通进司 finding 指针指向不可承 finding 的证据种类 ${record.kind}`);
175
+ }
176
+ if (record.githubId === undefined) {
177
+ throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 缺少 GitHub id`);
178
+ }
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;
184
+ const group = input.groups.find((candidateGroup) => identityKey(candidateGroup.identity) === identityKey(identity));
185
+ if (group === undefined) {
186
+ throw new CollectorFindingsValidationError(`通进司 finding 指针证据 ${evidenceId} 无归属身份组`);
187
+ }
188
+ group.findings.push({
189
+ identity,
190
+ source: {
191
+ kind: record.kind,
192
+ id: record.githubId,
193
+ evidenceId: record.evidenceId,
194
+ headRelation: headRelationFor(record, input.targetHead),
195
+ },
196
+ ...(category === undefined ? {} : { category: category.trim() }),
197
+ pointer: {
198
+ repository: input.repository,
199
+ 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,
205
+ },
206
+ });
207
+ }
208
+ }
@@ -16,6 +16,8 @@ import {
16
16
  type CollectorClock,
17
17
  type CollectorEvidenceRecord,
18
18
  type CollectorSnapshot,
19
+ type HeadRelation,
20
+ type WindowRelation,
19
21
  } from "./collector-evidence.ts";
20
22
  import {
21
23
  buildCollectorRequestBody,
@@ -26,22 +28,92 @@ import {
26
28
  import {
27
29
  collectorObserveArgsSchema,
28
30
  collectorOutputArgsSchema,
31
+ collectorReadArgsSchema,
29
32
  collectorRequestArgsSchema,
30
33
  collectorWaitArgsSchema,
31
34
  } from "./collector-tool-schemas.ts";
32
35
  import { COLLECTOR_OUTPUT_TOOL } from "./package-contracts/collector-output.ts";
33
36
 
34
37
  export const COLLECTOR_OBSERVE_TOOL = "ak_collector_observe";
38
+ export const COLLECTOR_READ_TOOL = "ak_collector_read";
35
39
  export const COLLECTOR_REQUEST_TOOL = "ak_collector_request";
36
40
  export const COLLECTOR_WAIT_TOOL = "ak_collector_wait";
37
41
  export { COLLECTOR_OUTPUT_TOOL };
38
42
 
39
43
  export const COLLECTOR_OPERATIONAL_TOOLS = [
40
44
  COLLECTOR_OBSERVE_TOOL,
45
+ COLLECTOR_READ_TOOL,
41
46
  COLLECTOR_REQUEST_TOOL,
42
47
  COLLECTOR_WAIT_TOOL,
43
48
  ] as const;
44
49
 
50
+ /**
51
+ * #641 chain①: observe context projects at most this many bytes of each
52
+ * material body; the full body stays reachable via the record pointer
53
+ * (evidenceId + htmlUrl) and is preserved in the volume, never unconditionally
54
+ * transcribed into model context or receipt.
55
+ */
56
+ export const COLLECTOR_OBSERVE_BODY_HEAD_BYTES = 512;
57
+
58
+ function utf8SafePrefix(text: string, maxBytes: number): string {
59
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
60
+ const bytes = Buffer.from(text, "utf8");
61
+ let end = maxBytes;
62
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
63
+ return `${bytes.subarray(0, end).toString("utf8")}…`;
64
+ }
65
+
66
+ export type ObserveEvidenceEntry = {
67
+ evidenceId: string;
68
+ kind: string;
69
+ authorLogin?: string | undefined;
70
+ state?: string | undefined;
71
+ body?: string | undefined;
72
+ commitOid?: string | null | undefined;
73
+ htmlUrl?: string | undefined;
74
+ path?: string | undefined;
75
+ line?: number | null | undefined;
76
+ side?: string | null | undefined;
77
+ authoritativeTime?: string | null | undefined;
78
+ windowRelation?: WindowRelation | undefined;
79
+ pullRequestReviewId?: number | null | undefined;
80
+ };
81
+
82
+ export type ObserveModelView = {
83
+ snapshotId: string;
84
+ observedAt: string;
85
+ completedAt: string;
86
+ prState: string;
87
+ headOid: string;
88
+ complete: boolean;
89
+ evidence: ObserveEvidenceEntry[];
90
+ requestAttempts: Array<{
91
+ attemptId: string;
92
+ requestId: string;
93
+ observedHead: string;
94
+ status: string;
95
+ marker: string;
96
+ recoverySnapshotId?: string | undefined;
97
+ }>;
98
+ };
99
+
100
+ /**
101
+ * #641 chain① model-context projection: identical shape to the volume view,
102
+ * with every material body bounded to a head preview. Full bodies remain
103
+ * pointer-reachable (evidenceId/htmlUrl) and stay in the ledger volume
104
+ * (collector-side; provider-visible observe details carry only this bounded
105
+ * projection), never unconditionally transcribed into model context.
106
+ */
107
+ export function projectObserveContextView(modelView: ObserveModelView): ObserveModelView {
108
+ return {
109
+ ...modelView,
110
+ evidence: modelView.evidence.map((entry) =>
111
+ entry.body === undefined
112
+ ? entry
113
+ : { ...entry, body: utf8SafePrefix(entry.body, COLLECTOR_OBSERVE_BODY_HEAD_BYTES) }),
114
+ };
115
+ }
116
+
45
117
  export type CollectorOperationalTool = (typeof COLLECTOR_OPERATIONAL_TOOLS)[number];
46
118
 
47
119
  export type CollectorRequestAttempt = {
@@ -117,7 +189,7 @@ export type CollectorLedger = {
117
189
  signal?: AbortSignal,
118
190
  ): Promise<{
119
191
  snapshot: CollectorSnapshot;
120
- modelView: unknown;
192
+ contextView: ObserveModelView;
121
193
  }>;
122
194
 
123
195
  request(
@@ -157,6 +229,8 @@ export function collectorToolArgumentsValid(
157
229
  switch (name) {
158
230
  case COLLECTOR_OBSERVE_TOOL:
159
231
  return Value.Check(collectorObserveArgsSchema, args);
232
+ case COLLECTOR_READ_TOOL:
233
+ return Value.Check(collectorReadArgsSchema, args);
160
234
  case COLLECTOR_REQUEST_TOOL:
161
235
  return Value.Check(collectorRequestArgsSchema, args);
162
236
  case COLLECTOR_WAIT_TOOL:
@@ -566,7 +640,7 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
566
640
  attempts,
567
641
  });
568
642
 
569
- return { snapshot, modelView };
643
+ return { snapshot, contextView: projectObserveContextView(modelView) };
570
644
  },
571
645
 
572
646
  async request(input, transport, clock, signal) {
@@ -780,12 +854,35 @@ export function createCollectorLedger(config: CollectorConfigState): CollectorLe
780
854
  return ledger;
781
855
  }
782
856
 
857
+ /**
858
+ * #641 chain①: single authoritative evidence-record projection shared by the
859
+ * observe model view and the pointer-open read tool.
860
+ */
861
+ export function projectEvidenceEntryView(record: CollectorEvidenceRecord): ObserveEvidenceEntry {
862
+ return {
863
+ evidenceId: record.evidenceId,
864
+ kind: record.kind,
865
+ authorLogin: record.authorLogin,
866
+ state: record.state,
867
+ body: record.body,
868
+ commitOid: record.commitOid,
869
+ htmlUrl: record.htmlUrl,
870
+ path: record.path,
871
+ // Single display fallback: current line, else originalLine.
872
+ line: record.line ?? record.originalLine,
873
+ side: record.side,
874
+ authoritativeTime: record.authoritativeTime,
875
+ windowRelation: record.windowRelation,
876
+ pullRequestReviewId: record.pullRequestReviewId,
877
+ };
878
+ }
879
+
783
880
  function buildObserveModelView(input: {
784
881
  snapshot: CollectorSnapshot;
785
882
  records: readonly CollectorEvidenceRecord[];
786
883
  requesterLogin: string | undefined;
787
884
  attempts: readonly CollectorRequestAttempt[];
788
- }): unknown {
885
+ }): ObserveModelView {
789
886
  const relevant = input.records;
790
887
  return {
791
888
  snapshotId: input.snapshot.snapshotId,
@@ -794,22 +891,7 @@ function buildObserveModelView(input: {
794
891
  prState: input.snapshot.prState,
795
892
  headOid: input.snapshot.headOid,
796
893
  complete: input.snapshot.complete,
797
- evidence: relevant.map((record) => ({
798
- evidenceId: record.evidenceId,
799
- kind: record.kind,
800
- authorLogin: record.authorLogin,
801
- state: record.state,
802
- body: record.body,
803
- commitOid: record.commitOid,
804
- htmlUrl: record.htmlUrl,
805
- path: record.path,
806
- // Single display fallback: current line, else originalLine.
807
- line: record.line ?? record.originalLine,
808
- side: record.side,
809
- authoritativeTime: record.authoritativeTime,
810
- windowRelation: record.windowRelation,
811
- pullRequestReviewId: record.pullRequestReviewId,
812
- })),
894
+ evidence: relevant.map((record): ObserveEvidenceEntry => projectEvidenceEntryView(record)),
813
895
  requestAttempts: input.attempts.map((attempt) => ({
814
896
  attemptId: attempt.attemptId,
815
897
  requestId: attempt.requestId,
@@ -1,11 +1,22 @@
1
1
  import { COLLECTOR_HOST } from "./collector-config.ts";
2
2
  import type { CollectorClock, CollectorEvidenceRecord, CollectorSnapshot } from "./collector-evidence.ts";
3
3
  import type { CollectorLedger, CollectorRequestAttempt } from "./collector-ledger.ts";
4
- import { extractCollectorEvidenceIdentityGroups, type ExtractedCollectorIdentityGroup } from "./collector-identity.ts";
5
- import { validateAcceptedCollectorReceipt } from "./package-contracts/collector-output.ts";
4
+ import {
5
+ enrichCollectorFindings,
6
+ extractCollectorEvidenceIdentityGroups,
7
+ type ExtractedCollectorIdentityGroup,
8
+ } from "./collector-identity.ts";
9
+ import { validateAcceptedCollectorReceipt, type CollectorEvidenceRecord as ReceiptEvidenceRecord } from "./package-contracts/collector-output.ts";
6
10
 
7
11
  export { validateAcceptedCollectorReceipt };
8
12
 
13
+ /**
14
+ * #641 chain① receipt-local evidence projection: machine identity, integrity
15
+ * digest and resolvable pointer fields only. Bodies/raw stay in the volume
16
+ * (observe details + GitHub pointer); they are never transcribed into the receipt.
17
+ */
18
+ export type CollectorReceiptEvidenceRecord = ReceiptEvidenceRecord;
19
+
9
20
  export type CollectorReceipt = {
10
21
  host: "github.com";
11
22
  repository: string;
@@ -19,15 +30,15 @@ export type CollectorReceipt = {
19
30
  groups: ExtractedCollectorIdentityGroup[];
20
31
  requestAttempts: CollectorRequestAttempt[];
21
32
  snapshots: CollectorSnapshot[];
22
- evidenceRecords: CollectorEvidenceRecord[];
33
+ evidenceRecords: CollectorReceiptEvidenceRecord[];
23
34
  };
24
35
 
25
36
  function fail(message: string): never { throw new Error(message); }
26
37
 
27
- /** Runtime output is intentionally empty: observed typed groups are the sole receipt source. */
38
+ /** Runtime output assembles findings pointers from the model submission into the typed receipt. */
28
39
  export function buildCollectorReceipt(
29
40
  ledger: CollectorLedger,
30
- _candidateRaw: unknown,
41
+ candidateRaw: unknown,
31
42
  clock?: CollectorClock,
32
43
  ): CollectorReceipt {
33
44
  ledger.assertNotFatal();
@@ -46,7 +57,7 @@ export function buildCollectorReceipt(
46
57
  if (finalSnapshot === undefined || !finalSnapshot.complete) fail("Collector final snapshot is incomplete");
47
58
  if (finalSnapshot.prState !== "OPEN") fail("Collector final snapshot PR state is not OPEN");
48
59
 
49
- const evidenceRecords = [...ledger.allEvidence()];
60
+ const evidenceRecords: CollectorEvidenceRecord[] = [...ledger.allEvidence()];
50
61
  const snapshots = [...ledger.allSnapshots()];
51
62
  const evidenceIndex = new Map(evidenceRecords.map((record) => [record.evidenceId, record]));
52
63
  const snapshotIndex = new Map(snapshots.map((snapshot) => [snapshot.snapshotId, snapshot]));
@@ -59,6 +70,16 @@ export function buildCollectorReceipt(
59
70
  }
60
71
 
61
72
  const groups = extractCollectorEvidenceIdentityGroups(evidenceRecords, finalSnapshot.headOid);
73
+ // #641 chain①: the collector LLM submits findings as pointer refs; the runtime
74
+ // enriches each with the machine locator from the same stored record.
75
+ enrichCollectorFindings({
76
+ candidate: candidateRaw,
77
+ records: evidenceRecords,
78
+ groups,
79
+ targetHead: finalSnapshot.headOid,
80
+ repository: ledger.config.repository.canonical,
81
+ prNumber: ledger.config.prNumber,
82
+ });
62
83
  for (const group of groups) {
63
84
  if (group.attendance !== true) fail("Collector group lacks attendance");
64
85
  for (const material of group.materials) {
@@ -82,6 +103,20 @@ export function buildCollectorReceipt(
82
103
  groups,
83
104
  requestAttempts: [...ledger.requestAttempts()],
84
105
  snapshots,
85
- evidenceRecords,
106
+ evidenceRecords: evidenceRecords.map(toReceiptEvidenceRecord),
107
+ };
108
+ }
109
+
110
+ function toReceiptEvidenceRecord(record: CollectorEvidenceRecord): CollectorReceiptEvidenceRecord {
111
+ return {
112
+ evidenceId: record.evidenceId,
113
+ kind: record.kind,
114
+ versionId: record.versionId,
115
+ contentDigest: record.contentDigest,
116
+ firstObservedAt: record.firstObservedAt,
117
+ ...(record.githubId === undefined ? {} : { githubId: record.githubId }),
118
+ ...(record.authorLogin === undefined ? {} : { authorLogin: record.authorLogin }),
119
+ ...(record.htmlUrl === undefined ? {} : { htmlUrl: record.htmlUrl }),
120
+ ...(record.authoritativeTime === undefined ? {} : { authoritativeTime: record.authoritativeTime }),
86
121
  };
87
122
  }