@akagilnc/pi-workflow-roles 0.1.5368 → 0.1.5375

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 (165) hide show
  1. package/dist/acp-host/production-host.js +719 -776
  2. package/dist/activation-ledger-git.js +2 -2
  3. package/dist/activation-ledger-topology.js +1 -7
  4. package/dist/analyst-entry.js +4 -8
  5. package/dist/analyst-gate-cycles-read.js +3 -11
  6. package/dist/analyst-index.js +3 -9
  7. package/dist/analyst-ledger.js +1 -11
  8. package/dist/analyst-metric-families/acceptance-success-rework.js +1 -3
  9. package/dist/analyst-metric-families/round-timeline.js +1 -3
  10. package/dist/analyst-page.js +2 -2
  11. package/dist/archivist-record-topology.js +2 -2
  12. package/dist/audit-escalation.js +2 -1
  13. package/dist/auditor-soul.js +2 -1
  14. package/dist/collector-config.js +6 -8
  15. package/dist/compliance-transport.js +2 -1
  16. package/dist/countersign-contracts.js +2 -1
  17. package/dist/diarist-contracts.js +3 -2
  18. package/dist/doctor-contracts.js +1 -3
  19. package/dist/doctor-evidence.js +7 -8
  20. package/dist/dossier-resolution.js +1 -3
  21. package/dist/engine-detour-usage.js +1 -3
  22. package/dist/gatekeeper-role.js +2 -9
  23. package/dist/gleaner-left-contracts.js +2 -1
  24. package/dist/headless-host/description.js +14 -17
  25. package/dist/headless-host/production-host.js +721 -780
  26. package/dist/host-session-record.js +3 -2
  27. package/dist/inspector-contracts.js +2 -1
  28. package/dist/ledger-session-read.js +1 -3
  29. package/dist/merger-contracts.js +9 -9
  30. package/dist/migrate-book-topology.js +348 -314
  31. package/dist/navigator-attendance.js +9 -11
  32. package/dist/navigator-invocation-identity.js +3 -2
  33. package/dist/navigator-public-session.js +3 -5
  34. package/dist/navigator-session-contracts.js +8 -10
  35. package/dist/navigator-work-base.js +5 -8
  36. package/dist/notary-contracts.js +3 -5
  37. package/dist/package-contracts/auditor-output.js +1 -3
  38. package/dist/package-contracts/fixer-packet.js +3 -6
  39. package/dist/package-contracts/gatekeeper-output.js +1 -3
  40. package/dist/package-contracts/navigator-output.js +8 -8
  41. package/dist/package-contracts/terminating-infrastructure.js +5 -9
  42. package/dist/package-contracts/terminating-tools.js +4 -3
  43. package/dist/public-cli/analyst-run.js +3 -2
  44. package/dist/public-cli/auto-resume.js +3 -8
  45. package/dist/public-cli/cli.js +9 -8
  46. package/dist/public-cli/config.js +8 -11
  47. package/dist/public-cli/countersign-run.js +1 -3
  48. package/dist/public-cli/host-providers.js +7 -14
  49. package/dist/public-cli/instruction-seat-run.js +4 -3
  50. package/dist/public-cli/invocation.js +9 -8
  51. package/dist/public-cli/main.js +1531 -1567
  52. package/dist/public-cli/post-admission.js +4 -3
  53. package/dist/public-cli/run-lifecycle.js +14 -26
  54. package/dist/public-cli/settlement.js +10 -26
  55. package/dist/public-role-summons.js +3 -2
  56. package/dist/receipt-delivery-policy.js +1 -3
  57. package/dist/role-run-placement.js +3 -5
  58. package/dist/role-run-relocation.js +12 -19
  59. package/dist/run-terminal-artifacts.js +1 -11
  60. package/dist/run-ticket-number.js +2 -4
  61. package/dist/session-assistant-usage.js +2 -1
  62. package/dist/session-dialogue.js +1 -14
  63. package/dist/sitian-reader.js +2 -4
  64. package/dist/submission-correctable-error.js +2 -1
  65. package/dist/submission-errors.js +2 -1
  66. package/dist/submission-gate.js +2 -1
  67. package/dist/submission-ledger.js +4 -5
  68. package/dist/ticket-provenance-contracts.js +1 -3
  69. package/dist/ticket-provenance.js +2 -1
  70. package/dist/typed-provider-http.js +4 -7
  71. package/dist/unknown-value.js +28 -0
  72. package/dist/user-dialogue-stdin.js +2 -1
  73. package/package.json +1 -1
  74. package/src/acp-host/role-turn-host.ts +4 -2
  75. package/src/activation-ledger-git.ts +3 -2
  76. package/src/activation-ledger-topology.ts +4 -12
  77. package/src/analyst-entry.ts +3 -11
  78. package/src/analyst-gate-cycles-read.ts +3 -15
  79. package/src/analyst-index.ts +3 -13
  80. package/src/analyst-ledger.ts +2 -16
  81. package/src/analyst-metric-families/acceptance-success-rework.ts +2 -4
  82. package/src/analyst-metric-families/round-timeline.ts +2 -4
  83. package/src/analyst-page.ts +2 -2
  84. package/src/archivist-record-topology.ts +2 -2
  85. package/src/audit-escalation.ts +2 -1
  86. package/src/auditor-soul.ts +3 -1
  87. package/src/book-topology-migration-placement.ts +5 -16
  88. package/src/book-topology-migrators-t11.ts +16 -15
  89. package/src/book-topology-mixed-volume-migrators.ts +3 -6
  90. package/src/book-topology-record-class-migrators.ts +7 -10
  91. package/src/book-topology-runs-migrator.ts +4 -3
  92. package/src/collector-config.ts +7 -9
  93. package/src/compliance-transport.ts +2 -1
  94. package/src/countersign-contracts.ts +2 -1
  95. package/src/diarist-contracts.ts +3 -2
  96. package/src/doctor-contracts.ts +3 -1
  97. package/src/doctor-evidence.ts +8 -7
  98. package/src/doctor-role.ts +4 -2
  99. package/src/dossier-resolution.ts +2 -4
  100. package/src/engine-detour-tool.ts +0 -2
  101. package/src/engine-detour-usage.ts +2 -4
  102. package/src/external-host-turn-loop.ts +3 -1
  103. package/src/factory-board.ts +1 -8
  104. package/src/gatekeeper-role.ts +2 -10
  105. package/src/gleaner-left-contracts.ts +3 -1
  106. package/src/headless-host/description.ts +15 -18
  107. package/src/headless-host/role-turn-host.ts +10 -9
  108. package/src/host-session-record.ts +4 -2
  109. package/src/inspector-contracts.ts +2 -1
  110. package/src/ledger-session-read.ts +2 -5
  111. package/src/merger-contracts.ts +10 -9
  112. package/src/navigator-attendance.ts +10 -12
  113. package/src/navigator-invocation-identity.ts +3 -2
  114. package/src/navigator-public-session.ts +4 -6
  115. package/src/navigator-session-contracts.ts +8 -11
  116. package/src/navigator-work-base.ts +5 -8
  117. package/src/navigator-work-context.ts +2 -1
  118. package/src/notary-contracts.ts +4 -6
  119. package/src/oauth-keepalive.ts +5 -18
  120. package/src/package-contracts/auditor-output.ts +2 -4
  121. package/src/package-contracts/fixer-packet.ts +4 -6
  122. package/src/package-contracts/gatekeeper-output.ts +2 -4
  123. package/src/package-contracts/judge-output.ts +0 -1
  124. package/src/package-contracts/navigator-output.ts +8 -8
  125. package/src/package-contracts/terminating-infrastructure.ts +6 -20
  126. package/src/package-contracts/terminating-tools.ts +8 -12
  127. package/src/pi/adapter.ts +0 -1
  128. package/src/pi/role-turn-host.ts +0 -1
  129. package/src/public-cli/analyst-run.ts +4 -2
  130. package/src/public-cli/auto-resume.ts +4 -11
  131. package/src/public-cli/cli.ts +10 -10
  132. package/src/public-cli/config.ts +8 -27
  133. package/src/public-cli/countersign-run.ts +2 -4
  134. package/src/public-cli/host-providers.ts +10 -27
  135. package/src/public-cli/instruction-seat-run.ts +5 -3
  136. package/src/public-cli/invocation.ts +11 -10
  137. package/src/public-cli/post-admission.ts +5 -3
  138. package/src/public-cli/run-lifecycle.ts +20 -41
  139. package/src/public-cli/settlement.ts +11 -33
  140. package/src/public-role-summons.ts +4 -3
  141. package/src/receipt-delivery-policy.ts +1 -4
  142. package/src/reviewer-role.ts +0 -1
  143. package/src/role-envelope.ts +5 -3
  144. package/src/role-run-placement.ts +4 -10
  145. package/src/role-run-relocation.ts +13 -23
  146. package/src/role-runtime.ts +9 -15
  147. package/src/run-terminal-artifacts.ts +2 -16
  148. package/src/run-ticket-number.ts +4 -10
  149. package/src/session-assistant-usage.ts +3 -1
  150. package/src/session-dialogue.ts +1 -4
  151. package/src/session-identity.ts +0 -1
  152. package/src/sitian-reader.ts +2 -4
  153. package/src/submission-correctable-error.ts +3 -1
  154. package/src/submission-errors.ts +2 -1
  155. package/src/submission-gate.ts +3 -1
  156. package/src/submission-ledger.ts +5 -6
  157. package/src/ticket-provenance-contracts.ts +1 -4
  158. package/src/ticket-provenance.ts +3 -1
  159. package/src/ticket-snapshot.ts +2 -4
  160. package/src/ticket-trajectory.ts +1 -8
  161. package/src/typed-provider-http.ts +5 -15
  162. package/src/unknown-value.ts +34 -0
  163. package/src/user-dialogue-stdin.ts +3 -1
  164. package/src/worker-role.ts +2 -5
  165. package/src/worker-submission-gates.ts +5 -7
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "./unknown-value.js";
1
2
  /**
2
3
  * 起居录(ticket-provenance)typed 形状 —— ADR 0075 / ADR 0081 / #1090。
3
4
  *
@@ -7,9 +8,6 @@
7
8
  */
8
9
  /** Sitian kind for per-ticket court diary volumes. */
9
10
  export const TICKET_PROVENANCE_KIND = "ticket-provenance";
10
- function isRecord(value) {
11
- return typeof value === "object" && value !== null && !Array.isArray(value);
12
- }
13
11
  function positiveInteger(value) {
14
12
  if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1)
15
13
  return value;
@@ -11,6 +11,7 @@ import { isSafePositiveTicketNumber } from "./run-ticket-number.js";
11
11
  import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.js";
12
12
  import { appendSitianRecordBlock, appendSitianRecord, resolveSitianRecordPath, } from "./sitian-facade.js";
13
13
  import { TICKET_PROVENANCE_KIND, } from "./ticket-provenance-contracts.js";
14
+ import { errorText } from "./unknown-value.js";
14
15
  /**
15
16
  * Typed input failure for diarist bounds/session path (reask, not infrastructure).
16
17
  * Accept hook discriminates with instanceof — never Error.message prefixes.
@@ -199,7 +200,7 @@ async function projectSessionRanges(input) {
199
200
  throw error;
200
201
  const code = errnoCode(error);
201
202
  if (code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR") {
202
- const detail = error instanceof Error ? error.message : String(error);
203
+ const detail = errorText(error);
203
204
  throw new TicketProvenanceInputError(`session unreadable: ${input.session.path} (${detail})`, { cause: error });
204
205
  }
205
206
  throw error;
@@ -1,5 +1,6 @@
1
1
  import { readFile, unlink, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { isEnoent, isRecord } from "./unknown-value.js";
3
4
  const TYPED_HTTP_FILE = "typed-provider-http.json";
4
5
  function typedProviderHttpPath(runDirectory) {
5
6
  return join(runDirectory, TYPED_HTTP_FILE);
@@ -8,9 +9,7 @@ async function clearTypedProviderHttpObservation(runDirectory) {
8
9
  try {
9
10
  await unlink(typedProviderHttpPath(runDirectory));
10
11
  } catch (error) {
11
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
12
- return;
13
- }
12
+ if (isEnoent(error)) return;
14
13
  throw error;
15
14
  }
16
15
  }
@@ -35,13 +34,11 @@ async function readLatestTypedProviderHttpObservation(runDirectory) {
35
34
  try {
36
35
  text = await readFile(typedProviderHttpPath(runDirectory), "utf8");
37
36
  } catch (error) {
38
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
39
- return void 0;
40
- }
37
+ if (isEnoent(error)) return void 0;
41
38
  throw error;
42
39
  }
43
40
  const raw = JSON.parse(text);
44
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
41
+ if (!isRecord(raw)) {
45
42
  throw new Error("typed provider HTTP observation must be a JSON object");
46
43
  }
47
44
  const record = raw;
@@ -0,0 +1,28 @@
1
+ import { Guard } from "typebox/guard";
2
+ /**
3
+ * Sole readers for a plain object, a Node errno, and an Error message.
4
+ * Callers choose which codes count; ENOTDIR is not ENOENT.
5
+ * Plain objects are TypeBox `Guard.IsObjectNotArray`.
6
+ */
7
+ export function isRecord(value) {
8
+ return Guard.IsObjectNotArray(value);
9
+ }
10
+ export function errnoCode(error) {
11
+ return error !== null && typeof error === "object" && "code" in error
12
+ && typeof error.code === "string"
13
+ ? error.code
14
+ : undefined;
15
+ }
16
+ /** `Error.message`, otherwise `String(value)`. */
17
+ export function errorText(error) {
18
+ return error instanceof Error ? error.message : String(error);
19
+ }
20
+ /** True only for ENOENT on an Error. ENOTDIR and every other code stay distinct. */
21
+ export function isEnoent(error) {
22
+ return error instanceof Error && errnoCode(error) === "ENOENT";
23
+ }
24
+ /** ENOENT or ENOTDIR on an Error. */
25
+ export function isMissingPathError(error) {
26
+ const code = errnoCode(error);
27
+ return error instanceof Error && (code === "ENOENT" || code === "ENOTDIR");
28
+ }
@@ -3,12 +3,13 @@
3
3
  * Pi's piped-stdin reader trims the wrapper; the body field keeps original bytes.
4
4
  * No size cap and no path/pointer substitute — the whole dialogue rides the envelope.
5
5
  */
6
+ import { isRecord } from "./unknown-value.js";
6
7
  export const USER_DIALOGUE_STDIN_KIND = "ak-user-dialogue";
7
8
  export function encodeUserDialogueStdin(body) {
8
9
  return JSON.stringify({ kind: USER_DIALOGUE_STDIN_KIND, body });
9
10
  }
10
11
  function isUserDialogueEnvelope(value) {
11
- if (value === null || typeof value !== "object" || Array.isArray(value))
12
+ if (!isRecord(value))
12
13
  return false;
13
14
  const record = value;
14
15
  return record.kind === USER_DIALOGUE_STDIN_KIND && typeof record.body === "string";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5368",
3
+ "version": "0.1.5375",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -27,6 +27,8 @@ import {
27
27
  } from "../prepared-role-turn.ts";
28
28
  import { acpModelId, type AcpHostDescription } from "./description.ts";
29
29
 
30
+ import { isRecord } from "../unknown-value.ts";
31
+
30
32
  /**
31
33
  * #959: collect free-form agent text from ACP session/update stream.
32
34
  * Used only when the navigator seat spoke prose without calling the output tool.
@@ -42,7 +44,7 @@ function acpAgentTextChunk(params: Readonly<Record<string, unknown>>): string |
42
44
  let textFallback: unknown;
43
45
 
44
46
  const nested = params.update;
45
- if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
47
+ if (isRecord(nested)) {
46
48
  const record = nested as Record<string, unknown>;
47
49
  kind = record.sessionUpdate;
48
50
  content = record.content;
@@ -55,7 +57,7 @@ function acpAgentTextChunk(params: Readonly<Record<string, unknown>>): string |
55
57
 
56
58
  if (kind !== "agent_message_chunk" && kind !== "agent_message") return undefined;
57
59
  if (typeof content === "string" && content.length > 0) return content;
58
- if (typeof content === "object" && content !== null && !Array.isArray(content)) {
60
+ if (isRecord(content)) {
59
61
  const record = content as Record<string, unknown>;
60
62
  if (typeof record.text === "string" && record.text.length > 0) return record.text;
61
63
  }
@@ -1,6 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { basename, dirname, isAbsolute, resolve } from "node:path";
3
3
 
4
+ import { errnoCode } from "./unknown-value.ts";
5
+
4
6
  /** Git discovery env vars that must not influence book-key resolution from cwd. */
5
7
  const GIT_DISCOVERY_ENV_KEYS = [
6
8
  "GIT_DIR",
@@ -60,8 +62,7 @@ export class ActivationGitRepositoryRequiredError extends Error {
60
62
 
61
63
  /** Spawn/OS failure identity for the git child — not a repository-status result. */
62
64
  function isGitSpawnInfrastructureError(error: unknown): boolean {
63
- if (error === null || typeof error !== "object" || !("code" in error)) return false;
64
- const code = (error as { code: unknown }).code;
65
+ const code = errnoCode(error);
65
66
  return code === "ENOENT" || code === "EACCES" || code === "EPERM";
66
67
  }
67
68
 
@@ -7,6 +7,10 @@ import {
7
7
  import { userInfo } from "node:os";
8
8
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
9
 
10
+ import { errnoCode, errorText } from "./unknown-value.ts";
11
+
12
+ export { errnoCode, errorText };
13
+
10
14
  /**
11
15
  * Typed activation-ledger path/fs failure. Callers discriminate with instanceof/code;
12
16
  * never by parsing message prose. Original filesystem causes are retained.
@@ -128,18 +132,6 @@ export function physicallyContainedIn(root: string, candidate: string): boolean
128
132
  return pathContainedIn(physicalPathIdentity(root), physicalPathIdentity(candidate));
129
133
  }
130
134
 
131
- export function errnoCode(error: unknown): string | undefined {
132
- return error !== null && typeof error === "object" && "code" in error
133
- && typeof (error as { code: unknown }).code === "string"
134
- ? (error as { code: string }).code
135
- : undefined;
136
- }
137
-
138
- export function errorText(error: unknown): string {
139
- if (!(error instanceof Error)) return String(error);
140
- return error.message;
141
- }
142
-
143
135
  /**
144
136
  * Ensure the configured ledger root is a physical directory identity.
145
137
  * lstat before create/realpath: a root symlink is never admitted, even when its
@@ -46,6 +46,8 @@ import {
46
46
  type AnalystUnreadableRun,
47
47
  } from "./analyst-page.ts";
48
48
 
49
+ import { isMissingPathError } from "./unknown-value.ts";
50
+
49
51
  /** #338 compute-if-missing failure — issue identity + real cause (CLI → ControlledFailure). */
50
52
  export class AnalystIssueComputeError extends Error {
51
53
  readonly code = "analyst-issue-compute-failed" as const;
@@ -80,14 +82,6 @@ export class AnalystIssueComputeError extends Error {
80
82
  }
81
83
  }
82
84
 
83
- function isMissingPathError(error: unknown): boolean {
84
- return (
85
- error instanceof Error
86
- && "code" in error
87
- && (error.code === "ENOENT" || error.code === "ENOTDIR")
88
- );
89
- }
90
-
91
85
  /** Issue-mode typed input — book × ticket scope (#399). */
92
86
  export type AnalystIssueModeInput = {
93
87
  readonly mode: "issue";
@@ -123,7 +117,7 @@ export type AnalystIssueModeInput = {
123
117
  * Sole sweep-mode input contract (#298/#329/#337).
124
118
  * Schema is the single definition; TS types are derived (no parallel hand shape).
125
119
  * projectRoot = string (not nonempty); changedLines optional finite non-negative;
126
- * 0 remains typed 空缺; no extra keys.
120
+ * 0 remains typed 空缺. Extra keys are not a reject reason.
127
121
  */
128
122
  export const analystSweepModeInputSchema = Type.Object(
129
123
  {
@@ -137,11 +131,9 @@ export const analystSweepModeInputSchema = Type.Object(
137
131
  Type.Number({ minimum: 0, maximum: Number.MAX_VALUE }),
138
132
  ),
139
133
  },
140
- { additionalProperties: false },
141
134
  ),
142
135
  ),
143
136
  },
144
- { additionalProperties: false },
145
137
  );
146
138
 
147
139
  /** Sweep-mode typed input — 已并 PR 清单 + LOC → 补算缺页 + 维护全库索引. */
@@ -38,6 +38,7 @@ import {
38
38
  type LedgerSessionRow,
39
39
  } from "./ledger-session-read.ts";
40
40
 
41
+ import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
41
42
 
42
43
  /** One completed gate round: direct officer receipt or historical province/officer pair. */
43
44
  /** Honest origin discriminant: direct summons vs historical province dispatch. */
@@ -88,10 +89,6 @@ const OFFICER_ARG_ALIASES: Readonly<Record<string, "inspector" | "notary">> = {
88
89
  notary: "notary",
89
90
  };
90
91
 
91
- function isRecord(value: unknown): value is Record<string, unknown> {
92
- return typeof value === "object" && value !== null && !Array.isArray(value);
93
- }
94
-
95
92
  function isParentAttemptBindingRow(row: LedgerSessionRow): boolean {
96
93
  return row.type === "custom" && row.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE;
97
94
  }
@@ -104,15 +101,6 @@ function intervalRowsForGateCall(
104
101
  return intervalRowsAroundAnchor(rows, callRowIndex, isParentAttemptBindingRow).rows;
105
102
  }
106
103
 
107
- /** Only true absence (ENOENT). ENOTDIR is damaged topology — must stay loud. */
108
- function isMissingDirectoryError(error: unknown): boolean {
109
- return (
110
- error instanceof Error
111
- && "code" in error
112
- && error.code === "ENOENT"
113
- );
114
- }
115
-
116
104
  function normalizeOfficerArg(raw: unknown): "inspector" | "notary" | undefined {
117
105
  if (typeof raw !== "string") return undefined;
118
106
  return OFFICER_ARG_ALIASES[raw.trim()];
@@ -438,7 +426,7 @@ async function resolveOfficerSessionFromPointerFile(
438
426
  raw = JSON.parse(await readFile(pointerPath, "utf8"));
439
427
  } catch (error) {
440
428
  throw new Error(
441
- `direct officer run pointer unreadable in ${pointerPath}: ${error instanceof Error ? error.message : String(error)}`,
429
+ `direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`,
442
430
  { cause: error },
443
431
  );
444
432
  }
@@ -490,7 +478,7 @@ export async function readAnalystGateCyclesFromAuditorRoles(
490
478
  .map((e) => e.name)
491
479
  .sort();
492
480
  } catch (error) {
493
- if (isMissingDirectoryError(error)) continue;
481
+ if (isEnoent(error)) continue;
494
482
  throw error;
495
483
  }
496
484
  for (const name of names) {
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { open, readFile, unlink } from "node:fs/promises";
22
22
  import { dirname, join } from "node:path";
23
+ import { setTimeout as sleep } from "node:timers/promises";
23
24
 
24
25
  import { writeFileAtomically } from "./atomic-write.ts";
25
26
  import {
@@ -28,6 +29,7 @@ import {
28
29
  physicalPathIdentity,
29
30
  } from "./activation-ledger-topology.ts";
30
31
  import { resolveAnalystBookKey } from "./analyst-book-key.ts";
32
+ import { isMissingPathError } from "./unknown-value.ts";
31
33
  import type {
32
34
  AnalystIssueMetricsPage,
33
35
  AnalystOptionalMetricNumber,
@@ -38,12 +40,6 @@ const LIBRARY_INDEX_LOCK_NAME = ".library-index.lock";
38
40
  const LIBRARY_INDEX_LOCK_TIMEOUT_MS = 30_000;
39
41
  const LIBRARY_INDEX_LOCK_RETRY_MS = 15;
40
42
 
41
- function sleep(ms: number): Promise<void> {
42
- return new Promise((resolve) => {
43
- setTimeout(resolve, ms);
44
- });
45
- }
46
-
47
43
  /**
48
44
  * Exclusive create lock for the library-index read→upsert→write critical section.
49
45
  * Same-directory sibling of the index file; not a second index and not a daemon.
@@ -270,13 +266,7 @@ export async function readAnalystLibraryIndexPage(
270
266
  try {
271
267
  raw = await readFile(path, "utf8");
272
268
  } catch (error) {
273
- if (
274
- error instanceof Error
275
- && "code" in error
276
- && (error.code === "ENOENT" || error.code === "ENOTDIR")
277
- ) {
278
- return undefined;
279
- }
269
+ if (isMissingPathError(error)) return undefined;
280
270
  throw error;
281
271
  }
282
272
  const parsed: unknown = JSON.parse(raw);
@@ -49,23 +49,9 @@ import {
49
49
  type AnalystGateCycleRound,
50
50
  } from "./analyst-gate-cycles-read.ts";
51
51
 
52
- export type { AnalystGateCycleRound } from "./analyst-gate-cycles-read.ts";
53
-
54
- function isMissingPathError(error: unknown): boolean {
55
- return (
56
- error instanceof Error
57
- && "code" in error
58
- && (error.code === "ENOENT" || error.code === "ENOTDIR")
59
- );
60
- }
52
+ import { isRecord, errorText, isMissingPathError } from "./unknown-value.ts";
61
53
 
62
- function errorText(error: unknown): string {
63
- return error instanceof Error ? error.message : String(error);
64
- }
65
-
66
- function isRecord(value: unknown): value is Record<string, unknown> {
67
- return typeof value === "object" && value !== null && !Array.isArray(value);
68
- }
54
+ export type { AnalystGateCycleRound } from "./analyst-gate-cycles-read.ts";
69
55
 
70
56
  /** Existing public-cli run-state live phases — not a new state machine. */
71
57
  const LIVE_RUN_STATES = new Set(["admitted", "running", "resumable"]);
@@ -14,6 +14,8 @@ import { medianNumber } from "../analyst-median.ts";
14
14
  import type { AnalystMetricFamilyModule } from "../analyst-metric-family.ts";
15
15
  import { packagedAnalystTerminal } from "../packaged-role-registry.ts";
16
16
 
17
+ import { isRecord } from "../unknown-value.ts";
18
+
17
19
  const WORKER_ROLES = new Set(["coder", "fixer"]);
18
20
 
19
21
  /** Lawful acceptance vocabulary by role (PRD 受理终态映射). */
@@ -95,10 +97,6 @@ export type AnalystAcceptanceSuccessReworkSection = {
95
97
  readonly rework: AnalystReworkLens;
96
98
  };
97
99
 
98
- function isRecord(value: unknown): value is Record<string, unknown> {
99
- return typeof value === "object" && value !== null && !Array.isArray(value);
100
- }
101
-
102
100
  /** Locate collector groups array: receipt.groups or top-level groups. */
103
101
  function findCollectorGroups(body: Record<string, unknown>): unknown {
104
102
  if (Array.isArray(body.groups)) return body.groups;
@@ -17,6 +17,8 @@ import type {
17
17
  AnalystUnreadableRun,
18
18
  } from "../analyst-page.ts";
19
19
 
20
+ import { isRecord } from "../unknown-value.ts";
21
+
20
22
  export type AnalystRoundTimelineTerminal =
21
23
  | {
22
24
  readonly kind: "receipt";
@@ -66,10 +68,6 @@ export type AnalystRoundTimelineSection = {
66
68
  readonly lanes: readonly AnalystRoundTimelineLane[];
67
69
  };
68
70
 
69
- function isRecord(value: unknown): value is Record<string, unknown> {
70
- return typeof value === "object" && value !== null && !Array.isArray(value);
71
- }
72
-
73
71
  function readClassCount(body: Record<string, unknown>): number | undefined {
74
72
  if (!isRecord(body.outcome)) return undefined;
75
73
  if (!isRecord(body.outcome.decisiveFacts)) return undefined;
@@ -6,9 +6,9 @@
6
6
  * top-level sections via directory discovery — B/C waves add family files
7
7
  * without forking the page writer or editing a shared registry list.
8
8
  */
9
- import { createHash } from "node:crypto";
10
9
  import { dirname, join } from "node:path";
11
10
 
11
+ import { sha256Hex } from "./sha256.ts";
12
12
  import { writeFileAtomically } from "./atomic-write.ts";
13
13
  import {
14
14
  assertLedgerFileInsideHome,
@@ -161,7 +161,7 @@ export function analystIssuePageKey(address: AnalystIssuePageAddress): string {
161
161
  } else if (address.scopeRootIdentity !== undefined) {
162
162
  parts.push("root", physicalPathIdentity(address.scopeRootIdentity));
163
163
  }
164
- return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
164
+ return sha256Hex(parts.join("\0")).slice(0, 32);
165
165
  }
166
166
 
167
167
  export function analystIssuePagePath(
@@ -7,9 +7,9 @@
7
7
  * books/<book>/navigator/<work-subject-digest>/. Callers must not re-hash
8
8
  * subject or re-join book/navigator/digest themselves.
9
9
  */
10
- import { createHash } from "node:crypto";
11
10
  import { join } from "node:path";
12
11
 
12
+ import { sha256Hex } from "./sha256.ts";
13
13
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
14
14
  import {
15
15
  activationBookDirectory,
@@ -52,7 +52,7 @@ export function resolveNavigatorWorkSubjectPlacement(input: {
52
52
  }
53
53
  }
54
54
  ledgerHome ??= resolveActivationLedgerHome(input.home);
55
- const digest = createHash("sha256").update(input.subject).digest("hex").slice(0, 32);
55
+ const digest = sha256Hex(input.subject).slice(0, 32);
56
56
  return {
57
57
  ledgerHome,
58
58
  sessionDir: join(
@@ -1,7 +1,8 @@
1
+ import { isRecord } from "./unknown-value.ts";
1
2
  /** Historical audit-escalation discriminator retained for reading pre-#1057 run artifacts. */
2
3
  export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
3
4
 
4
5
  export function isAuditEscalationResult(value: unknown): value is { readonly kind: typeof AUDIT_ESCALATION_KIND } {
5
- return typeof value === "object" && value !== null && !Array.isArray(value)
6
+ return isRecord(value)
6
7
  && (value as Record<string, unknown>).kind === AUDIT_ESCALATION_KIND;
7
8
  }
@@ -1,6 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
3
 
4
+ import { isRecord } from "./unknown-value.ts";
5
+
4
6
  import {
5
7
  joinPackageMaterials,
6
8
  readPackageMaterial,
@@ -126,7 +128,7 @@ export async function readAuditorResumeBinding(runDirectory: string): Promise<
126
128
  if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
127
129
  throw error;
128
130
  }
129
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
131
+ if (!isRecord(raw)) return undefined;
130
132
  const record = raw as { sourceRunPath?: unknown; auditorSubject?: unknown };
131
133
  if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "") return undefined;
132
134
  const subject = isAuditorSoulRole(record.auditorSubject)
@@ -19,22 +19,11 @@ import {
19
19
  readMigrationDerivedTicketNumber,
20
20
  } from "./run-ticket-number.ts";
21
21
 
22
+ import { isRecord, isEnoent } from "./unknown-value.ts";
23
+
22
24
  const TICKET_NUMBER_RE = /^[1-9][0-9]*$/;
23
25
  const RUN_DIR_NAME_RE = /^([^@]+)@([^@]+)$/;
24
26
 
25
- function isRecord(value: unknown): value is Record<string, unknown> {
26
- return typeof value === "object" && value !== null && !Array.isArray(value);
27
- }
28
-
29
- /** Sole migration ENOENT projection — non-ENOENT must propagate. */
30
- export function isMigrationEnoent(error: unknown): boolean {
31
- return (
32
- error instanceof Error &&
33
- "code" in error &&
34
- (error as NodeJS.ErrnoException).code === "ENOENT"
35
- );
36
- }
37
-
38
27
  /** Book-key directories under a books root; missing root → []. */
39
28
  export async function listMigrationBookKeys(
40
29
  booksDirectory: string,
@@ -46,7 +35,7 @@ export async function listMigrationBookKeys(
46
35
  .map((entry) => entry.name)
47
36
  .sort((a, b) => a.localeCompare(b));
48
37
  } catch (error) {
49
- if (isMigrationEnoent(error)) return [];
38
+ if (isEnoent(error)) return [];
50
39
  throw error;
51
40
  }
52
41
  }
@@ -59,7 +48,7 @@ export async function listMigrationDirents(
59
48
  const entries = await readdir(directory, { withFileTypes: true });
60
49
  return [...entries].sort((a, b) => a.name.localeCompare(b.name));
61
50
  } catch (error) {
62
- if (isMigrationEnoent(error)) return [];
51
+ if (isEnoent(error)) return [];
63
52
  throw error;
64
53
  }
65
54
  }
@@ -109,7 +98,7 @@ async function readProjectRootFromRun(
109
98
  return { projectRoot, sourcePage: page };
110
99
  }
111
100
  } catch (error) {
112
- if (isMigrationEnoent(error)) continue;
101
+ if (isEnoent(error)) continue;
113
102
  throw error;
114
103
  }
115
104
  }
@@ -25,7 +25,6 @@ import {
25
25
  import {
26
26
  findPlacedMigratingRun,
27
27
  isFlatRunsRelative,
28
- isMigrationEnoent,
29
28
  isTicketNumberString,
30
29
  listBackupRunLeaves,
31
30
  listMigrationBookKeys,
@@ -37,6 +36,8 @@ import {
37
36
  type RunDirectoryPathRewrite,
38
37
  } from "./role-run-relocation.ts";
39
38
 
39
+ import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
40
+
40
41
  const AUDITOR_ROLES_PARTITION = "auditor-roles";
41
42
  const ISSUES_PARTITION = "issues";
42
43
  const DEPRECATED_KINDS_PARTITION = "deprecated-kinds";
@@ -101,7 +102,7 @@ async function directoryExists(path: string): Promise<boolean> {
101
102
  const info = await stat(path);
102
103
  return info.isDirectory();
103
104
  } catch (error) {
104
- if (isMigrationEnoent(error)) return false;
105
+ if (isEnoent(error)) return false;
105
106
  throw error;
106
107
  }
107
108
  }
@@ -219,7 +220,7 @@ async function readJsonlParentSession(path: string): Promise<string | undefined>
219
220
  try {
220
221
  raw = await readFile(path, "utf8");
221
222
  } catch (error) {
222
- if (isMigrationEnoent(error)) return undefined;
223
+ if (isEnoent(error)) return undefined;
223
224
  throw error;
224
225
  }
225
226
  const newline = raw.indexOf("\n");
@@ -227,7 +228,7 @@ async function readJsonlParentSession(path: string): Promise<string | undefined>
227
228
  if (line.length === 0) return undefined;
228
229
  try {
229
230
  const header: unknown = JSON.parse(line);
230
- if (header === null || typeof header !== "object" || Array.isArray(header)) {
231
+ if (!isRecord(header)) {
231
232
  return undefined;
232
233
  }
233
234
  const parentSession = (header as { parentSession?: unknown }).parentSession;
@@ -268,13 +269,13 @@ async function findTrueVolumeParent(
268
269
  try {
269
270
  ledgerRaw = await readFile(join(volumeDirectory, CURRENT_SESSION_LEDGER), "utf8");
270
271
  } catch (error) {
271
- if (isMigrationEnoent(error)) return undefined;
272
+ if (isEnoent(error)) return undefined;
272
273
  throw error;
273
274
  }
274
275
  let sessionFileField: string;
275
276
  try {
276
277
  const ledger: unknown = JSON.parse(ledgerRaw);
277
- if (ledger === null || typeof ledger !== "object" || Array.isArray(ledger)) {
278
+ if (!isRecord(ledger)) {
278
279
  return undefined;
279
280
  }
280
281
  const sessionFile = (ledger as { sessionFile?: unknown }).sessionFile;
@@ -298,7 +299,7 @@ async function findTrueVolumeParent(
298
299
  const info = await lstat(liveSession);
299
300
  if (!info.isFile()) return undefined;
300
301
  } catch (error) {
301
- if (isMigrationEnoent(error)) return undefined;
302
+ if (isEnoent(error)) return undefined;
302
303
  throw error;
303
304
  }
304
305
 
@@ -413,7 +414,7 @@ async function readSessionFileField(path: string): Promise<string | undefined> {
413
414
  try {
414
415
  raw = await readFile(path, "utf8");
415
416
  } catch (error) {
416
- if (isMigrationEnoent(error)) return undefined;
417
+ if (isEnoent(error)) return undefined;
417
418
  throw error;
418
419
  }
419
420
  let parsed: unknown;
@@ -422,12 +423,12 @@ async function readSessionFileField(path: string): Promise<string | undefined> {
422
423
  } catch (error) {
423
424
  throw new Error(
424
425
  `book topology migration cannot read current-session pointer at ${path}: invalid JSON (${
425
- error instanceof Error ? error.message : String(error)
426
+ errorText(error)
426
427
  })`,
427
428
  { cause: error },
428
429
  );
429
430
  }
430
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
431
+ if (!isRecord(parsed)) {
431
432
  throw new Error(
432
433
  `book topology migration cannot read current-session pointer at ${path}: expected a JSON object`,
433
434
  );
@@ -480,7 +481,7 @@ async function mergeRewrittenCurrentSession(
480
481
  srcStat = await stat(srcPath);
481
482
  srcRaw = await readFile(srcPath, "utf8");
482
483
  } catch (error) {
483
- if (isMigrationEnoent(error)) return;
484
+ if (isEnoent(error)) return;
484
485
  throw error;
485
486
  }
486
487
 
@@ -488,7 +489,7 @@ async function mergeRewrittenCurrentSession(
488
489
  let nextSessionFile: string | undefined;
489
490
  try {
490
491
  const parsed: unknown = JSON.parse(srcRaw);
491
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
492
+ if (isRecord(parsed)) {
492
493
  const record = parsed as Record<string, unknown>;
493
494
  if (typeof record.sessionFile === "string") {
494
495
  const rewritten = rewritePathIntoDestination(
@@ -510,7 +511,7 @@ async function mergeRewrittenCurrentSession(
510
511
  try {
511
512
  destStat = await stat(destPath);
512
513
  } catch (error) {
513
- if (!isMigrationEnoent(error)) throw error;
514
+ if (!isEnoent(error)) throw error;
514
515
  }
515
516
  await writeWinningCurrentSession(
516
517
  destPath,
@@ -656,7 +657,7 @@ async function copyIssuesNonRunTree(source: string, destination: string): Promis
656
657
  `book topology migration refuses to overwrite ${to} with ${from}`,
657
658
  );
658
659
  } catch (error) {
659
- if (!isMigrationEnoent(error)) throw error;
660
+ if (!isEnoent(error)) throw error;
660
661
  }
661
662
  await cp(from, to, { preserveTimestamps: true });
662
663
  }
@@ -719,7 +720,7 @@ async function unlinkIfPresent(path: string): Promise<void> {
719
720
  try {
720
721
  await unlink(path);
721
722
  } catch (error) {
722
- if (!isMigrationEnoent(error)) throw error;
723
+ if (!isEnoent(error)) throw error;
723
724
  }
724
725
  }
725
726