@akagilnc/pi-workflow-roles 0.1.4929 → 0.1.4949
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/dist/acp-host/production-host.js +9180 -9080
- package/dist/headless-host/production-host.js +9178 -9078
- package/dist/migrate-book-topology.js +74 -27
- package/dist/public-cli/countersign-run.js +4 -0
- package/dist/public-cli/instruction-seat-run.js +18 -2
- package/dist/public-cli/invocation.js +40 -3
- package/dist/public-cli/main.js +567 -467
- package/dist/sitian-appender.js +38 -1
- package/dist/ticket-provenance-contracts.js +1 -1
- package/dist/ticket-provenance.js +28 -10
- package/package.json +1 -1
- package/souls/coder-output-guide.md +1 -0
- package/souls/fixer-output-guide.md +1 -0
- package/souls/quality-law.md +2 -0
- package/src/book-topology-record-class-migrators.ts +1 -1
- package/src/diarist-contracts.ts +1 -1
- package/src/diarist.ts +4 -2
- package/src/public-cli/countersign-run.ts +5 -0
- package/src/public-cli/instruction-seat-run.ts +16 -1
- package/src/public-cli/invocation.ts +44 -3
- package/src/role-runtime.ts +23 -22
- package/src/sitian-appender.ts +39 -1
- package/src/sitian-contracts.ts +2 -0
- package/src/ticket-provenance-contracts.ts +2 -2
- package/src/ticket-provenance.ts +36 -10
package/dist/sitian-appender.js
CHANGED
|
@@ -39,6 +39,36 @@ function sealTornTail(recordFile) {
|
|
|
39
39
|
appendFileSync(recordFile, "\n", "utf8");
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
function appendSitianRecordBlock(input, block) {
|
|
43
|
+
if (block === "") return;
|
|
44
|
+
try {
|
|
45
|
+
const { sessionDir, recordFile, ledgerHome } = resolveSitianRecordPath(input);
|
|
46
|
+
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
47
|
+
appendFileSync(recordFile, "", "utf8");
|
|
48
|
+
sealTornTail(recordFile);
|
|
49
|
+
const identityOf = (line) => {
|
|
50
|
+
try {
|
|
51
|
+
const row = JSON.parse(line);
|
|
52
|
+
return isRecord(row) && typeof row.identity === "string" ? row.identity : void 0;
|
|
53
|
+
} catch {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const identities = new Set(readFileSync(recordFile, "utf8").split("\n").map(identityOf).filter((id) => id !== void 0));
|
|
58
|
+
for (const line of block.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
59
|
+
const identity = identityOf(line);
|
|
60
|
+
if (identity !== void 0 && identities.has(identity)) continue;
|
|
61
|
+
appendFileSync(recordFile, line, "utf8");
|
|
62
|
+
if (identity !== void 0) identities.add(identity);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof SitianInfrastructureError) throw error;
|
|
66
|
+
throw new SitianInfrastructureError(
|
|
67
|
+
`Sitian appender persistence failure: ${errorText(error)}`,
|
|
68
|
+
{ cause: error }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
42
72
|
function findIdentityPointer(recordFile, identity, kind, level) {
|
|
43
73
|
if (!existsSync(recordFile)) return void 0;
|
|
44
74
|
const text = readFileSync(recordFile, "utf8");
|
|
@@ -170,6 +200,9 @@ function resolveSitianRecordPathInLedger(input, ledgerHome) {
|
|
|
170
200
|
);
|
|
171
201
|
sessionDir = paths.sessionDir;
|
|
172
202
|
recordFile = paths.recordFile;
|
|
203
|
+
} else if (category === "ticket-provenance" && input.runDirectory !== void 0) {
|
|
204
|
+
sessionDir = input.runDirectory;
|
|
205
|
+
recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
|
|
173
206
|
} else {
|
|
174
207
|
if (input.sessionParent === void 0 || input.sessionParent.length === 0 || !physicallyContainedIn(ledgerHome, input.sessionParent)) {
|
|
175
208
|
throw new Error("Sitian record ownership requires a parent session inside the ledger home");
|
|
@@ -177,6 +210,9 @@ function resolveSitianRecordPathInLedger(input, ledgerHome) {
|
|
|
177
210
|
sessionDir = join(dirname(input.sessionParent), category);
|
|
178
211
|
recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
|
|
179
212
|
}
|
|
213
|
+
if (!physicallyContainedIn(ledgerHome, sessionDir)) {
|
|
214
|
+
throw new Error("Sitian record ownership requires a directory inside the ledger home");
|
|
215
|
+
}
|
|
180
216
|
return { sessionDir, recordFile, ledgerHome };
|
|
181
217
|
}
|
|
182
218
|
function projectTicketRecordsPathShape() {
|
|
@@ -188,7 +224,7 @@ function projectTicketRecordsPathShape() {
|
|
|
188
224
|
return recordFile.replace(/\\/g, "/");
|
|
189
225
|
}
|
|
190
226
|
function resolveSitianRecordPath(input) {
|
|
191
|
-
const ledgerHome = input.home !== void 0 && input.home.length > 0 ? resolveActivationLedgerHome(input.home) : resolveActivationLedgerHomeForPath(input.sessionParent);
|
|
227
|
+
const ledgerHome = input.home !== void 0 && input.home.length > 0 ? resolveActivationLedgerHome(input.home) : resolveActivationLedgerHomeForPath(input.runDirectory ?? input.sessionParent);
|
|
192
228
|
return resolveSitianRecordPathInLedger(input, ledgerHome);
|
|
193
229
|
}
|
|
194
230
|
function appendSitianRecord(input) {
|
|
@@ -226,6 +262,7 @@ function appendSitianRecord(input) {
|
|
|
226
262
|
export {
|
|
227
263
|
S4_SUBMISSION_LEDGER_KINDS,
|
|
228
264
|
appendSitianRecord,
|
|
265
|
+
appendSitianRecordBlock,
|
|
229
266
|
projectTicketRecordsPathShape,
|
|
230
267
|
resolveSitianRecordPath,
|
|
231
268
|
resolveSitianRecordPathInLedger,
|
|
@@ -82,7 +82,7 @@ export function projectTicketProvenanceSessions(value) {
|
|
|
82
82
|
export function projectTicketProvenanceHeader(value) {
|
|
83
83
|
if (!isRecord(value))
|
|
84
84
|
return undefined;
|
|
85
|
-
const ticket = positiveInteger(value.ticket);
|
|
85
|
+
const ticket = value.ticket === null ? null : positiveInteger(value.ticket);
|
|
86
86
|
if (ticket === undefined)
|
|
87
87
|
return undefined;
|
|
88
88
|
if (typeof value.repo !== "string")
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { appendFileSync } from "node:fs";
|
|
9
|
-
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
10
10
|
import { basename, dirname, join, resolve } from "node:path";
|
|
11
11
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
12
12
|
import { ensureRealDirectoryTree, errnoCode, packageMachineHome, physicalPathIdentity, physicallyContainedIn, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
|
|
13
13
|
import { readLedgerSessionJsonlLines, } from "./ledger-session-read.js";
|
|
14
14
|
import { isSafePositiveTicketNumber } from "./run-ticket-number.js";
|
|
15
15
|
import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.js";
|
|
16
|
-
import { appendSitianRecord, resolveSitianRecordPath, } from "./sitian-facade.js";
|
|
16
|
+
import { appendSitianRecordBlock, appendSitianRecord, resolveSitianRecordPath, } from "./sitian-facade.js";
|
|
17
17
|
import { TICKET_PROVENANCE_KIND, projectTicketProvenanceHeader, projectTicketProvenanceLine, projectTicketProvenanceSessions, } from "./ticket-provenance-contracts.js";
|
|
18
18
|
/**
|
|
19
19
|
* Typed input failure for diarist bounds/session path (reask, not infrastructure).
|
|
@@ -73,11 +73,12 @@ export function ticketProvenanceSubject(ticketNumber) {
|
|
|
73
73
|
return String(ticketNumber);
|
|
74
74
|
}
|
|
75
75
|
/** Topology input only — no destination parameter (ADR 0065 record-entry). */
|
|
76
|
-
function ticketProvenanceRecordInput(ticketNumber, cwd, home) {
|
|
76
|
+
function ticketProvenanceRecordInput(ticketNumber, cwd, home, runDirectory) {
|
|
77
77
|
return {
|
|
78
78
|
level: "event",
|
|
79
79
|
kind: TICKET_PROVENANCE_KIND,
|
|
80
|
-
subject: ticketProvenanceSubject(ticketNumber),
|
|
80
|
+
...(ticketNumber === null ? {} : { subject: ticketProvenanceSubject(ticketNumber) }),
|
|
81
|
+
...(runDirectory === undefined ? {} : { runDirectory }),
|
|
81
82
|
cwd,
|
|
82
83
|
...(home === undefined ? {} : { home }),
|
|
83
84
|
};
|
|
@@ -140,8 +141,10 @@ function projectTicketProvenanceCommit(value) {
|
|
|
140
141
|
return { timestamp: record.timestamp, sessions, lines };
|
|
141
142
|
}
|
|
142
143
|
/** Read the unique diary file (empty/absent → no header, no lines). */
|
|
143
|
-
export async function readTicketProvenance(ticketNumber, cwd, home) {
|
|
144
|
-
const { recordFile } =
|
|
144
|
+
export async function readTicketProvenance(ticketNumber, cwd, home, runDirectory) {
|
|
145
|
+
const { recordFile } = ticketNumber === null
|
|
146
|
+
? resolveSitianRecordPath(ticketProvenanceRecordInput(null, cwd, home, runDirectory))
|
|
147
|
+
: resolveTicketProvenanceVolume(ticketNumber, cwd, home);
|
|
145
148
|
let text;
|
|
146
149
|
try {
|
|
147
150
|
text = await readFile(recordFile, "utf8");
|
|
@@ -252,7 +255,7 @@ export async function readTicketProvenance(ticketNumber, cwd, home) {
|
|
|
252
255
|
repo: header?.repo ?? resolveBookKeyFromGit(cwd),
|
|
253
256
|
ticket: ticketNumber,
|
|
254
257
|
createdAt: header?.createdAt ?? now,
|
|
255
|
-
updatedAt: now,
|
|
258
|
+
updatedAt: header?.updatedAt !== undefined && header.updatedAt > now ? header.updatedAt : now,
|
|
256
259
|
sessions: merged.sessions,
|
|
257
260
|
};
|
|
258
261
|
const carried = lines.map((entry) => ({
|
|
@@ -293,6 +296,21 @@ export function ensureTicketProvenanceVolume(ticketNumber, cwd, home) {
|
|
|
293
296
|
appendFileSync(path.recordFile, "", "utf8");
|
|
294
297
|
return { recordFile: path.recordFile, volumeDir: path.sessionDir };
|
|
295
298
|
}
|
|
299
|
+
/** Assign a run-owned unbound diary as its original block through Sitian. */
|
|
300
|
+
export async function rehomeUnboundTicketProvenance(runDirectory, ticketNumber, cwd, home) {
|
|
301
|
+
const source = join(runDirectory, "records.jsonl");
|
|
302
|
+
let content;
|
|
303
|
+
try {
|
|
304
|
+
content = await readFile(source, "utf8");
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (errnoCode(error) === "ENOENT")
|
|
308
|
+
return;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
appendSitianRecordBlock(ticketProvenanceRecordInput(ticketNumber, cwd, home), content);
|
|
312
|
+
await unlink(source);
|
|
313
|
+
}
|
|
296
314
|
/**
|
|
297
315
|
* Resolve a bound endpoint against this round's session lines.
|
|
298
316
|
* id → first physical line carrying that native id; line → that 1-based line.
|
|
@@ -583,7 +601,7 @@ async function projectSessionRanges(input) {
|
|
|
583
601
|
* Persistence appends one immutable commit through the Sitian appender seam.
|
|
584
602
|
*/
|
|
585
603
|
export async function reprojectTicketProvenance(input) {
|
|
586
|
-
const prior = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
|
|
604
|
+
const prior = await readTicketProvenance(input.ticketNumber, input.cwd, input.home, input.runDirectory);
|
|
587
605
|
if (input.sessions.length === 0) {
|
|
588
606
|
const now = new Date().toISOString();
|
|
589
607
|
return {
|
|
@@ -663,7 +681,7 @@ export async function reprojectTicketProvenance(input) {
|
|
|
663
681
|
});
|
|
664
682
|
const identity = `ticket-provenance:${createHash("sha256").update(identityMaterial).digest("hex")}`;
|
|
665
683
|
const pointer = appendSitianRecord({
|
|
666
|
-
...ticketProvenanceRecordInput(input.ticketNumber, input.cwd, input.home),
|
|
684
|
+
...ticketProvenanceRecordInput(input.ticketNumber, input.cwd, input.home, input.runDirectory),
|
|
667
685
|
identity,
|
|
668
686
|
payload: {
|
|
669
687
|
type: "ticket-provenance-append",
|
|
@@ -671,7 +689,7 @@ export async function reprojectTicketProvenance(input) {
|
|
|
671
689
|
lines: [...fresh, ...raw],
|
|
672
690
|
},
|
|
673
691
|
});
|
|
674
|
-
const folded = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
|
|
692
|
+
const folded = await readTicketProvenance(input.ticketNumber, input.cwd, input.home, input.runDirectory);
|
|
675
693
|
return {
|
|
676
694
|
recordFile: pointer.recordFile,
|
|
677
695
|
header: folded.header ?? header,
|
package/package.json
CHANGED
package/souls/quality-law.md
CHANGED
|
@@ -522,7 +522,7 @@ function tryBareTicketProvenanceVolume(
|
|
|
522
522
|
// Legacy SitianRecord rows carry a typed kind — leave them to line placement.
|
|
523
523
|
if (recordClassOfKind(first.value.kind) !== undefined) return undefined;
|
|
524
524
|
const header = projectTicketProvenanceHeader(first.value);
|
|
525
|
-
if (header === undefined) return undefined;
|
|
525
|
+
if (header === undefined || header.ticket === null) return undefined;
|
|
526
526
|
return { ticket: header.ticket, body };
|
|
527
527
|
}
|
|
528
528
|
|
package/src/diarist-contracts.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type DiaristOutput =
|
|
|
23
23
|
readonly status: "completed";
|
|
24
24
|
/**
|
|
25
25
|
* Typed court-target assertion (ADR 0075 `diarist-resolves-ticket-llm-layer`).
|
|
26
|
-
* Positive integer = 本庭对象=票N; null/absent = true-unbound (
|
|
26
|
+
* Positive integer = 本庭对象=票N; null/absent = true-unbound (先录入 unbound).
|
|
27
27
|
* LLM owns recognition; mechanical layer does not re-judge the number.
|
|
28
28
|
*/
|
|
29
29
|
readonly ticketNumber?: number | null;
|
package/src/diarist.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { TicketProvenanceSession } from "./ticket-provenance-contracts.ts";
|
|
|
12
12
|
* Populated by the envelope accept hook — never from model self-report.
|
|
13
13
|
*/
|
|
14
14
|
export type DiaristCommitFacts = {
|
|
15
|
-
readonly ticketNumber: number;
|
|
15
|
+
readonly ticketNumber: number | null;
|
|
16
16
|
readonly volumeRecordFile: string;
|
|
17
17
|
/** Dialogue lines written by this reproject. */
|
|
18
18
|
readonly lineCount: number;
|
|
@@ -24,9 +24,10 @@ export type DiaristCommitFacts = {
|
|
|
24
24
|
* this seam only copies source facts into the volume.
|
|
25
25
|
*/
|
|
26
26
|
export async function commitDiaristProjection(input: {
|
|
27
|
-
readonly ticketNumber: number;
|
|
27
|
+
readonly ticketNumber: number | null;
|
|
28
28
|
readonly cwd: string;
|
|
29
29
|
readonly home?: string;
|
|
30
|
+
readonly runDirectory?: string;
|
|
30
31
|
readonly sessions: readonly TicketProvenanceSession[];
|
|
31
32
|
}): Promise<DiaristCommitFacts> {
|
|
32
33
|
const result = await reprojectTicketProvenance({
|
|
@@ -34,6 +35,7 @@ export async function commitDiaristProjection(input: {
|
|
|
34
35
|
cwd: input.cwd,
|
|
35
36
|
...(input.home === undefined ? {} : { home: input.home }),
|
|
36
37
|
sessions: input.sessions,
|
|
38
|
+
...(input.runDirectory === undefined ? {} : { runDirectory: input.runDirectory }),
|
|
37
39
|
});
|
|
38
40
|
return {
|
|
39
41
|
ticketNumber: input.ticketNumber,
|
|
@@ -78,6 +78,7 @@ export type CourtDiaristIdentity =
|
|
|
78
78
|
|
|
79
79
|
export type CourtDiaristInvocationResult = {
|
|
80
80
|
readonly identity: CourtDiaristIdentity;
|
|
81
|
+
readonly admitted?: import("./invocation.ts").AdmittedRoleInvocation;
|
|
81
82
|
readonly failedWithoutEscalate?: { readonly diagnostic: string };
|
|
82
83
|
};
|
|
83
84
|
|
|
@@ -278,6 +279,7 @@ export async function invokeCourtDiarist(
|
|
|
278
279
|
kind: "escalate",
|
|
279
280
|
diagnostic: courtDiaristEscalateDiagnostic(roleOutcome, submissions),
|
|
280
281
|
},
|
|
282
|
+
...(result.admitted === undefined ? {} : { admitted: result.admitted }),
|
|
281
283
|
};
|
|
282
284
|
}
|
|
283
285
|
|
|
@@ -288,6 +290,7 @@ export async function invokeCourtDiarist(
|
|
|
288
290
|
: result.stderr?.trim() || `exit ${result.exitCode}`;
|
|
289
291
|
return {
|
|
290
292
|
identity: { kind: "unbound" },
|
|
293
|
+
...(result.admitted === undefined ? {} : { admitted: result.admitted }),
|
|
291
294
|
failedWithoutEscalate: {
|
|
292
295
|
diagnostic: `court diarist station failed for ${input.failureLabel}: ${diagnostic}`,
|
|
293
296
|
},
|
|
@@ -308,10 +311,12 @@ export async function invokeCourtDiarist(
|
|
|
308
311
|
ticketNumber: asserted,
|
|
309
312
|
...(courtTicketNumbers === undefined ? {} : { courtTicketNumbers }),
|
|
310
313
|
},
|
|
314
|
+
...(result.admitted === undefined ? {} : { admitted: result.admitted }),
|
|
311
315
|
};
|
|
312
316
|
}
|
|
313
317
|
return {
|
|
314
318
|
identity: { kind: "unbound" },
|
|
319
|
+
...(result.admitted === undefined ? {} : { admitted: result.admitted }),
|
|
315
320
|
};
|
|
316
321
|
}
|
|
317
322
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* starts two ordinary single-axis runs here. Countersign keeps deferred
|
|
5
5
|
* identity on this entry; the court diarist station stays in countersign-run.
|
|
6
6
|
*/
|
|
7
|
-
import { resolve } from "node:path";
|
|
7
|
+
import { resolve, sep } from "node:path";
|
|
8
8
|
|
|
9
9
|
import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
|
|
10
10
|
import { isSafePositiveTicketNumber, readBoardTicketNumber } from "../run-ticket-number.ts";
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
materializeCountersignInvocation,
|
|
33
33
|
persistAdmittedSourceRunPath,
|
|
34
34
|
recordAdmittedCorrelation,
|
|
35
|
+
recordChildDiaristRun,
|
|
35
36
|
relocateAdmittedRunToTicket,
|
|
36
37
|
withPreparedAttachments,
|
|
37
38
|
type AdmittedCountersignInvocation,
|
|
@@ -429,6 +430,7 @@ async function runCountersignBody(
|
|
|
429
430
|
let typedTicket: number | undefined;
|
|
430
431
|
let typedCourtTicketNumbers: readonly number[] | undefined;
|
|
431
432
|
let identityDiaristRan = false;
|
|
433
|
+
let unboundDiaristRunId: string | undefined;
|
|
432
434
|
|
|
433
435
|
if (env.runCourtDiaristStation === undefined) {
|
|
434
436
|
let outcome: Awaited<ReturnType<typeof invokeCourtDiarist>>;
|
|
@@ -452,6 +454,9 @@ async function runCountersignBody(
|
|
|
452
454
|
}, seatAdapters(admitted, env), env.principalAuthority, io) as SeatRunResult;
|
|
453
455
|
}
|
|
454
456
|
identityDiaristRan = true;
|
|
457
|
+
if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
|
|
458
|
+
unboundDiaristRunId = outcome.admitted.runId;
|
|
459
|
+
}
|
|
455
460
|
if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
|
|
456
461
|
const diagnostic = outcome.identity.kind === "escalate"
|
|
457
462
|
? outcome.identity.diagnostic
|
|
@@ -459,6 +464,7 @@ async function runCountersignBody(
|
|
|
459
464
|
await materializeAdmission(
|
|
460
465
|
isSafePositiveTicketNumber(env.boundTicketNumber) ? env.boundTicketNumber : undefined,
|
|
461
466
|
);
|
|
467
|
+
if (unboundDiaristRunId !== undefined) await recordChildDiaristRun(admitted, unboundDiaristRunId);
|
|
462
468
|
await markRunAdmitted(admitted, env.principalAuthority);
|
|
463
469
|
return await presentControlledFailure(admitted, {
|
|
464
470
|
timedOut: false,
|
|
@@ -479,6 +485,9 @@ async function runCountersignBody(
|
|
|
479
485
|
|
|
480
486
|
await materializeAdmission(typedTicket);
|
|
481
487
|
await markRunAdmitted(admitted, env.principalAuthority);
|
|
488
|
+
if (unboundDiaristRunId !== undefined) {
|
|
489
|
+
await recordChildDiaristRun(admitted, unboundDiaristRunId);
|
|
490
|
+
}
|
|
482
491
|
if (gateParentRunPath !== undefined) {
|
|
483
492
|
await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
|
|
484
493
|
admitted = { ...admitted, sourceRunPath: gateParentRunPath };
|
|
@@ -745,6 +754,9 @@ export async function runPublicInstructionSeat(
|
|
|
745
754
|
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
746
755
|
...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
|
|
747
756
|
}, io);
|
|
757
|
+
if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
|
|
758
|
+
await recordChildDiaristRun(admitted, outcome.admitted.runId);
|
|
759
|
+
}
|
|
748
760
|
if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
|
|
749
761
|
const diagnostic = outcome.identity.kind === "escalate"
|
|
750
762
|
? outcome.identity.diagnostic
|
|
@@ -758,6 +770,9 @@ export async function runPublicInstructionSeat(
|
|
|
758
770
|
}
|
|
759
771
|
if (outcome.identity.kind === "ticket") {
|
|
760
772
|
try {
|
|
773
|
+
if (outcome.admitted === undefined) throw new Error("court diarist ticket has no admitted run");
|
|
774
|
+
await bindAdmittedTicketNumber(outcome.admitted, outcome.identity.ticketNumber);
|
|
775
|
+
await relocateAdmittedRunToTicket(outcome.admitted, env.principalAuthority);
|
|
761
776
|
await bindAdmittedTicketNumber(admitted, outcome.identity.ticketNumber);
|
|
762
777
|
await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
|
|
763
778
|
} catch (error) {
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
type MergerInput,
|
|
84
84
|
} from "../merger-contracts.ts";
|
|
85
85
|
import { sha256Hex } from "../sha256.ts";
|
|
86
|
+
import { rehomeUnboundTicketProvenance } from "../ticket-provenance.ts";
|
|
86
87
|
import { uuidv7 } from "../uuidv7.ts";
|
|
87
88
|
import {
|
|
88
89
|
NOTARY_FIXED_KICKOFF,
|
|
@@ -661,9 +662,36 @@ export async function relocateAdmittedRunToTicket(
|
|
|
661
662
|
// Keep that failure before the filesystem commit point.
|
|
662
663
|
const principal = authority.seal(target);
|
|
663
664
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
665
|
+
if (admitted.role !== "diarist") {
|
|
666
|
+
const parentPage = JSON.parse(await readFile(admitted.admittedRequestPath, "utf8")) as Record<string, unknown>;
|
|
667
|
+
const childRunIds = parentPage.childDiaristRunIds;
|
|
668
|
+
for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
|
|
669
|
+
if (typeof childRunId !== "string") continue;
|
|
670
|
+
const childDirectory = join(dirname(oldRunDirectory), `${childRunId}@diarist`);
|
|
671
|
+
const childTarget = roleRunPlacement(ledgerHome, {
|
|
672
|
+
bookKey: admitted.bookKey,
|
|
673
|
+
subject: { ticketNumber: admitted.ticketNumber },
|
|
674
|
+
runId: childRunId,
|
|
675
|
+
role: "diarist",
|
|
676
|
+
});
|
|
677
|
+
authority.seal(childTarget);
|
|
678
|
+
if (!existsSync(childDirectory) && existsSync(childTarget.runDirectory)) continue;
|
|
679
|
+
await bindTicketNumberOnRunDirectory(childDirectory, admitted.ticketNumber);
|
|
680
|
+
await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
|
|
681
|
+
ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
|
|
682
|
+
await rename(childDirectory, childTarget.runDirectory);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// An identity diarist can file before its caller obtains a ticket. Both its
|
|
687
|
+
// own bind and the caller's later bind use this existing relocation seam.
|
|
688
|
+
if (admitted.role === "diarist") {
|
|
689
|
+
await rehomeUnboundTicketProvenance(oldRunDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Rename commits the run placement. Diary assignment above is an idempotent
|
|
693
|
+
// Sitian append, not part of an atomic transaction with this directory move.
|
|
694
|
+
// Persisted paths are resolved from typed run identity on read.
|
|
667
695
|
await rename(oldRunDirectory, target.runDirectory);
|
|
668
696
|
|
|
669
697
|
// rename moved the open lock inode with the directory. Transfer cleanup
|
|
@@ -697,6 +725,19 @@ export async function relocateAdmittedRunToTicket(
|
|
|
697
725
|
return { oldRunDirectory, newRunDirectory: target.runDirectory };
|
|
698
726
|
}
|
|
699
727
|
|
|
728
|
+
/** Persist the exact child identity; later parent binding never inspects siblings. */
|
|
729
|
+
export async function recordChildDiaristRun(
|
|
730
|
+
parent: AdmittedRoleInvocation,
|
|
731
|
+
childRunId: string,
|
|
732
|
+
): Promise<void> {
|
|
733
|
+
const page = JSON.parse(await readFile(parent.admittedRequestPath, "utf8")) as Record<string, unknown>;
|
|
734
|
+
const existing = Array.isArray(page.childDiaristRunIds)
|
|
735
|
+
? page.childDiaristRunIds.filter((runId): runId is string => typeof runId === "string")
|
|
736
|
+
: [];
|
|
737
|
+
const childDiaristRunIds = existing.includes(childRunId) ? existing : [...existing, childRunId];
|
|
738
|
+
await writeFile(parent.admittedRequestPath, `${JSON.stringify({ ...page, childDiaristRunIds }, null, 2)}\n`, "utf8");
|
|
739
|
+
}
|
|
740
|
+
|
|
700
741
|
/**
|
|
701
742
|
* Bind ticket identity onto a run directory's durable pages when the admitted
|
|
702
743
|
* object is not in hand (起居郎 accept hook after LLM assertion, #771).
|
package/src/role-runtime.ts
CHANGED
|
@@ -905,29 +905,30 @@ export function createDiaristRoleRuntime(
|
|
|
905
905
|
if (coords.boundTicketNumber === undefined) {
|
|
906
906
|
await bindTicketNumberOnRunDirectory(coords.runDirectory, ticketNumber);
|
|
907
907
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
908
|
+
}
|
|
909
|
+
const sessions = projectDiaristSessions(parameters);
|
|
910
|
+
if (sessions === undefined) {
|
|
911
|
+
throw new ParentQueueReaskError(DIARIST_BOUNDS_REASK);
|
|
912
|
+
}
|
|
913
|
+
let facts;
|
|
914
|
+
try {
|
|
915
|
+
facts = await commitDiaristProjection({
|
|
916
|
+
ticketNumber: ticketNumber ?? null,
|
|
917
|
+
cwd: coords.projectRoot,
|
|
918
|
+
home: coords.home,
|
|
919
|
+
runDirectory: coords.runDirectory,
|
|
920
|
+
sessions,
|
|
921
|
+
});
|
|
922
|
+
} catch (error) {
|
|
923
|
+
// Bound/session input failures → reask via typed identity (not message prefix).
|
|
924
|
+
// Unexpected infrastructure keeps its own identity (do not wash).
|
|
925
|
+
if (error instanceof ParentQueueReaskError) throw error;
|
|
926
|
+
if (error instanceof TicketProvenanceInputError) {
|
|
927
|
+
throw new ParentQueueReaskError(
|
|
928
|
+
`${DIARIST_BOUNDS_REASK}\n${error.message}`,
|
|
929
|
+
);
|
|
930
930
|
}
|
|
931
|
+
throw error;
|
|
931
932
|
}
|
|
932
933
|
return parameters;
|
|
933
934
|
},
|
package/src/sitian-appender.ts
CHANGED
|
@@ -59,6 +59,38 @@ function sealTornTail(recordFile: string): void {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Assign already-recorded lines, preserving their original text and malformed lines. */
|
|
63
|
+
export function appendSitianRecordBlock(input: SitianRecordInput, block: string): void {
|
|
64
|
+
if (block === "") return;
|
|
65
|
+
try {
|
|
66
|
+
const { sessionDir, recordFile, ledgerHome } = resolveSitianRecordPath(input);
|
|
67
|
+
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
68
|
+
appendFileSync(recordFile, "", "utf8");
|
|
69
|
+
sealTornTail(recordFile);
|
|
70
|
+
const identityOf = (line: string): string | undefined => {
|
|
71
|
+
try {
|
|
72
|
+
const row: unknown = JSON.parse(line);
|
|
73
|
+
return isRecord(row) && typeof row.identity === "string" ? row.identity : undefined;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const identities = new Set(readFileSync(recordFile, "utf8").split("\n").map(identityOf).filter((id): id is string => id !== undefined));
|
|
79
|
+
for (const line of block.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
80
|
+
const identity = identityOf(line);
|
|
81
|
+
if (identity !== undefined && identities.has(identity)) continue;
|
|
82
|
+
appendFileSync(recordFile, line, "utf8");
|
|
83
|
+
if (identity !== undefined) identities.add(identity);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof SitianInfrastructureError) throw error;
|
|
87
|
+
throw new SitianInfrastructureError(
|
|
88
|
+
`Sitian appender persistence failure: ${errorText(error)}`,
|
|
89
|
+
{ cause: error },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
function findIdentityPointer(
|
|
63
95
|
recordFile: string,
|
|
64
96
|
identity: string,
|
|
@@ -254,6 +286,9 @@ export function resolveSitianRecordPathInLedger(
|
|
|
254
286
|
);
|
|
255
287
|
sessionDir = paths.sessionDir;
|
|
256
288
|
recordFile = paths.recordFile;
|
|
289
|
+
} else if (category === "ticket-provenance" && input.runDirectory !== undefined) {
|
|
290
|
+
sessionDir = input.runDirectory;
|
|
291
|
+
recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
|
|
257
292
|
} else {
|
|
258
293
|
if (
|
|
259
294
|
input.sessionParent === undefined
|
|
@@ -265,6 +300,9 @@ export function resolveSitianRecordPathInLedger(
|
|
|
265
300
|
sessionDir = join(dirname(input.sessionParent), category);
|
|
266
301
|
recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
|
|
267
302
|
}
|
|
303
|
+
if (!physicallyContainedIn(ledgerHome, sessionDir)) {
|
|
304
|
+
throw new Error("Sitian record ownership requires a directory inside the ledger home");
|
|
305
|
+
}
|
|
268
306
|
|
|
269
307
|
return { sessionDir, recordFile, ledgerHome };
|
|
270
308
|
}
|
|
@@ -288,7 +326,7 @@ export function resolveSitianRecordPath(input: SitianRecordInput): SitianRecordP
|
|
|
288
326
|
const ledgerHome =
|
|
289
327
|
input.home !== undefined && input.home.length > 0
|
|
290
328
|
? resolveActivationLedgerHome(input.home)
|
|
291
|
-
: resolveActivationLedgerHomeForPath(input.sessionParent);
|
|
329
|
+
: resolveActivationLedgerHomeForPath(input.runDirectory ?? input.sessionParent);
|
|
292
330
|
return resolveSitianRecordPathInLedger(input, ledgerHome);
|
|
293
331
|
}
|
|
294
332
|
|
package/src/sitian-contracts.ts
CHANGED
|
@@ -63,6 +63,8 @@ export type SitianRecordInput = {
|
|
|
63
63
|
readonly identity?: string | undefined;
|
|
64
64
|
readonly subject?: SitianSubject | undefined;
|
|
65
65
|
readonly sessionParent?: string | undefined;
|
|
66
|
+
/** Run owner for ticket provenance before a ticket is known. */
|
|
67
|
+
readonly runDirectory?: string | undefined;
|
|
66
68
|
readonly priorEventId?: string | undefined;
|
|
67
69
|
readonly timestamp?: string | undefined;
|
|
68
70
|
readonly host?: string | undefined;
|
|
@@ -40,7 +40,7 @@ export type TicketProvenanceSession = {
|
|
|
40
40
|
/** 读取时折叠出的逻辑册子头。sessions 为累计区间;reopen 与跨宿主并入,不新建册。 */
|
|
41
41
|
export type TicketProvenanceHeader = {
|
|
42
42
|
readonly repo: string;
|
|
43
|
-
readonly ticket: number;
|
|
43
|
+
readonly ticket: number | null;
|
|
44
44
|
readonly createdAt: string;
|
|
45
45
|
readonly updatedAt: string;
|
|
46
46
|
readonly sessions: readonly TicketProvenanceSession[];
|
|
@@ -132,7 +132,7 @@ export function projectTicketProvenanceHeader(
|
|
|
132
132
|
value: unknown,
|
|
133
133
|
): TicketProvenanceHeader | undefined {
|
|
134
134
|
if (!isRecord(value)) return undefined;
|
|
135
|
-
const ticket = positiveInteger(value.ticket);
|
|
135
|
+
const ticket = value.ticket === null ? null : positiveInteger(value.ticket);
|
|
136
136
|
if (ticket === undefined) return undefined;
|
|
137
137
|
if (typeof value.repo !== "string") return undefined;
|
|
138
138
|
if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
|