@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +6312 -6376
  4. package/dist/analyst-gate-cycles-read.js +28 -78
  5. package/dist/analyst-ledger.js +26 -42
  6. package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
  7. package/dist/analyst-metric-families/round-timeline.js +2 -5
  8. package/dist/archivist-record-entry.js +4 -3
  9. package/dist/archivist-record-pointer.js +71 -36
  10. package/dist/atomic-write.js +2 -2
  11. package/dist/auditor-dossier-tool.js +11 -6
  12. package/dist/auditor-soul.js +5 -12
  13. package/dist/collector-config.js +6 -5
  14. package/dist/doctor-evidence.js +8 -4
  15. package/dist/engine-detour-usage.js +8 -22
  16. package/dist/headless-host/production-host.js +5969 -6011
  17. package/dist/host-descriptions.js +0 -4
  18. package/dist/host-session-record.js +266 -267
  19. package/dist/ledger-session-read.js +5 -0
  20. package/dist/migrate-book-topology.js +1369 -722
  21. package/dist/navigator-public-session.js +13 -17
  22. package/dist/notary-source-run.js +8 -16
  23. package/dist/packaged-role-registry.js +5 -56
  24. package/dist/pi/durable-principal.js +2 -3
  25. package/dist/pi/role-turn-host.js +79 -15
  26. package/dist/public-cli/auto-resume.js +8 -47
  27. package/dist/public-cli/instruction-seat-run.js +9 -7
  28. package/dist/public-cli/invocation.js +67 -100
  29. package/dist/public-cli/main.js +2689 -2532
  30. package/dist/public-cli/post-admission.js +96 -55
  31. package/dist/public-cli/run-lifecycle.js +44 -100
  32. package/dist/public-cli/settlement.js +184 -429
  33. package/dist/public-cli/terminal.js +0 -12
  34. package/dist/role-run-placement.js +53 -16
  35. package/dist/role-run-relocation.js +111 -52
  36. package/dist/run-dossier-files.js +30 -0
  37. package/dist/run-dossier.js +322 -0
  38. package/dist/run-terminal-artifacts.js +69 -202
  39. package/dist/run-ticket-number.js +49 -7
  40. package/dist/session-assistant-usage.js +9 -18
  41. package/dist/session-identity.js +49 -19
  42. package/dist/sitian-appender.js +29 -18
  43. package/dist/sitian-facade.js +11 -0
  44. package/dist/sitian-reader.js +44 -29
  45. package/dist/submission-gate.js +1 -1
  46. package/dist/submission-ledger.js +4 -4
  47. package/dist/ticket-provenance-contracts.js +2 -8
  48. package/dist/ticket-provenance.js +25 -9
  49. package/package.json +1 -1
  50. package/scripts/build-package.mjs +2 -0
  51. package/src/acp-host/production-host.ts +1 -1
  52. package/src/acp-host/role-turn-host.ts +11 -4
  53. package/src/analyst-gate-cycles-read.ts +31 -88
  54. package/src/analyst-ledger.ts +36 -51
  55. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  56. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  57. package/src/analyst-metric-families/round-timeline.ts +2 -5
  58. package/src/analyst-page.ts +3 -3
  59. package/src/archivist-record-entry.ts +6 -5
  60. package/src/archivist-record-pointer.ts +92 -42
  61. package/src/atomic-write.ts +3 -1
  62. package/src/auditor-dossier-tool.ts +14 -8
  63. package/src/auditor-soul.ts +5 -10
  64. package/src/book-topology-migration-placement.ts +16 -3
  65. package/src/book-topology-record-class-migrators.ts +4 -2
  66. package/src/book-topology-runs-migrator.ts +6 -3
  67. package/src/collector-config.ts +5 -3
  68. package/src/doctor-contracts.ts +1 -1
  69. package/src/doctor-evidence.ts +7 -3
  70. package/src/engine-detour-tool.ts +1 -1
  71. package/src/engine-detour-usage.ts +12 -25
  72. package/src/external-host-turn-loop.ts +25 -0
  73. package/src/factory-board.ts +16 -81
  74. package/src/headless-host/production-host.ts +1 -4
  75. package/src/headless-host/role-turn-host.ts +56 -21
  76. package/src/host-descriptions.ts +0 -6
  77. package/src/host-session-record.ts +224 -140
  78. package/src/ledger-session-read.ts +6 -0
  79. package/src/navigator-public-session.ts +13 -18
  80. package/src/navigator-work-context.ts +3 -1
  81. package/src/notary-role.ts +2 -1
  82. package/src/notary-source-run.ts +8 -18
  83. package/src/packaged-role-registry.ts +1 -65
  84. package/src/pi/adapter.ts +50 -7
  85. package/src/pi/durable-principal.ts +2 -4
  86. package/src/pi/role-turn-host.ts +85 -14
  87. package/src/public-cli/auto-resume.ts +7 -43
  88. package/src/public-cli/instruction-seat-run.ts +13 -6
  89. package/src/public-cli/invocation.ts +90 -144
  90. package/src/public-cli/post-admission.ts +111 -72
  91. package/src/public-cli/run-lifecycle.ts +47 -115
  92. package/src/public-cli/settlement.ts +221 -589
  93. package/src/public-cli/terminal.ts +1 -61
  94. package/src/role-envelope.ts +4 -2
  95. package/src/role-run-placement.ts +65 -17
  96. package/src/role-run-relocation.ts +110 -70
  97. package/src/role-runtime.ts +46 -69
  98. package/src/run-dossier-files.ts +33 -0
  99. package/src/run-dossier.ts +357 -0
  100. package/src/run-terminal-artifacts.ts +84 -240
  101. package/src/run-ticket-number.ts +82 -14
  102. package/src/session-assistant-usage.ts +8 -20
  103. package/src/session-identity.ts +52 -21
  104. package/src/sitian-appender.ts +33 -26
  105. package/src/sitian-facade.ts +22 -0
  106. package/src/sitian-reader.ts +62 -32
  107. package/src/submission-gate.ts +1 -1
  108. package/src/submission-ledger.ts +4 -4
  109. package/src/ticket-provenance-contracts.ts +3 -6
  110. package/src/ticket-provenance.ts +23 -11
  111. package/src/ticket-snapshot.ts +3 -2
  112. package/src/ticket-trajectory.ts +95 -109
@@ -1,18 +1,15 @@
1
1
  /**
2
- * Sole nested-volume reader for gate-cycle facts under session/auditor-roles/.
2
+ * Sole reader for gate-cycle facts of the officers pointed at from current.json `officers`.
3
3
  *
4
- * Consumers: Analyst sole ledger scan (classifyScopedRun) and Terminal gate
5
- * projection (#478). Metric families must not open a second disk scan — they
6
- * consume retained facts. Terminal settlement reuses this same pairing seam
7
- * (no second auditor-roles scanner).
4
+ * Consumer: Analyst sole ledger scan (classifyScopedRun). Metric families must
5
+ * not open a second disk scan — they consume retained facts.
8
6
  *
9
7
  * Naming: records may carry pre-#440 menxia/jishizhong/fubaolang tool faces or
10
8
  * the current gatekeeper/inspector/notary English face. Projection always uses
11
9
  * the current English officer identity (inspector | notary).
12
10
  *
13
- * Missing auditor-roles directory (ENOENT only) → empty rounds (lawful zero).
14
- * Path present but not a directory (ENOTDIR) and discovered nested JSONL that
15
- * fails canonical read/parse must fail loudly (never silently under-count).
11
+ * Absent `officers` section → empty rounds (lawful zero). A pointer whose
12
+ * officer session fails canonical read/parse must fail loudly (never silently under-count).
16
13
  * An accepted gate terminating receipt (isError:false pair on dispatch/officer
17
14
  * tool) whose required typed facts are unusable — status, dispatch officer, or
18
15
  * first/last span missing/unknown/unparseable/inverted — also fails loudly via
@@ -25,9 +22,6 @@
25
22
  * `ak_auditor_parent_attempt_binding.parent.attemptEntryId` — never seat/time guessing.
26
23
  * An orphan accepted dispatch must not consume a later same-seat direct officer.
27
24
  */
28
- import { readdir } from "node:fs/promises";
29
- import { join } from "node:path";
30
-
31
25
  import {
32
26
  AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
33
27
  } from "./compliance-transport.ts";
@@ -38,7 +32,10 @@ import {
38
32
  type LedgerSessionRow,
39
33
  } from "./ledger-session-read.ts";
40
34
 
41
- import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
35
+ import { isRecord, isEnoent } from "./unknown-value.ts";
36
+ import { retainedRunPathsMatch } from "./role-run-relocation.ts";
37
+ import { runDirectoryOfSessionFile } from "./role-run-placement.ts";
38
+ import { readOfficerPointers } from "./archivist-record-pointer.ts";
42
39
 
43
40
  /** One completed gate round: direct officer receipt or historical province/officer pair. */
44
41
  /** Honest origin discriminant: direct summons vs historical province dispatch. */
@@ -416,93 +413,39 @@ function pairGateRounds(
416
413
  .map((round, index) => ({ ...round, roundIndex: index + 1 }));
417
414
  }
418
415
 
419
- /** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
420
- async function resolveOfficerSessionFromPointerFile(
421
- pointerPath: string,
422
- ): Promise<{ sessionFile: string; officer?: "inspector" | "notary" }> {
423
- const { readFile } = await import("node:fs/promises");
424
- let raw: unknown;
425
- try {
426
- raw = JSON.parse(await readFile(pointerPath, "utf8"));
427
- } catch (error) {
428
- throw new Error(
429
- `direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`,
430
- { cause: error },
431
- );
432
- }
433
- if (!isRecord(raw) || raw.kind !== "direct-officer-run-pointer" || raw.version !== 1) {
434
- throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
435
- }
436
- const sessionFile = raw.sessionFile;
437
- if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
438
- throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
439
- }
440
- const officer = raw.officer === "inspector" || raw.officer === "notary"
441
- ? raw.officer : undefined;
442
- return { sessionFile, ...(officer === undefined ? {} : { officer }) };
443
- }
444
-
445
416
  /**
446
- * Read and pair gate-cycle rounds from one or more auditor-roles directories.
447
- * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
448
- * files that name the independent officer session 正本.
449
- * ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
450
- * (failure honesty — damaged topology must not wash to zero rounds).
417
+ * Read and pair gate-cycle rounds from the officer pointers booked in one
418
+ * run's current.json `officers` section; each pointer names the independent
419
+ * officer session 正本. Absent section → [] (lawful zero). A damaged pointer or
420
+ * officer session propagates — damaged topology must not wash to zero rounds.
451
421
  *
452
422
  * Pass `parentSessionFile` to keep only rounds whose attempt binding names that parent.
453
423
  */
454
- export async function readAnalystGateCyclesFromAuditorRoles(
455
- auditorRolesDirectory: string | readonly string[],
424
+ export async function readAnalystGateCyclesFromOfficers(
425
+ parentRunDirectory: string,
456
426
  options: {
457
427
  readonly parentSessionFile?: string;
458
428
  } = {},
459
429
  ): Promise<readonly AnalystGateCycleRound[]> {
460
- const directories = typeof auditorRolesDirectory === "string"
461
- ? [auditorRolesDirectory]
462
- : auditorRolesDirectory;
463
430
  const volumes: ClassifiedVolume[] = [];
464
- // Same officer session pointed at N times (historical multi-mint pointers, or
465
- // one parent booking many leaves) must classify once — else each accepted
466
- // seal is counted ×N in terminal gate-round (#753 acceptance A).
431
+ // Same officer session pointed at N times must classify once — else each
432
+ // accepted seal is counted ×N in gate-cycle statistics (#753 acceptance A).
467
433
  const classifiedOfficerSessions = new Set<string>();
468
- for (const directory of directories) {
469
- let names: string[];
470
- try {
471
- const entries = await readdir(directory, { withFileTypes: true });
472
- names = entries
473
- .filter(
474
- (e) =>
475
- e.isFile()
476
- && (e.name.endsWith(".jsonl") || e.name.endsWith(".pointer.json")),
477
- )
478
- .map((e) => e.name)
479
- .sort();
480
- } catch (error) {
481
- if (isEnoent(error)) continue;
482
- throw error;
483
- }
484
- for (const name of names) {
485
- const path = join(directory, name);
486
- const fromPointer = name.endsWith(".pointer.json");
487
- const pointer = fromPointer
488
- ? await resolveOfficerSessionFromPointerFile(path)
489
- : undefined;
490
- const sessionPath = pointer?.sessionFile ?? path;
491
- if (fromPointer) {
492
- if (classifiedOfficerSessions.has(sessionPath)) continue;
493
- classifiedOfficerSessions.add(sessionPath);
494
- }
495
- const classified = await classifyAuditorVolume(sessionPath, pointer?.officer);
496
- for (const volume of classified) {
497
- if (
498
- options.parentSessionFile !== undefined &&
499
- volume.parentSessionFile !== undefined &&
500
- volume.parentSessionFile !== options.parentSessionFile
501
- ) {
502
- continue;
503
- }
504
- volumes.push(volume);
434
+ for (const pointer of await readOfficerPointers(parentRunDirectory)) {
435
+ if (classifiedOfficerSessions.has(pointer.sessionFile)) continue;
436
+ classifiedOfficerSessions.add(pointer.sessionFile);
437
+ const officer = pointer.officer === "inspector" || pointer.officer === "notary"
438
+ ? pointer.officer : undefined;
439
+ const classified = await classifyAuditorVolume(pointer.sessionFile, officer);
440
+ for (const volume of classified) {
441
+ if (
442
+ options.parentSessionFile !== undefined &&
443
+ volume.parentSessionFile !== undefined &&
444
+ !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))
445
+ ) {
446
+ continue;
505
447
  }
448
+ volumes.push(volume);
506
449
  }
507
450
  }
508
451
  return pairGateRounds(volumes);
@@ -12,6 +12,7 @@
12
12
  * A2: classifyScopedRun retains typed per-run facts (frame span, tool intervals,
13
13
  * terminal face) for metric-family modules — no longer discarded after checks.
14
14
  */
15
+ import { readSectionSync } from "./run-dossier.ts";
15
16
  import { readdir, readFile } from "node:fs/promises";
16
17
  import { basename, join } from "node:path";
17
18
 
@@ -21,9 +22,14 @@ import {
21
22
  physicalPathIdentity,
22
23
  resolveActivationLedgerHome,
23
24
  } from "./activation-ledger-topology.ts";
24
- import { listBookRunDirectories } from "./role-run-placement.ts";
25
+ import {
26
+ listBookRunDirectories,
27
+ parseRunLeaf,
28
+ sessionFileOf,
29
+ } from "./role-run-placement.ts";
25
30
  import { autopsyWriterLock, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
26
31
  import { readRunTicketNumber } from "./run-ticket-number.ts";
32
+ import { rewriteRunDirectoryPathValue } from "./role-run-relocation.ts";
27
33
  import {
28
34
  extractSessionModelSequence,
29
35
  extractSessionTimestampSpan,
@@ -34,8 +40,8 @@ import {
34
40
  type SessionToolInterval,
35
41
  } from "./ledger-session-read.ts";
36
42
  import {
37
- readRunTerminalArtifact,
38
- type RunTerminalArtifactFile,
43
+ readRunTerminal,
44
+ type RunTerminalFace,
39
45
  } from "./run-terminal-artifacts.ts";
40
46
  import type {
41
47
  AnalystFirstFrameAt,
@@ -45,7 +51,7 @@ import type {
45
51
  AnalystUnreadableRun,
46
52
  } from "./analyst-page.ts";
47
53
  import {
48
- readAnalystGateCyclesFromAuditorRoles,
54
+ readAnalystGateCyclesFromOfficers,
49
55
  type AnalystGateCycleRound,
50
56
  } from "./analyst-gate-cycles-read.ts";
51
57
 
@@ -57,7 +63,7 @@ export type { AnalystGateCycleRound } from "./analyst-gate-cycles-read.ts";
57
63
  const LIVE_RUN_STATES = new Set(["admitted", "running", "resumable"]);
58
64
 
59
65
  /**
60
- * Read lifecycle state from the existing run-state.json face.
66
+ * Read lifecycle state from the state.jsonl run-state fact row.
61
67
  * Used only to distinguish live in-flight runs from terminal no-receipt.
62
68
  */
63
69
  async function readExistingRunLifecycleState(
@@ -124,19 +130,11 @@ async function classifyGhostCandidate(input: {
124
130
  };
125
131
  }
126
132
 
127
- function parseRunDirectoryName(
128
- name: string,
129
- ): { runId: string; role: string } | undefined {
130
- const at = name.lastIndexOf("@");
131
- if (at <= 0 || at === name.length - 1) return undefined;
132
- return { runId: name.slice(0, at), role: name.slice(at + 1) };
133
- }
134
-
135
133
  /**
136
134
  * Invocation scope faces used for issue 圈定 (C4 / #399).
137
135
  * projectRoot is retained for narrow path match and conflict facts;
138
136
  * ticketNumber is the #176 typed face when present (integer ≥ 1).
139
- * Single read of invocation.json — no second parse kernel.
137
+ * Single read of the current.json invocation section — no second parse kernel.
140
138
  */
141
139
  type InvocationScopeFields = {
142
140
  readonly projectRoot: string;
@@ -156,15 +154,8 @@ async function listLedgerBookNames(booksRoot: string): Promise<string[]> {
156
154
  async function readInvocationScopeFields(
157
155
  runDirectory: string,
158
156
  ): Promise<InvocationScopeFields | undefined> {
159
- let raw: string;
160
- try {
161
- raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
162
- } catch (error) {
163
- if (isMissingPathError(error)) return undefined;
164
- throw error;
165
- }
166
- const parsed: unknown = JSON.parse(raw);
167
- if (!isRecord(parsed)) return undefined;
157
+ const parsed = readSectionSync(runDirectory, "invocation");
158
+ if (parsed === undefined) return undefined;
168
159
  if (typeof parsed.projectRoot !== "string" || parsed.projectRoot.trim() === "") {
169
160
  return undefined;
170
161
  }
@@ -214,20 +205,13 @@ async function resolveSessionFile(
214
205
  runDirectory: string,
215
206
  ): Promise<string> {
216
207
  // Prefer invocation.sessionFile when present; fall back to S-family principal.
217
- try {
218
- const raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
219
- const parsed: unknown = JSON.parse(raw);
220
- if (
221
- isRecord(parsed)
222
- && typeof parsed.sessionFile === "string"
223
- && parsed.sessionFile.trim() !== ""
224
- ) {
225
- return parsed.sessionFile;
226
- }
227
- } catch (error) {
228
- if (!isMissingPathError(error)) throw error;
208
+ const parsed = readSectionSync(runDirectory, "invocation");
209
+ if (parsed !== undefined && typeof parsed.sessionFile === "string" && parsed.sessionFile.trim() !== "") {
210
+ return typeof parsed.runDirectory === "string"
211
+ ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory) as string
212
+ : parsed.sessionFile;
229
213
  }
230
- return join(runDirectory, "session", "session.jsonl");
214
+ return sessionFileOf(runDirectory);
231
215
  }
232
216
 
233
217
  /**
@@ -327,7 +311,7 @@ export type AnalystRunTerminalFace =
327
311
  | { readonly status: "absent" }
328
312
  | {
329
313
  readonly status: "present";
330
- readonly file: RunTerminalArtifactFile;
314
+ readonly face: RunTerminalFace;
331
315
  readonly body: Record<string, unknown>;
332
316
  /** Nonblank producer role — sole owner is readRunTerminalArtifact. */
333
317
  readonly role: string;
@@ -352,9 +336,9 @@ export type AnalystReadableRunFacts = {
352
336
  */
353
337
  readonly models: readonly string[];
354
338
  /**
355
- * Paired gate-cycle rounds from session/auditor-roles/ (#446).
356
- * Missing directory → empty (lawful zero rounds).
357
- * Damaged discovered nested JSONL → leg unreadable (`auditor-roles` source).
339
+ * Paired gate-cycle rounds of the officers booked in current.json (#446).
340
+ * No officers booked → empty (lawful zero rounds).
341
+ * A damaged officer pointer or session → leg unreadable (`auditor-roles` source).
358
342
  */
359
343
  readonly gateCycles: readonly AnalystGateCycleRound[];
360
344
  };
@@ -515,10 +499,14 @@ async function classifyScopedRun(input: {
515
499
  }
516
500
 
517
501
  try {
518
- const artifact = await readRunTerminalArtifact(input.runDirectory);
502
+ const read = readRunTerminal(input.runDirectory);
503
+ // A no_receipt terminal is the explicit form of "no accepted receipt".
504
+ const artifact = read.status === "present" && read.face === "no_receipt"
505
+ ? { status: "absent" as const }
506
+ : read;
519
507
  if (artifact.status === "unreadable") {
520
508
  missingSources.push("terminal-artifact");
521
- reasons.push(`${artifact.file}: ${artifact.reason}`);
509
+ reasons.push(`terminal: ${artifact.reason}`);
522
510
  } else if (artifact.status === "absent") {
523
511
  if (lifecycle === "admitted" || lifecycle === "running") {
524
512
  // No terminal yet: ghost report only, or live in-flight omit.
@@ -530,10 +518,10 @@ async function classifyScopedRun(input: {
530
518
  }
531
519
  terminal = { status: "absent" };
532
520
  } else {
533
- // role already required nonblank by readRunTerminalArtifact (single owner).
521
+ // role already required nonblank by readRunTerminal (single owner).
534
522
  terminal = {
535
523
  status: "present",
536
- file: artifact.file,
524
+ face: artifact.face,
537
525
  body: artifact.body,
538
526
  role: artifact.body.role as string,
539
527
  };
@@ -574,16 +562,13 @@ async function classifyScopedRun(input: {
574
562
  );
575
563
  }
576
564
 
577
- // Nested auditor-roles gate pairs stay inside the sole scan (families must
565
+ // Officer gate pairs stay inside the sole scan (families must
578
566
  // not readdir this tree again). Missing directory → []. Damaged discovered
579
567
  // nested JSONL is page-local unreadable — never silently under-count rounds.
580
568
  let gateCycles: readonly AnalystGateCycleRound[];
581
569
  try {
582
- const parentSessionFile = join(input.runDirectory, "session", "session.jsonl");
583
- gateCycles = await readAnalystGateCyclesFromAuditorRoles(
584
- join(input.runDirectory, "session", "auditor-roles"),
585
- { parentSessionFile },
586
- );
570
+ const parentSessionFile = sessionFileOf(input.runDirectory);
571
+ gateCycles = await readAnalystGateCyclesFromOfficers(input.runDirectory, { parentSessionFile });
587
572
  } catch (error) {
588
573
  return {
589
574
  kind: "unreadable",
@@ -697,7 +682,7 @@ export async function scanAnalystIssueRuns(input: {
697
682
 
698
683
  for (const runDirectory of runDirectories) {
699
684
  const runName = basename(runDirectory);
700
- const parsed = parseRunDirectoryName(runName);
685
+ const parsed = parseRunLeaf(runName);
701
686
  if (parsed === undefined) continue;
702
687
 
703
688
  let scopeFields: InvocationScopeFields | undefined;
@@ -97,7 +97,10 @@ export type AnalystAcceptanceSuccessReworkSection = {
97
97
  readonly rework: AnalystReworkLens;
98
98
  };
99
99
 
100
- /** Locate collector groups array: receipt.groups or top-level groups. */
100
+ /**
101
+ * Locate collector groups array from the canonical typed receipt faces.
102
+ * Same leaf order as extractStatus: body / receipt / decisiveFacts / payloads.
103
+ */
101
104
  function findCollectorGroups(body: Record<string, unknown>): unknown {
102
105
  if (Array.isArray(body.groups)) return body.groups;
103
106
  const receipt = body.receipt;
@@ -106,6 +109,15 @@ function findCollectorGroups(body: Record<string, unknown>): unknown {
106
109
  if (isRecord(outcome)) {
107
110
  const facts = outcome.decisiveFacts;
108
111
  if (isRecord(facts) && Array.isArray(facts.groups)) return facts.groups;
112
+ // Typed sealed receipt: settlement keeps payloads on attempt-history; the
113
+ // terminal reader rehydrates them via attemptHistoryIdentity. Groups are the
114
+ // collector leaf on that receipt — never invent a second discriminator.
115
+ if (Array.isArray(outcome.payloads)) {
116
+ for (let index = outcome.payloads.length - 1; index >= 0; index -= 1) {
117
+ const payload = outcome.payloads[index];
118
+ if (isRecord(payload) && Array.isArray(payload.groups)) return payload.groups;
119
+ }
120
+ }
109
121
  }
110
122
  return undefined;
111
123
  }
@@ -116,6 +128,17 @@ export function extractStatus(body: Record<string, unknown>): string | undefined
116
128
  if (isRecord(outcome) && typeof outcome.status === "string" && outcome.status.trim() !== "") {
117
129
  return outcome.status;
118
130
  }
131
+ // Typed sealed receipt: settlement keeps payloads on attempt-history; the
132
+ // terminal reader rehydrates them via attemptHistoryIdentity. Status is the role's
133
+ // own leaf on that receipt — never invent a second mapping.
134
+ if (isRecord(outcome) && Array.isArray(outcome.payloads)) {
135
+ for (let index = outcome.payloads.length - 1; index >= 0; index -= 1) {
136
+ const payload = outcome.payloads[index];
137
+ if (isRecord(payload) && typeof payload.status === "string" && payload.status.trim() !== "") {
138
+ return payload.status;
139
+ }
140
+ }
141
+ }
119
142
  const receipt = body.receipt;
120
143
  if (isRecord(receipt) && typeof receipt.status === "string" && receipt.status.trim() !== "") {
121
144
  return receipt.status;
@@ -26,7 +26,7 @@ export type AnalystGateCyclesLeg = {
26
26
  readonly runId: string;
27
27
  readonly book: string;
28
28
  readonly role: string;
29
- /** Paired dispatch↔officer volume count (0 when no auditor-roles). */
29
+ /** Paired dispatch↔officer volume count (0 when no officers booked). */
30
30
  readonly roundCount: number;
31
31
  readonly rounds: readonly AnalystGateCyclesRoundRow[];
32
32
  };
@@ -82,13 +82,10 @@ function projectTerminal(facts: AnalystReadableRunFacts): AnalystRoundTimelineTe
82
82
  if (facts.terminal.status === "absent") {
83
83
  return { kind: "death", channel: "no-receipt" };
84
84
  }
85
- if (facts.terminal.file === "error.json") {
85
+ if (facts.terminal.face === "error") {
86
86
  return { kind: "death", channel: "error" };
87
87
  }
88
- if (facts.terminal.file === "audit-incomplete.json") {
89
- return { kind: "death", channel: "audit-incomplete" };
90
- }
91
- // report.json — receipt face; classCount only when producer wrote a number.
88
+ // report face — receipt; classCount only when producer wrote a number.
92
89
  const status = extractStatus(facts.terminal.body);
93
90
  const classCount = readClassCount(facts.terminal.body);
94
91
  const receiptStatus = status ?? "unparsed";
@@ -28,9 +28,9 @@ export type AnalystMissingSource =
28
28
  /** Model-groups mode: leg has no usable session model identity. */
29
29
  | "session-model"
30
30
  /**
31
- * Nested session/auditor-roles volume was discovered but failed canonical
32
- * read/parse (#446). Missing auditor-roles directory is lawful zero rounds,
33
- * not this face.
31
+ * A booked officer pointer or its session failed canonical read/parse (#446).
32
+ * No officers booked is lawful zero rounds, not this face.
33
+ * Public label stays `auditor-roles` (终局来源标签以外不得改名).
34
34
  */
35
35
  | "auditor-roles";
36
36
 
@@ -31,6 +31,7 @@ import {
31
31
  } from "./archivist-record-topology.ts";
32
32
  import type { HostRecordSession, RecordSessionHost } from "./host-contracts.ts";
33
33
  import { piRecordSessionHost } from "./pi/record-session-host.ts";
34
+ import { sitianVolumeDirectory } from "./sitian-appender.ts";
34
35
  export {
35
36
  DIRECT_OFFICER_RUN_POINTER_KIND,
36
37
  bookDirectOfficerRunPointer,
@@ -47,7 +48,7 @@ const CURRENT_SESSION_LEDGER = "current-session.json";
47
48
  type CurrentSessionRecord = { readonly sessionFile: string };
48
49
 
49
50
  function readCurrentSession(sessionDir: string): string {
50
- const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
51
+ const ledger = currentSessionLedgerPath(sessionDir);
51
52
  try {
52
53
  const value: unknown = JSON.parse(readFileSync(ledger, "utf8"));
53
54
  if (
@@ -68,7 +69,7 @@ function readCurrentSession(sessionDir: string): string {
68
69
  }
69
70
 
70
71
  function writeCurrentSession(sessionDir: string, sessionFile: string): void {
71
- const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
72
+ const ledger = currentSessionLedgerPath(sessionDir);
72
73
  try {
73
74
  writeFileSync(ledger, `${JSON.stringify({ sessionFile })}\n`, { flag: "wx" });
74
75
  } catch (error) {
@@ -292,7 +293,7 @@ export type RecordSessionParent = {
292
293
  export type CreateRecordSessionOptions = {
293
294
  /** Role working directory passed to the record session; not a placement input. */
294
295
  readonly cwd: string;
295
- /** What kind of record this is (e.g. "auditor-roles"). Single path segment; not a destination path. */
296
+ /** What kind of record this is (e.g. "attendance"). Single path segment; not a destination path. */
296
297
  readonly kind: string;
297
298
  /** Parent session — nesting authority for ordinary kinds; optional parentSession link for navigator. */
298
299
  readonly parent?: RecordSessionParent;
@@ -372,7 +373,7 @@ export type RecordSessionOpen = {
372
373
  * symlink). New principals mint under the already-validated sessionDir.
373
374
  * Navigator work-subject nests use their AK ledger; worker-submission-gate asks the
374
375
  * host to continue its recent native session without selecting a stored file path.
375
- * Other ordinary no-subject children (auditor-roles, …) always mint fresh.
376
+ * Other ordinary no-subject children (attendance, …) always mint fresh.
376
377
  * New persisted principals materialize their deferred session header before return so
377
378
  * custom-entry-only writers do not need a parallel delayed-header helper.
378
379
  *
@@ -420,7 +421,7 @@ export function createRecordSessionOpen(
420
421
  // Ordinary kinds: durable parent's file is the sole nesting authority. A divergent
421
422
  // SessionManager directory must not create a second placement route.
422
423
  // Do not restore generic bookDir/<kind> fallback for foreign/unrooted parents.
423
- sessionDir = join(dirname(parentResolved), options.kind);
424
+ sessionDir = sitianVolumeDirectory(dirname(parentResolved), options.kind);
424
425
  parentSession = parentFile;
425
426
  mayResumeSameNest = options.kind === WORKER_SUBMISSION_GATE_KIND;
426
427
  }