@akagilnc/pi-workflow-roles 0.1.4204 → 0.1.4228
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 +1203 -770
- package/dist/atomic-write.js +23 -0
- package/dist/diarist-contracts.js +22 -8
- package/dist/headless-host/production-host.js +1191 -758
- package/dist/ledger-session-read.js +52 -21
- package/dist/migrate-book-topology.js +238 -205
- package/dist/public-cli/case-dossier-delivery.js +0 -1
- package/dist/public-cli/main.js +303 -261
- package/dist/session-dialogue.js +221 -0
- package/dist/sitian-facade.js +1 -0
- package/dist/sitian-volume.js +67 -0
- package/dist/ticket-provenance-contracts.js +122 -39
- package/dist/ticket-provenance.js +289 -204
- package/package.json +1 -1
- package/src/book-topology-migration.ts +0 -7
- package/src/book-topology-record-class-migrators.ts +203 -99
- package/src/diarist-contracts.ts +42 -29
- package/src/diarist-role.ts +77 -31
- package/src/diarist.ts +31 -67
- package/src/ledger-session-read.ts +70 -27
- package/src/package-contracts/reviewer-output.ts +1 -1
- package/src/public-cli/case-dossier-delivery.ts +0 -1
- package/src/reviewer-role.ts +3 -3
- package/src/role-runtime.ts +56 -14
- package/src/session-dialogue.ts +225 -0
- package/src/sitian-appender.ts +1 -1
- package/src/sitian-facade.ts +7 -1
- package/src/sitian-volume.ts +91 -0
- package/src/ticket-provenance-contracts.ts +158 -101
- package/src/ticket-provenance.ts +387 -263
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* #866 (T10): record-class partition migrators (build only; execution is #861).
|
|
3
3
|
*
|
|
4
4
|
* Line-closed placement by each row's typed kind (never by source directory alone):
|
|
5
|
-
* - ticket-provenance → <ticket>/
|
|
5
|
+
* - ticket-provenance → <ticket>/records.jsonl (live topology; human view cancelled #900)
|
|
6
6
|
* - submission-ledger kinds → owning run session/submission-ledger/
|
|
7
7
|
* - attempt-history → owning run session/attempt-history/
|
|
8
8
|
* - unknown ownership → unbound/ (never silent discard)
|
|
@@ -34,30 +34,13 @@ import {
|
|
|
34
34
|
type MigrationItemOutcome,
|
|
35
35
|
} from "./book-topology-migration.ts";
|
|
36
36
|
import { S4_SUBMISSION_LEDGER_KINDS } from "./sitian-appender.ts";
|
|
37
|
-
import {
|
|
38
|
-
projectTicketProvenanceEntry,
|
|
39
|
-
TICKET_PROVENANCE_HUMAN_VIEW,
|
|
40
|
-
} from "./ticket-provenance-contracts.ts";
|
|
41
|
-
import { renderTicketProvenanceMarkdown } from "./ticket-provenance.ts";
|
|
37
|
+
import { projectTicketProvenanceHeader } from "./ticket-provenance-contracts.ts";
|
|
42
38
|
|
|
43
39
|
const TICKET_PROVENANCE = "ticket-provenance";
|
|
44
40
|
const SUBMISSION_LEDGER = "submission-ledger";
|
|
45
41
|
const ATTEMPT_HISTORY = "attempt-history";
|
|
46
42
|
const MISPLACED = "misplaced-record-class";
|
|
47
43
|
|
|
48
|
-
function noteTouchedTicket(
|
|
49
|
-
context: BookTopologyMigrationContext,
|
|
50
|
-
bookKey: string,
|
|
51
|
-
ticketNumber: number,
|
|
52
|
-
): void {
|
|
53
|
-
let set = context.touchedTicketsByBook.get(bookKey);
|
|
54
|
-
if (set === undefined) {
|
|
55
|
-
set = new Set<number>();
|
|
56
|
-
context.touchedTicketsByBook.set(bookKey, set);
|
|
57
|
-
}
|
|
58
|
-
set.add(ticketNumber);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
44
|
type RecordClass = typeof TICKET_PROVENANCE | typeof SUBMISSION_LEDGER | typeof ATTEMPT_HISTORY;
|
|
62
45
|
|
|
63
46
|
type JsonLine =
|
|
@@ -108,9 +91,20 @@ async function ensureDir(path: string): Promise<void> {
|
|
|
108
91
|
await mkdir(path, { recursive: true });
|
|
109
92
|
}
|
|
110
93
|
|
|
94
|
+
/** One provisional line written to a ticket dest before a bare volume may supersede it. */
|
|
95
|
+
type DestLinePlacement = {
|
|
96
|
+
readonly raw: string;
|
|
97
|
+
/** Index into the partition outcomes array — flipped to unbound if bare replaces dest. */
|
|
98
|
+
readonly outcomeIndex: number;
|
|
99
|
+
};
|
|
100
|
+
|
|
111
101
|
/** Per-destination identity cache — one read per file, O(1) subsequent checks. */
|
|
112
102
|
class RecordWriteCache {
|
|
113
103
|
private readonly identities = new Map<string, Set<string>>();
|
|
104
|
+
/** Dest paths written as whole #901 bare volumes — legacy must not append under them. */
|
|
105
|
+
private readonly bareVolumeFiles = new Set<string>();
|
|
106
|
+
/** Lines currently on a dest that are still claimed `placed` in outcomes. */
|
|
107
|
+
private readonly destPlacements = new Map<string, DestLinePlacement[]>();
|
|
114
108
|
|
|
115
109
|
private async load(recordFile: string): Promise<Set<string>> {
|
|
116
110
|
const cached = this.identities.get(recordFile);
|
|
@@ -125,19 +119,81 @@ class RecordWriteCache {
|
|
|
125
119
|
return set;
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
|
|
122
|
+
markBareVolume(recordFile: string): void {
|
|
123
|
+
this.bareVolumeFiles.add(recordFile);
|
|
124
|
+
this.identities.set(recordFile, new Set());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
isBareVolume(recordFile: string): boolean {
|
|
128
|
+
return this.bareVolumeFiles.has(recordFile);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* After a bare whole-file write, register its lines so a later bare for the
|
|
133
|
+
* same ticket can rehome them and flip dispositions (multi-bare honesty).
|
|
134
|
+
*/
|
|
135
|
+
setDestPlacements(recordFile: string, placements: readonly DestLinePlacement[]): void {
|
|
136
|
+
this.destPlacements.set(recordFile, [...placements]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Install a bare volume as the sole ticket file. Any lines already placed on
|
|
141
|
+
* this dest (legacy appends or a prior bare) are moved to unbound and their
|
|
142
|
+
* outcomes flipped — report stays honest.
|
|
143
|
+
*/
|
|
144
|
+
async replaceWholeBare(
|
|
145
|
+
recordFile: string,
|
|
146
|
+
body: string,
|
|
147
|
+
outcomes: MigrationItemOutcome[],
|
|
148
|
+
unboundFileForRaw: (raw: string) => string,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const prior = this.destPlacements.get(recordFile) ?? [];
|
|
151
|
+
for (const placement of prior) {
|
|
152
|
+
await this.append(unboundFileForRaw(placement.raw), placement.raw);
|
|
153
|
+
const previous = outcomes[placement.outcomeIndex];
|
|
154
|
+
if (previous !== undefined) {
|
|
155
|
+
outcomes[placement.outcomeIndex] = {
|
|
156
|
+
disposition: "unbound",
|
|
157
|
+
source: previous.source,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.destPlacements.delete(recordFile);
|
|
162
|
+
await ensureDir(dirname(recordFile));
|
|
163
|
+
const text = body.endsWith("\n") ? body : `${body}\n`;
|
|
164
|
+
await writeFile(recordFile, text, "utf8");
|
|
165
|
+
this.markBareVolume(recordFile);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async append(
|
|
169
|
+
recordFile: string,
|
|
170
|
+
raw: string,
|
|
171
|
+
options?: { readonly outcomeIndex?: number },
|
|
172
|
+
): Promise<void> {
|
|
129
173
|
await ensureDir(dirname(recordFile));
|
|
130
174
|
const line = raw.endsWith("\n") ? raw : `${raw}\n`;
|
|
131
175
|
const parsed = parseJsonlLine(raw);
|
|
132
176
|
if (parsed.ok && typeof parsed.value.identity === "string") {
|
|
133
177
|
const existing = await this.load(recordFile);
|
|
134
|
-
if (existing.has(parsed.value.identity))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
if (!existing.has(parsed.value.identity)) {
|
|
179
|
+
existing.add(parsed.value.identity);
|
|
180
|
+
await appendFile(recordFile, line, "utf8");
|
|
181
|
+
}
|
|
182
|
+
// Identity hit skips a second physical copy, but the source row still claims
|
|
183
|
+
// this dest — register outcome below so bare replace flips every claim.
|
|
184
|
+
} else {
|
|
185
|
+
if (!this.identities.has(recordFile)) {
|
|
186
|
+
// Ensure subsequent identity loads see a live map even when first rows lack identity.
|
|
187
|
+
this.identities.set(recordFile, await this.load(recordFile));
|
|
188
|
+
}
|
|
189
|
+
await appendFile(recordFile, line, "utf8");
|
|
190
|
+
}
|
|
191
|
+
// Register outcome claims on dest even when identity skipped a write.
|
|
192
|
+
if (options?.outcomeIndex !== undefined && !this.bareVolumeFiles.has(recordFile)) {
|
|
193
|
+
const list = this.destPlacements.get(recordFile) ?? [];
|
|
194
|
+
list.push({ raw, outcomeIndex: options.outcomeIndex });
|
|
195
|
+
this.destPlacements.set(recordFile, list);
|
|
139
196
|
}
|
|
140
|
-
await appendFile(recordFile, line, "utf8");
|
|
141
197
|
}
|
|
142
198
|
}
|
|
143
199
|
|
|
@@ -342,6 +398,8 @@ async function placeTicketProvenanceLine(
|
|
|
342
398
|
source: string,
|
|
343
399
|
raw: string,
|
|
344
400
|
value: Record<string, unknown> | undefined,
|
|
401
|
+
/** Outcomes length at call time = index of the outcome the caller is about to push. */
|
|
402
|
+
outcomeIndex: number,
|
|
345
403
|
): Promise<MigrationItemOutcome> {
|
|
346
404
|
if (value === undefined) {
|
|
347
405
|
await writes.append(unboundCategoryFile(context.booksDirectory, bookKey, TICKET_PROVENANCE, stableKey(raw)), raw);
|
|
@@ -353,11 +411,30 @@ async function placeTicketProvenanceLine(
|
|
|
353
411
|
return { disposition: "unbound", source };
|
|
354
412
|
}
|
|
355
413
|
// Live topology (docs/dossier-topology.md): ticket root holds records.jsonl.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
414
|
+
const dest = join(
|
|
415
|
+
context.booksDirectory,
|
|
416
|
+
bookKey,
|
|
417
|
+
String(ticketNumber),
|
|
418
|
+
"records.jsonl",
|
|
359
419
|
);
|
|
360
|
-
|
|
420
|
+
// Never append legacy SitianRecord under a bare #901 volume (header must stay first).
|
|
421
|
+
if (writes.isBareVolume(dest)) {
|
|
422
|
+
await writes.append(
|
|
423
|
+
unboundCategoryFile(context.booksDirectory, bookKey, TICKET_PROVENANCE, stableKey(raw)),
|
|
424
|
+
raw,
|
|
425
|
+
);
|
|
426
|
+
return { disposition: "unbound", source };
|
|
427
|
+
}
|
|
428
|
+
const existing = await readJsonlLines(dest);
|
|
429
|
+
if (tryBareTicketProvenanceVolume(existing) !== undefined) {
|
|
430
|
+
writes.markBareVolume(dest);
|
|
431
|
+
await writes.append(
|
|
432
|
+
unboundCategoryFile(context.booksDirectory, bookKey, TICKET_PROVENANCE, stableKey(raw)),
|
|
433
|
+
raw,
|
|
434
|
+
);
|
|
435
|
+
return { disposition: "unbound", source };
|
|
436
|
+
}
|
|
437
|
+
await writes.append(dest, raw, { outcomeIndex });
|
|
361
438
|
return { disposition: "placed", source };
|
|
362
439
|
}
|
|
363
440
|
|
|
@@ -409,10 +486,11 @@ async function placeRecordClassLine(
|
|
|
409
486
|
raw: string,
|
|
410
487
|
value: Record<string, unknown>,
|
|
411
488
|
fallbackCategory: RecordClass,
|
|
489
|
+
outcomeIndex: number,
|
|
412
490
|
): Promise<MigrationItemOutcome> {
|
|
413
491
|
const recordClass = recordClassOfKind(value.kind);
|
|
414
492
|
if (recordClass === TICKET_PROVENANCE) {
|
|
415
|
-
return placeTicketProvenanceLine(context, bookKey, writes, source, raw, value);
|
|
493
|
+
return placeTicketProvenanceLine(context, bookKey, writes, source, raw, value, outcomeIndex);
|
|
416
494
|
}
|
|
417
495
|
if (recordClass === SUBMISSION_LEDGER || recordClass === ATTEMPT_HISTORY) {
|
|
418
496
|
return placeRunOwnedLine(context, bookKey, writes, recordClass, source, raw, value);
|
|
@@ -426,6 +504,59 @@ async function placeRecordClassLine(
|
|
|
426
504
|
return { disposition: "unbound", source };
|
|
427
505
|
}
|
|
428
506
|
|
|
507
|
+
/**
|
|
508
|
+
* #901 bare diary volume: first non-empty line is a header without SitianRecord
|
|
509
|
+
* `kind`. Recognise the whole file as one ticket volume — do not split rows into unbound.
|
|
510
|
+
*/
|
|
511
|
+
function tryBareTicketProvenanceVolume(
|
|
512
|
+
lines: readonly string[],
|
|
513
|
+
): { readonly ticket: number; readonly body: readonly string[] } | undefined {
|
|
514
|
+
const body: string[] = [];
|
|
515
|
+
for (const raw of lines) {
|
|
516
|
+
if (raw.trim() === "") continue;
|
|
517
|
+
body.push(raw);
|
|
518
|
+
}
|
|
519
|
+
if (body.length === 0) return undefined;
|
|
520
|
+
const first = parseJsonlLine(body[0]!);
|
|
521
|
+
if (!first.ok) return undefined;
|
|
522
|
+
// Legacy SitianRecord rows carry a typed kind — leave them to line placement.
|
|
523
|
+
if (recordClassOfKind(first.value.kind) !== undefined) return undefined;
|
|
524
|
+
const header = projectTicketProvenanceHeader(first.value);
|
|
525
|
+
if (header === undefined) return undefined;
|
|
526
|
+
return { ticket: header.ticket, body };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function placeBareTicketProvenanceVolume(
|
|
530
|
+
context: BookTopologyMigrationContext,
|
|
531
|
+
bookKey: string,
|
|
532
|
+
writes: RecordWriteCache,
|
|
533
|
+
filePath: string,
|
|
534
|
+
bare: { readonly ticket: number; readonly body: readonly string[] },
|
|
535
|
+
outcomes: MigrationItemOutcome[],
|
|
536
|
+
): Promise<void> {
|
|
537
|
+
const dest = join(
|
|
538
|
+
context.booksDirectory,
|
|
539
|
+
bookKey,
|
|
540
|
+
String(bare.ticket),
|
|
541
|
+
"records.jsonl",
|
|
542
|
+
);
|
|
543
|
+
// Whole-file bare install. Prior dest lines (legacy or earlier bare) rehome to
|
|
544
|
+
// unbound with flipped dispositions — header stays first; no silent erase.
|
|
545
|
+
const body = bare.body.map((raw) => (raw.endsWith("\n") ? raw : `${raw}\n`)).join("");
|
|
546
|
+
await writes.replaceWholeBare(dest, body, outcomes, (raw) =>
|
|
547
|
+
unboundCategoryFile(context.booksDirectory, bookKey, TICKET_PROVENANCE, stableKey(raw)),
|
|
548
|
+
);
|
|
549
|
+
const barePlacements: DestLinePlacement[] = [];
|
|
550
|
+
for (let index = 0; index < bare.body.length; index += 1) {
|
|
551
|
+
const source = lineSource(context.backupBooksDirectory, filePath, index);
|
|
552
|
+
const outcomeIndex = outcomes.length;
|
|
553
|
+
outcomes.push({ disposition: "placed", source });
|
|
554
|
+
barePlacements.push({ raw: bare.body[index]!, outcomeIndex });
|
|
555
|
+
}
|
|
556
|
+
// Register so a later bare for the same ticket can supersede honestly.
|
|
557
|
+
writes.setDestPlacements(dest, barePlacements);
|
|
558
|
+
}
|
|
559
|
+
|
|
429
560
|
async function migrateJsonlFileByKind(
|
|
430
561
|
context: BookTopologyMigrationContext,
|
|
431
562
|
bookKey: string,
|
|
@@ -435,6 +566,13 @@ async function migrateJsonlFileByKind(
|
|
|
435
566
|
outcomes: MigrationItemOutcome[],
|
|
436
567
|
): Promise<void> {
|
|
437
568
|
const lines = await readJsonlLines(filePath);
|
|
569
|
+
if (fallbackCategory === TICKET_PROVENANCE) {
|
|
570
|
+
const bare = tryBareTicketProvenanceVolume(lines);
|
|
571
|
+
if (bare !== undefined) {
|
|
572
|
+
await placeBareTicketProvenanceVolume(context, bookKey, writes, filePath, bare, outcomes);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
438
576
|
for (let index = 0; index < lines.length; index += 1) {
|
|
439
577
|
const raw = lines[index]!;
|
|
440
578
|
if (raw.trim() === "") continue;
|
|
@@ -443,14 +581,33 @@ async function migrateJsonlFileByKind(
|
|
|
443
581
|
if (!parsed.ok) {
|
|
444
582
|
// Malformed: preserve under the home partition's unbound bucket.
|
|
445
583
|
if (fallbackCategory === TICKET_PROVENANCE) {
|
|
446
|
-
outcomes.push(
|
|
584
|
+
outcomes.push(
|
|
585
|
+
await placeTicketProvenanceLine(
|
|
586
|
+
context,
|
|
587
|
+
bookKey,
|
|
588
|
+
writes,
|
|
589
|
+
source,
|
|
590
|
+
raw,
|
|
591
|
+
undefined,
|
|
592
|
+
outcomes.length,
|
|
593
|
+
),
|
|
594
|
+
);
|
|
447
595
|
} else {
|
|
448
596
|
outcomes.push(await placeRunOwnedLine(context, bookKey, writes, fallbackCategory, source, raw, undefined));
|
|
449
597
|
}
|
|
450
598
|
continue;
|
|
451
599
|
}
|
|
452
600
|
outcomes.push(
|
|
453
|
-
await placeRecordClassLine(
|
|
601
|
+
await placeRecordClassLine(
|
|
602
|
+
context,
|
|
603
|
+
bookKey,
|
|
604
|
+
writes,
|
|
605
|
+
source,
|
|
606
|
+
raw,
|
|
607
|
+
parsed.value,
|
|
608
|
+
fallbackCategory,
|
|
609
|
+
outcomes.length,
|
|
610
|
+
),
|
|
454
611
|
);
|
|
455
612
|
}
|
|
456
613
|
}
|
|
@@ -482,65 +639,11 @@ async function mergeOfferedIdentities(
|
|
|
482
639
|
}
|
|
483
640
|
}
|
|
484
641
|
|
|
485
|
-
/**
|
|
486
|
-
* Derive the human-read face once from the merged authoritative records.jsonl.
|
|
487
|
-
* Never copy any pre-existing md as if it were the complete view.
|
|
488
|
-
*/
|
|
489
|
-
async function writeMergedTicketHumanView(
|
|
490
|
-
destDir: string,
|
|
491
|
-
ticketNumber: number,
|
|
492
|
-
): Promise<void> {
|
|
493
|
-
const lines = await readJsonlLines(join(destDir, "records.jsonl"));
|
|
494
|
-
const entries = [];
|
|
495
|
-
const unprojected: unknown[] = [];
|
|
496
|
-
for (const raw of lines) {
|
|
497
|
-
if (raw.trim() === "") continue;
|
|
498
|
-
const parsed = parseJsonlLine(raw);
|
|
499
|
-
if (!parsed.ok) continue;
|
|
500
|
-
if (recordClassOfKind(parsed.value.kind) !== TICKET_PROVENANCE) continue;
|
|
501
|
-
const entry = projectTicketProvenanceEntry(parsed.value.payload);
|
|
502
|
-
if (entry === undefined) {
|
|
503
|
-
if (parsed.value.payload !== undefined) unprojected.push(parsed.value.payload);
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
entries.push(entry);
|
|
507
|
-
}
|
|
508
|
-
await ensureDir(destDir);
|
|
509
|
-
await writeFile(
|
|
510
|
-
join(destDir, TICKET_PROVENANCE_HUMAN_VIEW),
|
|
511
|
-
renderTicketProvenanceMarkdown({
|
|
512
|
-
ticketNumber,
|
|
513
|
-
entries,
|
|
514
|
-
...(unprojected.length > 0 ? { unprojected } : {}),
|
|
515
|
-
}),
|
|
516
|
-
"utf8",
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* After all sources for a ticket have contributed records + offered-identities,
|
|
522
|
-
* emit the single derived human view from the merged JSONL.
|
|
523
|
-
*/
|
|
524
|
-
async function finalizeTicketCompanions(
|
|
525
|
-
context: BookTopologyMigrationContext,
|
|
526
|
-
bookKey: string,
|
|
527
|
-
ticketNumbers: ReadonlySet<number>,
|
|
528
|
-
): Promise<void> {
|
|
529
|
-
for (const ticketNumber of ticketNumbers) {
|
|
530
|
-
const destDir = join(context.booksDirectory, bookKey, String(ticketNumber));
|
|
531
|
-
try {
|
|
532
|
-
await stat(join(destDir, "records.jsonl"));
|
|
533
|
-
} catch (error) {
|
|
534
|
-
if (isMigrationEnoent(error)) continue;
|
|
535
|
-
throw error;
|
|
536
|
-
}
|
|
537
|
-
await writeMergedTicketHumanView(destDir, ticketNumber);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
642
|
/** Collect typed ticket numbers present in a source volume's records.jsonl. */
|
|
542
643
|
async function ticketNumbersInVolume(volumeDir: string): Promise<number[]> {
|
|
543
644
|
const lines = await readJsonlLines(join(volumeDir, "records.jsonl"));
|
|
645
|
+
const bare = tryBareTicketProvenanceVolume(lines);
|
|
646
|
+
if (bare !== undefined) return [bare.ticket];
|
|
544
647
|
const out: number[] = [];
|
|
545
648
|
const seen = new Set<number>();
|
|
546
649
|
for (const raw of lines) {
|
|
@@ -563,7 +666,6 @@ async function mergeCompanionsFromVolume(
|
|
|
563
666
|
writes: RecordWriteCache,
|
|
564
667
|
): Promise<void> {
|
|
565
668
|
for (const ticketNumber of await ticketNumbersInVolume(volumeDir)) {
|
|
566
|
-
noteTouchedTicket(context, bookKey, ticketNumber);
|
|
567
669
|
const destDir = join(context.booksDirectory, bookKey, String(ticketNumber));
|
|
568
670
|
await ensureDir(destDir);
|
|
569
671
|
await mergeOfferedIdentities(destDir, volumeDir, writes);
|
|
@@ -578,8 +680,6 @@ async function migrateHomePartitionBook(
|
|
|
578
680
|
outcomes: MigrationItemOutcome[],
|
|
579
681
|
): Promise<void> {
|
|
580
682
|
const backupBook = join(context.backupBooksDirectory, bookKey);
|
|
581
|
-
// Touches accumulate via noteTouchedTicket — human view finalizes once after
|
|
582
|
-
// misplaced lines also land (not here).
|
|
583
683
|
// listVolumeRecordFiles covers partition-root records.jsonl + hashed sub-volumes.
|
|
584
684
|
const rootFiles = await listVolumeRecordFiles(join(backupBook, category));
|
|
585
685
|
for (const filePath of rootFiles) {
|
|
@@ -609,8 +709,6 @@ async function migrateHomePartitionBook(
|
|
|
609
709
|
}
|
|
610
710
|
await migrateJsonlFileByKind(context, bookKey, writes, recordsFile, TICKET_PROVENANCE, outcomes);
|
|
611
711
|
await mergeCompanionsFromVolume(context, bookKey, volumeDir, writes);
|
|
612
|
-
// Ticket-dir name itself is a typed ticket when the volume lives under it.
|
|
613
|
-
noteTouchedTicket(context, bookKey, Number(entry.name));
|
|
614
712
|
}
|
|
615
713
|
}
|
|
616
714
|
}
|
|
@@ -685,7 +783,17 @@ async function migrateMisplacedBook(
|
|
|
685
783
|
|
|
686
784
|
const source = lineSource(context.backupBooksDirectory, filePath, index);
|
|
687
785
|
if (recordClass === TICKET_PROVENANCE) {
|
|
688
|
-
outcomes.push(
|
|
786
|
+
outcomes.push(
|
|
787
|
+
await placeTicketProvenanceLine(
|
|
788
|
+
context,
|
|
789
|
+
bookKey,
|
|
790
|
+
writes,
|
|
791
|
+
source,
|
|
792
|
+
raw,
|
|
793
|
+
parsed.value,
|
|
794
|
+
outcomes.length,
|
|
795
|
+
),
|
|
796
|
+
);
|
|
689
797
|
} else {
|
|
690
798
|
outcomes.push(
|
|
691
799
|
await placeRunOwnedLine(context, bookKey, writes, recordClass, source, raw, parsed.value),
|
|
@@ -763,10 +871,6 @@ export const misplacedRecordClassPartitionMigrator: BookTopologyPartitionMigrato
|
|
|
763
871
|
async migrate(context) {
|
|
764
872
|
const outcomes: MigrationItemOutcome[] = [];
|
|
765
873
|
await forEachBook(context, (bookKey, writes) => migrateMisplacedBook(context, bookKey, writes, outcomes));
|
|
766
|
-
// All record-class lines (home + misplaced) have landed — one human-view pass.
|
|
767
|
-
for (const [bookKey, tickets] of context.touchedTicketsByBook) {
|
|
768
|
-
await finalizeTicketCompanions(context, bookKey, tickets);
|
|
769
|
-
}
|
|
770
874
|
return reconcileMigrationPartition(MISPLACED, "lines", outcomes);
|
|
771
875
|
},
|
|
772
876
|
};
|
package/src/diarist-contracts.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public 起居郎 (diarist) terminating receipt contracts — ADR 0075
|
|
2
|
+
* Public 起居郎 (diarist) terminating receipt contracts — ADR 0075 `diarist-is-role` / #901.
|
|
3
3
|
* Lawful explicit releases: completed (入录) | escalate (认不出本庭对象上抛).
|
|
4
|
-
* Machine facts about the volume come from the mechanical
|
|
4
|
+
* Machine facts about the volume come from the mechanical reproject seam, never
|
|
5
5
|
* from model self-report (锚定宪法); this module owns the receipt shape only.
|
|
6
6
|
*
|
|
7
|
-
* #
|
|
8
|
-
* Mechanical layer does not judge LLM output (no quote/ticket/relevance verify).
|
|
7
|
+
* #901: LLM submits bounds (+ optional amendments); mechanical layer projects.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
projectTicketProvenanceAmendments,
|
|
12
|
+
projectTicketProvenanceSessions,
|
|
13
|
+
type TicketProvenanceAmendment,
|
|
14
|
+
type TicketProvenanceSession,
|
|
14
15
|
} from "./ticket-provenance-contracts.ts";
|
|
15
16
|
import {
|
|
16
17
|
isSafePositiveTicketNumber,
|
|
@@ -20,31 +21,17 @@ import {
|
|
|
20
21
|
export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
|
|
21
22
|
export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* One whole block the diarist chose to enter (ADR 0075 誊录整块对话)。
|
|
25
|
-
* LLM supplies the block bytes + source pointer; mechanical layer appends as-is.
|
|
26
|
-
*/
|
|
27
|
-
export type DiaristEntrySubmission = {
|
|
28
|
-
readonly sourceKind: TicketProvenanceSourceKind;
|
|
29
|
-
readonly sourceRef: TicketProvenanceSourceRef;
|
|
30
|
-
/** Whole-block transcript — not a pointer-only stand-in. */
|
|
31
|
-
readonly transcript: string;
|
|
32
|
-
readonly timestamp: string;
|
|
33
|
-
/** Human-facing note (relation to this case). Not a machine gate. */
|
|
34
|
-
readonly note?: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
24
|
export type DiaristOutput =
|
|
38
25
|
| {
|
|
39
26
|
readonly status: "completed";
|
|
40
27
|
/**
|
|
41
|
-
* Typed court-target assertion (ADR 0075
|
|
28
|
+
* Typed court-target assertion (ADR 0075 `diarist-resolves-ticket-llm-layer`).
|
|
42
29
|
* Positive integer = 本庭对象=票N; null/absent = true-unbound (真无票→无录).
|
|
43
30
|
* LLM owns recognition; mechanical layer does not re-judge the number.
|
|
44
31
|
*/
|
|
45
32
|
readonly ticketNumber?: number | null;
|
|
46
33
|
/**
|
|
47
|
-
* #871 typed co-review set (ADR 0075
|
|
34
|
+
* #871 typed co-review set (ADR 0075 `diarist-resolves-ticket-llm-layer` narrow extension).
|
|
48
35
|
* Positive integers = tickets that should each receive a diary this court (includes main).
|
|
49
36
|
* Field absent → no new set this turn (resume keeps stored run fact; first court defaults to [main]).
|
|
50
37
|
* Explicit empty array → single-ticket face [main] whole-set replace.
|
|
@@ -52,8 +39,17 @@ export type DiaristOutput =
|
|
|
52
39
|
* Mechanical layer only type-projects/dedupes and guarantees principal membership when applying a set.
|
|
53
40
|
*/
|
|
54
41
|
readonly courtTicketNumbers?: readonly number[] | null;
|
|
55
|
-
/**
|
|
56
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Dialogue bounds: one entry per session volume, each with one or more ranges.
|
|
44
|
+
* Empty / absent = lawful empty selection (no dialogue this turn).
|
|
45
|
+
* Malformed endpoints → accept hook reasks (`reask-not-explode`).
|
|
46
|
+
*/
|
|
47
|
+
readonly sessions?: readonly TicketProvenanceSession[];
|
|
48
|
+
/**
|
|
49
|
+
* Optional amendments for unparsable source lines (s + line + speaker + text).
|
|
50
|
+
* Absent = no amendments this turn (not a shape rejection).
|
|
51
|
+
*/
|
|
52
|
+
readonly amendments?: readonly TicketProvenanceAmendment[];
|
|
57
53
|
}
|
|
58
54
|
| {
|
|
59
55
|
/** LLM cannot tell which ticket this summons is about — escalate, never wash into 无录. */
|
|
@@ -180,8 +176,25 @@ export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
|
|
|
180
176
|
throw new Error("Diarist output has no execution discriminator");
|
|
181
177
|
}
|
|
182
178
|
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Project sessions from a completed diarist payload.
|
|
181
|
+
* Absent key → empty (lawful). Present but unusable → undefined (caller reasks).
|
|
182
|
+
*/
|
|
183
|
+
export function projectDiaristSessions(
|
|
184
|
+
value: unknown,
|
|
185
|
+
): readonly TicketProvenanceSession[] | undefined {
|
|
186
|
+
const raw = (value as { sessions?: unknown } | null)?.sessions;
|
|
187
|
+
if (raw === undefined) return [];
|
|
188
|
+
return projectTicketProvenanceSessions(raw);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Project optional amendments. Absent / non-array → empty (not a rejection).
|
|
193
|
+
*/
|
|
194
|
+
export function projectDiaristAmendments(
|
|
195
|
+
value: unknown,
|
|
196
|
+
): readonly TicketProvenanceAmendment[] {
|
|
197
|
+
const raw = (value as { amendments?: unknown } | null)?.amendments;
|
|
198
|
+
if (raw === undefined) return [];
|
|
199
|
+
return projectTicketProvenanceAmendments(raw);
|
|
187
200
|
}
|
package/src/diarist-role.ts
CHANGED
|
@@ -16,13 +16,10 @@ export {
|
|
|
16
16
|
export type { DiaristOutput };
|
|
17
17
|
export { validateRecordedDiaristOutput };
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// provider `required` would cut off that very branch; sourceRef/timestamp are
|
|
24
|
-
// not branched on at all.
|
|
25
|
-
/** 起居郎交卷形状;形状指引,非 schema 闸。 */
|
|
19
|
+
/**
|
|
20
|
+
* 起居郎交卷形状;形状指引,非 schema 闸。
|
|
21
|
+
* #901:交边界(sessions)+ 可选坏行补写(amendments);正文由机械投影。
|
|
22
|
+
*/
|
|
26
23
|
export const diaristOutputSchema = withInfrastructureFailureDeclaration(
|
|
27
24
|
openToolObject(
|
|
28
25
|
Type.Object({
|
|
@@ -46,34 +43,82 @@ export const diaristOutputSchema = withInfrastructureFailureDeclaration(
|
|
|
46
43
|
description: "status 为 escalate 时:认不出本庭对象的原因",
|
|
47
44
|
}),
|
|
48
45
|
),
|
|
49
|
-
|
|
46
|
+
sessions: Type.Optional(
|
|
47
|
+
Type.Array(
|
|
48
|
+
Type.Object(
|
|
49
|
+
{
|
|
50
|
+
path: Type.Optional(
|
|
51
|
+
Type.String({ description: "会话卷绝对或可读路径" }),
|
|
52
|
+
),
|
|
53
|
+
ranges: Type.Optional(
|
|
54
|
+
Type.Array(
|
|
55
|
+
Type.Object(
|
|
56
|
+
{
|
|
57
|
+
from: Type.Optional(
|
|
58
|
+
Type.Object(
|
|
59
|
+
{
|
|
60
|
+
id: Type.Optional(Type.String()),
|
|
61
|
+
line: Type.Optional(Type.Unknown()),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
additionalProperties: true,
|
|
65
|
+
description: "起点:原生 id 或本轮行号二选一",
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
to: Type.Optional(
|
|
70
|
+
Type.Object(
|
|
71
|
+
{
|
|
72
|
+
id: Type.Optional(Type.String()),
|
|
73
|
+
line: Type.Optional(Type.Unknown()),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
additionalProperties: true,
|
|
77
|
+
description: "终点:原生 id 或本轮行号二选一",
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
{ additionalProperties: true, description: "一段对话区间" },
|
|
83
|
+
),
|
|
84
|
+
{ description: "本卷本轮各区间;至少一段" },
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
{ additionalProperties: true, description: "一卷会话的边界" },
|
|
89
|
+
),
|
|
90
|
+
{
|
|
91
|
+
description:
|
|
92
|
+
"本票对话边界;空列表=本轮无对话可划。端点无法指名时走 reask,不中止。",
|
|
93
|
+
},
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
amendments: Type.Optional(
|
|
50
97
|
Type.Array(
|
|
51
98
|
Type.Object(
|
|
52
99
|
{
|
|
53
|
-
|
|
54
|
-
description:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
sessionFile: Type.Optional(Type.String()),
|
|
60
|
-
entryId: Type.Optional(Type.Unknown()),
|
|
61
|
-
path: Type.Optional(Type.String()),
|
|
62
|
-
url: Type.Optional(Type.String()),
|
|
63
|
-
},
|
|
64
|
-
{ additionalProperties: true, description: "不可变源指针" },
|
|
65
|
-
)),
|
|
66
|
-
transcript: Type.Optional(Type.String({
|
|
67
|
-
description: "整块原文(誊录整块,不指针化)",
|
|
68
|
-
})),
|
|
69
|
-
timestamp: Type.Optional(Type.String({ description: "源时间戳 ISO" })),
|
|
70
|
-
note: Type.Optional(
|
|
71
|
-
Type.String({ description: "该材料与本案的关系(人读)" }),
|
|
100
|
+
s: Type.Optional(
|
|
101
|
+
Type.Unknown({ description: "卷下标(sessions 位置)" }),
|
|
102
|
+
),
|
|
103
|
+
line: Type.Optional(
|
|
104
|
+
Type.Unknown({ description: "源卷 1-based 行号" }),
|
|
72
105
|
),
|
|
106
|
+
speaker: Type.Optional(
|
|
107
|
+
Type.String({ description: "owner | runner" }),
|
|
108
|
+
),
|
|
109
|
+
text: Type.Optional(
|
|
110
|
+
Type.String({ description: "补写正文(原话)" }),
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
additionalProperties: true,
|
|
115
|
+
description: "一条坏行补写;缺字段的成员机械跳过",
|
|
73
116
|
},
|
|
74
|
-
{ additionalProperties: true, description: "一条入录整块" },
|
|
75
117
|
),
|
|
76
|
-
{
|
|
118
|
+
{
|
|
119
|
+
description:
|
|
120
|
+
"可选;机械解析不了的行交此补写。缺本字段=无补写,不拒收。",
|
|
121
|
+
},
|
|
77
122
|
),
|
|
78
123
|
),
|
|
79
124
|
}),
|
|
@@ -92,7 +137,8 @@ export type DiaristRuntimeDependencies = {
|
|
|
92
137
|
export const DIARIST_TOOL_SPEC = {
|
|
93
138
|
name: DIARIST_OUTPUT_TOOL_NAME,
|
|
94
139
|
label: "起居郎输出",
|
|
95
|
-
description:
|
|
96
|
-
|
|
140
|
+
description:
|
|
141
|
+
"起居郎交本票对话边界(sessions)与可选坏行补写(amendments);认不出本庭对象则 escalate。",
|
|
142
|
+
promptSnippet: "起居郎交边界与可选补写",
|
|
97
143
|
parameters: diaristOutputSchema,
|
|
98
144
|
} as const;
|