@akagilnc/pi-workflow-roles 0.1.4800 → 0.1.4870

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 (143) hide show
  1. package/dist/acp-host/production-host.js +19434 -18218
  2. package/dist/analyst-book-key.js +63 -0
  3. package/dist/analyst-cohort.js +167 -0
  4. package/dist/analyst-entry.js +320 -0
  5. package/dist/analyst-index.js +247 -0
  6. package/dist/analyst-ledger.js +561 -0
  7. package/dist/analyst-median.js +16 -0
  8. package/dist/analyst-metric-families/acceptance-success-rework.js +249 -0
  9. package/dist/analyst-metric-families/b2-frame-buckets-actions.js +189 -0
  10. package/dist/analyst-metric-families/gate-cycles.js +77 -0
  11. package/dist/analyst-metric-families/leg-wall-clock.js +56 -0
  12. package/dist/analyst-metric-families/round-timeline.js +126 -0
  13. package/dist/analyst-metric-families.js +23 -0
  14. package/dist/analyst-metric-family.js +11 -0
  15. package/dist/analyst-model-groups.js +135 -0
  16. package/dist/analyst-page.js +182 -0
  17. package/dist/atomic-write.js +23 -0
  18. package/dist/auditor-soul.js +1 -1
  19. package/dist/countersign-contracts.js +0 -2
  20. package/dist/diarist-contracts.js +0 -1
  21. package/dist/doctor-contracts.js +2 -2
  22. package/dist/gatekeeper-role.js +15 -16
  23. package/dist/gleaner-left-contracts.js +0 -2
  24. package/dist/headless-host/production-host.js +16967 -15749
  25. package/dist/host-contracts.js +0 -4
  26. package/dist/inspector-contracts.js +0 -2
  27. package/dist/institutional-resolution.js +4 -2
  28. package/dist/merger-contracts.js +0 -2
  29. package/dist/migrate-book-topology.js +3908 -3089
  30. package/dist/navigator-public-session.js +4 -3
  31. package/dist/notary-contracts.js +0 -2
  32. package/dist/package-contracts/auditor-output.js +0 -1
  33. package/dist/package-contracts/collector-output.js +0 -2
  34. package/dist/package-contracts/fixer-output.js +0 -2
  35. package/dist/package-contracts/gatekeeper-output.js +0 -2
  36. package/dist/package-contracts/judge-output.js +0 -2
  37. package/dist/package-contracts/navigator-output.js +0 -2
  38. package/dist/package-contracts/reviewer-output.js +0 -2
  39. package/dist/package-contracts/terminating-tools.js +32 -92
  40. package/dist/package-contracts/worker-output.js +0 -4
  41. package/dist/packaged-role-registry.js +518 -36
  42. package/dist/pi/role-turn-host.js +2 -1
  43. package/dist/public-cli/analyst-run.js +185 -0
  44. package/dist/public-cli/cli.js +885 -0
  45. package/dist/public-cli/config.js +12 -9
  46. package/dist/public-cli/countersign-run.js +4 -311
  47. package/dist/public-cli/instruction-seat-run.js +628 -158
  48. package/dist/public-cli/invocation.js +806 -1550
  49. package/dist/public-cli/main.js +6962 -9882
  50. package/dist/public-cli/option-definitions.js +51 -99
  51. package/dist/public-cli/post-admission.js +30 -2
  52. package/dist/public-cli/run-lifecycle.js +234 -353
  53. package/dist/public-cli/seat-ticket-binding.js +6 -0
  54. package/dist/public-cli/settlement.js +261 -675
  55. package/dist/public-cli/turn-request.js +76 -0
  56. package/dist/public-role-summons.js +38 -112
  57. package/dist/role-activation-flags.js +23 -45
  58. package/dist/secretariat-contracts.js +0 -2
  59. package/dist/session-identity.js +17 -8
  60. package/dist/session-opening-materials.js +3 -2
  61. package/extensions/role-runtime.ts +6 -80
  62. package/package.json +1 -1
  63. package/souls/coder.md +1 -2
  64. package/src/acp-host/role-turn-host.ts +4 -1
  65. package/src/analyst-metric-families/acceptance-success-rework.ts +3 -2
  66. package/src/auditor-role.ts +1 -2
  67. package/src/auditor-soul.ts +1 -1
  68. package/src/collector-role.ts +2 -2
  69. package/src/countersign-contracts.ts +0 -1
  70. package/src/countersign-role.ts +1 -1
  71. package/src/diarist-contracts.ts +0 -1
  72. package/src/diarist-role.ts +0 -1
  73. package/src/doctor-auditor.ts +1 -2
  74. package/src/doctor-contracts.ts +1 -1
  75. package/src/doctor-role.ts +3 -2
  76. package/src/external-host-turn-loop.ts +1 -1
  77. package/src/factory-board.ts +10 -7
  78. package/src/gatekeeper-role.ts +16 -10
  79. package/src/gleaner-left-contracts.ts +0 -1
  80. package/src/gleaner-left-role.ts +0 -1
  81. package/src/headless-host/role-turn-host.ts +7 -1
  82. package/src/host-contracts.ts +10 -6
  83. package/src/inspector-contracts.ts +0 -1
  84. package/src/inspector-role.ts +0 -2
  85. package/src/institutional-resolution.ts +6 -4
  86. package/src/judge-role.ts +3 -3
  87. package/src/merger-contracts.ts +0 -1
  88. package/src/merger-role.ts +2 -2
  89. package/src/navigator-public-session.ts +7 -6
  90. package/src/navigator-work-context.ts +6 -8
  91. package/src/notary-contracts.ts +0 -1
  92. package/src/notary-role.ts +2 -3
  93. package/src/package-contracts/auditor-output.ts +0 -1
  94. package/src/package-contracts/collector-output.ts +0 -1
  95. package/src/package-contracts/fixer-output.ts +0 -1
  96. package/src/package-contracts/gatekeeper-output.ts +0 -1
  97. package/src/package-contracts/judge-output.ts +0 -1
  98. package/src/package-contracts/navigator-output.ts +0 -1
  99. package/src/package-contracts/reviewer-output.ts +0 -1
  100. package/src/package-contracts/terminating-tools.ts +27 -99
  101. package/src/package-contracts/worker-output.ts +0 -2
  102. package/src/packaged-role-registry.ts +604 -35
  103. package/src/pi/role-turn-host.ts +2 -1
  104. package/src/public-cli/cli.ts +64 -259
  105. package/src/public-cli/config.ts +18 -10
  106. package/src/public-cli/countersign-run.ts +14 -449
  107. package/src/public-cli/instruction-seat-run.ts +758 -277
  108. package/src/public-cli/invocation.ts +1097 -2051
  109. package/src/public-cli/option-definitions.ts +62 -127
  110. package/src/public-cli/post-admission.ts +47 -2
  111. package/src/public-cli/run-lifecycle.ts +303 -613
  112. package/src/public-cli/seat-ticket-binding.ts +6 -0
  113. package/src/public-cli/settlement.ts +399 -1185
  114. package/src/public-cli/turn-request.ts +92 -1
  115. package/src/public-role-summons.ts +44 -128
  116. package/src/reviewer-role.ts +3 -2
  117. package/src/role-activation-flags.ts +20 -41
  118. package/src/role-runtime-dependencies.ts +25 -26
  119. package/src/role-runtime.ts +121 -232
  120. package/src/secretariat-contracts.ts +0 -1
  121. package/src/secretariat-role.ts +0 -2
  122. package/src/session-identity.ts +23 -8
  123. package/src/session-opening-materials.ts +2 -1
  124. package/src/worker-role.ts +3 -4
  125. package/dist/doctor-auditor.js +0 -25
  126. package/dist/public-cli/diarist-run.js +0 -130
  127. package/dist/public-cli/doctor-run.js +0 -87
  128. package/dist/public-cli/inspector-run.js +0 -151
  129. package/dist/public-cli/judge-run.js +0 -114
  130. package/dist/public-cli/notary-run.js +0 -174
  131. package/dist/public-cli/reviewer-run.js +0 -339
  132. package/src/public-cli/coder-run.ts +0 -255
  133. package/src/public-cli/collector-run.ts +0 -189
  134. package/src/public-cli/diarist-run.ts +0 -233
  135. package/src/public-cli/doctor-run.ts +0 -167
  136. package/src/public-cli/fixer-run.ts +0 -265
  137. package/src/public-cli/gleaner-left-run.ts +0 -167
  138. package/src/public-cli/inspector-run.ts +0 -261
  139. package/src/public-cli/judge-run.ts +0 -196
  140. package/src/public-cli/merger-run.ts +0 -294
  141. package/src/public-cli/notary-run.ts +0 -279
  142. package/src/public-cli/reviewer-run.ts +0 -480
  143. package/src/public-cli/secretariat-run.ts +0 -265
@@ -0,0 +1,23 @@
1
+ import acceptanceSuccessReworkFamily from "./analyst-metric-families/acceptance-success-rework.js";
2
+ import b2FrameBucketsActionsFamily from "./analyst-metric-families/b2-frame-buckets-actions.js";
3
+ import gateCyclesFamily from "./analyst-metric-families/gate-cycles.js";
4
+ import legWallClockFamily from "./analyst-metric-families/leg-wall-clock.js";
5
+ import roundTimelineFamily from "./analyst-metric-families/round-timeline.js";
6
+ /**
7
+ * Production issue-page family modules in stable id order.
8
+ * Static graph — reachable from source and from the public CLI bundle alike.
9
+ */
10
+ const ISSUE_METRIC_FAMILIES = [
11
+ acceptanceSuccessReworkFamily,
12
+ b2FrameBucketsActionsFamily,
13
+ gateCyclesFamily,
14
+ legWallClockFamily,
15
+ roundTimelineFamily,
16
+ ].sort((a, b) => a.id.localeCompare(b.id));
17
+ /**
18
+ * Load the registered issue-page metric families.
19
+ * Always resolves the static package registry (never readdir of a sibling tree).
20
+ */
21
+ export async function loadAnalystIssueMetricFamilies() {
22
+ return ISSUE_METRIC_FAMILIES;
23
+ }
@@ -0,0 +1,11 @@
1
+ /** Fold discovered family modules into one section bag (Object.assign order). */
2
+ export function composeAnalystMetricFamilySections(families, input) {
3
+ const sections = {};
4
+ for (const family of families) {
5
+ const piece = family.contribute(input);
6
+ if (piece === undefined)
7
+ continue;
8
+ Object.assign(sections, piece);
9
+ }
10
+ return sections;
11
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Analyst model-group mode kernel (PRD #298 output ④ / ticket #331).
3
+ *
4
+ * Per-leg raw model grouping (single | mixed:ordered-dedup whole-leg unit)
5
+ * over a caller-typed issue set. Combination mapping is display-alias only:
6
+ * raw group identity and denominators never merge.
7
+ *
8
+ * Acceptance / success faces reuse the B3 terminal map (受理≠成功, planned
9
+ * out of success den) — no second outcome kernel.
10
+ */
11
+ import { buildAcceptanceSuccessReworkSection } from "./analyst-metric-families/acceptance-success-rework.js";
12
+ import { medianNumber } from "./analyst-median.js";
13
+ /**
14
+ * Build the raw group key for one leg from its ordered-unique model sequence.
15
+ * Empty sequence → undefined (no inventable group; caller lists as typed vacancy).
16
+ * One model → that raw id. Multi → `mixed:` + `+`-joined ordered uniques.
17
+ */
18
+ export function analystModelGroupKey(models) {
19
+ if (models.length === 0)
20
+ return undefined;
21
+ if (models.length === 1)
22
+ return models[0];
23
+ return `mixed:${models.join("+")}`;
24
+ }
25
+ function rate(numerator, denominator) {
26
+ if (denominator === 0)
27
+ return undefined;
28
+ return numerator / denominator;
29
+ }
30
+ function displayNameFor(rawGroupKey, combinationMapping) {
31
+ if (combinationMapping === undefined)
32
+ return rawGroupKey;
33
+ const aliased = combinationMapping[rawGroupKey];
34
+ return aliased === undefined ? rawGroupKey : aliased;
35
+ }
36
+ function sortUnreadable(unreadable) {
37
+ return [...unreadable].sort((a, b) => {
38
+ if (a.book !== b.book)
39
+ return a.book.localeCompare(b.book);
40
+ return a.runId.localeCompare(b.runId);
41
+ });
42
+ }
43
+ /**
44
+ * Legs with no usable session model identity cannot join any model group.
45
+ * Reuse the typed unreadable path (locatable missing-source) — not an empty-key group.
46
+ */
47
+ function modelIdentityAbsentEntry(run) {
48
+ return {
49
+ runId: run.runId,
50
+ book: run.book,
51
+ missingSources: ["session-model"],
52
+ reason: "session has no usable model identity",
53
+ firstFrameAt: { status: "present", at: run.frameSpan.startedAt },
54
+ // Readable legs already admitted a full span — retain end edge for lastActivityAt.
55
+ lastFrameAt: { status: "present", at: run.frameSpan.endedAt },
56
+ };
57
+ }
58
+ /**
59
+ * Aggregate readable runs into model groups.
60
+ * Stats identity is always the raw key; mapping only projects displayName.
61
+ * Model-identity vacancy is excluded from every group den and listed as unreadable.
62
+ */
63
+ export function buildAnalystModelGroupsPage(input) {
64
+ // Split before B3 projection: only model-bearing legs enter group dens.
65
+ // Carry the non-empty raw key so aggregation need not re-check vacancy.
66
+ const groupedRuns = [];
67
+ const modelAbsent = [];
68
+ for (const run of input.runs) {
69
+ const rawGroupKey = analystModelGroupKey(run.models);
70
+ if (rawGroupKey === undefined) {
71
+ modelAbsent.push(modelIdentityAbsentEntry(run));
72
+ }
73
+ else {
74
+ groupedRuns.push({ run, rawGroupKey });
75
+ }
76
+ }
77
+ const acceptance = buildAcceptanceSuccessReworkSection(groupedRuns.map(({ run }) => run));
78
+ const legByRunId = new Map((acceptance?.legs ?? []).map((leg) => [leg.runId, leg]));
79
+ const byRaw = new Map();
80
+ for (const { run, rawGroupKey } of groupedRuns) {
81
+ const leg = legByRunId.get(run.runId);
82
+ if (leg === undefined) {
83
+ // Readable run must project through B3 map; missing is an invariant break.
84
+ throw new Error(`analyst model-groups: missing acceptance projection for run ${run.runId}`);
85
+ }
86
+ let acc = byRaw.get(rawGroupKey);
87
+ if (acc === undefined) {
88
+ acc = {
89
+ acceptedCount: 0,
90
+ successCount: 0,
91
+ successEligibleCount: 0,
92
+ noReceiptCount: 0,
93
+ walls: [],
94
+ };
95
+ byRaw.set(rawGroupKey, acc);
96
+ }
97
+ if (leg.accepted)
98
+ acc.acceptedCount += 1;
99
+ if (leg.success)
100
+ acc.successCount += 1;
101
+ if (leg.successEligible)
102
+ acc.successEligibleCount += 1;
103
+ if (leg.noReceipt)
104
+ acc.noReceiptCount += 1;
105
+ acc.walls.push(leg.wallMs);
106
+ }
107
+ const rawKeys = [...byRaw.keys()].sort((a, b) => a.localeCompare(b));
108
+ const groups = rawKeys.map((rawGroupKey) => {
109
+ const acc = byRaw.get(rawGroupKey);
110
+ const legCount = acc.walls.length;
111
+ return {
112
+ rawGroupKey,
113
+ displayName: displayNameFor(rawGroupKey, input.combinationMapping),
114
+ legCount,
115
+ acceptedCount: acc.acceptedCount,
116
+ acceptanceRate: rate(acc.acceptedCount, legCount),
117
+ successCount: acc.successCount,
118
+ successEligibleCount: acc.successEligibleCount,
119
+ successRate: rate(acc.successCount, acc.successEligibleCount),
120
+ noReceiptCount: acc.noReceiptCount,
121
+ noReceiptRate: rate(acc.noReceiptCount, legCount),
122
+ wallClockMedianMs: medianNumber(acc.walls),
123
+ };
124
+ });
125
+ const unreadable = sortUnreadable([...input.unreadable, ...modelAbsent]);
126
+ return {
127
+ kind: "analyst-model-groups",
128
+ mode: "model-groups",
129
+ projectRoots: [...input.projectRoots].sort((a, b) => a.localeCompare(b)),
130
+ groups,
131
+ legCount: groupedRuns.length,
132
+ unreadableCount: unreadable.length,
133
+ unreadable,
134
+ };
135
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Analyst issue metrics page envelope + atomic persistence (ADR 0068 / PRD #298).
3
+ *
4
+ * A1 minimum fields: issue scope (projectRoot) + leg list + unreadable exclusion.
5
+ * A2: metric-family modules under analyst-metric-families/ contribute optional
6
+ * top-level sections via directory discovery — B/C waves add family files
7
+ * without forking the page writer or editing a shared registry list.
8
+ */
9
+ import { createHash } from "node:crypto";
10
+ import { dirname, join } from "node:path";
11
+ import { writeFileAtomically } from "./atomic-write.js";
12
+ import { assertLedgerFileInsideHome, ensureRealDirectoryTree, physicalPathIdentity, } from "./activation-ledger-topology.js";
13
+ import { loadAnalystIssueMetricFamilies } from "./analyst-metric-families.js";
14
+ import { composeAnalystMetricFamilySections } from "./analyst-metric-family.js";
15
+ export function analystIssuePageKey(address) {
16
+ const parts = ["book", address.bookKey];
17
+ if (address.issueNumber !== undefined) {
18
+ parts.push("ticket", String(address.issueNumber));
19
+ }
20
+ else if (address.scopeRootIdentity !== undefined) {
21
+ parts.push("root", physicalPathIdentity(address.scopeRootIdentity));
22
+ }
23
+ return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
24
+ }
25
+ export function analystIssuePagePath(ledgerHome, address) {
26
+ return join(ledgerHome, "analyst", "issues", `${analystIssuePageKey(address)}.json`);
27
+ }
28
+ export function analystIssuePageAddressFromPage(page) {
29
+ return {
30
+ bookKey: page.bookKey,
31
+ ...(page.issueNumber === undefined ? {} : { issueNumber: page.issueNumber }),
32
+ ...(page.scopeRootIdentity === undefined
33
+ ? {}
34
+ : { scopeRootIdentity: page.scopeRootIdentity }),
35
+ };
36
+ }
37
+ function sortLegs(legs) {
38
+ return [...legs].sort((a, b) => {
39
+ if (a.book !== b.book)
40
+ return a.book.localeCompare(b.book);
41
+ if (a.role !== b.role)
42
+ return a.role.localeCompare(b.role);
43
+ return a.runId.localeCompare(b.runId);
44
+ });
45
+ }
46
+ function sortUnreadable(unreadable) {
47
+ return [...unreadable].sort((a, b) => {
48
+ if (a.book !== b.book)
49
+ return a.book.localeCompare(b.book);
50
+ return a.runId.localeCompare(b.runId);
51
+ });
52
+ }
53
+ function sortScopeConflicts(conflicts) {
54
+ return [...conflicts].sort((a, b) => {
55
+ // Caller dual-param (no runId) sorts before run conflicts; then by root.
56
+ const aRun = a.runId ?? "";
57
+ const bRun = b.runId ?? "";
58
+ if (aRun !== bRun)
59
+ return aRun.localeCompare(bRun);
60
+ return a.projectRoot.localeCompare(b.projectRoot);
61
+ });
62
+ }
63
+ /**
64
+ * Admit caller LOC at the typed input boundary.
65
+ * Finite non-negative only; 0 remains a lawful typed 空缺 signal (not rejected).
66
+ * NaN / negative / ±Infinity are structural rejects (sweep attach + issue mode).
67
+ */
68
+ export function assertAnalystChangedLinesInput(changedLines) {
69
+ if (changedLines === undefined)
70
+ return;
71
+ if (typeof changedLines !== "number" || !Number.isFinite(changedLines) || changedLines < 0) {
72
+ throw new Error(`analyst changedLines must be a finite non-negative number, got ${String(changedLines)}`);
73
+ }
74
+ }
75
+ /**
76
+ * Normalize caller LOC: only omit or 0 → typed 空缺 (PRD efficiency口径).
77
+ * Callers must pass {@link assertAnalystChangedLinesInput} first.
78
+ */
79
+ export function normalizeAnalystChangedLines(changedLines) {
80
+ assertAnalystChangedLinesInput(changedLines);
81
+ if (changedLines === undefined || changedLines === 0) {
82
+ return { status: "absent" };
83
+ }
84
+ return { status: "present", value: changedLines };
85
+ }
86
+ /**
87
+ * 耗时/千行 = totalElapsedMs ÷ (changedLines/1000).
88
+ * LOC absent/0 → typed 空缺 (no division; never emit 0 or ∞ as stand-in).
89
+ */
90
+ export function computeAnalystMsPerKLines(totalElapsedMs, changedLines) {
91
+ if (changedLines.status === "absent")
92
+ return { status: "absent" };
93
+ return {
94
+ status: "present",
95
+ value: totalElapsedMs / (changedLines.value / 1000),
96
+ };
97
+ }
98
+ function frameSpanWallMs(span) {
99
+ return Date.parse(span.endedAt) - Date.parse(span.startedAt);
100
+ }
101
+ /**
102
+ * Σ readable wall clocks + max end-frame across ALL runs (C1 efficiency / index).
103
+ * Unreadable runs never enter totalElapsedMs; their available lastFrameAt still
104
+ * competes for lastActivityAt (PRD ②: 全部 run 末帧之最大者).
105
+ */
106
+ export function summarizeAnalystRunEfficiency(runs, unreadable = []) {
107
+ let totalElapsedMs = 0;
108
+ let latestEndedAt;
109
+ for (const run of runs) {
110
+ totalElapsedMs += frameSpanWallMs(run.frameSpan);
111
+ const endedAt = run.frameSpan.endedAt;
112
+ if (latestEndedAt === undefined || endedAt > latestEndedAt) {
113
+ latestEndedAt = endedAt;
114
+ }
115
+ }
116
+ for (const entry of unreadable) {
117
+ if (entry.lastFrameAt.status !== "present")
118
+ continue;
119
+ const endedAt = entry.lastFrameAt.at;
120
+ if (latestEndedAt === undefined || endedAt > latestEndedAt) {
121
+ latestEndedAt = endedAt;
122
+ }
123
+ }
124
+ const lastActivityAt = latestEndedAt === undefined
125
+ ? { status: "absent" }
126
+ : { status: "present", at: latestEndedAt };
127
+ return { totalElapsedMs, lastActivityAt };
128
+ }
129
+ export async function buildAnalystIssueMetricsPage(input) {
130
+ // Discover before compose/write — missing family tree fails loud with native
131
+ // ENOENT/ENOTDIR (no empty-registry wash) and never emits a success page.
132
+ const families = await loadAnalystIssueMetricFamilies();
133
+ // Sole run→leg projection owner: page envelope maps typed runs to A1 legs.
134
+ const legs = sortLegs(input.runs.map((run) => ({
135
+ runId: run.runId,
136
+ book: run.book,
137
+ role: run.role,
138
+ })));
139
+ const unreadable = sortUnreadable(input.unreadable);
140
+ const scopeConflicts = sortScopeConflicts(input.scopeConflicts ?? []);
141
+ const projectRoot = physicalPathIdentity(input.projectRoot);
142
+ const { totalElapsedMs, lastActivityAt } = summarizeAnalystRunEfficiency(input.runs, unreadable);
143
+ const changedLines = normalizeAnalystChangedLines(input.changedLines);
144
+ const msPerKLines = computeAnalystMsPerKLines(totalElapsedMs, changedLines);
145
+ const envelope = {
146
+ kind: "analyst-issue-metrics",
147
+ mode: "issue",
148
+ bookKey: input.bookKey,
149
+ projectRoot,
150
+ // exactOptionalPropertyTypes: only materialize when caller supplied it.
151
+ ...(input.issueNumber === undefined ? {} : { issueNumber: input.issueNumber }),
152
+ ...(input.scopeRootIdentity === undefined
153
+ ? {}
154
+ : { scopeRootIdentity: physicalPathIdentity(input.scopeRootIdentity) }),
155
+ legs,
156
+ unreadable,
157
+ unreadableCount: unreadable.length,
158
+ scopeConflicts,
159
+ totalElapsedMs,
160
+ changedLines,
161
+ msPerKLines,
162
+ lastActivityAt,
163
+ };
164
+ const sections = composeAnalystMetricFamilySections(families, {
165
+ projectRoot,
166
+ runs: input.runs,
167
+ unreadable,
168
+ });
169
+ return { ...envelope, ...sections };
170
+ }
171
+ /**
172
+ * Atomically replace the issue metrics page (idempotent overwrite).
173
+ * Directory creation and file placement go through the ledger home physical
174
+ * containment owner (ADR 0038 / 0048) — never plain recursive mkdir alone.
175
+ */
176
+ export async function writeAnalystIssueMetricsPage(ledgerHome, page) {
177
+ const path = analystIssuePagePath(ledgerHome, analystIssuePageAddressFromPage(page));
178
+ ensureRealDirectoryTree(ledgerHome, dirname(path));
179
+ assertLedgerFileInsideHome(path, ledgerHome);
180
+ await writeFileAtomically(path, `${JSON.stringify(page, null, 2)}\n`);
181
+ return path;
182
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Same-directory temp + rename atomic file replace.
3
+ * Shared primitive for ledger-adjacent typed pages (analyst metrics, etc.).
4
+ * Does not open/truncate an existing destination inode, so hard-linked twins
5
+ * keep prior bytes until the directory entry is swapped.
6
+ * Parent directory must already exist — callers that write under the package
7
+ * ledger home own confinement via ensureRealDirectoryTree (ADR 0038).
8
+ */
9
+ import { randomUUID } from "node:crypto";
10
+ import { rename, rm, writeFile } from "node:fs/promises";
11
+ import { dirname, join } from "node:path";
12
+ export async function writeFileAtomically(destination, contents) {
13
+ const parent = dirname(destination);
14
+ const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
15
+ try {
16
+ await writeFile(temporary, contents);
17
+ await rename(temporary, destination);
18
+ }
19
+ catch (error) {
20
+ await rm(temporary, { force: true }).catch(() => undefined);
21
+ throw error;
22
+ }
23
+ }
@@ -40,7 +40,7 @@ export const AUDITOR_SESSION_MATERIALS = {
40
40
  doctor: ["CLAUDE.md", "souls/doctor-auditor.md"],
41
41
  };
42
42
  export function isAuditorSoulRole(value) {
43
- return value === "judge" || value === "doctor";
43
+ return typeof value === "string" && Object.hasOwn(AUDITOR_SESSION_MATERIALS, value);
44
44
  }
45
45
  /** Resolve audited subject from explicit value or the subject-input env. */
46
46
  export function resolveAuditorSubject(raw) {
@@ -1,5 +1,4 @@
1
1
  const COUNTERSIGN_OUTPUT_TOOL_NAME = "ak_countersign_output";
2
- const COUNTERSIGN_ACCEPTED_TEXT = "\u7ED9\u4E8B\u4E2D\u56DE\u6267\u5DF2\u63A5\u53D7";
3
2
  function validateRecordedCountersignOutput(verdict) {
4
3
  if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) {
5
4
  throw new Error("Countersign verdict has no execution discriminator");
@@ -19,7 +18,6 @@ function validateRecordedCountersignOutput(verdict) {
19
18
  throw new Error("Countersign verdict has no execution discriminator");
20
19
  }
21
20
  export {
22
- COUNTERSIGN_ACCEPTED_TEXT,
23
21
  COUNTERSIGN_OUTPUT_TOOL_NAME,
24
22
  validateRecordedCountersignOutput
25
23
  };
@@ -9,7 +9,6 @@
9
9
  import { projectTicketProvenanceSessions, } from "./ticket-provenance-contracts.js";
10
10
  import { isSafePositiveTicketNumber, requireSafePositiveTicketNumber, } from "./run-ticket-number.js";
11
11
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
12
- export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
13
12
  /**
14
13
  * Sole shape projection for a typed co-review ticket set (#871).
15
14
  * Positive safe integers, first-seen order, duplicates dropped. Not content judgment.
@@ -4,7 +4,7 @@ import { openToolObjectFromUnion } from "./open-tool-schema.js";
4
4
  import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.js";
5
5
  const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
6
6
  const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
7
- const DOCTOR_ACCEPTED_TEXT = "\u592A\u533B\u7F72\u56DE\u6267\u5DF2\u63A5\u53D7";
7
+ const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
8
8
  const DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT = "\u592A\u533B\u7F72\u56DE\u6267\u5DF2\u63A5\u53D7\uFF1B\u5BA1\u8BA1\u65E0\u56DE\u6267";
9
9
  const DOCTOR_OUTPUT_TOOL_DESCRIPTION = "\u63D0\u4EA4\u552F\u4E00\u7EC8\u5C40\u5355\u6848\u8BC1\u8BCD\uFF1Bcompleted \u5141\u8BB8\u7A7A findings\u3002";
10
10
  const DOCTOR_TARGET_KINDS = ["law", "gate", "template", "station", "seat"];
@@ -116,7 +116,7 @@ function validateDoctorOutput(value, _patient, _store) {
116
116
  }
117
117
  export {
118
118
  DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT,
119
- DOCTOR_ACCEPTED_TEXT,
119
+ DOCTOR_AUDIT_TOOL_NAME,
120
120
  DOCTOR_EVIDENCE_TOOL_NAME,
121
121
  DOCTOR_OUTPUT_TOOL_DESCRIPTION,
122
122
  DOCTOR_OUTPUT_TOOL_NAME,
@@ -1,4 +1,5 @@
1
1
  import { auditorRunDirectory } from "./auditor-dossier-tool.js";
2
+ import { packagedGateDecision, packagedGateStageLabel } from "./packaged-role-registry.js";
2
3
  import { GatekeeperDecisionError } from "./submission-errors.js";
3
4
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.js";
4
5
  import { GATEKEEPER_OUTPUT_TOOL_NAME, gatekeeperDecisionSchema, gatekeeperOutputSchema, } from "./package-contracts/gatekeeper-output.js";
@@ -6,23 +7,19 @@ import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
6
7
  export const INSPECTOR_OUTPUT_TOOL = INSPECTOR_OUTPUT_TOOL_NAME;
7
8
  export const NOTARY_OUTPUT_TOOL = "ak_notary_output";
8
9
  function gateSeatLabel(stage) {
9
- if (stage === "inspector")
10
- return "台院";
11
- if (stage === "auditor")
12
- return "审刑院";
13
- if (stage === "countersign")
14
- return "给事中";
15
- return "符宝郎";
10
+ return packagedGateStageLabel(stage) ?? stage;
16
11
  }
12
+ /** Subject kind → review officer. One table; default is notary. */
13
+ const GATE_OFFICER_BY_SUBJECT = {
14
+ worker_completion: "inspector",
15
+ judge_compliance: "auditor",
16
+ judge_draft: "notary",
17
+ countersign_verdict: "notary",
18
+ secretariat_verdict: "countersign",
19
+ };
17
20
  /** Subject kind → review officer (#753 countersign/notary, #756 judge/auditor + worker/inspector, #969 secretariat/countersign). */
18
21
  export function gateOfficerForSubject(subject) {
19
- if (subject.kind === "worker_completion")
20
- return "inspector";
21
- if (subject.kind === "judge_compliance")
22
- return "auditor";
23
- if (subject.kind === "secretariat_verdict")
24
- return "countersign";
25
- return "notary";
22
+ return GATE_OFFICER_BY_SUBJECT[subject.kind];
26
23
  }
27
24
  export { GatekeeperDecisionError } from "./submission-errors.js";
28
25
  /**
@@ -90,7 +87,7 @@ function projectOfficerDecision(officer, decision, fallbackStatus) {
90
87
  const record = readRecord(decision);
91
88
  // 给事中: only countersignStatus, strict three-word map; never generic status,
92
89
  // never unmapped raw word, never fallbackStatus (#969 / ADR 0040/0055).
93
- if (officer === "countersign") {
90
+ if (packagedGateDecision(officer) === "countersign-status") {
94
91
  const countersignStatus = record !== undefined && typeof record.countersignStatus === "string"
95
92
  ? record.countersignStatus
96
93
  : undefined;
@@ -244,7 +241,9 @@ export const OFFICER_CONCLUSION_REASK = "上次交卷的结论不是 pass、boun
244
241
  export const COUNTERSIGN_CONCLUSION_REASK = "上次交卷的 countersignStatus 不是 converged、continue、escalate 三态之一。请重新输出,countersignStatus 写明其一;封驳或上呈的话就是给对方看的原文。";
245
242
  /** Pick the re-ask line for the officer under review. */
246
243
  export function officerConclusionReask(officer) {
247
- return officer === "countersign" ? COUNTERSIGN_CONCLUSION_REASK : OFFICER_CONCLUSION_REASK;
244
+ return packagedGateDecision(officer) === "countersign-status"
245
+ ? COUNTERSIGN_CONCLUSION_REASK
246
+ : OFFICER_CONCLUSION_REASK;
248
247
  }
249
248
  /**
250
249
  * Summon (via injected seam) + project. Default summonGateOfficer drive lives
@@ -1,5 +1,4 @@
1
1
  const GLEANER_LEFT_OUTPUT_TOOL_NAME = "ak_gleaner_left_output";
2
- const GLEANER_LEFT_ACCEPTED_TEXT = "\u5DE6\u62FE\u9057\u56DE\u6267\u5DF2\u63A5\u53D7";
3
2
  const GLEANER_LEFT_BASE_FLAG = {
4
3
  name: "ak-gleaner-left-base",
5
4
  definition: {
@@ -23,7 +22,6 @@ function validateRecordedGleanerLeftOutput(value) {
23
22
  throw new Error("Gleaner-left output has no execution discriminator");
24
23
  }
25
24
  export {
26
- GLEANER_LEFT_ACCEPTED_TEXT,
27
25
  GLEANER_LEFT_BASE_FLAG,
28
26
  GLEANER_LEFT_OUTPUT_TOOL_NAME,
29
27
  validateRecordedGleanerLeftOutput