@akagilnc/pi-workflow-roles 0.1.3708 → 0.1.3716
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 +4 -0
- package/README.zh-CN.md +4 -0
- package/dist/acp-host/production-host.js +262 -518
- package/dist/adr-path-refs.js +1 -1
- package/dist/diarist-contracts.js +31 -21
- package/dist/diarist-role.js +17 -8
- package/dist/diarist.js +39 -388
- package/dist/package-contracts/terminating-tools.js +1 -1
- package/dist/packaged-role-registry.js +4 -8
- package/dist/pi/role-turn-host.js +1 -7
- package/dist/public-cli/config.js +106 -2
- package/dist/public-cli/main.js +263 -1488
- package/dist/public-cli/option-definitions.js +3 -0
- package/dist/role-runtime.js +60 -88
- package/dist/ticket-provenance-contracts.js +0 -5
- package/dist/ticket-provenance.js +2 -149
- package/package.json +1 -1
- package/resources/diarist-collect.md +21 -11
- package/src/acp-host/role-envelope.ts +1 -1
- package/src/adr-path-refs.ts +1 -1
- package/src/diarist-contracts.ts +48 -32
- package/src/diarist-role.ts +31 -16
- package/src/diarist.ts +54 -635
- package/src/host-contracts.ts +1 -5
- package/src/package-contracts/terminating-tools.ts +1 -1
- package/src/packaged-role-registry.ts +4 -8
- package/src/pi/role-turn-host.ts +1 -7
- package/src/public-cli/cli.ts +54 -0
- package/src/public-cli/config.ts +157 -1
- package/src/public-cli/countersign-run.ts +2 -3
- package/src/public-cli/diarist-run.ts +15 -117
- package/src/public-cli/option-definitions.ts +3 -0
- package/src/role-runtime.ts +73 -105
- package/src/ticket-provenance-contracts.ts +6 -12
- package/src/ticket-provenance.ts +1 -207
- package/dist/diarist-mechanical.js +0 -411
- package/src/diarist-mechanical.ts +0 -571
package/src/diarist.ts
CHANGED
|
@@ -1,658 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 起居郎
|
|
3
|
-
* Semantic collection is the diarist role's own LLM turn
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* 起居郎 volume commit — ADR 0075 `diarist-is-role` / #779.
|
|
3
|
+
* Semantic collection is the diarist role's own LLM turn (finds materials itself).
|
|
4
|
+
* This module only appends submitted whole blocks idempotently and refreshes the
|
|
5
|
+
* human view. No frozen catalog, no quote reverse-verify, no ticket-number
|
|
6
|
+
* re-judgment of LLM output (#779 owner: 机械层凭什么能判断llm的输出).
|
|
6
7
|
* No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
|
|
7
8
|
*/
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
9
|
-
|
|
10
|
-
import { extractReferencedAdrPaths } from "./adr-path-refs.ts";
|
|
11
|
-
import {
|
|
12
|
-
createGhApiRunner,
|
|
13
|
-
createGhCollectorGitHubTransport,
|
|
14
|
-
projectGhIssueBody,
|
|
15
|
-
type CollectorGitHubTransport,
|
|
16
|
-
type GhApiRunner,
|
|
17
|
-
} from "./collector-github.ts";
|
|
18
9
|
import {
|
|
19
|
-
blockToLlmEntry,
|
|
20
|
-
buildDiaristAnchors,
|
|
21
|
-
mechanicalSafeguardPipeline,
|
|
22
|
-
readAdrDecisionKeyBlocks,
|
|
23
|
-
readCcSessionBlocks,
|
|
24
|
-
readIssueFaceBlocks,
|
|
25
|
-
type DiaristAnchorSet,
|
|
26
|
-
type DiaristIssueFace,
|
|
27
|
-
type DiaristSourceBlock,
|
|
28
|
-
} from "./diarist-mechanical.ts";
|
|
29
|
-
import type { DiaristSelection } from "./diarist-contracts.ts";
|
|
30
|
-
import { parseGitHubOriginRemote } from "./reviewer-pinned-git.ts";
|
|
31
|
-
import {
|
|
32
|
-
appendIssueSourceFailureDiagnostic,
|
|
33
|
-
appendQuoteVerifyFailureDiagnostic,
|
|
34
10
|
appendTicketProvenanceEntry,
|
|
35
11
|
ensureTicketProvenanceVolume,
|
|
36
|
-
readOfferedIdentities,
|
|
37
12
|
readTicketProvenance,
|
|
38
|
-
recordOfferedIdentities,
|
|
39
|
-
resolveTicketProvenanceVolume,
|
|
40
|
-
ticketProvenanceEntryIdentity,
|
|
41
13
|
writeTicketProvenanceHumanView,
|
|
42
14
|
} from "./ticket-provenance.ts";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
export type { DiaristIssueFace } from "./diarist-mechanical.ts";
|
|
47
|
-
|
|
48
|
-
/** Live existence check for an asserted ticket number. */
|
|
49
|
-
export type TicketExistenceChecker = (input: {
|
|
50
|
-
readonly owner: string;
|
|
51
|
-
readonly repo: string;
|
|
52
|
-
readonly ticketNumber: number;
|
|
53
|
-
readonly signal?: AbortSignal;
|
|
54
|
-
}) => Promise<boolean>;
|
|
55
|
-
|
|
56
|
-
export type DiaristTicketVerificationReason =
|
|
57
|
-
| "number-not-in-instruction"
|
|
58
|
-
| "ticket-missing"
|
|
59
|
-
| "origin-unresolved"
|
|
60
|
-
| "assertion-uninterpretable";
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Honest failure of mechanical ticket verification (ADR 0075).
|
|
64
|
-
* Must settle as failure — never wash into true-unbound / 无录.
|
|
65
|
-
*/
|
|
66
|
-
export class DiaristTicketVerificationError extends Error {
|
|
67
|
-
readonly code = "diarist-ticket-verification" as const;
|
|
68
|
-
readonly reason: DiaristTicketVerificationReason;
|
|
69
|
-
constructor(
|
|
70
|
-
reason: DiaristTicketVerificationReason,
|
|
71
|
-
message: string,
|
|
72
|
-
options?: ErrorOptions,
|
|
73
|
-
) {
|
|
74
|
-
super(message, options);
|
|
75
|
-
this.name = "DiaristTicketVerificationError";
|
|
76
|
-
this.reason = reason;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Decision-key exact identity: complete decimal number N appears in instruction.
|
|
82
|
-
* A longer number's digit substring (e.g. 82 inside 582) is not N.
|
|
83
|
-
* Verifies a typed claim — does not harvest candidates from prose.
|
|
84
|
-
*/
|
|
85
|
-
export function instructionContainsTicketNumber(
|
|
86
|
-
instruction: string,
|
|
87
|
-
ticketNumber: number,
|
|
88
|
-
): boolean {
|
|
89
|
-
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) return false;
|
|
90
|
-
// Complete decimal token: not preceded or followed by another digit.
|
|
91
|
-
// Plain string (not template) so the digit-class backslash survives emit.
|
|
92
|
-
return new RegExp("(?<!\\d)" + String(ticketNumber) + "(?!\\d)").test(
|
|
93
|
-
instruction,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Production live-ticket check over shared gh issue-body projection.
|
|
99
|
-
* Available issue face → exists; unavailable/invalid → missing.
|
|
100
|
-
*/
|
|
101
|
-
export function createGhTicketExistenceChecker(options?: {
|
|
102
|
-
readonly runner?: GhApiRunner;
|
|
103
|
-
}): TicketExistenceChecker {
|
|
104
|
-
const runner = options?.runner ?? createGhApiRunner();
|
|
105
|
-
return async (input) => {
|
|
106
|
-
const projected = await projectGhIssueBody(runner, {
|
|
107
|
-
owner: input.owner,
|
|
108
|
-
repo: input.repo,
|
|
109
|
-
ticketNumber: input.ticketNumber,
|
|
110
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
111
|
-
});
|
|
112
|
-
return projected.status === "available";
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Mechanical verify of an LLM typed ticket assertion (ADR 0075).
|
|
118
|
-
* Complete decimal of N must appear in the summons; ticket must exist live.
|
|
119
|
-
* Throws DiaristTicketVerificationError — caller must not wash into 无录.
|
|
120
|
-
*/
|
|
121
|
-
export async function verifyAssertedTicketNumber(input: {
|
|
122
|
-
readonly ticketNumber: number;
|
|
123
|
-
readonly instruction: string;
|
|
124
|
-
readonly projectRoot: string;
|
|
125
|
-
readonly checkExistence?: TicketExistenceChecker;
|
|
126
|
-
readonly signal?: AbortSignal;
|
|
127
|
-
}): Promise<void> {
|
|
128
|
-
const n = input.ticketNumber;
|
|
129
|
-
if (!Number.isSafeInteger(n) || n < 1) {
|
|
130
|
-
throw new DiaristTicketVerificationError(
|
|
131
|
-
"assertion-uninterpretable",
|
|
132
|
-
`diarist ticket assertion must be a safe integer >= 1, got ${String(n)}`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
if (!instructionContainsTicketNumber(input.instruction, n)) {
|
|
136
|
-
throw new DiaristTicketVerificationError(
|
|
137
|
-
"number-not-in-instruction",
|
|
138
|
-
`diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
const origin = resolveDiaristGithubOrigin(input.projectRoot);
|
|
142
|
-
if (origin === undefined) {
|
|
143
|
-
throw new DiaristTicketVerificationError(
|
|
144
|
-
"origin-unresolved",
|
|
145
|
-
`diarist ticket assertion #${n} requires a resolvable github.com origin remote for live verification`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
const checkExistence = input.checkExistence ?? createGhTicketExistenceChecker();
|
|
149
|
-
let exists: boolean;
|
|
150
|
-
try {
|
|
151
|
-
exists = await checkExistence({
|
|
152
|
-
owner: origin.owner,
|
|
153
|
-
repo: origin.repo,
|
|
154
|
-
ticketNumber: n,
|
|
155
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
156
|
-
});
|
|
157
|
-
} catch (error) {
|
|
158
|
-
throw new DiaristTicketVerificationError(
|
|
159
|
-
"ticket-missing",
|
|
160
|
-
`diarist ticket assertion #${n} live verification failed`,
|
|
161
|
-
{ cause: error },
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
if (!exists) {
|
|
165
|
-
throw new DiaristTicketVerificationError(
|
|
166
|
-
"ticket-missing",
|
|
167
|
-
`diarist ticket assertion #${n} does not exist as a live issue on ${origin.owner}/${origin.repo}`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Typed reasons when bound-ticket issue face cannot be acquired honestly. */
|
|
173
|
-
export type DiaristIssueSourceReason =
|
|
174
|
-
| "origin-unresolved"
|
|
175
|
-
| "issue-unavailable"
|
|
176
|
-
| "issue-not-json"
|
|
177
|
-
| "issue-not-object"
|
|
178
|
-
| "issue-is-pull-request"
|
|
179
|
-
| "issue-body-invalid"
|
|
180
|
-
| "comments-failed";
|
|
181
|
-
|
|
182
|
-
/** Bound-ticket issue source failure — not a soft degrade to empty face. */
|
|
183
|
-
export class DiaristIssueSourceError extends Error {
|
|
184
|
-
readonly code = "diarist-issue-source" as const;
|
|
185
|
-
readonly reason: DiaristIssueSourceReason;
|
|
186
|
-
constructor(
|
|
187
|
-
reason: DiaristIssueSourceReason,
|
|
188
|
-
message: string,
|
|
189
|
-
options?: ErrorOptions,
|
|
190
|
-
) {
|
|
191
|
-
super(message, options);
|
|
192
|
-
this.name = "DiaristIssueSourceError";
|
|
193
|
-
this.reason = reason;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Issue-face fetch for the diarist seat. Unavailability is a typed
|
|
199
|
-
* DiaristIssueSourceError — there is no soft-undefined face.
|
|
200
|
-
*/
|
|
201
|
-
export type DiaristIssueFaceFetcher = (input: {
|
|
202
|
-
readonly owner: string;
|
|
203
|
-
readonly repo: string;
|
|
204
|
-
readonly ticketNumber: number;
|
|
205
|
-
readonly signal?: AbortSignal;
|
|
206
|
-
}) => Promise<DiaristIssueFace>;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Production issue-face capability over shared gh execution seams.
|
|
210
|
-
* Body fetch/parse is sole-owned by projectGhIssueBody; this maps hard disposition
|
|
211
|
-
* (typed DiaristIssueSourceError) instead of Reviewer Spec soft-undefined.
|
|
212
|
-
* Comment list failures after body success keep true cause under comments-failed.
|
|
213
|
-
*/
|
|
214
|
-
export function createDiaristIssueFaceFetcher(options?: {
|
|
215
|
-
readonly runner?: GhApiRunner;
|
|
216
|
-
readonly transport?: CollectorGitHubTransport;
|
|
217
|
-
}): DiaristIssueFaceFetcher {
|
|
218
|
-
const runner = options?.runner ?? createGhApiRunner();
|
|
219
|
-
const transport = options?.transport ?? createGhCollectorGitHubTransport();
|
|
220
|
-
return async (input) => {
|
|
221
|
-
const projected = await projectGhIssueBody(runner, input);
|
|
222
|
-
let body: string;
|
|
223
|
-
if (projected.status === "available") {
|
|
224
|
-
body = projected.body;
|
|
225
|
-
} else if (projected.status === "unavailable") {
|
|
226
|
-
if (projected.reason === "pull-request") {
|
|
227
|
-
throw new DiaristIssueSourceError(
|
|
228
|
-
"issue-is-pull-request",
|
|
229
|
-
`ticket #${input.ticketNumber} resolves to a pull request, not an issue face`,
|
|
230
|
-
projected.cause === undefined ? undefined : { cause: projected.cause },
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
throw new DiaristIssueSourceError(
|
|
234
|
-
"issue-unavailable",
|
|
235
|
-
`issue face unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`,
|
|
236
|
-
projected.cause === undefined ? undefined : { cause: projected.cause },
|
|
237
|
-
);
|
|
238
|
-
} else if (projected.reason === "not-json") {
|
|
239
|
-
throw new DiaristIssueSourceError(
|
|
240
|
-
"issue-not-json",
|
|
241
|
-
`issue face payload is not JSON for ${input.owner}/${input.repo}#${input.ticketNumber}`,
|
|
242
|
-
projected.cause === undefined ? undefined : { cause: projected.cause },
|
|
243
|
-
);
|
|
244
|
-
} else if (projected.reason === "not-object") {
|
|
245
|
-
throw new DiaristIssueSourceError(
|
|
246
|
-
"issue-not-object",
|
|
247
|
-
`issue face payload must be a JSON object for ${input.owner}/${input.repo}#${input.ticketNumber}`,
|
|
248
|
-
);
|
|
249
|
-
} else {
|
|
250
|
-
throw new DiaristIssueSourceError(
|
|
251
|
-
"issue-body-invalid",
|
|
252
|
-
`issue face body must be string or null for ${input.owner}/${input.repo}#${input.ticketNumber}`,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
let listed;
|
|
256
|
-
try {
|
|
257
|
-
listed = await transport.listIssueComments({
|
|
258
|
-
owner: input.owner,
|
|
259
|
-
repo: input.repo,
|
|
260
|
-
prNumber: input.ticketNumber,
|
|
261
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
262
|
-
});
|
|
263
|
-
} catch (error) {
|
|
264
|
-
throw new DiaristIssueSourceError(
|
|
265
|
-
"comments-failed",
|
|
266
|
-
`issue comments fetch failed for ${input.owner}/${input.repo}#${input.ticketNumber}`,
|
|
267
|
-
{ cause: error },
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
const bodyUrl = `https://github.com/${input.owner}/${input.repo}/issues/${input.ticketNumber}`;
|
|
271
|
-
return {
|
|
272
|
-
body,
|
|
273
|
-
bodyUrl,
|
|
274
|
-
comments: listed.items.map((c) => ({
|
|
275
|
-
id: c.id,
|
|
276
|
-
body: c.body,
|
|
277
|
-
createdAt: c.createdAt,
|
|
278
|
-
htmlUrl: c.htmlUrl,
|
|
279
|
-
})),
|
|
280
|
-
};
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** github.com owner/repo from project origin remote; undefined when absent/non-github. */
|
|
285
|
-
export function resolveDiaristGithubOrigin(
|
|
286
|
-
projectRoot: string,
|
|
287
|
-
): { readonly owner: string; readonly repo: string } | undefined {
|
|
288
|
-
let remoteUrl: string;
|
|
289
|
-
try {
|
|
290
|
-
remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
291
|
-
cwd: projectRoot,
|
|
292
|
-
encoding: "utf8",
|
|
293
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
294
|
-
}).trim();
|
|
295
|
-
} catch {
|
|
296
|
-
return undefined;
|
|
297
|
-
}
|
|
298
|
-
if (remoteUrl.length === 0) return undefined;
|
|
299
|
-
return parseGitHubOriginRemote(remoteUrl);
|
|
300
|
-
}
|
|
15
|
+
import { projectTicketProvenanceEntry } from "./ticket-provenance-contracts.ts";
|
|
16
|
+
import type { DiaristEntrySubmission } from "./diarist-contracts.ts";
|
|
301
17
|
|
|
302
18
|
/**
|
|
303
|
-
*
|
|
304
|
-
* the
|
|
19
|
+
* Honest machine facts about what this turn actually committed to the volume.
|
|
20
|
+
* Populated by the envelope accept hook — never from model self-report.
|
|
305
21
|
*/
|
|
306
|
-
export async function loadDiaristIssueFace(input: {
|
|
307
|
-
readonly ticketNumber: number;
|
|
308
|
-
readonly projectRoot: string;
|
|
309
|
-
readonly home?: string;
|
|
310
|
-
readonly fetcher?: DiaristIssueFaceFetcher;
|
|
311
|
-
}): Promise<DiaristIssueFace> {
|
|
312
|
-
const persistAndThrow = (error: DiaristIssueSourceError): DiaristIssueSourceError => {
|
|
313
|
-
appendIssueSourceFailureDiagnostic({
|
|
314
|
-
ticketNumber: input.ticketNumber,
|
|
315
|
-
cwd: input.projectRoot,
|
|
316
|
-
...(input.home === undefined ? {} : { home: input.home }),
|
|
317
|
-
cause: error.message,
|
|
318
|
-
reason: error.reason,
|
|
319
|
-
});
|
|
320
|
-
return error;
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const origin = resolveDiaristGithubOrigin(input.projectRoot);
|
|
324
|
-
if (origin === undefined) {
|
|
325
|
-
throw persistAndThrow(
|
|
326
|
-
new DiaristIssueSourceError(
|
|
327
|
-
"origin-unresolved",
|
|
328
|
-
`bound ticket #${input.ticketNumber} issue face requires a resolvable github.com origin remote`,
|
|
329
|
-
),
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const fetcher = input.fetcher ?? createDiaristIssueFaceFetcher();
|
|
334
|
-
try {
|
|
335
|
-
return await fetcher({
|
|
336
|
-
owner: origin.owner,
|
|
337
|
-
repo: origin.repo,
|
|
338
|
-
ticketNumber: input.ticketNumber,
|
|
339
|
-
});
|
|
340
|
-
} catch (error) {
|
|
341
|
-
throw persistAndThrow(
|
|
342
|
-
error instanceof DiaristIssueSourceError
|
|
343
|
-
? error
|
|
344
|
-
: new DiaristIssueSourceError(
|
|
345
|
-
"issue-unavailable",
|
|
346
|
-
`issue face fetch failed for ${origin.owner}/${origin.repo}#${input.ticketNumber}`,
|
|
347
|
-
{ cause: error },
|
|
348
|
-
),
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** One frozen candidate the diarist turn may select by index. */
|
|
354
|
-
export type DiaristSourceCandidate = DiaristSourceBlock & {
|
|
355
|
-
readonly candidateIndex: number;
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Frozen catalog handed to the diarist turn and re-read by the envelope at
|
|
360
|
-
* accept time. Carries summons text + run coordinates so accept can verify an
|
|
361
|
-
* LLM ticket assertion (ADR 0075) without re-deriving ambient state.
|
|
362
|
-
* ticketNumber absent = first summons still open for LLM assertion.
|
|
363
|
-
*/
|
|
364
|
-
export type DiaristSourceCatalog = {
|
|
365
|
-
/** Bound before turn when book already records the ticket; else LLM asserts. */
|
|
366
|
-
readonly ticketNumber?: number;
|
|
367
|
-
/** Caller summons — sole text mechanical verify reads for complete-decimal check. */
|
|
368
|
-
readonly instruction: string;
|
|
369
|
-
/** Run dossier pages accept binds after a verified first-summons assertion. */
|
|
370
|
-
readonly runDirectory: string;
|
|
371
|
-
readonly projectRoot: string;
|
|
372
|
-
readonly cwd: string;
|
|
373
|
-
readonly home?: string;
|
|
374
|
-
readonly candidates: readonly DiaristSourceCandidate[];
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Identities already processed for this ticket:
|
|
379
|
-
* - volume record identities (selected / verify-fail residue)
|
|
380
|
-
* - offered watermark (blocks shown to the diarist, selected or not)
|
|
381
|
-
*/
|
|
382
|
-
async function loadSeenEntryIdentities(
|
|
383
|
-
ticketNumber: number,
|
|
384
|
-
cwd: string,
|
|
385
|
-
home?: string,
|
|
386
|
-
): Promise<ReadonlySet<string>> {
|
|
387
|
-
const { recordFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
|
|
388
|
-
const { records } = await readSitianRecords(recordFile);
|
|
389
|
-
const seen = new Set<string>();
|
|
390
|
-
for (const record of records) {
|
|
391
|
-
if (typeof record.identity === "string" && record.identity.length > 0) {
|
|
392
|
-
seen.add(record.identity);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
for (const identity of readOfferedIdentities(ticketNumber, cwd, home)) {
|
|
396
|
-
seen.add(identity);
|
|
397
|
-
}
|
|
398
|
-
return seen;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function blockEntryIdentity(
|
|
402
|
-
ticketNumber: number,
|
|
403
|
-
block: DiaristSourceBlock,
|
|
404
|
-
): string {
|
|
405
|
-
return ticketProvenanceEntryIdentity({
|
|
406
|
-
ticketNumber,
|
|
407
|
-
sourceKind: block.sourceKind,
|
|
408
|
-
sourceRef: block.sourceRef,
|
|
409
|
-
transcript: block.transcript,
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Single typed candidate stream: cc sessions + GitHub issue face/comments/decree + referenced ADRs.
|
|
415
|
-
* Mechanical layer does not prose-filter for relevance (锚定宪法).
|
|
416
|
-
* Attachments are never merged in as fake issue-body-comment.
|
|
417
|
-
*/
|
|
418
|
-
function loadSourceBlocks(input: {
|
|
419
|
-
readonly cwd: string;
|
|
420
|
-
readonly issueFace?: DiaristIssueFace;
|
|
421
|
-
readonly sessionCwds?: readonly string[];
|
|
422
|
-
}): DiaristSourceBlock[] {
|
|
423
|
-
const cwds = input.sessionCwds ?? [input.cwd];
|
|
424
|
-
const blocks: DiaristSourceBlock[] = [...readCcSessionBlocks({ cwds })];
|
|
425
|
-
if (input.issueFace !== undefined) {
|
|
426
|
-
blocks.push(...readIssueFaceBlocks({ face: input.issueFace }));
|
|
427
|
-
const faceText = [
|
|
428
|
-
input.issueFace.body,
|
|
429
|
-
...input.issueFace.comments.map((c) => c.body),
|
|
430
|
-
].join("\n");
|
|
431
|
-
const adrPaths = extractReferencedAdrPaths(faceText);
|
|
432
|
-
if (adrPaths.length > 0) {
|
|
433
|
-
blocks.push(
|
|
434
|
-
...readAdrDecisionKeyBlocks({
|
|
435
|
-
cwd: input.cwd,
|
|
436
|
-
adrPaths,
|
|
437
|
-
}),
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return blocks;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
export type PrepareDiaristSourceCatalogInput = {
|
|
445
|
-
/** Bound ticket when known before the turn; omit on first-summons identity round. */
|
|
446
|
-
readonly ticketNumber?: number;
|
|
447
|
-
readonly instruction: string;
|
|
448
|
-
readonly runDirectory: string;
|
|
449
|
-
readonly projectRoot: string;
|
|
450
|
-
readonly cwd: string;
|
|
451
|
-
/** Explicit package home (admitted run / tests); never process.env.HOME (#604). */
|
|
452
|
-
readonly home?: string;
|
|
453
|
-
/** Frozen GitHub issue face (body + comments) from the shared gh seam. */
|
|
454
|
-
readonly issueFace?: DiaristIssueFace;
|
|
455
|
-
/** Extra cwd roots whose cc project folders are scanned. */
|
|
456
|
-
readonly sessionCwds?: readonly string[];
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Mechanical half A — source enumeration into a frozen catalog.
|
|
461
|
-
* When ticketNumber is known: establishes the per-ticket volume + human view
|
|
462
|
-
* (ADR 0075 `ticket-provenance-file` 每票一份起居录) and offers only blocks not
|
|
463
|
-
* yet on the volume / offered watermark (增量幂等).
|
|
464
|
-
* When ticketNumber is absent (first summons): offers session candidates without
|
|
465
|
-
* minting a volume — the LLM turn asserts identity; accept verifies and mints.
|
|
466
|
-
*/
|
|
467
|
-
export async function prepareDiaristSourceCatalog(
|
|
468
|
-
input: PrepareDiaristSourceCatalogInput,
|
|
469
|
-
): Promise<DiaristSourceCatalog> {
|
|
470
|
-
const known = input.ticketNumber;
|
|
471
|
-
if (known !== undefined) {
|
|
472
|
-
ensureTicketProvenanceVolume(known, input.cwd, input.home);
|
|
473
|
-
const volume = await readTicketProvenance(known, input.cwd, input.home);
|
|
474
|
-
writeTicketProvenanceHumanView({
|
|
475
|
-
ticketNumber: known,
|
|
476
|
-
cwd: input.cwd,
|
|
477
|
-
...(input.home === undefined ? {} : { home: input.home }),
|
|
478
|
-
entries: volume.entries,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const rawBlocks = loadSourceBlocks(input);
|
|
483
|
-
// Safeguard only (notify filter + dedupe) — never prose-based exclusion.
|
|
484
|
-
const safeguarded = mechanicalSafeguardPipeline(rawBlocks);
|
|
485
|
-
const seen =
|
|
486
|
-
known === undefined
|
|
487
|
-
? new Set<string>()
|
|
488
|
-
: await loadSeenEntryIdentities(known, input.cwd, input.home);
|
|
489
|
-
const fresh = safeguarded.filter(
|
|
490
|
-
(block) =>
|
|
491
|
-
known === undefined || !seen.has(blockEntryIdentity(known, block)),
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
return {
|
|
495
|
-
...(known === undefined ? {} : { ticketNumber: known }),
|
|
496
|
-
instruction: input.instruction,
|
|
497
|
-
runDirectory: input.runDirectory,
|
|
498
|
-
projectRoot: input.projectRoot,
|
|
499
|
-
cwd: input.cwd,
|
|
500
|
-
...(input.home === undefined ? {} : { home: input.home }),
|
|
501
|
-
candidates: fresh.map((block, candidateIndex) => ({ ...block, candidateIndex })),
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function serializeDiaristSourceCatalog(catalog: DiaristSourceCatalog): string {
|
|
506
|
-
return JSON.stringify(catalog);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** Read a frozen catalog written by the seat. Unreadable/malformed fails loudly. */
|
|
510
|
-
export function loadDiaristSourceCatalog(path: string): DiaristSourceCatalog {
|
|
511
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
512
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
513
|
-
throw new Error(`diarist source catalog is not an object (${path})`);
|
|
514
|
-
}
|
|
515
|
-
const record = parsed as Record<string, unknown>;
|
|
516
|
-
if (typeof record.cwd !== "string") {
|
|
517
|
-
throw new Error(`diarist source catalog is missing cwd (${path})`);
|
|
518
|
-
}
|
|
519
|
-
if (typeof record.instruction !== "string") {
|
|
520
|
-
throw new Error(`diarist source catalog is missing instruction (${path})`);
|
|
521
|
-
}
|
|
522
|
-
if (typeof record.runDirectory !== "string") {
|
|
523
|
-
throw new Error(`diarist source catalog is missing runDirectory (${path})`);
|
|
524
|
-
}
|
|
525
|
-
if (typeof record.projectRoot !== "string") {
|
|
526
|
-
throw new Error(`diarist source catalog is missing projectRoot (${path})`);
|
|
527
|
-
}
|
|
528
|
-
if (
|
|
529
|
-
record.ticketNumber !== undefined &&
|
|
530
|
-
(typeof record.ticketNumber !== "number" ||
|
|
531
|
-
!Number.isSafeInteger(record.ticketNumber) ||
|
|
532
|
-
record.ticketNumber < 1)
|
|
533
|
-
) {
|
|
534
|
-
throw new Error(`diarist source catalog ticketNumber is not a safe ticket (${path})`);
|
|
535
|
-
}
|
|
536
|
-
if (!Array.isArray(record.candidates)) {
|
|
537
|
-
throw new Error(`diarist source catalog is missing candidates (${path})`);
|
|
538
|
-
}
|
|
539
|
-
return parsed as DiaristSourceCatalog;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* 失败真因 — typed causes for submitted rows that never reached the volume.
|
|
544
|
-
* Names the cause only; the per-row detail stays on the volume diagnostic.
|
|
545
|
-
*/
|
|
546
|
-
export type DiaristFailureCause =
|
|
547
|
-
/** Row pointed at no candidate in this turn's frozen catalog. */
|
|
548
|
-
| "unknown-candidate"
|
|
549
|
-
/** Row's quotes failed verbatim reverse-verify. */
|
|
550
|
-
| "quote-verify-rejected";
|
|
551
|
-
|
|
552
|
-
/** Honest machine facts about what this turn actually committed to the volume. */
|
|
553
22
|
export type DiaristCommitFacts = {
|
|
554
23
|
readonly ticketNumber: number;
|
|
555
|
-
/**
|
|
556
|
-
readonly offered: number;
|
|
557
|
-
/** New entries this turn actually put on the volume (entry-count delta). */
|
|
24
|
+
/** New body entries this turn actually put on the volume (entry-count delta). */
|
|
558
25
|
readonly appended: number;
|
|
559
|
-
/**
|
|
560
|
-
readonly
|
|
561
|
-
/** Offered identities newly written to the watermark this turn. */
|
|
562
|
-
readonly watermarked: number;
|
|
26
|
+
/** Submitted rows that could not form a lawful volume entry (shape only). */
|
|
27
|
+
readonly dropped: number;
|
|
563
28
|
readonly volumeRecordFile: string;
|
|
564
29
|
readonly humanViewFile: string;
|
|
565
30
|
/**
|
|
566
|
-
* What this turn's collection amounted to.
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
* different events. Why rows were dropped is `failureCauses`.
|
|
31
|
+
* What this turn's collection amounted to.
|
|
32
|
+
* `empty-selection` = LLM submitted no rows; `nothing-appended` = every row
|
|
33
|
+
* dropped at the write seam; both differ from a successful append.
|
|
570
34
|
*/
|
|
571
|
-
readonly collectorStatus:
|
|
572
|
-
| "ok"
|
|
573
|
-
| "empty-selection"
|
|
574
|
-
| "nothing-appended"
|
|
575
|
-
| "skipped-no-fresh";
|
|
576
|
-
/**
|
|
577
|
-
* 失败真因: empty when every submitted row landed. Populated whenever rows
|
|
578
|
-
* were dropped — including a partial turn whose collectorStatus is `ok`.
|
|
579
|
-
*/
|
|
580
|
-
readonly failureCauses: readonly DiaristFailureCause[];
|
|
35
|
+
readonly collectorStatus: "ok" | "empty-selection" | "nothing-appended";
|
|
581
36
|
};
|
|
582
37
|
|
|
583
38
|
/**
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
* never bounces the receipt (第 0 条) and never enters the volume as an entry.
|
|
39
|
+
* Append LLM-submitted whole blocks under a bound ticket.
|
|
40
|
+
* Entry identity keeps the volume idempotent across re-summons.
|
|
41
|
+
* Shape projection is write-seam self-check only — not content judgment (#779).
|
|
588
42
|
*/
|
|
589
|
-
export async function
|
|
590
|
-
readonly
|
|
591
|
-
readonly
|
|
43
|
+
export async function commitDiaristEntries(input: {
|
|
44
|
+
readonly ticketNumber: number;
|
|
45
|
+
readonly cwd: string;
|
|
46
|
+
readonly home?: string;
|
|
47
|
+
readonly entries: readonly DiaristEntrySubmission[];
|
|
592
48
|
}): Promise<DiaristCommitFacts> {
|
|
593
|
-
const ticketNumber = input
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
for (const selection of input.selections) {
|
|
613
|
-
const block = input.catalog.candidates[selection.candidateIndex];
|
|
614
|
-
if (block === undefined) {
|
|
615
|
-
unknownCandidate = true;
|
|
616
|
-
continue;
|
|
617
|
-
}
|
|
618
|
-
const projected = blockToLlmEntry(block, {
|
|
619
|
-
anchors,
|
|
620
|
-
quotes: selection.quotes,
|
|
621
|
-
...(selection.note === undefined ? {} : { note: selection.note }),
|
|
49
|
+
const { ticketNumber, cwd } = input;
|
|
50
|
+
const homeOpt = input.home === undefined ? {} : { home: input.home };
|
|
51
|
+
const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.home);
|
|
52
|
+
|
|
53
|
+
const before = await readTicketProvenance(ticketNumber, cwd, input.home);
|
|
54
|
+
let dropped = 0;
|
|
55
|
+
|
|
56
|
+
for (const submitted of input.entries) {
|
|
57
|
+
const entry = projectTicketProvenanceEntry({
|
|
58
|
+
basis: {
|
|
59
|
+
method: "llm-semantic",
|
|
60
|
+
anchors: [`#${ticketNumber}`],
|
|
61
|
+
...(submitted.note === undefined ? {} : { note: submitted.note }),
|
|
62
|
+
},
|
|
63
|
+
sourceKind: submitted.sourceKind,
|
|
64
|
+
sourceRef: submitted.sourceRef,
|
|
65
|
+
transcript: submitted.transcript,
|
|
66
|
+
timestamp: submitted.timestamp,
|
|
622
67
|
});
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
// Single diagnostic expression — never a disguised diary entry.
|
|
626
|
-
appendQuoteVerifyFailureDiagnostic({
|
|
627
|
-
ticketNumber,
|
|
628
|
-
cwd,
|
|
629
|
-
...homeOpt,
|
|
630
|
-
cause: projected.cause,
|
|
631
|
-
});
|
|
68
|
+
if (entry === undefined) {
|
|
69
|
+
dropped += 1;
|
|
632
70
|
continue;
|
|
633
71
|
}
|
|
634
72
|
appendTicketProvenanceEntry({
|
|
635
73
|
ticketNumber,
|
|
636
74
|
cwd,
|
|
637
75
|
...homeOpt,
|
|
638
|
-
entry
|
|
76
|
+
entry,
|
|
639
77
|
source: "diarist",
|
|
640
78
|
});
|
|
641
|
-
acceptedRows += 1;
|
|
642
79
|
}
|
|
643
80
|
|
|
644
|
-
|
|
645
|
-
// mid-commit retries the batch on the next summons. Entry identity and
|
|
646
|
-
// quote-verify diagnostic identity are stable — retry duplicates neither.
|
|
647
|
-
const identities = input.catalog.candidates.map((block) =>
|
|
648
|
-
blockEntryIdentity(ticketNumber, block),
|
|
649
|
-
);
|
|
650
|
-
const alreadyWatermarked = readOfferedIdentities(ticketNumber, cwd, input.catalog.home);
|
|
651
|
-
const watermarked = identities.filter((identity) => !alreadyWatermarked.has(identity)).length;
|
|
652
|
-
recordOfferedIdentities({ ticketNumber, cwd, ...homeOpt, identities });
|
|
653
|
-
|
|
654
|
-
// Refresh the human view from the full volume (includes prior summons).
|
|
655
|
-
const volume = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
|
|
81
|
+
const volume = await readTicketProvenance(ticketNumber, cwd, input.home);
|
|
656
82
|
const humanViewFile = writeTicketProvenanceHumanView({
|
|
657
83
|
ticketNumber,
|
|
658
84
|
cwd,
|
|
@@ -660,25 +86,18 @@ export async function commitDiaristSelections(input: {
|
|
|
660
86
|
entries: volume.entries,
|
|
661
87
|
});
|
|
662
88
|
|
|
89
|
+
const appended = volume.entries.length - before.entries.length;
|
|
663
90
|
return {
|
|
664
91
|
ticketNumber,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
rejectedQuotes,
|
|
668
|
-
watermarked,
|
|
92
|
+
appended,
|
|
93
|
+
dropped,
|
|
669
94
|
volumeRecordFile: volumePaths.recordFile,
|
|
670
95
|
humanViewFile,
|
|
671
96
|
collectorStatus:
|
|
672
|
-
|
|
673
|
-
? "
|
|
674
|
-
:
|
|
675
|
-
? "
|
|
676
|
-
:
|
|
677
|
-
? "empty-selection"
|
|
678
|
-
: "nothing-appended",
|
|
679
|
-
failureCauses: [
|
|
680
|
-
...(unknownCandidate ? (["unknown-candidate"] as const) : []),
|
|
681
|
-
...(rejectedQuotes > 0 ? (["quote-verify-rejected"] as const) : []),
|
|
682
|
-
],
|
|
97
|
+
appended > 0
|
|
98
|
+
? "ok"
|
|
99
|
+
: input.entries.length === 0
|
|
100
|
+
? "empty-selection"
|
|
101
|
+
: "nothing-appended",
|
|
683
102
|
};
|
|
684
103
|
}
|