@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +6312 -6376
  4. package/dist/analyst-gate-cycles-read.js +28 -78
  5. package/dist/analyst-ledger.js +26 -42
  6. package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
  7. package/dist/analyst-metric-families/round-timeline.js +2 -5
  8. package/dist/archivist-record-entry.js +4 -3
  9. package/dist/archivist-record-pointer.js +71 -36
  10. package/dist/atomic-write.js +2 -2
  11. package/dist/auditor-dossier-tool.js +11 -6
  12. package/dist/auditor-soul.js +5 -12
  13. package/dist/collector-config.js +6 -5
  14. package/dist/doctor-evidence.js +8 -4
  15. package/dist/engine-detour-usage.js +8 -22
  16. package/dist/headless-host/production-host.js +5969 -6011
  17. package/dist/host-descriptions.js +0 -4
  18. package/dist/host-session-record.js +266 -267
  19. package/dist/ledger-session-read.js +5 -0
  20. package/dist/migrate-book-topology.js +1369 -722
  21. package/dist/navigator-public-session.js +13 -17
  22. package/dist/notary-source-run.js +8 -16
  23. package/dist/packaged-role-registry.js +5 -56
  24. package/dist/pi/durable-principal.js +2 -3
  25. package/dist/pi/role-turn-host.js +79 -15
  26. package/dist/public-cli/auto-resume.js +8 -47
  27. package/dist/public-cli/instruction-seat-run.js +9 -7
  28. package/dist/public-cli/invocation.js +67 -100
  29. package/dist/public-cli/main.js +2689 -2532
  30. package/dist/public-cli/post-admission.js +96 -55
  31. package/dist/public-cli/run-lifecycle.js +44 -100
  32. package/dist/public-cli/settlement.js +184 -429
  33. package/dist/public-cli/terminal.js +0 -12
  34. package/dist/role-run-placement.js +53 -16
  35. package/dist/role-run-relocation.js +111 -52
  36. package/dist/run-dossier-files.js +30 -0
  37. package/dist/run-dossier.js +322 -0
  38. package/dist/run-terminal-artifacts.js +69 -202
  39. package/dist/run-ticket-number.js +49 -7
  40. package/dist/session-assistant-usage.js +9 -18
  41. package/dist/session-identity.js +49 -19
  42. package/dist/sitian-appender.js +29 -18
  43. package/dist/sitian-facade.js +11 -0
  44. package/dist/sitian-reader.js +44 -29
  45. package/dist/submission-gate.js +1 -1
  46. package/dist/submission-ledger.js +4 -4
  47. package/dist/ticket-provenance-contracts.js +2 -8
  48. package/dist/ticket-provenance.js +25 -9
  49. package/package.json +1 -1
  50. package/scripts/build-package.mjs +2 -0
  51. package/src/acp-host/production-host.ts +1 -1
  52. package/src/acp-host/role-turn-host.ts +11 -4
  53. package/src/analyst-gate-cycles-read.ts +31 -88
  54. package/src/analyst-ledger.ts +36 -51
  55. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  56. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  57. package/src/analyst-metric-families/round-timeline.ts +2 -5
  58. package/src/analyst-page.ts +3 -3
  59. package/src/archivist-record-entry.ts +6 -5
  60. package/src/archivist-record-pointer.ts +92 -42
  61. package/src/atomic-write.ts +3 -1
  62. package/src/auditor-dossier-tool.ts +14 -8
  63. package/src/auditor-soul.ts +5 -10
  64. package/src/book-topology-migration-placement.ts +16 -3
  65. package/src/book-topology-record-class-migrators.ts +4 -2
  66. package/src/book-topology-runs-migrator.ts +6 -3
  67. package/src/collector-config.ts +5 -3
  68. package/src/doctor-contracts.ts +1 -1
  69. package/src/doctor-evidence.ts +7 -3
  70. package/src/engine-detour-tool.ts +1 -1
  71. package/src/engine-detour-usage.ts +12 -25
  72. package/src/external-host-turn-loop.ts +25 -0
  73. package/src/factory-board.ts +16 -81
  74. package/src/headless-host/production-host.ts +1 -4
  75. package/src/headless-host/role-turn-host.ts +56 -21
  76. package/src/host-descriptions.ts +0 -6
  77. package/src/host-session-record.ts +224 -140
  78. package/src/ledger-session-read.ts +6 -0
  79. package/src/navigator-public-session.ts +13 -18
  80. package/src/navigator-work-context.ts +3 -1
  81. package/src/notary-role.ts +2 -1
  82. package/src/notary-source-run.ts +8 -18
  83. package/src/packaged-role-registry.ts +1 -65
  84. package/src/pi/adapter.ts +50 -7
  85. package/src/pi/durable-principal.ts +2 -4
  86. package/src/pi/role-turn-host.ts +85 -14
  87. package/src/public-cli/auto-resume.ts +7 -43
  88. package/src/public-cli/instruction-seat-run.ts +13 -6
  89. package/src/public-cli/invocation.ts +90 -144
  90. package/src/public-cli/post-admission.ts +111 -72
  91. package/src/public-cli/run-lifecycle.ts +47 -115
  92. package/src/public-cli/settlement.ts +221 -589
  93. package/src/public-cli/terminal.ts +1 -61
  94. package/src/role-envelope.ts +4 -2
  95. package/src/role-run-placement.ts +65 -17
  96. package/src/role-run-relocation.ts +110 -70
  97. package/src/role-runtime.ts +46 -69
  98. package/src/run-dossier-files.ts +33 -0
  99. package/src/run-dossier.ts +357 -0
  100. package/src/run-terminal-artifacts.ts +84 -240
  101. package/src/run-ticket-number.ts +82 -14
  102. package/src/session-assistant-usage.ts +8 -20
  103. package/src/session-identity.ts +52 -21
  104. package/src/sitian-appender.ts +33 -26
  105. package/src/sitian-facade.ts +22 -0
  106. package/src/sitian-reader.ts +62 -32
  107. package/src/submission-gate.ts +1 -1
  108. package/src/submission-ledger.ts +4 -4
  109. package/src/ticket-provenance-contracts.ts +3 -6
  110. package/src/ticket-provenance.ts +23 -11
  111. package/src/ticket-snapshot.ts +3 -2
  112. package/src/ticket-trajectory.ts +95 -109
@@ -5,7 +5,11 @@
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { appendFileSync } from "node:fs";
8
- import { dirname, join } from "node:path";
8
+ import { basename, dirname, join } from "node:path";
9
+
10
+ import { parseRunLeaf, sessionDirectoryOf } from "./role-run-placement.ts";
11
+ import { RUN_HISTORY_FILE, RUN_HISTORY_KINDS, RUN_LOG_FILE, RUN_STATE_FILE, RUN_STATE_KINDS } from "./run-dossier-files.ts";
12
+ import { ticketNumberFromSitianSubject } from "./run-ticket-number.ts";
9
13
 
10
14
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
11
15
  import {
@@ -39,20 +43,8 @@ export function appendSitianRecordBlock(input: SitianRecordInput, block: string)
39
43
  }
40
44
  }
41
45
 
42
- /** Authorized S4 submission ledger kinds that share a common run submission volume. */
43
- export const S4_SUBMISSION_LEDGER_KINDS = new Set([
44
- "candidate",
45
- "roundContext",
46
- "outcome",
47
- "sealed",
48
- "post-seal-anomaly"
49
- ]);
50
-
51
- /** Compute the volume partition key for directory placement. */
46
+ /** Volume directory name for a record kind: the kind itself. */
52
47
  export function resolveSitianVolumeCategory(kind: string): string {
53
- if (S4_SUBMISSION_LEDGER_KINDS.has(kind)) {
54
- return "submission-ledger";
55
- }
56
48
  return kind;
57
49
  }
58
50
 
@@ -65,6 +57,18 @@ type SitianRecordPath = {
65
57
  /** Sole records leaf under every sitian volume directory. */
66
58
  const SITIAN_RECORDS_LEAF = "records.jsonl" as const;
67
59
 
60
+ export function sitianVolumeDirectory(sessionDirectory: string, category: string): string {
61
+ return join(sessionDirectory, category);
62
+ }
63
+
64
+ export function sitianRunVolumeDirectory(runDirectory: string, category: string): string {
65
+ return sitianVolumeDirectory(sessionDirectoryOf(runDirectory), category);
66
+ }
67
+
68
+ export function sitianVolumeRecordsFile(volumeDirectory: string): string {
69
+ return join(volumeDirectory, SITIAN_RECORDS_LEAF);
70
+ }
71
+
68
72
  /**
69
73
  * Ticket-provenance under-book paths (docs/dossier-topology.md sole authority).
70
74
  * Writer ticket branch and owner-visible shape share this join — one code path.
@@ -75,7 +79,7 @@ function ticketProvenanceUnderBookPaths(
75
79
  ticketId: string,
76
80
  ): { sessionDir: string; recordFile: string } {
77
81
  const sessionDir = join(activationBookDirectory(ledgerHome, bookKey), ticketId);
78
- return { sessionDir, recordFile: join(sessionDir, SITIAN_RECORDS_LEAF) };
82
+ return { sessionDir, recordFile: sitianVolumeRecordsFile(sessionDir) };
79
83
  }
80
84
 
81
85
  /** Pure topology owner shared by ambient writes and explicit-home submission reads. */
@@ -85,14 +89,7 @@ export function resolveSitianRecordPathInLedger(
85
89
  ): SitianRecordPath {
86
90
  const category = resolveSitianVolumeCategory(input.kind);
87
91
  const ticketNumber = category === "ticket-provenance"
88
- ? typeof input.subject === "string" && /^[1-9][0-9]*$/.test(input.subject)
89
- ? input.subject
90
- : typeof input.subject === "object"
91
- && typeof input.subject.ticketNumber === "number"
92
- && Number.isSafeInteger(input.subject.ticketNumber)
93
- && input.subject.ticketNumber > 0
94
- ? String(input.subject.ticketNumber)
95
- : undefined
92
+ ? ticketNumberFromSitianSubject(input.subject)
96
93
  : undefined;
97
94
 
98
95
  let sessionDir: string;
@@ -107,8 +104,10 @@ export function resolveSitianRecordPathInLedger(
107
104
  sessionDir = paths.sessionDir;
108
105
  recordFile = paths.recordFile;
109
106
  } else if (category === "ticket-provenance" && input.runDirectory !== undefined) {
107
+ // True-unbound diary staging: the run's history volume (#1161). Never a
108
+ // top-level run/records.jsonl — that leaf is only the ticket-root diary.
110
109
  sessionDir = input.runDirectory;
111
- recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
110
+ recordFile = join(sessionDir, RUN_HISTORY_FILE);
112
111
  } else {
113
112
  if (
114
113
  input.sessionParent === undefined
@@ -117,8 +116,16 @@ export function resolveSitianRecordPathInLedger(
117
116
  ) {
118
117
  throw new Error("Sitian record ownership requires a parent session inside the ledger home");
119
118
  }
120
- sessionDir = join(dirname(input.sessionParent), category);
121
- recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
119
+ const sessionParentDir = dirname(input.sessionParent);
120
+ const runDirectory = dirname(sessionParentDir);
121
+ if (basename(sessionParentDir) === "session" && parseRunLeaf(basename(runDirectory)) !== undefined) {
122
+ // A role run's own session: every record kind shares the run's one log (#1161).
123
+ sessionDir = runDirectory;
124
+ recordFile = join(runDirectory, RUN_HISTORY_KINDS.has(input.kind) ? RUN_HISTORY_FILE : RUN_STATE_KINDS.has(input.kind) ? RUN_STATE_FILE : RUN_LOG_FILE);
125
+ } else {
126
+ sessionDir = sitianVolumeDirectory(sessionParentDir, category);
127
+ recordFile = sitianVolumeRecordsFile(sessionDir);
128
+ }
122
129
  }
123
130
  if (!physicallyContainedIn(ledgerHome, sessionDir)) {
124
131
  throw new Error("Sitian record ownership requires a directory inside the ledger home");
@@ -6,6 +6,7 @@
6
6
  * - sitianReport → appender (SitianRecord rows, append + identity claim)
7
7
  * Read paths: readSitianRecords (canonical rows).
8
8
  */
9
+ import { sessionFileOf } from "./role-run-placement.ts";
9
10
  import { appendSitianRecord } from "./sitian-appender.ts";
10
11
  import type { RecordPointer, SitianRecordInput } from "./sitian-contracts.ts";
11
12
 
@@ -20,3 +21,24 @@ export * from "./sitian-reader.ts";
20
21
  export function sitianReport(input: SitianRecordInput): RecordPointer {
21
22
  return appendSitianRecord(input);
22
23
  }
24
+
25
+ /**
26
+ * One record of a role run: history.jsonl for the history kinds and state.jsonl for the
27
+ * state kinds (see the appender), log.jsonl for the run's own diagnostics (host dossier pointers,
28
+ * dispatch / resume / post-admission faults, host stderr). Same single entry
29
+ * as every other record; the run is named, never a file path.
30
+ */
31
+ export function reportRunRecord(
32
+ runDirectory: string,
33
+ kind: string,
34
+ payload: unknown,
35
+ source: string,
36
+ ): RecordPointer {
37
+ return appendSitianRecord({
38
+ level: "event",
39
+ kind,
40
+ sessionParent: sessionFileOf(runDirectory),
41
+ source,
42
+ payload,
43
+ });
44
+ }
@@ -6,7 +6,6 @@
6
6
  * - Zero whitewashing, zero deduplication.
7
7
  * - Canonical rows after malformed lines are always reachable.
8
8
  */
9
- import { existsSync } from "node:fs";
10
9
  import { readFile } from "node:fs/promises";
11
10
 
12
11
  import type {
@@ -15,46 +14,77 @@ import type {
15
14
  SitianRecord,
16
15
  } from "./sitian-contracts.ts";
17
16
 
18
- import { isRecord, errorText } from "./unknown-value.ts";
17
+ import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
19
18
 
20
- /** Read a Sitian record volume with full traversal and non-destructive diagnostics. */
21
- export async function readSitianRecords(recordFile: string): Promise<SitianReadResult> {
22
- if (!existsSync(recordFile)) {
23
- return { records: [], diagnostics: [] };
19
+ /** One non-blank JSONL line through the sole Sitian decoder. Blank → undefined. */
20
+ export type SitianLineDecode =
21
+ | { readonly ok: true; readonly record: SitianRecord }
22
+ | { readonly ok: false; readonly diagnostic: SitianMalformedDiagnostic };
23
+
24
+ /**
25
+ * Decode one Sitian JSONL line. Callers that must keep original bytes use this
26
+ * with the source line; volume readers fold it into records + diagnostics.
27
+ */
28
+ export function decodeSitianRecordLine(
29
+ line: string,
30
+ lineNumber: number,
31
+ ): SitianLineDecode | undefined {
32
+ if (!line.trim()) return undefined;
33
+ try {
34
+ const parsed = JSON.parse(line);
35
+ if (isRecord(parsed)) {
36
+ return { ok: true, record: parsed as unknown as SitianRecord };
37
+ }
38
+ const typeDesc = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
39
+ return {
40
+ ok: false,
41
+ diagnostic: {
42
+ kind: "malformed",
43
+ line: lineNumber,
44
+ raw: line,
45
+ error: `expected JSON object, got ${typeDesc}`,
46
+ },
47
+ };
48
+ } catch (error) {
49
+ return {
50
+ ok: false,
51
+ diagnostic: {
52
+ kind: "malformed",
53
+ line: lineNumber,
54
+ raw: line,
55
+ error: errorText(error),
56
+ },
57
+ };
24
58
  }
59
+ }
25
60
 
26
- const text = await readFile(recordFile, "utf8");
61
+ /**
62
+ * Sole decoder for one Sitian JSONL volume's text. Sync so the run-dossier
63
+ * renderer and the async file reader share one implementation.
64
+ */
65
+ export function parseSitianRecordText(text: string): SitianReadResult {
27
66
  const lines = text.split("\n");
28
-
29
67
  const records: SitianRecord[] = [];
30
68
  const diagnostics: SitianMalformedDiagnostic[] = [];
31
69
 
32
70
  for (let index = 0; index < lines.length; index += 1) {
33
- const line = lines[index]!;
34
- if (!line.trim()) continue;
35
-
36
- try {
37
- const parsed = JSON.parse(line);
38
- if (isRecord(parsed)) {
39
- records.push(parsed as unknown as SitianRecord);
40
- } else {
41
- const typeDesc = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
42
- diagnostics.push({
43
- kind: "malformed",
44
- line: index + 1,
45
- raw: line,
46
- error: `expected JSON object, got ${typeDesc}`,
47
- });
48
- }
49
- } catch (error) {
50
- diagnostics.push({
51
- kind: "malformed",
52
- line: index + 1,
53
- raw: line,
54
- error: errorText(error),
55
- });
56
- }
71
+ const decoded = decodeSitianRecordLine(lines[index]!, index + 1);
72
+ if (decoded === undefined) continue;
73
+ if (decoded.ok) records.push(decoded.record);
74
+ else diagnostics.push(decoded.diagnostic);
57
75
  }
58
76
 
59
77
  return { records, diagnostics };
60
78
  }
79
+
80
+ /** Read a Sitian record volume with full traversal and non-destructive diagnostics. */
81
+ export async function readSitianRecords(recordFile: string): Promise<SitianReadResult> {
82
+ // Direct read: only ENOENT is absence. EACCES/EPERM and other IO keep their cause
83
+ // (Node fs docs: do not existsSync/access before open — #1161 C3).
84
+ try {
85
+ return parseSitianRecordText(await readFile(recordFile, "utf8"));
86
+ } catch (error) {
87
+ if (isEnoent(error)) return { records: [], diagnostics: [] };
88
+ throw error;
89
+ }
90
+ }
@@ -91,7 +91,7 @@ export function createDefaultGateOfficerSummon(options: {
91
91
  }
92
92
 
93
93
  /**
94
- * Book a typed pointer under parent session/auditor-roles (archivist-owned write).
94
+ * Book a typed pointer in the parent run's current.json officers section.
95
95
  * Offline mocks without a real session leave no nested volume (lawful zero).
96
96
  */
97
97
  function bookDirectOfficerPointer(
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { sessionFileIn, sessionFileOf } from "./role-run-placement.ts";
2
2
 
3
3
  import {
4
4
  resolveActivationLedgerHome,
@@ -133,7 +133,7 @@ async function submissionRecordFile(cwd: string, runId: string, scope: Submissio
133
133
  if (sessionParent === undefined) {
134
134
  const discoveredRun = await findRunDirectoryById(scope.home, runId);
135
135
  if (discoveredRun === undefined) return undefined;
136
- sessionParent = join(discoveredRun, "session", "session.jsonl");
136
+ sessionParent = sessionFileOf(discoveredRun);
137
137
  }
138
138
  return resolveSitianRecordPathInLedger({
139
139
  level: "event",
@@ -432,12 +432,12 @@ async function restoreState(cwd: string, runId: string, scope: SubmissionLedgerR
432
432
  /** Sole HostContext-derived session parent for ledger restore/append (never process.env). */
433
433
  function sessionParentFromHostContext(context: HostContext): string | undefined {
434
434
  const runDirectory = runDirectoryFromHostContext(context);
435
- if (runDirectory !== undefined) return join(runDirectory, "session", "session.jsonl");
435
+ if (runDirectory !== undefined) return sessionFileOf(runDirectory);
436
436
  const sessionFile = context.sessionManager.getSessionFile?.();
437
437
  if (typeof sessionFile === "string" && sessionFile.length > 0) return sessionFile;
438
438
  const sessionDir = context.sessionManager.getSessionDir?.();
439
439
  if (typeof sessionDir === "string" && sessionDir.length > 0) {
440
- return join(sessionDir, "session.jsonl");
440
+ return sessionFileIn(sessionDir);
441
441
  }
442
442
  return undefined;
443
443
  }
@@ -1,4 +1,6 @@
1
+ import { parseTicketNumber } from "./run-ticket-number.ts";
1
2
  import { isRecord } from "./unknown-value.ts";
3
+
2
4
  /**
3
5
  * 起居录(ticket-provenance)typed 形状 —— ADR 0075 / ADR 0081 / #1090。
4
6
  *
@@ -63,12 +65,7 @@ export type TicketProvenanceLine = {
63
65
  };
64
66
 
65
67
  function positiveInteger(value: unknown): number | undefined {
66
- if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) return value;
67
- if (typeof value === "string" && /^[1-9][0-9]*$/.test(value)) {
68
- const parsed = Number(value);
69
- if (Number.isSafeInteger(parsed)) return parsed;
70
- }
71
- return undefined;
68
+ return parseTicketNumber(value);
72
69
  }
73
70
 
74
71
  function projectBound(value: unknown): TicketProvenanceBound | undefined {
@@ -3,8 +3,8 @@
3
3
  * 一册=一个追加式 records.jsonl。每轮新投影经司天台 appender 追加为不可变提交;
4
4
  * 纯追加、不回读历史去重、不折叠。
5
5
  */
6
- import { readFile, unlink } from "node:fs/promises";
7
- import { basename, dirname, join, resolve } from "node:path";
6
+ import { readFile } from "node:fs/promises";
7
+ import { join, resolve } from "node:path";
8
8
 
9
9
  import {
10
10
  errnoCode,
@@ -17,6 +17,8 @@ import {
17
17
  type LedgerSessionLine,
18
18
  } from "./ledger-session-read.ts";
19
19
  import { isSafePositiveTicketNumber } from "./run-ticket-number.ts";
20
+ import { runDirectoryOfSessionFile, sessionFileOf } from "./role-run-placement.ts";
21
+ import { RUN_HISTORY_FILE } from "./run-dossier-files.ts";
20
22
  import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.ts";
21
23
  import {
22
24
  appendSitianRecordBlock,
@@ -24,6 +26,7 @@ import {
24
26
  resolveSitianRecordPath,
25
27
  type SitianRecordInput,
26
28
  } from "./sitian-facade.ts";
29
+ import { decodeSitianRecordLine } from "./sitian-reader.ts";
27
30
  import {
28
31
  TICKET_PROVENANCE_KIND,
29
32
  type TicketProvenanceBound,
@@ -76,10 +79,7 @@ function isLedgerRoleSessionFile(absolute: string, home?: string): boolean {
76
79
  : packageMachineHome();
77
80
  const ledgerHome = resolveActivationLedgerHome(machineHome);
78
81
  if (!physicallyContainedIn(ledgerHome, absolute)) return false;
79
- return (
80
- basename(absolute) === "session.jsonl" &&
81
- basename(dirname(absolute)) === "session"
82
- );
82
+ return sessionFileOf(runDirectoryOfSessionFile(absolute)) === absolute;
83
83
  }
84
84
 
85
85
  /** Real I/O seam gate: only host session stores or sitian role-run session.jsonl. */
@@ -152,16 +152,28 @@ export async function rehomeUnboundTicketProvenance(
152
152
  cwd: string,
153
153
  home: string,
154
154
  ): Promise<void> {
155
- const source = join(runDirectory, "records.jsonl");
156
- let content: string;
155
+ const ticketInput = ticketProvenanceRecordInput(ticketNumber, cwd, home);
156
+
157
+ // Live staging: ticket-provenance rows in the run's history.jsonl. Append-only
158
+ // history keeps the staging rows; the ticket-root diary is the home.
159
+ // Old run/records.jsonl is not a runtime input (#1161); only migration tools read it.
160
+ let history: string;
157
161
  try {
158
- content = await readFile(source, "utf8");
162
+ history = await readFile(join(runDirectory, RUN_HISTORY_FILE), "utf8");
159
163
  } catch (error) {
160
164
  if (errnoCode(error) === "ENOENT") return;
161
165
  throw error;
162
166
  }
163
- appendSitianRecordBlock(ticketProvenanceRecordInput(ticketNumber, cwd, home), content);
164
- await unlink(source);
167
+ const block = history
168
+ .split("\n")
169
+ .flatMap((line, index) => {
170
+ const decoded = decodeSitianRecordLine(line, index + 1);
171
+ if (decoded === undefined || !decoded.ok) return [];
172
+ if (decoded.record.kind !== TICKET_PROVENANCE_KIND) return [];
173
+ return [`${line}\n`];
174
+ })
175
+ .join("");
176
+ if (block !== "") appendSitianRecordBlock(ticketInput, block);
165
177
  }
166
178
 
167
179
  /**
@@ -14,6 +14,7 @@
14
14
  import { createGhApiRunner, type GhApiRunner } from "./gh-api-runner.ts";
15
15
 
16
16
  import { isRecord } from "./unknown-value.ts";
17
+ import { isPositiveTicketNumber } from "./run-ticket-number.ts";
17
18
 
18
19
  export type BookRepoBinding = {
19
20
  bookKey: string;
@@ -171,7 +172,7 @@ function parseClosedAt(raw: unknown, label: string): string | null {
171
172
  function parseTicketNode(raw: unknown, label: string): ParsedTicketNode {
172
173
  if (!isRecord(raw)) throw new Error(`${label} is not an object`);
173
174
  const number = raw.number;
174
- if (typeof number !== "number" || !Number.isInteger(number) || number < 1) {
175
+ if (!isPositiveTicketNumber(number)) {
175
176
  throw new Error(`${label}.number invalid`);
176
177
  }
177
178
  if (typeof raw.title !== "string") throw new Error(`${label}.title missing`);
@@ -536,7 +537,7 @@ export function createGhTicketSnapshotTransport(
536
537
  const closedNumbers = [
537
538
  ...new Set(
538
539
  input.closedIssueNumbers.filter(
539
- (n) => Number.isInteger(n) && n > 0 && !tickets.has(n),
540
+ (n) => isPositiveTicketNumber(n) && !tickets.has(n),
540
541
  ),
541
542
  ),
542
543
  ].sort((a, b) => a - b);