dsh-data-domain-knowledge 0.1.0-alpha.2 → 0.1.0-alpha.3

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 (117) hide show
  1. package/README.md +31 -7
  2. package/cordis.patch.yml +11 -5
  3. package/lib/client.js +5557 -3056
  4. package/lib/doc-page.js +7740 -0
  5. package/lib/types/convert/host.d.ts.map +1 -1
  6. package/lib/types/convert/host.js +36 -9
  7. package/lib/types/convert/host.js.map +1 -1
  8. package/lib/types/diagnosis/cards.d.ts +8 -7
  9. package/lib/types/diagnosis/cards.d.ts.map +1 -1
  10. package/lib/types/diagnosis/cards.js +119 -49
  11. package/lib/types/diagnosis/cards.js.map +1 -1
  12. package/lib/types/diagnosis/run.d.ts +51 -0
  13. package/lib/types/diagnosis/run.d.ts.map +1 -0
  14. package/lib/types/diagnosis/run.js +239 -0
  15. package/lib/types/diagnosis/run.js.map +1 -0
  16. package/lib/types/diagnosis/schema.d.ts +25 -2
  17. package/lib/types/diagnosis/schema.d.ts.map +1 -1
  18. package/lib/types/diagnosis/schema.js +64 -25
  19. package/lib/types/diagnosis/schema.js.map +1 -1
  20. package/lib/types/diagnosis/seed-source.d.ts +17 -2
  21. package/lib/types/diagnosis/seed-source.d.ts.map +1 -1
  22. package/lib/types/diagnosis/seed-source.js +18 -1
  23. package/lib/types/diagnosis/seed-source.js.map +1 -1
  24. package/lib/types/diagnosis/seed.d.ts +32 -42
  25. package/lib/types/diagnosis/seed.d.ts.map +1 -1
  26. package/lib/types/diagnosis/seed.js +187 -51
  27. package/lib/types/diagnosis/seed.js.map +1 -1
  28. package/lib/types/diagnosis/store.d.ts +114 -26
  29. package/lib/types/diagnosis/store.d.ts.map +1 -1
  30. package/lib/types/diagnosis/store.js +336 -167
  31. package/lib/types/diagnosis/store.js.map +1 -1
  32. package/lib/types/diagnosis/types.d.ts +168 -16
  33. package/lib/types/diagnosis/types.d.ts.map +1 -1
  34. package/lib/types/diagnosis/types.js +1 -1
  35. package/lib/types/diagnosis/types.js.map +1 -1
  36. package/lib/types/document-page.d.ts +39 -0
  37. package/lib/types/document-page.d.ts.map +1 -0
  38. package/lib/types/document-page.js +43 -0
  39. package/lib/types/document-page.js.map +1 -0
  40. package/lib/types/fs-atomic.d.ts +12 -0
  41. package/lib/types/fs-atomic.d.ts.map +1 -0
  42. package/lib/types/fs-atomic.js +42 -0
  43. package/lib/types/fs-atomic.js.map +1 -0
  44. package/lib/types/index.d.ts.map +1 -1
  45. package/lib/types/index.js +214 -24
  46. package/lib/types/index.js.map +1 -1
  47. package/lib/types/knowledge-bases.d.ts +10 -2
  48. package/lib/types/knowledge-bases.d.ts.map +1 -1
  49. package/lib/types/knowledge-bases.js +20 -18
  50. package/lib/types/knowledge-bases.js.map +1 -1
  51. package/lib/types/proposals.d.ts +166 -0
  52. package/lib/types/proposals.d.ts.map +1 -0
  53. package/lib/types/proposals.js +238 -0
  54. package/lib/types/proposals.js.map +1 -0
  55. package/lib/types/runtime.d.ts +199 -7
  56. package/lib/types/runtime.d.ts.map +1 -1
  57. package/lib/types/runtime.js +728 -145
  58. package/lib/types/runtime.js.map +1 -1
  59. package/lib/types/settings-schema.d.ts +2 -2
  60. package/lib/types/types.d.ts +61 -10
  61. package/lib/types/types.d.ts.map +1 -1
  62. package/lib/types/types.js +56 -8
  63. package/lib/types/types.js.map +1 -1
  64. package/lib/types/wiki-types.d.ts +8 -1
  65. package/lib/types/wiki-types.d.ts.map +1 -1
  66. package/lib/types/wiki-types.js +16 -25
  67. package/lib/types/wiki-types.js.map +1 -1
  68. package/package.json +4 -3
  69. package/preset/tita-data-agent/agent.cordis.yml +11 -5
  70. package/src/client/DirectoryPicker.tsx +12 -12
  71. package/src/client/InfoHint.tsx +21 -8
  72. package/src/client/IntentTab.tsx +20 -26
  73. package/src/client/KnowledgeBaseTab.tsx +190 -80
  74. package/src/client/KnowledgePanel.tsx +42 -9
  75. package/src/client/KnowledgeStepRail.tsx +3 -0
  76. package/src/client/NodeSettings.tsx +20 -22
  77. package/src/client/PreviewSections.tsx +87 -33
  78. package/src/client/ProposalReview.tsx +313 -0
  79. package/src/client/TypeDefinitionDrawer.tsx +72 -38
  80. package/src/client/WikiTypeEditor.tsx +193 -77
  81. package/src/client/arch/AgentArchitectureGraph.tsx +143 -14
  82. package/src/client/arch/NodeLogicPanel.tsx +6 -3
  83. package/src/client/arch/model.ts +164 -26
  84. package/src/client/architecture.css +29 -9
  85. package/src/client/architecture.tsx +54 -29
  86. package/src/client/attribution/AttributionGraph.tsx +481 -34
  87. package/src/client/attribution/AttributionTab.tsx +507 -106
  88. package/src/client/attribution/CaseView.tsx +9 -14
  89. package/src/client/attribution/GraphStage.tsx +25 -18
  90. package/src/client/attribution/RunView.tsx +27 -9
  91. package/src/client/attribution/StageChrome.tsx +296 -140
  92. package/src/client/attribution/graph-options.ts +611 -47
  93. package/src/client/attribution/model.ts +559 -30
  94. package/src/client/convert/ConvertPanel.tsx +6 -1
  95. package/src/client/doc-page.css +134 -0
  96. package/src/client/doc-page.tsx +263 -0
  97. package/src/client/panel-state.ts +105 -0
  98. package/src/client/request.ts +36 -0
  99. package/src/client/styles.css +317 -43
  100. package/src/client/useFitScale.ts +15 -2
  101. package/src/client/wiki-type-groups.ts +86 -0
  102. package/src/convert/host.ts +41 -7
  103. package/src/diagnosis/cards.ts +120 -50
  104. package/src/diagnosis/run.ts +245 -0
  105. package/src/diagnosis/schema.ts +67 -26
  106. package/src/diagnosis/seed-source.ts +28 -3
  107. package/src/diagnosis/seed.ts +187 -85
  108. package/src/diagnosis/store.ts +358 -174
  109. package/src/diagnosis/types.ts +181 -16
  110. package/src/document-page.ts +51 -0
  111. package/src/fs-atomic.ts +42 -0
  112. package/src/index.ts +209 -22
  113. package/src/knowledge-bases.ts +20 -18
  114. package/src/proposals.ts +337 -0
  115. package/src/runtime.ts +736 -135
  116. package/src/types.ts +113 -19
  117. package/src/wiki-types.ts +17 -27
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 归因图谱的**领域模型**:节点 / 边 / 图 的形状与枚举。
3
3
  *
4
- * 为什么它不在 client 里:宿主也要读写同一套结构(`05_diagnosis/*.jsonl`、
4
+ * 为什么它不在 client 里:宿主也要读写同一套结构(`01_source_truth/agent_diagnosis_case/`、
5
5
  * `/diagnosis/graph`)。视图是消费者,不是所有者 —— 模型放在领域层,客户端和宿主
6
6
  * 各自 import,谁都不能私自改形状。
7
7
  *
@@ -14,26 +14,55 @@ import type { ProblemClassId } from './problem-classes.js'
14
14
  /** 类型层 / 引用层 / 实例层。轴不同,不能合成一种节点。 */
15
15
  export type NodeLayer = 'type' | 'reference' | 'instance'
16
16
 
17
- /** 九类归因节点。与 wiki 卡片的 `entity_type` 一一对应。 */
17
+ /**
18
+ * 九类归因节点。与 wiki 卡片的 `entity_type` 一一对应(`asset` 与三类执行产物除外)。
19
+ *
20
+ * **两个名字用的是下划线**(`check_point` / `failure_reason`),卡片类型用连字符
21
+ * (`diagnosis/check-point` / `diagnosis/failure-reason`):kind 是代码里的标识符,
22
+ * 卡片类型是 `entity_type`,后者沿用 `diagnosis/*` 已有的连字符习惯。
23
+ */
18
24
  export type AttributionNodeKind =
19
25
  | 'symptom'
20
26
  | 'hypothesis'
21
- | 'check'
22
- | 'failure-mode'
27
+ | 'check_point'
28
+ | 'failure_reason'
23
29
  | 'asset'
24
30
  | 'case'
25
31
  | 'evidence'
26
32
  | 'cause'
27
33
  | 'unknown'
28
34
 
29
- /** 失败模式切面。分类只影响排序与分组,不影响「能查到什么」。 */
30
- export type FailureMode = 'upstream-missing' | 'spec-change' | 'data-processing' | 'consumption' | 'permission'
35
+ /**
36
+ * 失败原因切面 —— **原因出在链路的哪一环**(五个取值之一)。
37
+ *
38
+ * 原名叫 `FailureMode`(失败模式)。改名的理由:「failure mode」是可靠性工程(FMEA)
39
+ * 里的术语,指**故障长什么样**(「间歇性失效」「启动即失败」);而这一类装的是
40
+ * **原因出在链路的哪一环**。沿用旧名会让人往里塞「数据质量差」这种既不是形态、
41
+ * 也不是环节的东西 —— 而那正是 schema 校验要拦的东西。
42
+ */
43
+ export type FailureReason = 'upstream-missing' | 'spec-change' | 'data-processing' | 'consumption' | 'permission'
44
+
45
+ /** 一次查询的结论。`inconclusive` 是**一等取值**:便宜检查给不出结论,这本身就是知识。 */
46
+ export type EvidenceVerdict = 'supports' | 'refutes' | 'inconclusive'
47
+
48
+ /**
49
+ * 资产的种类(表 / 指标 / 任务 / 链路)。
50
+ *
51
+ * 它由**卡片声明**,不由代码从 id 猜:`assets[]` 的每一项可以写成 `"dwd_ord_di"`,
52
+ * 也可以写成 `{ id: "dwd_ord_di", type: "table" }`。猜前缀(`dwd_` 是表、`metric_` 是指标)
53
+ * 看着方便,但它会在有人给表起名 `t_xxx` 的那天静默失效 —— 而失效的表现是
54
+ * 「查询依据那一段的排序莫名其妙地变了」,没人会想到去查一个前缀规则。
55
+ *
56
+ * 声明了类型时,「查询依据」那一段按它分组(§7.7 ④);没声明就退回 id 序,
57
+ * 两种都稳定、可复现。
58
+ */
59
+ export type AssetType = 'table' | 'metric' | 'job' | 'linkage'
31
60
 
32
61
  /**
33
62
  * 问题侧的分类字段(只出现在 symptom 节点上)。
34
63
  *
35
- * 与 `FailureMode` 是**两根不同的轴,不能合并**:这一对说的是「问题是什么样」,
36
- * `failureMode` 说的是「原因出在哪一环」。前者挂在现象上、值班同学看到异常就能填;
64
+ * 与 `FailureReason` 是**两根不同的轴,不能合并**:这一对说的是「问题是什么样」,
65
+ * `failureReason` 说的是「原因出在哪一环」。前者挂在现象上、值班同学看到异常就能填;
37
66
  * 后者挂在假设上、要等排查有结论。合成一根轴,就会出现「异常波动归因属于哪个问题大类」
38
67
  * 这种没有答案的问题(0021 §4.5 与本文件顶部的分层原则)。
39
68
  */
@@ -44,11 +73,12 @@ export interface ProblemTaxonomy {
44
73
  problemSubclass?: string
45
74
  }
46
75
 
47
- /** 九种边。层内定义可能性与顺序,跨层定义证据与复用。 */
76
+ /** 十种边。层内定义可能性与顺序,跨层定义证据与复用。 */
48
77
  export type AttributionEdgeKind =
49
78
  | 'may_be_caused_by'
50
79
  | 'verified_by'
51
80
  | 'categorized_as'
81
+ | 'queries'
52
82
  | 'produces'
53
83
  | 'supports'
54
84
  | 'refutes'
@@ -63,7 +93,7 @@ export interface AttributionNode extends ProblemTaxonomy {
63
93
  title: string
64
94
  /** 一句话判据。图谱视图在 tooltip 与侧栏里用它,而不是靠标题猜。 */
65
95
  summary?: string
66
- failureMode?: FailureMode
96
+ failureReason?: FailureReason
67
97
  /** 被复用次数:节点大小由它决定,这是「图有没有在被用」最直接的读数。 */
68
98
  reuseCount?: number
69
99
  /** 最低成本检查的耗时(分钟),单位与 0021 §4.4 一致。 */
@@ -84,19 +114,50 @@ export interface AttributionNode extends ProblemTaxonomy {
84
114
  * 没有它,查重会把所有相似情形并进一条假设,图会慢慢失去分辨力。
85
115
  */
86
116
  notWhen?: string
87
- /** 检查的判定阈值:看到什么算真 / 算假。缺了它,这一步就无法被自动化或复核。 */
117
+ /** 检查点的判定阈值:看到什么算真 / 算假。缺了它,这一步就无法被自动化或复核。 */
88
118
  verdictTrue?: string
89
119
  verdictFalse?: string
90
120
  /**
91
- * 失败模式节点的稳定编码(= `FailureMode`)。只出现在 `failure-mode` 节点上。
121
+ * 检查点的**查询依据**:这条检查要去查哪些对象(表 / 指标 / 任务 / 链路)。
92
122
  *
93
- * 假设卡片里写的是 `failure_mode: data-processing`(编码),而边要连到**节点**,
123
+ * 与假设上的 `assets` 是两件事,落在两种边上:这里是 `queries`(这条检查要去查什么),
124
+ * 那里是 `indexed_from`(这条假设落在哪个对象上)。
125
+ */
126
+ assets?: string[]
127
+ /**
128
+ * 资产的种类。**只出现在 `asset` 节点上**,且只有卡片声明过才有值。
129
+ *
130
+ * 它读起来像是「节点的一个属性」,其实来源是**别的卡片**:资产节点由引用反推,
131
+ * 而种类写在引用它的那对 `assets[]` 里(`{ id, type }` 形态)。
132
+ */
133
+ assetType?: AssetType
134
+ /**
135
+ * 失败原因节点的稳定编码(= `FailureReason`)。只出现在 `failure_reason` 节点上。
136
+ *
137
+ * 假设卡片里写的是 `failure_reason: data-processing`(编码),而边要连到**节点**,
94
138
  * 所以需要一条「编码 → 节点 id」的可逆映射。少了它,`categorized_as` 边就只能靠
95
- * 「F1 恰好是第一个失败模式」这种位置约定去猜 —— 那是会在某次重排后静默出错的东西。
139
+ * 「F1 恰好是第一个失败原因」这种位置约定去猜 —— 那是会在某次重排后静默出错的东西。
96
140
  */
97
- code?: FailureMode
141
+ code?: FailureReason
98
142
  /** 边或节点是否已被判定失效(原因已修好 / 90 天未走)。 */
99
143
  stale?: boolean
144
+
145
+ /* ── 以下是**实例层执行产物**(evidence)专属字段:一次查询的输入与输出 ── */
146
+
147
+ /** 这次实际跑的查询语句。可复用的查询模板归 `check_point.runbook`,不是这里。 */
148
+ query?: string
149
+ /** 看到什么。 */
150
+ result?: string
151
+ /** 这个结果算不算数:支持 / 否定 / 给不出结论。 */
152
+ verdict?: EvidenceVerdict
153
+ /** **为什么**算数或不算数。评估要有对称性:采信与剔除都得能说理由。 */
154
+ why?: string
155
+ /** 什么时候跑的。 */
156
+ at?: string
157
+ /** 谁跑的。 */
158
+ by?: string
159
+ /** 原始输出指向哪里(日志 / 结果集 / 附件)。 */
160
+ rawRef?: string
100
161
  }
101
162
 
102
163
  export interface AttributionEdge {
@@ -125,6 +186,45 @@ export interface AttributionGraphData {
125
186
  stats?: Partial<AttributionStats>
126
187
  }
127
188
 
189
+ /**
190
+ * 案例 = **一篇 wiki 定义 + 一条有序排查路径**。
191
+ *
192
+ * 它住在领域层而不是示例里(原来是 `seed.ts`):案例是编译产物的一种
193
+ * (`cardsToGraph()` 从卡片把它算出来,`/diagnosis/graph` 把它发给界面),
194
+ * 而示例只是**一份**案例的取值来源。视图要渲染「服务端给的案例」,
195
+ * 那它 import 的形状就不该从示例模块里拿。
196
+ *
197
+ * 两段各有明确消费者,不能合并:
198
+ * - `sections` 是给人读、给向量召回的散文(可 diff、可评审、可追溯);
199
+ * - `path` 是给图和剪枝用的结构(有序、可判真假、带被否定的分支)。
200
+ *
201
+ * 序和枚举一旦写进散文,归并算法就再也读不到它们了 —— 所以路径单独成段。
202
+ */
203
+ export interface CaseSection {
204
+ heading: string
205
+ body: string
206
+ }
207
+
208
+ export interface CasePathStep {
209
+ nodeId: string
210
+ label: string
211
+ note: string
212
+ /** 这一步的结论是「否定」:它把某个假设置于死地,而不是坐实。 */
213
+ refuted?: boolean
214
+ }
215
+
216
+ export interface AttributionCase {
217
+ id: string
218
+ title: string
219
+ symptomId: string
220
+ sections: CaseSection[]
221
+ path: CasePathStep[]
222
+ durationMinutes: number
223
+ /** 误导段:最贵的那条知识。没有它,下次还会有人在同一个假设上烧掉四十分钟。 */
224
+ misleading?: { nodeId: string; label: string; wastedMinutes: number; why: string }
225
+ rootCause: string
226
+ }
227
+
128
228
  export interface AttributionStats {
129
229
  typeNodes: number
130
230
  referenceNodes: number
@@ -134,7 +234,7 @@ export interface AttributionStats {
134
234
  * 现象(问题侧)节点数。
135
235
  *
136
236
  * 单独数出来,是因为它和图例的关系最容易被看错:图例里的五种颜色**全是原因侧**
137
- * (失败模式),而问题侧有几个节点、有没有分类,必须能一眼读到 —— 否则「图上那些
237
+ * (失败原因),而问题侧有几个节点、有没有分类,必须能一眼读到 —— 否则「图上那些
138
238
  * 颜色在说问题还是说原因」就只能靠猜。
139
239
  */
140
240
  symptoms: number
@@ -150,3 +250,68 @@ export interface AttributionStats {
150
250
  /** 僵尸边:90 天未被走过的 `may_be_caused_by`。 */
151
251
  zombies: number
152
252
  }
253
+
254
+ /* ────────────────────────────────────────────────────────────────────────────
255
+ * 一次排查的记录(`01_source_truth/agent_diagnosis_case/`)
256
+ *
257
+ * 它**不是卡片**:读图时才被解析成 `evidence` / `cause` 节点与
258
+ * `produces` / `supports` / `refutes` / `confirmed_as` 四条执行边。
259
+ *
260
+ * 为什么要有它,而不是继续用 jsonl:排查产物要能被 `query_source_truth` 读到、
261
+ * 被卡片引用、被 SourceRaw 已有的治理覆盖(分类、版本、检索),就得是一条 SourceRaw。
262
+ * 自造一个 jsonl 格式,等于在知识库旁边再养一套只读不进链路的数据。
263
+ * ──────────────────────────────────────────────────────────────────────────── */
264
+
265
+ /** 一次查询的产出。**含被否定与给不出结论的那些** —— 它们才是最贵的知识。 */
266
+ export interface DiagnosisRunEvidence {
267
+ /** 证据 id,如 `E1`。同一次排查内唯一。 */
268
+ id: string
269
+ /** 跑的是哪个检查点。`produces` 边的起点。 */
270
+ checkPointId: string
271
+ /** 这次实际跑的查询语句(不是可复用的模板)。 */
272
+ query: string
273
+ /** 看到什么。 */
274
+ result: string
275
+ /** 这个结果算不算数。 */
276
+ verdict: EvidenceVerdict
277
+ /** 为什么算数 / 不算数。`supports` 与 `refutes` 两条边共用它。 */
278
+ why: string
279
+ /** 支持或否定的假设 id。`inconclusive` 时可以缺省。 */
280
+ hypothesisId?: string
281
+ at: string
282
+ by: string
283
+ rawRef?: string
284
+ }
285
+
286
+ /** 确认下来的根因。 */
287
+ export interface DiagnosisRunCause {
288
+ /** 被确认的假设。`confirmed_as` 边的起点。 */
289
+ hypothesisId: string
290
+ /** 根因节点 id,如 `C1`。缺省时按 `C-<hypothesisId>` 生成。 */
291
+ id?: string
292
+ /** 根因一句话。 */
293
+ text: string
294
+ fixAction?: string
295
+ confirmedAt: string
296
+ }
297
+
298
+ /**
299
+ * 一次排查的完整记录。落到 SourceRaw 正文里的 ```json diagnosis-run 块。
300
+ *
301
+ * 它与案例卡片的区别是**谁写的、有没有被归纳过**:这里是 Agent 跑完一圈留下的
302
+ * 原始记录,案例卡片是**之后**归纳出来的知识(而且必须经人确认才生效)。
303
+ */
304
+ export interface DiagnosisRunRecord {
305
+ /** 本次排查的 runId。同时用作 SourceRaw 的 `externalId`,重跑不产生第二条记录。 */
306
+ runId: string
307
+ /** 入口现象 id。 */
308
+ symptomId: string
309
+ startedAt: string
310
+ durationMinutes: number
311
+ /** 走过的路径(有序)。含被否定的分支 —— 那是这次排查最值钱的部分。 */
312
+ path: CasePathStep[]
313
+ evidence: DiagnosisRunEvidence[]
314
+ rootCause?: DiagnosisRunCause
315
+ /** 误导段:哪个假设把这次排查带偏了、浪费了多少分钟、为什么。 */
316
+ misleading?: Array<{ nodeId: string; label: string; wastedMinutes: number; why: string }>
317
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 独立整页(`/knowledge/documents/:docId/view`)的**分页规则**。
3
+ *
4
+ * 它单独一个模块而不是写在 `index.ts` 的路由里,理由是这个函数值得被直接测:
5
+ * 「一页一段、段与段首尾相接、一个字都不丢」是那条路由唯一的硬约束,而它藏在
6
+ * HTTP 处理里就只能靠起服务来验。纯函数搬出来之后 `tests/document-page.test.mjs`
7
+ * 可以直接对着它断言(含「所有页拼起来逐字等于原文」这条)。
8
+ *
9
+ * @module dsh-data-domain-knowledge/document-page
10
+ */
11
+
12
+ /**
13
+ * 一次取多少字符、一次最多能取多少。
14
+ *
15
+ * 「打开要等半天」的成因就是把整份正文一次发过来。这两个数就是那条规矩的落点:
16
+ * 任何一次响应都只是**一段**,要看全文就一页一页地要。上限也刻意压着 ——
17
+ * 20 万字符已经能把一份普通文档读完,再大就回到了「一次发全文」。
18
+ */
19
+ export const DOC_PAGE_CHARS = 20000
20
+ export const DOC_PAGE_CHARS_MAX = 200000
21
+
22
+ export interface DocumentContentPage {
23
+ text: string
24
+ offset: number
25
+ nextOffset: number
26
+ totalChars: number
27
+ eof: boolean
28
+ }
29
+
30
+ /**
31
+ * 取正文的一页,切点**对齐到空行**。
32
+ *
33
+ * 为什么非要对齐:Markdown 的块(表格、代码块、列表)从中间断开会被渲染成两个残缺的块,
34
+ * 而整页是**每页各自渲染**的(见 `doc-page.tsx` 第 2 条)。一段表格被切成两半,读者看到的
35
+ * 就是两张破表。空行是块边界,所以往回找最近的空行。
36
+ *
37
+ * 只在后半段找:从前半段找会把页面缩得太短(一次只发几百字符,翻页翻到手软)。
38
+ * 找不到(比如一个超长代码块)就硬切 —— 页大小上限比块的完整性更要紧,那是这个页面
39
+ * 唯一不能失控的量。
40
+ */
41
+ export function documentContentPage(body: string, offset: number, limit: number): DocumentContentPage {
42
+ const totalChars = body.length
43
+ const start = Math.min(Math.max(offset, 0), totalChars)
44
+ let end = Math.min(start + limit, totalChars)
45
+ if (end < totalChars) {
46
+ const floor = start + Math.floor(limit / 2)
47
+ const boundary = body.slice(floor, end).lastIndexOf('\n\n')
48
+ if (boundary >= 0) end = floor + boundary + 2
49
+ }
50
+ return { text: body.slice(start, end), offset: start, nextOffset: end, totalChars, eof: end >= totalChars }
51
+ }
@@ -0,0 +1,42 @@
1
+ import { mkdir, rename, unlink, writeFile } from 'node:fs/promises'
2
+ import { dirname } from 'node:path'
3
+
4
+ /**
5
+ * 原子写:先写同目录的临时文件,再 `rename` 覆盖目标。
6
+ *
7
+ * 抽成独立模块的理由是**它有两个消费者**:宿主运行时(SourceRaw / 卡片 / 编译产物)
8
+ * 与提议存储(`proposals.ts`)。提议是「关于 Agent 动作的待办留痕」,不属于知识树,
9
+ * 所以它不能反过来依赖 runtime —— 那会把分层倒过来(`diagnosis/` 与 `proposals.ts`
10
+ * 都是被 runtime 消费的领域模块)。
11
+ */
12
+
13
+ export async function writeTextAtomic(path: string, content: string): Promise<void> {
14
+ // 原子写需要一个与目标同目录的临时文件,所以目标目录必须先存在。0030 起
15
+ // SourceRaw 落在 `<段>/<source_type>/` 下,而 `ensureLayout` 只建段目录 —— 少了
16
+ // 这一行,第一次写某一种分类就会 ENOENT(实测)。
17
+ await mkdir(dirname(path), { recursive: true })
18
+ const temporary = `${path}.${process.pid}.${Date.now().toString(36)}.tmp`
19
+ try {
20
+ await writeFile(temporary, content, 'utf8')
21
+ await rename(temporary, path)
22
+ } finally {
23
+ await unlinkIfExists(temporary)
24
+ }
25
+ }
26
+
27
+ export async function writeBufferAtomic(path: string, content: Buffer): Promise<void> {
28
+ await mkdir(dirname(path), { recursive: true })
29
+ const temporary = `${path}.${process.pid}.${Date.now().toString(36)}.tmp`
30
+ try {
31
+ await writeFile(temporary, content)
32
+ await rename(temporary, path)
33
+ } finally {
34
+ await unlinkIfExists(temporary)
35
+ }
36
+ }
37
+
38
+ export async function unlinkIfExists(path: string): Promise<void> {
39
+ try {
40
+ await unlink(path)
41
+ } catch { /* rename already consumed the temp file */ }
42
+ }