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
@@ -0,0 +1,337 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { mkdir, readFile, readdir, unlink } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { writeTextAtomic } from './fs-atomic.js'
5
+ import { validateCardAgainstType } from './wiki-types.js'
6
+ import type { CardSchemaIssue, WikiTypeDef } from './types.js'
7
+
8
+ /**
9
+ * 卡片**提议 → 确认**的两段门(0021 §6.1 / §6.2、0040 §7)。
10
+ *
11
+ * ## 它解决什么
12
+ *
13
+ * 类型层是**收敛**的那一层 —— 它错一条,所有现象的排序一起错;可它又必须**能不断补充**,
14
+ * 否则新根因永远进不来。两个要求只有一个解:**让补充低摩擦(Agent 每次排查都提议),
15
+ * 让生效高门槛(人确认)**。
16
+ *
17
+ * ## 为什么不能靠 `status: draft`
18
+ *
19
+ * 今天 `status` 只是标签:`draft` 卡照样进 wikiNotes、进图、被召回(只有 `problem`
20
+ * 被召回路径排除)。要让「未确认不生效」真正成立,就得让提议**根本不落卡片**。
21
+ * 所以这一层的关键词是「不落 `02_wiki/`」—— 未确认的提议不参与编译、不参与召回、
22
+ * 不在图上出现,它连一张卡片都还不是。
23
+ *
24
+ * ## 提议存在哪(**这一条与 PRD 的措辞有一处偏离,理由写在下面**)
25
+ *
26
+ * PRD 说「提议挂在触发它的那条 SourceRaw 上,停在来源侧」。这里按**逻辑上挂在它上面**
27
+ * 实现:每条提议都带 `sourceRef`,按它索引;但**物理上落在记忆段**
28
+ * (`<agentRoot>/memory/<库 id>/card-proposals/`)。
29
+ *
30
+ * 三条理由:
31
+ * 1. **它不属于「源头」。** `01_source_truth/` 的性质是「删掉就丢数据」。提议删掉只丢
32
+ * 待办 —— 重跑一次提议就能回来。把可重建的东西放进源头段,会让「这段能不能删」
33
+ * 这个问题失去唯一答案(0021 §9 的三段判据)。
34
+ * 2. **它更不属于知识侧。** 落 `02_wiki/` 正是这道门要拦的事。
35
+ * 3. **记忆段就是为这种东西存在的。** 它的定义是「关于这个 Agent 的动作留痕,不是关于
36
+ * 知识的断言」—— 一条待审的提议正好是 Agent 的动作留痕。
37
+ *
38
+ * 逻辑上「挂在 SourceRaw 上」这条没有丢:`sourceRef` 是必填字段,review 界面按它分组,
39
+ * 来源被删掉时提议会如实报「来源已不存在」而不是静默生效。
40
+ *
41
+ * ## 判重与拒绝留痕
42
+ *
43
+ * 提议 id 由「来源 + 动作 + 类型 + 实体 id」算出来,**稳定**:同一件事重跑一次提议
44
+ * 不会产生第二条,只会原地更新。内容指纹决定**拒绝之后要不要再提**:
45
+ * 指纹没变就不再提(不打扰人),指纹变了才重新提(知识确实变了)。
46
+ */
47
+
48
+ export type ProposalAction = 'create' | 'update' | 'merge'
49
+ export type ProposalTrigger = 'extraction' | 'agent' | 'diagnosis-run'
50
+ export type ProposalStatus = 'pending' | 'confirmed' | 'rejected'
51
+
52
+ export interface CardProposal {
53
+ /** 稳定 id:`p-<hash(sourceRef|action|entityType|entityId)>`。 */
54
+ id: string
55
+ /** 触发这条提议的那条 SourceRaw。 */
56
+ sourceRef: string
57
+ trigger: ProposalTrigger
58
+ action: ProposalAction
59
+ entityType: string
60
+ entityId: string
61
+ title: string
62
+ summary?: string
63
+ /** 卡片正文(小节 + 结构化块),与 `writeCard` 的 `content` 同形。 */
64
+ content: string
65
+ sourceRefs: string[]
66
+ metadata?: Record<string, unknown>
67
+ status: ProposalStatus
68
+ /** 内容指纹:拒绝之后同一条不再重复提。 */
69
+ fingerprint: string
70
+ createdAt: string
71
+ updatedAt: string
72
+ /** **提议阶段**跑出来的三层校验问题。确认阶段会再跑一遍。 */
73
+ issues: CardSchemaIssue[]
74
+ /** `update` / `merge` 的目标卡片当时的正文指纹 —— 提议之后卡片又被人改过时能发现。 */
75
+ baseHash?: string
76
+ /** 已经确认、但还没落卡(等人或等 Agent 调 `write_knowledge_card`)。 */
77
+ confirmedAt?: string
78
+ confirmedBy?: string
79
+ /** 落卡时间。有它就说明这条提议已经变成卡片了。 */
80
+ writtenAt?: string
81
+ reviewedAt?: string
82
+ reviewedBy?: string
83
+ /** 拒绝原因。**必须留档** —— 「为什么这条不算」本身就是知识。 */
84
+ reason?: string
85
+ }
86
+
87
+ /** 提一条提议时给的东西。`id` / `status` / 时间戳由 store 生成。 */
88
+ export interface ProposalInput {
89
+ sourceRef: string
90
+ trigger: ProposalTrigger
91
+ action: ProposalAction
92
+ entityType: string
93
+ entityId: string
94
+ title: string
95
+ summary?: string
96
+ content: string
97
+ sourceRefs?: string[]
98
+ metadata?: Record<string, unknown>
99
+ baseHash?: string
100
+ }
101
+
102
+ export interface ProposalSchema {
103
+ types: WikiTypeDef[]
104
+ }
105
+
106
+ const TRIGGER_LABEL: Record<ProposalTrigger, string> = {
107
+ extraction: '编译时自动抽取',
108
+ agent: 'Agent 主动提议',
109
+ 'diagnosis-run': '一次排查结束',
110
+ }
111
+
112
+ export function triggerLabel(trigger: ProposalTrigger): string {
113
+ return TRIGGER_LABEL[trigger] ?? trigger
114
+ }
115
+
116
+ function shortHash(value: string): string {
117
+ return createHash('sha256').update(value).digest('hex').slice(0, 16)
118
+ }
119
+
120
+ /**
121
+ * 提议的身份:**同一件事重跑不会产生第二条**。
122
+ *
123
+ * 它**刻意不含 `sourceRef`**。同一张卡片常常由多份来源共同支撑(同一份口径在两篇文档里
124
+ * 都写了、一次排查与它的复盘各指一遍)—— 把来源算进身份,同一条提议就会按来源裂成
125
+ * 好几条,人 review 时要在队列里连着点头三次,而且每次点的都是同一件事。
126
+ *
127
+ * 来源没有被丢掉:它是**多值**的。`sourceRef` 记「最先触发它的那一条」,
128
+ * `sourceRefs` 累积全部触发它的来源(见 `upsert` 的合并规则)。
129
+ */
130
+ export function proposalIdOf(input: Pick<ProposalInput, 'action' | 'entityType' | 'entityId'>): string {
131
+ return `p-${shortHash(`${input.action}|${input.entityType}|${input.entityId}`)}`
132
+ }
133
+
134
+ /**
135
+ * 内容指纹。
136
+ *
137
+ * 它只包含**会改变卡片内容的东西**(标题、正文、出处):状态流转、时间戳不参与 ——
138
+ * 否则一次无意义的写入就会让「拒绝过的那条」重新冒出来。
139
+ */
140
+ export function fingerprintOf(input: Pick<ProposalInput, 'entityType' | 'entityId' | 'title' | 'content' | 'sourceRefs'>): string {
141
+ const refs = [...(input.sourceRefs ?? [])].sort().join(',')
142
+ return shortHash(`${input.entityType}|${input.entityId}|${input.title}|${input.content}|${refs}`)
143
+ }
144
+
145
+ /** 卡片正文的指纹,用来发现「提议之后卡片又被人改过」。 */
146
+ export function contentHashOf(content: string): string {
147
+ return shortHash(content)
148
+ }
149
+
150
+ export interface ProposalStoreHost {
151
+ /** 记忆段根(`<agentRoot>/memory/<库 id>`)。 */
152
+ memoryRoot: string
153
+ /** 当前生效的卡片 schema。提议阶段与确认阶段各跑一遍校验。 */
154
+ schema: () => ProposalSchema
155
+ }
156
+
157
+ export class CardProposalStore {
158
+ constructor(private readonly host: ProposalStoreHost) {}
159
+
160
+ private get dir(): string { return join(this.host.memoryRoot, 'card-proposals') }
161
+
162
+ /** 全部提议,新的在前。读不动就返回空 —— 提议是可重建的待办,不是数据。 */
163
+ async list(): Promise<CardProposal[]> {
164
+ let names: string[]
165
+ try { names = await readdir(this.dir) } catch { return [] }
166
+ const result: CardProposal[] = []
167
+ for (const name of names) {
168
+ if (!name.endsWith('.json')) continue
169
+ try {
170
+ const value = JSON.parse(await readFile(join(this.dir, name), 'utf8')) as CardProposal
171
+ if (value && typeof value.id === 'string') result.push(value)
172
+ } catch { /* 坏掉的一条不该让整张待办清单消失 */ }
173
+ }
174
+ return result.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
175
+ }
176
+
177
+ async get(id: string): Promise<CardProposal | undefined> {
178
+ if (!/^p-[a-f0-9]{16}$/.test(id)) return undefined
179
+ try {
180
+ const value = JSON.parse(await readFile(join(this.dir, `${id}.json`), 'utf8')) as CardProposal
181
+ return typeof value?.id === 'string' ? value : undefined
182
+ } catch { return undefined }
183
+ }
184
+
185
+ async pending(): Promise<CardProposal[]> {
186
+ return (await this.list()).filter(item => item.status === 'pending')
187
+ }
188
+
189
+ /**
190
+ * 写入一批提议。返回真正**新增或更新**的那些(被指纹判重挡掉的不会出现在结果里)。
191
+ *
192
+ * 三条判重规则,按已有提议的状态分:
193
+ * - `pending`:原地更新(来源可能又长了一节,正文跟着变),保留 `createdAt`;
194
+ * - `confirmed` / `written`:指纹相同就跳过(人已经说过是了,不必再问一遍);
195
+ * - `rejected`:指纹相同就跳过(不重复打扰人),**指纹变了才重新提**(知识确实变了)。
196
+ */
197
+ async upsert(inputs: ProposalInput[]): Promise<CardProposal[]> {
198
+ if (inputs.length === 0) return []
199
+ await mkdir(this.dir, { recursive: true })
200
+ const existing = new Map((await this.list()).map(item => [item.id, item]))
201
+ const schema = this.host.schema()
202
+ const touched: CardProposal[] = []
203
+ const now = new Date().toISOString()
204
+
205
+ for (const input of inputs) {
206
+ const id = proposalIdOf(input)
207
+ const fingerprint = fingerprintOf(input)
208
+ const previous = existing.get(id)
209
+ if (previous && previous.fingerprint === fingerprint && previous.status !== 'pending') continue
210
+ if (previous && previous.status === 'pending' && previous.fingerprint === fingerprint) continue
211
+
212
+ const issues = this.issuesOf(schema, input)
213
+ /**
214
+ * 出处**合并**而不是覆盖。
215
+ *
216
+ * 提议的身份不含来源(见 `proposalIdOf`),所以第二份来源再提同一张卡片时命中的是
217
+ * 同一条提议。覆盖的话,第一份来源的出处会凭空消失 —— 而「这张卡片由哪几份来源
218
+ * 共同支撑」正是 review 时最需要看到的东西。
219
+ */
220
+ const sourceRefs = [...new Set([...(previous?.sourceRefs ?? []), ...(input.sourceRefs ?? [input.sourceRef])])]
221
+ const next: CardProposal = {
222
+ id,
223
+ sourceRef: previous?.sourceRef ?? input.sourceRef,
224
+ trigger: input.trigger,
225
+ action: input.action,
226
+ entityType: input.entityType,
227
+ entityId: input.entityId,
228
+ title: input.title,
229
+ ...(input.summary ? { summary: input.summary } : {}),
230
+ content: input.content,
231
+ sourceRefs,
232
+ ...(input.metadata ? { metadata: input.metadata } : {}),
233
+ status: 'pending',
234
+ fingerprint,
235
+ createdAt: previous?.createdAt ?? now,
236
+ updatedAt: now,
237
+ issues,
238
+ ...(input.baseHash ? { baseHash: input.baseHash } : {}),
239
+ }
240
+ await writeTextAtomic(join(this.dir, `${id}.json`), JSON.stringify(next, null, 2))
241
+ existing.set(id, next)
242
+ touched.push(next)
243
+ }
244
+ return touched
245
+ }
246
+
247
+ /** 提议阶段的三层校验。缺字段 / 图断了 / 知识没用,各报各的。 */
248
+ private issuesOf(schema: ProposalSchema, input: ProposalInput): CardSchemaIssue[] {
249
+ const type = schema.types.find(def => def.id === input.entityType)
250
+ if (!type) {
251
+ return [{ level: 'format', field: 'entity_type', message: `未知卡片类型:${input.entityType}` }]
252
+ }
253
+ return validateCardAgainstType(type, {
254
+ title: input.title,
255
+ content: input.content,
256
+ sourceRefs: input.sourceRefs ?? [input.sourceRef],
257
+ metadata: input.metadata ?? {},
258
+ ...(input.summary ? { summary: input.summary } : {}),
259
+ })
260
+ }
261
+
262
+ /**
263
+ * 人点了「通过」。
264
+ *
265
+ * **确认与落卡是两件事**,所以这里只标状态、不写卡片:
266
+ * - 面板上的主操作是「确认并写入」,它先调这里再调 `writeCard`;
267
+ * - 副操作是「仅确认」,落卡交给 Agent 的 `write_knowledge_card`(它要求这条提议
268
+ * 已确认且尚未落卡)。两条路都过同一道门。
269
+ */
270
+ async confirm(id: string, by: string, patch?: { content?: string; title?: string; summary?: string }): Promise<CardProposal | undefined> {
271
+ const previous = await this.get(id)
272
+ if (!previous || previous.status !== 'pending') return undefined
273
+ const next: CardProposal = {
274
+ ...previous,
275
+ ...(patch?.title !== undefined ? { title: patch.title } : {}),
276
+ ...(patch?.summary !== undefined ? { summary: patch.summary } : {}),
277
+ ...(patch?.content !== undefined ? { content: patch.content } : {}),
278
+ status: 'confirmed',
279
+ confirmedAt: new Date().toISOString(),
280
+ confirmedBy: by,
281
+ reviewedAt: new Date().toISOString(),
282
+ reviewedBy: by,
283
+ updatedAt: new Date().toISOString(),
284
+ }
285
+ // 人改过之后必须**再跑一遍**校验:提议阶段那一次是 Agent 自己补的,
286
+ // 这一次的修改来自另一个人(0021 §6.5 说清了为什么要两道都跑)。
287
+ next.issues = this.issuesOf(this.host.schema(), {
288
+ sourceRef: next.sourceRef, trigger: next.trigger, action: next.action,
289
+ entityType: next.entityType, entityId: next.entityId, title: next.title,
290
+ ...(next.summary ? { summary: next.summary } : {}),
291
+ content: next.content, sourceRefs: next.sourceRefs,
292
+ ...(next.metadata ? { metadata: next.metadata } : {}),
293
+ })
294
+ next.fingerprint = fingerprintOf(next)
295
+ await writeTextAtomic(join(this.dir, `${id}.json`), JSON.stringify(next, null, 2))
296
+ return next
297
+ }
298
+
299
+ /** 落卡成功之后回填。没有这一步,「已确认但没落卡」和「已经落卡了」分不开。 */
300
+ async markWritten(id: string): Promise<CardProposal | undefined> {
301
+ const previous = await this.get(id)
302
+ if (!previous) return undefined
303
+ const next: CardProposal = { ...previous, writtenAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
304
+ await writeTextAtomic(join(this.dir, `${id}.json`), JSON.stringify(next, null, 2))
305
+ return next
306
+ }
307
+
308
+ /**
309
+ * 人点了「拒绝」。原因留档 —— 它同时是「下次不要再提同一条」的判据。
310
+ *
311
+ * 只有 `pending` 能被拒绝:一条已经确认(甚至已经落卡)的提议再被拒,会让
312
+ * 「卡片在库里、提议却是拒绝态」这种自相矛盾的状态出现在待办区里。
313
+ */
314
+ async reject(id: string, reason: string, by: string): Promise<CardProposal | undefined> {
315
+ const previous = await this.get(id)
316
+ if (!previous || previous.status !== 'pending') return undefined
317
+ const next: CardProposal = {
318
+ ...previous,
319
+ status: 'rejected',
320
+ reason: reason.trim() || '(没有写原因)',
321
+ reviewedAt: new Date().toISOString(),
322
+ reviewedBy: by,
323
+ updatedAt: new Date().toISOString(),
324
+ }
325
+ await writeTextAtomic(join(this.dir, `${id}.json`), JSON.stringify(next, null, 2))
326
+ return next
327
+ }
328
+
329
+ /** 删掉一条提议(清理用)。已落卡的提议删掉不影响卡片本身。 */
330
+ async remove(id: string): Promise<boolean> {
331
+ if (!/^p-[a-f0-9]{16}$/.test(id)) return false
332
+ try {
333
+ await unlink(join(this.dir, `${id}.json`))
334
+ return true
335
+ } catch { return false }
336
+ }
337
+ }