@akagilnc/pi-workflow-roles 0.1.4204 → 0.1.4228
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.
- package/dist/acp-host/production-host.js +1203 -770
- package/dist/atomic-write.js +23 -0
- package/dist/diarist-contracts.js +22 -8
- package/dist/headless-host/production-host.js +1191 -758
- package/dist/ledger-session-read.js +52 -21
- package/dist/migrate-book-topology.js +238 -205
- package/dist/public-cli/case-dossier-delivery.js +0 -1
- package/dist/public-cli/main.js +303 -261
- package/dist/session-dialogue.js +221 -0
- package/dist/sitian-facade.js +1 -0
- package/dist/sitian-volume.js +67 -0
- package/dist/ticket-provenance-contracts.js +122 -39
- package/dist/ticket-provenance.js +289 -204
- package/package.json +1 -1
- package/src/book-topology-migration.ts +0 -7
- package/src/book-topology-record-class-migrators.ts +203 -99
- package/src/diarist-contracts.ts +42 -29
- package/src/diarist-role.ts +77 -31
- package/src/diarist.ts +31 -67
- package/src/ledger-session-read.ts +70 -27
- package/src/package-contracts/reviewer-output.ts +1 -1
- package/src/public-cli/case-dossier-delivery.ts +0 -1
- package/src/reviewer-role.ts +3 -3
- package/src/role-runtime.ts +56 -14
- package/src/session-dialogue.ts +225 -0
- package/src/sitian-appender.ts +1 -1
- package/src/sitian-facade.ts +7 -1
- package/src/sitian-volume.ts +91 -0
- package/src/ticket-provenance-contracts.ts +158 -101
- package/src/ticket-provenance.ts +387 -263
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对话事实适配(ADR 0075 `dialogue-only` / `no-tool-output` / `speaker-required`)。
|
|
3
|
+
*
|
|
4
|
+
* 「对话」=人类输入内容 + 助手回话内容,由各宿主的**结构化会话事件**适配而来。
|
|
5
|
+
* 排除按来源、不按正文:说话人自己写下的文字一律原样保留,其中引用了 ADR 段落
|
|
6
|
+
* 或工具结果也不删(#901 Out of Scope「不做正文过滤」)。
|
|
7
|
+
*
|
|
8
|
+
* 不入录的是工具结果载荷、附件记录与注入块——它们是独立的机器事件,不是谁说的话。
|
|
9
|
+
*
|
|
10
|
+
* 适配规则按宿主并列,不硬套某一宿主的类型名;一行产出 0..n 条对话事实。
|
|
11
|
+
*/
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 原生 id:行级或消息体级主键的单一实现(bound 解析与落盘共用)。
|
|
17
|
+
* Pi/CC:行上 `uuid`/`id`;Codex:`response_item.payload.id`(或 `message.id`)。
|
|
18
|
+
*/
|
|
19
|
+
export function nativeEventId(row) {
|
|
20
|
+
for (const key of ["uuid", "id"]) {
|
|
21
|
+
const value = row[key];
|
|
22
|
+
if (typeof value === "string" && value !== "")
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
for (const nestKey of ["message", "payload"]) {
|
|
26
|
+
const nested = row[nestKey];
|
|
27
|
+
if (!isRecord(nested))
|
|
28
|
+
continue;
|
|
29
|
+
for (const key of ["uuid", "id"]) {
|
|
30
|
+
const value = nested[key];
|
|
31
|
+
if (typeof value === "string" && value !== "")
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
/** 说话人文本块类型:Pi/CC `text`;Codex rollout `input_text` / `output_text`。 */
|
|
38
|
+
const SPEAKER_TEXT_PART_TYPES = new Set(["text", "input_text", "output_text"]);
|
|
39
|
+
/** 文本块串接:content 为字符串即其本身,为数组则只取说话人文本片段(逐块过滤)。 */
|
|
40
|
+
function textParts(content) {
|
|
41
|
+
if (typeof content === "string")
|
|
42
|
+
return content === "" ? [] : [content];
|
|
43
|
+
if (!Array.isArray(content))
|
|
44
|
+
return [];
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const part of content) {
|
|
47
|
+
if (!isRecord(part) || !SPEAKER_TEXT_PART_TYPES.has(String(part.type)))
|
|
48
|
+
continue;
|
|
49
|
+
const text = part.text;
|
|
50
|
+
if (typeof text === "string" && text !== "")
|
|
51
|
+
out.push(text);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 取出 Pi/CC 消息体,或 Codex `response_item.payload.type=message`。
|
|
57
|
+
* developer 与注入用的 user 不在此层用正文识别——见整卷 Codex 规则。
|
|
58
|
+
*/
|
|
59
|
+
function responseItemMessage(row) {
|
|
60
|
+
if (row.type !== "response_item" || !isRecord(row.payload))
|
|
61
|
+
return undefined;
|
|
62
|
+
if (row.payload.type !== "message")
|
|
63
|
+
return undefined;
|
|
64
|
+
return row.payload;
|
|
65
|
+
}
|
|
66
|
+
function messageBody(row) {
|
|
67
|
+
if (isRecord(row.message))
|
|
68
|
+
return row.message;
|
|
69
|
+
return responseItemMessage(row);
|
|
70
|
+
}
|
|
71
|
+
function speakerOf(message) {
|
|
72
|
+
if (message.role === "assistant")
|
|
73
|
+
return "runner";
|
|
74
|
+
if (message.role === "user")
|
|
75
|
+
return "owner";
|
|
76
|
+
// developer / toolResult 等不是说话人回话。
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Codex `content_item_kinds`:行上结构化来源种类(不读正文)。
|
|
81
|
+
* 真 user 带 `user.text` / `user.image`;注入为 `agents_md.instructions`、
|
|
82
|
+
* `environments.environment_context`、`plugins.recommendations` 等。
|
|
83
|
+
*/
|
|
84
|
+
function codexContentItemKinds(message) {
|
|
85
|
+
const pass = message.internal_chat_message_metadata_passthrough;
|
|
86
|
+
if (!isRecord(pass) || !Array.isArray(pass.content_item_kinds))
|
|
87
|
+
return undefined;
|
|
88
|
+
const kinds = [];
|
|
89
|
+
for (const kind of pass.content_item_kinds) {
|
|
90
|
+
if (typeof kind === "string" && kind !== "")
|
|
91
|
+
kinds.push(kind);
|
|
92
|
+
}
|
|
93
|
+
return kinds;
|
|
94
|
+
}
|
|
95
|
+
/** response_item role=user 是否为真说话人输入(kinds 含 user.*)。 */
|
|
96
|
+
function isCodexOwnerResponseItemUser(message) {
|
|
97
|
+
if (message.role !== "user")
|
|
98
|
+
return false;
|
|
99
|
+
const kinds = codexContentItemKinds(message);
|
|
100
|
+
// 无 kinds 的旧卷:不能结构化分辨注入与真 user,宁缺勿把注入当陛下。
|
|
101
|
+
if (kinds === undefined)
|
|
102
|
+
return false;
|
|
103
|
+
return kinds.some((kind) => kind.startsWith("user."));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 入队事件适配:人类这次输入经队列的来源。
|
|
107
|
+
* 配对的出队事件不另取,也不做正文比对去重——被吸收的插话正是靠入队事件入录
|
|
108
|
+
* (它没有独立的消息记录,#901 用户故事 11)。
|
|
109
|
+
*/
|
|
110
|
+
function fromQueueEvent(row) {
|
|
111
|
+
if (row.type !== "queue-operation" || row.operation !== "enqueue")
|
|
112
|
+
return [];
|
|
113
|
+
const content = row.content;
|
|
114
|
+
if (typeof content !== "string" || content === "")
|
|
115
|
+
return [];
|
|
116
|
+
const id = nativeEventId(row);
|
|
117
|
+
return [{ speaker: "owner", text: content, ...(id === undefined ? {} : { id }) }];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 消息事件适配:助手回话,以及未经入队事件记录的人类输入。
|
|
121
|
+
* 思考块、工具调用块、工具结果块按块排除;同消息的说话人 text 保留
|
|
122
|
+
* (#901 排除按来源不按正文;混合块不得整条丢弃)。
|
|
123
|
+
* Codex response_item user:仅 `content_item_kinds` 含 `user.*` 时入录。
|
|
124
|
+
*/
|
|
125
|
+
function fromMessageEvent(row) {
|
|
126
|
+
const message = messageBody(row);
|
|
127
|
+
if (message === undefined)
|
|
128
|
+
return [];
|
|
129
|
+
if (message.role === "toolResult")
|
|
130
|
+
return [];
|
|
131
|
+
const speaker = speakerOf(message);
|
|
132
|
+
if (speaker === undefined)
|
|
133
|
+
return [];
|
|
134
|
+
// Codex response_item user:按行上 kinds 结构化判定,不咬正文、不靠 turn 序。
|
|
135
|
+
if (row.type === "response_item" && speaker === "owner") {
|
|
136
|
+
if (!isCodexOwnerResponseItemUser(message))
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const parts = textParts(message.content);
|
|
140
|
+
if (parts.length === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const text = parts.join("");
|
|
143
|
+
if (text === "")
|
|
144
|
+
return [];
|
|
145
|
+
// 与 bound lookup 同源:nativeEventId 已含 payload/message id。
|
|
146
|
+
const id = nativeEventId(row);
|
|
147
|
+
return [{ speaker, text, ...(id === undefined ? {} : { id }) }];
|
|
148
|
+
}
|
|
149
|
+
/** 该行是否为可产出 owner 对话正文的消息(工具结果块不算;旁路 text 算)。 */
|
|
150
|
+
function isOwnerDialogueMessage(row) {
|
|
151
|
+
// CC/Pi queue pairing only — Codex owner path is separate.
|
|
152
|
+
if (row.type === "response_item" || row.type === "event_msg")
|
|
153
|
+
return false;
|
|
154
|
+
return fromMessageEvent(row).some((event) => event.speaker === "owner");
|
|
155
|
+
}
|
|
156
|
+
/** 该行是否为 runner 回话事件(用于解除未物化的 dequeue 配对)。 */
|
|
157
|
+
function isRunnerMessage(row) {
|
|
158
|
+
const message = messageBody(row);
|
|
159
|
+
return message !== undefined && message.role === "assistant";
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 逐次来源配对:仅当对应 enqueue 已由 fromQueueEvent 实际形成 retained owner
|
|
163
|
+
* 对话时,才把随后的物化 user 当副本跳过。FIFO 对齐 enqueue→dequeue/remove。
|
|
164
|
+
* 无正文 enqueue、旧宿主或其他不可投影形状:dequeue 不产生 skip,后续真人
|
|
165
|
+
* user 原样保留。未物化的 retained dequeue(被吸收插话)遇 runner 解除。
|
|
166
|
+
* 不用「卷内曾出现 enqueue」整卷布尔,也不按正文过滤。
|
|
167
|
+
*/
|
|
168
|
+
function ownerMessagesMaterializingQueue(rows) {
|
|
169
|
+
const skip = new Set();
|
|
170
|
+
/** 各 enqueue 是否已留下 owner 对话,按入队顺序等 dequeue/remove 消费。 */
|
|
171
|
+
const retainedByEnqueue = [];
|
|
172
|
+
let pendingSkips = 0;
|
|
173
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
174
|
+
const row = rows[index];
|
|
175
|
+
if (row === undefined)
|
|
176
|
+
continue;
|
|
177
|
+
if (row.type === "queue-operation") {
|
|
178
|
+
if (row.operation === "enqueue") {
|
|
179
|
+
retainedByEnqueue.push(fromQueueEvent(row).length > 0);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (row.operation === "dequeue" || row.operation === "remove") {
|
|
183
|
+
const retained = retainedByEnqueue.shift();
|
|
184
|
+
// 只在 enqueue 真留下对话且本事件是 dequeue 物化时才跳过后续 user。
|
|
185
|
+
// remove 只消费队列槽,不制造 skip(无物化消息)。
|
|
186
|
+
if (row.operation === "dequeue" && retained === true) {
|
|
187
|
+
pendingSkips += 1;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (isRunnerMessage(row)) {
|
|
193
|
+
pendingSkips = 0;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (pendingSkips > 0 && isOwnerDialogueMessage(row)) {
|
|
197
|
+
skip.add(index);
|
|
198
|
+
pendingSkips -= 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return skip;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 整卷适配:返回与入参行等长的对话事实数组(该行不是对话则为空数组)。
|
|
205
|
+
* 需要整卷视野:enqueue/dequeue 配对跨行。
|
|
206
|
+
*/
|
|
207
|
+
export function adaptSessionDialogue(rows) {
|
|
208
|
+
const skipOwner = ownerMessagesMaterializingQueue(rows);
|
|
209
|
+
return rows.map((row, index) => {
|
|
210
|
+
if (row === undefined)
|
|
211
|
+
return [];
|
|
212
|
+
const queued = fromQueueEvent(row);
|
|
213
|
+
if (queued.length > 0)
|
|
214
|
+
return queued;
|
|
215
|
+
// Codex event_msg.user_message 无 content_item_kinds 等 provenance,旧 exec
|
|
216
|
+
// 卷中与 worker entrypoint 注入同形——无法证明为 owner 则不取(不猜、不咬正文)。
|
|
217
|
+
if (skipOwner.has(index))
|
|
218
|
+
return [];
|
|
219
|
+
return fromMessageEvent(row);
|
|
220
|
+
});
|
|
221
|
+
}
|
package/dist/sitian-facade.js
CHANGED
|
@@ -2,6 +2,7 @@ import { appendSitianRecord } from "./sitian-appender.js";
|
|
|
2
2
|
export * from "./sitian-contracts.js";
|
|
3
3
|
export * from "./sitian-appender.js";
|
|
4
4
|
export * from "./sitian-reader.js";
|
|
5
|
+
export * from "./sitian-volume.js";
|
|
5
6
|
function sitianReport(input) {
|
|
6
7
|
return appendSitianRecord(input);
|
|
7
8
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitian whole-file volume I/O (ADR 0065 records-owner / record-entry).
|
|
3
|
+
*
|
|
4
|
+
* Owns destination resolution, create, raw read, and whole-file rewrite for
|
|
5
|
+
* kinds whose persistence shape is not the appender SitianRecord row
|
|
6
|
+
* (ticket-provenance header + bare dialogue lines, #901). Appender kernel
|
|
7
|
+
* stays append-only; this module does not restore append watermarks.
|
|
8
|
+
*/
|
|
9
|
+
import { appendFileSync } from "node:fs";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { ensureRealDirectoryTree, errorText, } from "./activation-ledger-topology.js";
|
|
12
|
+
import { writeFileAtomically } from "./atomic-write.js";
|
|
13
|
+
import { resolveSitianRecordPath } from "./sitian-appender.js";
|
|
14
|
+
import { SitianInfrastructureError, } from "./sitian-contracts.js";
|
|
15
|
+
/** Resolve a volume path from ambient topology — callers never pick the destination. */
|
|
16
|
+
export function resolveSitianVolume(input) {
|
|
17
|
+
const path = resolveSitianRecordPath(input);
|
|
18
|
+
return { recordFile: path.recordFile, volumeDir: path.sessionDir };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ensure the volume directory + file exist.
|
|
22
|
+
* Append-open creates an absent volume without truncating concurrent first writers.
|
|
23
|
+
*/
|
|
24
|
+
export function ensureSitianVolume(input) {
|
|
25
|
+
try {
|
|
26
|
+
const { sessionDir, recordFile, ledgerHome } = resolveSitianRecordPath(input);
|
|
27
|
+
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
28
|
+
appendFileSync(recordFile, "", "utf8");
|
|
29
|
+
return { recordFile, volumeDir: sessionDir };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof SitianInfrastructureError)
|
|
33
|
+
throw error;
|
|
34
|
+
throw new SitianInfrastructureError(`Sitian volume ensure failure: ${errorText(error)}`, { cause: error });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Raw volume text. Absent file → text undefined (not an error). */
|
|
38
|
+
export async function readSitianVolumeText(input) {
|
|
39
|
+
const { recordFile } = resolveSitianVolume(input);
|
|
40
|
+
try {
|
|
41
|
+
return { recordFile, text: await readFile(recordFile, "utf8") };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ENOENT") {
|
|
45
|
+
return { recordFile, text: undefined };
|
|
46
|
+
}
|
|
47
|
+
throw new SitianInfrastructureError(`Sitian volume read failure: ${errorText(error)}`, { cause: error });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Replace the entire volume body (whole-file reproject).
|
|
52
|
+
* Same-directory temp + rename — never truncate the live inode in place
|
|
53
|
+
* (#901 sole diary; reuse atomic-write, no parallel persist mechanism).
|
|
54
|
+
* Not append; no identity claim / torn-tail watermark — those stay on appender.
|
|
55
|
+
*/
|
|
56
|
+
export async function rewriteSitianVolume(input) {
|
|
57
|
+
try {
|
|
58
|
+
const volume = ensureSitianVolume(input);
|
|
59
|
+
await writeFileAtomically(volume.recordFile, input.body);
|
|
60
|
+
return volume;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof SitianInfrastructureError)
|
|
64
|
+
throw error;
|
|
65
|
+
throw new SitianInfrastructureError(`Sitian volume rewrite failure: ${errorText(error)}`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1,63 +1,146 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 起居录(ticket-provenance)typed
|
|
3
|
-
*
|
|
2
|
+
* 起居录(ticket-provenance)typed 形状 —— ADR 0075「2026-09-14 修订」/ #901。
|
|
3
|
+
*
|
|
4
|
+
* 一册=一个文件:第一行册子头,其后每行一条对话。
|
|
5
|
+
* 每轮按册子头当前各区间**重投影**这份唯一文件:册子头与各条定位可更新,
|
|
6
|
+
* 正文原样不改(`single-volume` / `one-volume-per-issue`)。
|
|
4
7
|
*/
|
|
5
8
|
/** Sitian kind for per-ticket court diary volumes. */
|
|
6
9
|
export const TICKET_PROVENANCE_KIND = "ticket-provenance";
|
|
7
|
-
/** Human-read view filename co-located with the JSONL volume. */
|
|
8
|
-
export const TICKET_PROVENANCE_HUMAN_VIEW = "起居录.md";
|
|
9
|
-
/** Payload discriminator: diagnostic residue (not a diary body entry). */
|
|
10
|
-
export const TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC = "diagnostic";
|
|
11
10
|
function isRecord(value) {
|
|
12
11
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
13
|
+
function positiveInteger(value) {
|
|
14
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1)
|
|
15
|
+
return value;
|
|
16
|
+
if (typeof value === "string" && /^[1-9][0-9]*$/.test(value)) {
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
if (Number.isSafeInteger(parsed))
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
function nonNegativeInteger(value) {
|
|
24
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value)) {
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (Number.isSafeInteger(parsed))
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function projectSpeaker(value) {
|
|
34
|
+
return value === "owner" || value === "runner" ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
function projectBound(value) {
|
|
37
|
+
if (!isRecord(value))
|
|
38
|
+
return undefined;
|
|
39
|
+
const id = typeof value.id === "string" && value.id !== "" ? value.id : undefined;
|
|
40
|
+
const line = positiveInteger(value.line);
|
|
41
|
+
if (id === undefined && line === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
return { ...(id === undefined ? {} : { id }), ...(line === undefined ? {} : { line }) };
|
|
44
|
+
}
|
|
19
45
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
46
|
+
* 投影起居郎交上来的边界。
|
|
47
|
+
* 端点无法指名、卷路径缺失即返回 undefined——调用者据此走既有 reask 请其重交,
|
|
48
|
+
* 不猜、不补、不中止本轮(`reask-not-explode`)。
|
|
22
49
|
*/
|
|
23
|
-
export function
|
|
50
|
+
export function projectTicketProvenanceSessions(value) {
|
|
51
|
+
// Empty array = lawful empty selection (no dialogue ranges this turn).
|
|
52
|
+
// Non-array / malformed member = unusable bounds → caller reasks.
|
|
53
|
+
if (!Array.isArray(value))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (value.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
const sessions = [];
|
|
58
|
+
for (const raw of value) {
|
|
59
|
+
if (!isRecord(raw))
|
|
60
|
+
return undefined;
|
|
61
|
+
const path = raw.path;
|
|
62
|
+
if (typeof path !== "string" || path.trim() === "")
|
|
63
|
+
return undefined;
|
|
64
|
+
if (!Array.isArray(raw.ranges) || raw.ranges.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
const ranges = [];
|
|
67
|
+
for (const rawRange of raw.ranges) {
|
|
68
|
+
if (!isRecord(rawRange))
|
|
69
|
+
return undefined;
|
|
70
|
+
const from = projectBound(rawRange.from);
|
|
71
|
+
const to = projectBound(rawRange.to);
|
|
72
|
+
if (from === undefined || to === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
ranges.push({ from, to });
|
|
75
|
+
}
|
|
76
|
+
sessions.push({ path, ranges });
|
|
77
|
+
}
|
|
78
|
+
return sessions;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 投影补写集合。缺该字段=无补写(不拒收);成员形状不成立的整条跳过——
|
|
82
|
+
* 它没有绑定位置,放不回去。
|
|
83
|
+
*/
|
|
84
|
+
export function projectTicketProvenanceAmendments(value) {
|
|
85
|
+
if (!Array.isArray(value))
|
|
86
|
+
return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const raw of value) {
|
|
89
|
+
if (!isRecord(raw))
|
|
90
|
+
continue;
|
|
91
|
+
const s = nonNegativeInteger(raw.s);
|
|
92
|
+
const line = positiveInteger(raw.line);
|
|
93
|
+
const speaker = projectSpeaker(raw.speaker);
|
|
94
|
+
const text = raw.text;
|
|
95
|
+
if (s === undefined || line === undefined || speaker === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
if (typeof text !== "string")
|
|
98
|
+
continue;
|
|
99
|
+
out.push({ s, line, speaker, text });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** 读回册子头(文件第一行)。形状不成立即 undefined——按无册处理,不猜。 */
|
|
104
|
+
export function projectTicketProvenanceHeader(value) {
|
|
24
105
|
if (!isRecord(value))
|
|
25
106
|
return undefined;
|
|
26
|
-
|
|
107
|
+
const ticket = positiveInteger(value.ticket);
|
|
108
|
+
if (ticket === undefined)
|
|
27
109
|
return undefined;
|
|
28
|
-
|
|
29
|
-
if (typeof value.diagnosticKind !== "string" ||
|
|
30
|
-
!DIAGNOSTIC_KINDS.has(value.diagnosticKind)) {
|
|
110
|
+
if (typeof value.repo !== "string")
|
|
31
111
|
return undefined;
|
|
32
|
-
|
|
33
|
-
if (typeof value.cause !== "string" || value.cause.length === 0) {
|
|
112
|
+
if (typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") {
|
|
34
113
|
return undefined;
|
|
35
114
|
}
|
|
36
|
-
|
|
115
|
+
// Malformed sessions stay unusable — do not wash into lawful empty (失败诚实).
|
|
116
|
+
const sessions = projectTicketProvenanceSessions(value.sessions);
|
|
117
|
+
if (sessions === undefined)
|
|
37
118
|
return undefined;
|
|
38
|
-
}
|
|
39
119
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
120
|
+
repo: value.repo,
|
|
121
|
+
ticket,
|
|
122
|
+
createdAt: value.createdAt,
|
|
123
|
+
updatedAt: value.updatedAt,
|
|
124
|
+
sessions,
|
|
45
125
|
};
|
|
46
126
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
* Shape is not an admission gate for role output; this is the diarist/write seam
|
|
50
|
-
* self-check so garbage does not enter the volume.
|
|
51
|
-
* Diagnostics (recordClass discriminator) are not entries.
|
|
52
|
-
*/
|
|
53
|
-
export function projectTicketProvenanceEntry(value) {
|
|
127
|
+
/** 读回一条对话行。 */
|
|
128
|
+
export function projectTicketProvenanceLine(value) {
|
|
54
129
|
if (!isRecord(value))
|
|
55
130
|
return undefined;
|
|
56
|
-
|
|
131
|
+
const speaker = projectSpeaker(value.speaker);
|
|
132
|
+
const s = nonNegativeInteger(value.s);
|
|
133
|
+
if (speaker === undefined || s === undefined)
|
|
57
134
|
return undefined;
|
|
58
|
-
|
|
59
|
-
if (typeof value.sourceKind !== "string" || typeof value.transcript !== "string") {
|
|
135
|
+
if (typeof value.text !== "string")
|
|
60
136
|
return undefined;
|
|
61
|
-
|
|
62
|
-
|
|
137
|
+
const line = positiveInteger(value.line);
|
|
138
|
+
const id = typeof value.id === "string" && value.id !== "" ? value.id : undefined;
|
|
139
|
+
return {
|
|
140
|
+
speaker,
|
|
141
|
+
s,
|
|
142
|
+
...(line === undefined ? {} : { line }),
|
|
143
|
+
...(id === undefined ? {} : { id }),
|
|
144
|
+
text: value.text,
|
|
145
|
+
};
|
|
63
146
|
}
|