@akagilnc/pi-workflow-roles 0.1.5066 → 0.1.5079

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.
@@ -1,13 +1,24 @@
1
- /** One shared terminating receipt contract for review officers (#1028). */
1
+ /** One shared terminating receipt contract for review officers (#1028 / #1055). */
2
2
  import { Type } from "typebox";
3
- import { openToolObject } from "./open-tool-schema.js";
4
3
  import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.js";
5
4
  export const REVIEW_SUBMISSION_OUTPUT_TOOL_NAME = "ak_submission_output";
6
- /** Open receipt: the queue reads only status; all other submitted fields ride unchanged. */
7
- export const reviewSubmissionSchema = withTerminatingOutputDeclarations(openToolObject(Type.Object({
8
- status: Type.Optional(Type.Unknown({
9
- description: "converged | continue | escalate — 非三态时请重读后重交,勿改标。",
10
- })),
5
+ /** Single schema copy of the review queue words. Runtime membership reads this set. */
6
+ export const REVIEW_QUEUE_WORDS = ["converged", "continue", "escalate"];
7
+ export const REVIEW_QUEUE_STATUSES = new Set(REVIEW_QUEUE_WORDS);
8
+ /**
9
+ * One object root. Codex structured output rejects a root anyOf, and status
10
+ * stays a required three-state enum with no omitted-status failure branch.
11
+ * A review seat that can still submit records a real external-dependency
12
+ * failure as escalate plus the existing infrastructureFailure field.
13
+ */
14
+ export const reviewSubmissionSchema = withTerminatingOutputDeclarations(Type.Object({
15
+ status: Type.Union([
16
+ Type.Literal(REVIEW_QUEUE_WORDS[0]),
17
+ Type.Literal(REVIEW_QUEUE_WORDS[1]),
18
+ Type.Literal(REVIEW_QUEUE_WORDS[2]),
19
+ ], {
20
+ description: "审核队列三态枚举,必填。审核席仍能交卷而完成审核所需的外部依赖发生真实基础设施失败时,status 取 escalate,细节写入 infrastructureFailure。不得省略 status,不得用另两个枚举值充当放行或封驳。包侧不因格式拒收。",
21
+ }),
11
22
  findings: Type.Optional(Type.Unknown({ description: "审核发现,原样留存" })),
12
23
  reason: Type.Optional(Type.Unknown({ description: "上呈理由" })),
13
24
  violations: Type.Optional(Type.Unknown({ description: "违规条目" })),
@@ -17,4 +28,4 @@ export const reviewSubmissionSchema = withTerminatingOutputDeclarations(openTool
17
28
  note: Type.Optional(Type.Unknown({ description: "裁决附注" })),
18
29
  evidence: Type.Optional(Type.Unknown({ description: "留存证据" })),
19
30
  decisionGate: Type.Optional(Type.Unknown({ description: "需人权威处置的问题与选项;可说明 question、options,原样留存" })),
20
- })));
31
+ }, { additionalProperties: true }));
@@ -50,6 +50,23 @@ export class WorkerUnfinishedReasonReminderError extends Error {
50
50
  this.name = "WorkerUnfinishedReasonReminderError";
51
51
  }
52
52
  }
53
+ /**
54
+ * Host constraint missed a routing discriminator (#1055).
55
+ * Names live only in the submission schema. This notice carries the field and the received value.
56
+ */
57
+ export function unreadableDiscriminatorNotice(field, received) {
58
+ if (received === undefined)
59
+ return `读不出 ${field}`;
60
+ return `读不出 ${field}:${readableGateItem(received)}`;
61
+ }
62
+ /** The received discriminator only — never the rest of the receipt. */
63
+ export function receivedDiscriminator(receipt, field) {
64
+ if (receipt === null || typeof receipt !== "object" || Array.isArray(receipt))
65
+ return undefined;
66
+ if (!Object.hasOwn(receipt, field))
67
+ return undefined;
68
+ return receipt[field];
69
+ }
53
70
  /**
54
71
  * Parent seat status unreadable for queueing (#753).
55
72
  * Correctable back to the parent itself — not a gate officer bounce, no officer field.
@@ -6,7 +6,8 @@ import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
6
6
  import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from "./sitian-facade.js";
7
7
  import { findRunDirectoryById } from "./public-cli/run-lifecycle.js";
8
8
  import { isCorrectableExecuteError } from "./submission-correctable-error.js";
9
- import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
9
+ import { failOnInfrastructureFailureDeclaration, infrastructureFailureDiagnostic } from "./package-contracts/terminating-infrastructure.js";
10
+ import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.js";
10
11
  /**
11
12
  * Admitted run identity for the ledger subject.
12
13
  * Prefer AK_ROLE_RUN_DIR via sole runDirectory→runId parser (public-CLI correlation);
@@ -480,11 +481,22 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
480
481
  });
481
482
  let result;
482
483
  try {
483
- failOnInfrastructureFailureDeclaration(params, {
484
- failInfrastructure(error, ctx) {
485
- failInfrastructure(error, ctx);
486
- },
487
- }, context, toolCallId, tool.bounceInfrastructureDeclaration);
484
+ // A review escalate that carries the failure declaration is the
485
+ // seat's own submission. Every other declaration stays on the
486
+ // host-failure seam. Tool identity comes from the registry map.
487
+ const reviewEscalate = tool.name === REVIEW_SUBMISSION_OUTPUT_TOOL_NAME
488
+ && infrastructureFailureDiagnostic(params) !== undefined
489
+ && params !== null
490
+ && typeof params === "object"
491
+ && !Array.isArray(params)
492
+ && params.status === "escalate";
493
+ if (!reviewEscalate) {
494
+ failOnInfrastructureFailureDeclaration(params, {
495
+ failInfrastructure(error, ctx) {
496
+ failInfrastructure(error, ctx);
497
+ },
498
+ }, context, toolCallId, tool.bounceInfrastructureDeclaration);
499
+ }
488
500
  result = await tool.execute(toolCallId, params, signal, update, context);
489
501
  }
490
502
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5066",
3
+ "version": "0.1.5079",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,11 +22,11 @@
22
22
  对于修复票,判断根因是什么、证据在哪;对于新功能设计票,判断设计是不是合理,是不是陛下想要的方向。以及因果链本身站不站得住。
23
23
  此步不成立即封驳,不必再走后三步。
24
24
 
25
- **三、相抵** 票面,相关票与在飞分支与法源有无抵触。
25
+ **三、相抵** 票面,相关票与在飞分支与法源有无抵触。票面自相抵同为相抵。
26
26
 
27
27
  **四、缺失** 法源要求的,票面有没有漏:适用法只查两类:票面与某条 ADR 决定相抵的,和把某条决定留给别的票承接的,票面须写明该条与「由 #x 承接」;这两类之外的 ADR 没引不算缺失,不得为此封驳;验收面须覆盖票面每条主张;票面里每一条行为都须答得出《送庭前三问》第一问——由哪条接缝、哪条 run 担它:起、停、续、终、接各归谁没写的行为,即缺失;前置产物、决定、答复须在;承重主张的源头不存在、不支持、已过时,同为缺失。缺失列表在能力范围内一次扫完,尽量避免因缺失导致多轮往返。
28
28
 
29
- **五、擅加** 票面有没有超出法源的机制、约束、验收、术语。
29
+ **五、擅加** 票面有没有超出法源的机制、约束、验收、术语。票面按类各定一套行为或文案的,就是在要分类机制;陛下没点名要的,上呈,附单一做法。
30
30
 
31
31
  各步取证按《审法·取证授权》:工具与方式不受限,读码、开卷、传召、下探针皆可;不得删除、覆写或移动 worktree 与临时目录之外的既存路径(《审法·取证边界》)。交付时候清理回原样
32
32
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  * 陛下要什么——先用一两句写出他真正要的结果;后附撑这个读法的原话,逐字,带 uuid;源卷没有 id 的,给起居录里的来源标识。他的质问——他在问这张票、这条约束凭什么存在——要用事实或法源答到他问的那个点上,不是答字面;答不到的票不送庭,上呈。没有原话的票,写它出自哪张父票(授权在父票的对话里,给 uuid),或现状违反了哪条法源。
6
6
  * 现象——现在看得见的错是什么;一个能复现、能开卷核的实例就够。新造的东西没有现象,写要造什么。
7
- * 目的——改完以后看得见的行为是什么。
7
+ * 目的——改完以后看得见的行为是什么。一句;不按现象实例逐个定行为或文案,实例只作验收样本。
8
8
  * 怎么验——从真实入口走一遍,看见什么算对;覆盖票面每条主张。
9
9
  * 适用法——`docs/adr/...md` 路径,一行一条,不必复述其决定。只有本票与它某条决定相抵、或把某条决定留给别的票承接时,才在那一行写明是哪一条。
10
10
 
@@ -4,7 +4,8 @@ import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
4
4
  import type { HostContext } from "./host-contracts.ts";
5
5
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
6
6
  import type { PublicSummonResult } from "./public-role-summons.ts";
7
- import { OFFICER_CONCLUSION_REASK } from "./gatekeeper-role.ts";
7
+ import { officerConclusionReask } from "./gatekeeper-role.ts";
8
+ import { receivedDiscriminator } from "./submission-errors.ts";
8
9
  import { coalesceSubmissionRows } from "./public-cli/terminal.ts";
9
10
  import { readableGateItem } from "./readable-gate-item.ts";
10
11
 
@@ -226,7 +227,7 @@ export async function runComplianceAudit(options: RunComplianceAuditOptions): Pr
226
227
  const summoned = await summon(subject, runDirectory, options.signal, reask, submission);
227
228
  const decision = await projectAuditorTerminal(summoned);
228
229
  if (decision.status === "received") {
229
- reask = OFFICER_CONCLUSION_REASK;
230
+ reask = officerConclusionReask(receivedDiscriminator(decision.reply, "status"));
230
231
  continue;
231
232
  }
232
233
  return decision;
@@ -5,7 +5,7 @@
5
5
  * is defaulted, rewritten, or dropped (ADR 0055).
6
6
  */
7
7
 
8
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
8
+ import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
9
9
 
10
10
  export const COUNTERSIGN_OUTPUT_TOOL_NAME = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
11
11
 
@@ -37,7 +37,7 @@ export function validateRecordedCountersignOutput(verdict: unknown): Countersign
37
37
  if (typeof status !== "string") {
38
38
  throw new Error("Countersign verdict has no execution discriminator");
39
39
  }
40
- if (["converged", "continue", "escalate"].includes(status)) {
40
+ if (REVIEW_QUEUE_STATUSES.has(status)) {
41
41
  return verdict as CountersignVerdict;
42
42
  }
43
43
  throw new Error("Countersign verdict has no execution discriminator");
@@ -11,13 +11,9 @@ export type { CountersignVerdict };
11
11
  // #836 r16 class 1: fix/note/decisionGate are LLM/human-read narrative content —
12
12
  // no code branches on their length or nested presence. `status` alone is the
13
13
  // machine discriminator the queue reads.
14
- // #836 (2026-09-11 御批 / ADR 0003 Amendment): registered-to-provider closed
15
- // value domains reject an unknown status before the submission
16
- // ledger ever records it, defeating 读不出三态→resume 说话者本人. Kept open
17
- // (Type.Unknown) like every other gate-queue status field in this package
18
- // (notary/auditor/gatekeeper/navigator); src/role-runtime.ts:997-1025 still
19
- // classifies the recorded value and reasks the countersign itself when it
20
- // isn't one of the three states — code, not the transport, does that work.
14
+ // #1055: the three-state field is an enum+required host generation constraint
15
+ // on reviewSubmissionSchema. Package execute still does not reject; an unreadable
16
+ // value reasks the countersign itself with the received value.
21
17
  /** 给事中票庭审读五问的交卷形状(ADR 0074);形状指引,非 schema 闸。 */
22
18
  export const countersignVerdictSchema = reviewSubmissionSchema;
23
19
  export type CountersignVerdictParameters = import("./review-submission.ts").ReviewSubmission;
@@ -4,9 +4,9 @@ import type { HostContext } from "./host-contracts.ts";
4
4
  import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
5
5
  import { packagedGateStageLabel } from "./packaged-role-registry.ts";
6
6
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
7
- import { GatekeeperDecisionError } from "./submission-errors.ts";
7
+ import { GatekeeperDecisionError, receivedDiscriminator, unreadableDiscriminatorNotice } from "./submission-errors.ts";
8
8
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.ts";
9
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
9
+ import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
10
10
  import {
11
11
  GATEKEEPER_OUTPUT_TOOL_NAME,
12
12
  gatekeeperDecisionSchema,
@@ -66,6 +66,8 @@ export type GatekeeperResult =
66
66
  readonly status: "needs_reask";
67
67
  readonly officer: GateOfficer;
68
68
  readonly receipt: unknown;
69
+ /** Discriminator the queue already read. Not written back onto the receipt. */
70
+ readonly receivedStatus?: unknown;
69
71
  readonly runId?: string;
70
72
  }
71
73
  | { readonly status: "no_receipt"; readonly stage: GateOfficer; readonly reason: string; readonly facts: NoReceiptLifecycleFacts }
@@ -210,12 +212,9 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
210
212
  * `fallbackStatus` is the terminal outcome.status when the receipt body has no status key
211
213
  * (keeps missing-args sentinel intact as the receipt).
212
214
  */
213
- /** Map the shared review verdict onto the gate queue words (#1028). */
215
+ /** Map the shared review verdict onto the gate queue words (#1028 / #1055). */
214
216
  function reviewQueueStatus(status: string): string | undefined {
215
- if (status === "converged") return "converged";
216
- if (status === "continue") return "continue";
217
- if (status === "escalate") return "escalate";
218
- return undefined;
217
+ return REVIEW_QUEUE_STATUSES.has(status) ? status : undefined;
219
218
  }
220
219
 
221
220
  function projectOfficerDecision(
@@ -235,7 +234,7 @@ function projectOfficerDecision(
235
234
  if (queueStatus === "continue" || queueStatus === "escalate") {
236
235
  return { status: queueStatus, officer, receipt };
237
236
  }
238
- return { status: "needs_reask", officer, receipt };
237
+ return { status: "needs_reask", officer, receipt, receivedStatus: status };
239
238
  }
240
239
 
241
240
  /**
@@ -371,12 +370,14 @@ function projectOfficerTerminal(
371
370
  summoned,
372
371
  );
373
372
  }
373
+ const receipt = thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome);
374
374
  return withOfficerRunId(
375
375
  {
376
376
  status: "needs_reask",
377
377
  officer,
378
378
  // This-court receipt only (#879).
379
- receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
379
+ receipt,
380
+ receivedStatus: receivedDiscriminator(receipt, "status"),
380
381
  },
381
382
  summoned,
382
383
  );
@@ -391,16 +392,11 @@ export type GatekeeperProjection = {
391
392
  };
392
393
 
393
394
  /**
394
- * Plain-language re-ask when the officer conclusion is not converged|continue|escalate.
395
- * Not a packaged engine handbook line (#755 exception for 读不出三态).
396
- * Every review officer uses the same `status` field and three queue words (#1028).
395
+ * Officer conclusion was missing or not a queue word (#1055).
396
+ * `received` is the status value only, not the rest of the receipt.
397
397
  */
398
- export const OFFICER_CONCLUSION_REASK =
399
- "上次交卷的 status 不是 converged、continue、escalate 三态之一。请重新输出,status 写明其一;continue 或 escalate 的话就是给对方看的原文。" as const;
400
-
401
- /** Pick the re-ask line for the officer under review. */
402
- export function officerConclusionReask(officer: GateOfficer): string {
403
- return OFFICER_CONCLUSION_REASK;
398
+ export function officerConclusionReask(received: unknown): string {
399
+ return unreadableDiscriminatorNotice("status", received);
404
400
  }
405
401
 
406
402
  /**
@@ -4,7 +4,7 @@
4
4
  * Dual path: gate-dispatched and independently callable (#568 / ADR 0074).
5
5
  */
6
6
 
7
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
7
+ import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "./review-submission.ts";
8
8
 
9
9
  export const INSPECTOR_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
10
10
  export const INSPECTOR_SOURCE_RUN_FLAG = {
@@ -30,7 +30,10 @@ export function validateRecordedInspectorOutput(value: unknown): InspectorOutput
30
30
  } catch {
31
31
  throw new Error("Inspector output has no execution discriminator");
32
32
  }
33
- if (status === "converged" || status === "continue" || status === "escalate") {
33
+ if (typeof status !== "string") {
34
+ throw new Error("Inspector output has no execution discriminator");
35
+ }
36
+ if (REVIEW_QUEUE_STATUSES.has(status)) {
34
37
  return value as InspectorOutput;
35
38
  }
36
39
  throw new Error("Inspector output has no execution discriminator");
package/src/judge-role.ts CHANGED
@@ -1,19 +1,15 @@
1
1
  import type { RoleHost, HostContext, HostToolResult, HostGatekeeperActions } from "./host-contracts.ts";
2
2
  import type { Static } from "typebox";
3
- import { reviewSubmissionSchema } from "./review-submission.ts";
4
- import { GatekeeperDecisionError, ParentQueueReaskError } from "./submission-errors.ts";
3
+ import { REVIEW_QUEUE_STATUSES, reviewSubmissionSchema } from "./review-submission.ts";
4
+ import { GatekeeperDecisionError, ParentQueueReaskError, unreadableDiscriminatorNotice } from "./submission-errors.ts";
5
5
  import { projectGatekeeperEscalation } from "./audit-escalation.ts";
6
6
 
7
7
  import { readableGateItem } from "./readable-gate-item.ts";
8
8
  import {
9
9
  JUDGE_OUTPUT_TOOL_NAME,
10
- validateAcceptedJudgeDetails,
11
10
  type JudgeVerdict,
12
11
  } from "./package-contracts/judge-output.ts";
13
12
 
14
- const JUDGE_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]);
15
- const JUDGE_STATUS_REASK =
16
- "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
17
13
 
18
14
  export { JUDGE_OUTPUT_TOOL_NAME };
19
15
  export type { JudgeVerdict };
@@ -25,8 +21,6 @@ export type { JudgeVerdict };
25
21
  // The shared open schema preserves role-specific prose without adding another contract.
26
22
  export const judgeVerdictSchema = reviewSubmissionSchema;
27
23
 
28
- type JudgeVerdictParameters = Static<typeof judgeVerdictSchema>;
29
-
30
24
  export type JudgeRoleDependencies = {
31
25
  loadSoul(): Promise<string>;
32
26
  };
@@ -36,10 +30,6 @@ export type JudgeRoleHostActions = HostGatekeeperActions & {
36
30
  bindPriorGatePass(toolCallId: string, receipt: unknown): void;
37
31
  };
38
32
 
39
- export function validateVerdict(verdict: JudgeVerdictParameters): JudgeVerdict {
40
- return validateAcceptedJudgeDetails(verdict);
41
- }
42
-
43
33
 
44
34
  export function createJudgeRoleRuntime(
45
35
  pi: RoleHost,
@@ -70,19 +60,21 @@ export function createJudgeRoleRuntime(
70
60
  && typeof (parameters as Record<string, unknown>).status === "string"
71
61
  ? (parameters as Record<string, unknown>).status as string
72
62
  : undefined;
73
- if (rawStatus === undefined || !JUDGE_QUEUE_STATUSES.has(rawStatus)) {
74
- throw new ParentQueueReaskError(JUDGE_STATUS_REASK);
63
+ if (rawStatus === undefined || !REVIEW_QUEUE_STATUSES.has(rawStatus)) {
64
+ const received = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
65
+ ? (parameters as Record<string, unknown>).status
66
+ : undefined;
67
+ throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", received));
75
68
  }
76
69
  if (rawStatus === "escalate") {
77
70
  // Parent escalate → throw to caller as-is; officers do not attend (#753 / #756).
78
- const verdict = validateVerdict(parameters);
79
71
  return {
80
72
  content: [],
81
- details: verdict,
73
+ details: parameters,
82
74
  terminate: true as const,
83
75
  };
84
76
  }
85
- const verdict = validateVerdict(parameters);
77
+ // Queue membership above is the only three-state check. The receipt is not re-judged.
86
78
  // Candidate verdict is already on the parent session books as this
87
79
  // tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
88
80
  // #753: 符宝郎内闸 — continue returns the raw receipt as a nonterminal tool result.
@@ -101,7 +93,7 @@ export function createJudgeRoleRuntime(
101
93
  if (draftPass?.status === "continue") {
102
94
  return {
103
95
  content: [{ type: "text" as const, text: readableGateItem(draftPass.receipt) }],
104
- details: verdict,
96
+ details: parameters,
105
97
  terminate: false,
106
98
  };
107
99
  }
@@ -121,18 +113,18 @@ export function createJudgeRoleRuntime(
121
113
  if (compliancePass?.status === "continue") {
122
114
  return {
123
115
  content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
124
- details: verdict,
116
+ details: parameters,
125
117
  terminate: false,
126
118
  };
127
119
  }
128
120
  return {
129
121
  content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
130
- details: verdict,
122
+ details: parameters,
131
123
  terminate: true as const,
132
124
  };
133
125
  } catch (error) {
134
126
  if (error instanceof GatekeeperDecisionError && error.result.status === "escalate") {
135
- return projectGatekeeperEscalation(error.result, verdict);
127
+ return projectGatekeeperEscalation(error.result, parameters);
136
128
  }
137
129
  throw error;
138
130
  }
@@ -12,7 +12,7 @@ function declarationIdentity(schema: TSchema): string {
12
12
  return JSON.stringify(semantic);
13
13
  }
14
14
 
15
- /** Collapse transport variants into one provider-compatible, zero-required open object. */
15
+ /** Collapse transport variants into one provider-compatible open object. Non-three-state tools stay zero-required. */
16
16
  export function openToolObjectFromUnion(schema: UnionSchema): TSchema {
17
17
  const declarations = new Map<string, TSchema[]>();
18
18
  for (const variant of schema.anyOf) {
@@ -35,10 +35,15 @@ export function openToolObjectFromUnion(schema: UnionSchema): TSchema {
35
35
  return object;
36
36
  }
37
37
 
38
- /** Open a pre-existing object transport schema without changing declarations. */
39
- export function openToolObject(schema: TSchema & { properties: Record<string, TSchema> }): TSchema {
38
+ /** Open a pre-existing object transport schema. Every field stays optional. */
39
+ export function openToolObject(
40
+ schema: TSchema & { properties: Record<string, TSchema> },
41
+ ): TSchema {
40
42
  const object = Type.Object(
41
- Object.fromEntries(Object.entries(schema.properties).map(([name, declaration]) => [name, Type.Optional(described(name, declaration))])),
43
+ Object.fromEntries(Object.entries(schema.properties).map(([name, declaration]) => [
44
+ name,
45
+ Type.Optional(described(name, declaration)),
46
+ ])),
42
47
  { additionalProperties: true },
43
48
  );
44
49
  (object as unknown as { required: string[] }).required = [];
@@ -1,7 +1,7 @@
1
1
  /** Package-owned Judge output leaf — no role registration surface. */
2
2
 
3
3
 
4
- import { REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "../review-submission.ts";
4
+ import { REVIEW_QUEUE_STATUSES, REVIEW_SUBMISSION_OUTPUT_TOOL_NAME } from "../review-submission.ts";
5
5
 
6
6
  export const JUDGE_OUTPUT_TOOL_NAME: string = REVIEW_SUBMISSION_OUTPUT_TOOL_NAME;
7
7
 
@@ -39,9 +39,7 @@ export function validateAcceptedJudgeDetails(verdict: unknown): JudgeVerdict {
39
39
  if (typeof status !== "string") {
40
40
  throw new Error("Judge verdict has no execution discriminator");
41
41
  }
42
- if (
43
- ["converged", "continue", "escalate"].includes(status)
44
- ) {
42
+ if (REVIEW_QUEUE_STATUSES.has(status)) {
45
43
  return verdict as JudgeVerdict;
46
44
  }
47
45
  throw new Error("Judge verdict has no execution discriminator");
@@ -50,8 +50,9 @@ const infrastructureFailureDeclarationSchema = Type.Object(
50
50
 
51
51
  /**
52
52
  * Compose declarations shared by terminating output tools: infrastructure failure
53
- * and the optional role-asserted ticket identity. Returns an open object (additionalProperties: true,
54
- * required: []) with the base schema's properties plus the shared declaration.
53
+ * and the optional role-asserted ticket identity. Returns an open object (additionalProperties: true)
54
+ * with the base schema's properties plus the shared declaration. Incoming required
55
+ * keys that still exist are kept; every other key stays optional.
55
56
  * Static typing is preserved on the base (`as S`), so existing
56
57
  * `Static<typeof ...>` derived parameter types are unchanged.
57
58
  */
@@ -78,7 +79,11 @@ export function withTerminatingOutputDeclarations<
78
79
  ],
79
80
  };
80
81
  const object = Type.Object(properties, { additionalProperties: true });
81
- (object as unknown as { required: string[] }).required = [];
82
+ const incoming = (schema as { required?: unknown }).required;
83
+ const preserved = Array.isArray(incoming)
84
+ ? incoming.filter((key): key is string => typeof key === "string" && Object.hasOwn(properties, key))
85
+ : [];
86
+ (object as unknown as { required: string[] }).required = preserved;
82
87
  return object as unknown as S;
83
88
  }
84
89
 
@@ -115,7 +120,7 @@ function isInfrastructureFailureDeclaration(
115
120
  }
116
121
 
117
122
  /** Non-empty trimmed diagnostic from the declaration, else undefined. */
118
- function infrastructureFailureDiagnostic(
123
+ export function infrastructureFailureDiagnostic(
119
124
  parameters: unknown,
120
125
  ): string | undefined {
121
126
  if (!isInfrastructureFailureDeclaration(parameters)) return undefined;
@@ -1,18 +1,33 @@
1
- /** One shared terminating receipt contract for review officers (#1028). */
1
+ /** One shared terminating receipt contract for review officers (#1028 / #1055). */
2
2
  import { Type } from "typebox";
3
3
 
4
- import { openToolObject } from "./open-tool-schema.ts";
5
4
  import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
6
5
 
7
6
  export const REVIEW_SUBMISSION_OUTPUT_TOOL_NAME = "ak_submission_output" as const;
8
7
 
9
- /** Open receipt: the queue reads only status; all other submitted fields ride unchanged. */
8
+ /** Single schema copy of the review queue words. Runtime membership reads this set. */
9
+ export const REVIEW_QUEUE_WORDS = ["converged", "continue", "escalate"] as const;
10
+ export type ReviewQueueWord = (typeof REVIEW_QUEUE_WORDS)[number];
11
+ export const REVIEW_QUEUE_STATUSES: ReadonlySet<string> = new Set(REVIEW_QUEUE_WORDS);
12
+
13
+ /**
14
+ * One object root. Codex structured output rejects a root anyOf, and status
15
+ * stays a required three-state enum with no omitted-status failure branch.
16
+ * A review seat that can still submit records a real external-dependency
17
+ * failure as escalate plus the existing infrastructureFailure field.
18
+ */
10
19
  export const reviewSubmissionSchema = withTerminatingOutputDeclarations(
11
- openToolObject(
12
- Type.Object({
13
- status: Type.Optional(Type.Unknown({
14
- description: "converged | continue | escalate — 非三态时请重读后重交,勿改标。",
15
- })),
20
+ Type.Object({
21
+ status: Type.Union(
22
+ [
23
+ Type.Literal(REVIEW_QUEUE_WORDS[0]),
24
+ Type.Literal(REVIEW_QUEUE_WORDS[1]),
25
+ Type.Literal(REVIEW_QUEUE_WORDS[2]),
26
+ ],
27
+ {
28
+ description: "审核队列三态枚举,必填。审核席仍能交卷而完成审核所需的外部依赖发生真实基础设施失败时,status 取 escalate,细节写入 infrastructureFailure。不得省略 status,不得用另两个枚举值充当放行或封驳。包侧不因格式拒收。",
29
+ },
30
+ ),
16
31
  findings: Type.Optional(Type.Unknown({ description: "审核发现,原样留存" })),
17
32
  reason: Type.Optional(Type.Unknown({ description: "上呈理由" })),
18
33
  violations: Type.Optional(Type.Unknown({ description: "违规条目" })),
@@ -22,8 +37,7 @@ export const reviewSubmissionSchema = withTerminatingOutputDeclarations(
22
37
  note: Type.Optional(Type.Unknown({ description: "裁决附注" })),
23
38
  evidence: Type.Optional(Type.Unknown({ description: "留存证据" })),
24
39
  decisionGate: Type.Optional(Type.Unknown({ description: "需人权威处置的问题与选项;可说明 question、options,原样留存" })),
25
- }),
26
- ),
40
+ }, { additionalProperties: true }),
27
41
  );
28
42
 
29
43
  export type ReviewSubmission = { readonly status?: unknown; readonly [key: string]: unknown };
@@ -314,7 +314,8 @@ export {
314
314
  gateOfficerForSubject,
315
315
  };
316
316
  export type { GatekeeperResult, GatekeeperSubject, SubmissionGateNonPassResult, GateOfficer, RunGatekeeperOptions } from "./gatekeeper-role.ts";
317
- import { ParentQueueReaskError } from "./submission-errors.ts";
317
+ import { ParentQueueReaskError, unreadableDiscriminatorNotice } from "./submission-errors.ts";
318
+ import { REVIEW_QUEUE_STATUSES } from "./review-submission.ts";
318
319
 
319
320
  export {
320
321
  DOCTOR_EVIDENCE_TOOL_NAME,
@@ -872,8 +873,6 @@ function readDiaristRunCoordinates(ctx: HostContext): {
872
873
  const DIARIST_BOUNDS_REASK =
873
874
  "边界无法使用。请重交 sessions:每卷 path + ranges,每端以原生 id 或本轮行号二选一指名。" as const;
874
875
  const DIARIST_ROUTING_STATUSES = new Set(["completed", "escalate"]);
875
- const DIARIST_STATUS_REASK =
876
- "status 不是 completed、escalate 之一。请重新交卷,status 写明其一。" as const;
877
876
 
878
877
  /**
879
878
  * #708 / #779 / #901: 起居郎 public seat on the shared filed-officer envelope.
@@ -900,7 +899,7 @@ export function createDiaristRoleRuntime(
900
899
  // before interpreting any other submitted field; caller-owned data stays open.
901
900
  const status = submitted?.status;
902
901
  if (typeof status !== "string" || !DIARIST_ROUTING_STATUSES.has(status)) {
903
- throw new ParentQueueReaskError(DIARIST_STATUS_REASK);
902
+ throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", submitted?.status));
904
903
  }
905
904
  if (status === "escalate") return parameters;
906
905
  const assertion = readDiaristTicketAssertion(submitted);
@@ -943,26 +942,9 @@ export function createDiaristRoleRuntime(
943
942
  );
944
943
  }
945
944
 
946
- /** Shared review-submission status words the queue reads (#1028). */
947
- const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]);
948
-
949
- /**
950
- * Plain-language re-ask when status is not a known queue word.
951
- * Back to countersign itself — notary is not summoned (#753).
952
- */
953
- const COUNTERSIGN_STATUS_REASK =
954
- "status 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
955
-
956
- /** Secretariat status words the submission gate reads. */
945
+ /** Secretariat status words the submission gate reads. Not the review three-state field. */
957
946
  const SECRETARIAT_QUEUE_STATUSES = new Set(["converged", "escalate"]);
958
947
 
959
- /**
960
- * Plain-language re-ask when secretariatStatus is not a known queue word.
961
- * Back to secretariat itself — 给事中 is not summoned for escalation.
962
- */
963
- const SECRETARIAT_STATUS_REASK =
964
- "secretariatStatus 不是 converged、escalate 之一。请重新交卷,status 写明其一。" as const;
965
-
966
948
  /**
967
949
  * Secretariat output is terminal; converged submissions enter the shared
968
950
  * submission gate on every host that mounts gatekeeper actions.
@@ -989,7 +971,9 @@ export function createSecretariatRoleRuntime(
989
971
  ? record.secretariatStatus
990
972
  : undefined;
991
973
  if (status === undefined || !SECRETARIAT_QUEUE_STATUSES.has(status)) {
992
- throw new ParentQueueReaskError(SECRETARIAT_STATUS_REASK);
974
+ throw new ParentQueueReaskError(
975
+ unreadableDiscriminatorNotice("secretariatStatus", record?.secretariatStatus),
976
+ );
993
977
  }
994
978
  if (status === "escalate") {
995
979
  // Parent escalate → throw to caller as-is; countersign does not attend.
@@ -1132,10 +1116,10 @@ export function createCountersignRoleRuntime(
1132
1116
  record !== undefined && typeof record.status === "string"
1133
1117
  ? record.status
1134
1118
  : undefined;
1135
- if (status === undefined || !COUNTERSIGN_QUEUE_STATUSES.has(status)) {
1119
+ if (status === undefined || !REVIEW_QUEUE_STATUSES.has(status)) {
1136
1120
  // Parent status unreadable → back to countersign itself; do not summon notary,
1137
1121
  // do not forge an officer bounce face (#753).
1138
- throw new ParentQueueReaskError(COUNTERSIGN_STATUS_REASK);
1122
+ throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", record?.status));
1139
1123
  }
1140
1124
  if (status === "escalate") {
1141
1125
  // Parent escalate → throw to caller as-is; notary does not attend (#753).
@@ -58,6 +58,22 @@ export class WorkerUnfinishedReasonReminderError extends Error {
58
58
  }
59
59
  }
60
60
 
61
+ /**
62
+ * Host constraint missed a routing discriminator (#1055).
63
+ * Names live only in the submission schema. This notice carries the field and the received value.
64
+ */
65
+ export function unreadableDiscriminatorNotice(field: string, received: unknown): string {
66
+ if (received === undefined) return `读不出 ${field}`;
67
+ return `读不出 ${field}:${readableGateItem(received)}`;
68
+ }
69
+
70
+ /** The received discriminator only — never the rest of the receipt. */
71
+ export function receivedDiscriminator(receipt: unknown, field: string): unknown {
72
+ if (receipt === null || typeof receipt !== "object" || Array.isArray(receipt)) return undefined;
73
+ if (!Object.hasOwn(receipt, field)) return undefined;
74
+ return (receipt as Record<string, unknown>)[field];
75
+ }
76
+
61
77
  /**
62
78
  * Parent seat status unreadable for queueing (#753).
63
79
  * Correctable back to the parent itself — not a gate officer bounce, no officer field.