hafez 1.4.0 → 2.1.0
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/README.md +26 -33
- package/dist/apply.d.ts +5 -2
- package/dist/apply.js +249 -114
- package/dist/batch-ops.d.ts +95 -26
- package/dist/batch-ops.js +178 -18
- package/dist/cli/args.d.ts +43 -5
- package/dist/cli/args.js +69 -8
- package/dist/cli/batch-errors.js +19 -11
- package/dist/cli/block-hook.d.ts +9 -0
- package/dist/cli/block-hook.js +90 -0
- package/dist/cli/commands-schema.js +2 -0
- package/dist/cli/commands.d.ts +4 -3
- package/dist/cli/commands.js +300 -99
- package/dist/cli/format.d.ts +7 -3
- package/dist/cli/format.js +27 -10
- package/dist/cli/index.d.ts +18 -4
- package/dist/cli/index.js +73 -31
- package/dist/cli/install.d.ts +65 -0
- package/dist/cli/install.js +333 -0
- package/dist/cli/onboard.d.ts +15 -2
- package/dist/cli/onboard.js +273 -90
- package/dist/cli/resolve-vault.d.ts +11 -4
- package/dist/cli/resolve-vault.js +13 -11
- package/dist/cli/schema-introspect.js +4 -3
- package/dist/cli/upgrade.d.ts +15 -4
- package/dist/cli/upgrade.js +103 -44
- package/dist/closest.d.ts +8 -0
- package/dist/closest.js +65 -0
- package/dist/contracts.d.ts +17 -1
- package/dist/contracts.js +46 -0
- package/dist/db.d.ts +15 -2
- package/dist/db.js +69 -48
- package/dist/digest.d.ts +6 -4
- package/dist/digest.js +28 -17
- package/dist/document.d.ts +41 -3
- package/dist/document.js +218 -62
- package/dist/export-okf.d.ts +13 -0
- package/dist/export-okf.js +11 -7
- package/dist/harness.d.ts +58 -0
- package/dist/harness.js +50 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +94 -104
- package/dist/merge.js +7 -5
- package/dist/near-match.d.ts +9 -0
- package/dist/near-match.js +34 -0
- package/dist/stale.d.ts +41 -0
- package/dist/stale.js +43 -0
- package/dist/stopwords.d.ts +6 -0
- package/dist/stopwords.js +25 -0
- package/dist/surfaces/items.d.ts +366 -0
- package/dist/surfaces/items.js +527 -0
- package/dist/surfaces/surfaces.d.ts +32 -0
- package/dist/surfaces/surfaces.js +230 -0
- package/dist/types.d.ts +49 -16
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +109 -0
- package/dist/vault.js +1 -1
- package/package.json +4 -3
- package/skill/hafez/SKILL.md +68 -0
- package/skill/hafez-save/SKILL.md +29 -0
- package/dist/cli/help-agent.d.ts +0 -1
- package/dist/cli/help-agent.js +0 -210
- package/skill/SKILL.md +0 -137
package/dist/batch-ops.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type { BatchOperation, CreateEntityFields, CreateKnowledgeFields, KnowledgeSubtype, SessionLogEntry, UpdateFields } from './types.js';
|
|
2
|
+
import type { BatchOperation, CreateEntityFields, CreateKnowledgeFields, DeletableKind, KnowledgeSubtype, SessionLogEntry, UpdateFields } from './types.js';
|
|
3
|
+
export declare const splitCommaList: (value: string) => string[];
|
|
3
4
|
export declare const SessionLogSchema: z.ZodObject<{
|
|
4
5
|
type: z.ZodEnum<{
|
|
5
6
|
[x: string]: string;
|
|
@@ -21,20 +22,24 @@ export declare const UpdateFieldsSchema: z.ZodObject<{
|
|
|
21
22
|
body: z.ZodOptional<z.ZodString>;
|
|
22
23
|
agent: z.ZodString;
|
|
23
24
|
}, z.core.$strict>>;
|
|
25
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
24
26
|
brief: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
25
27
|
add_action: z.ZodOptional<z.ZodString>;
|
|
26
28
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
27
29
|
complete_action: z.ZodOptional<z.ZodString>;
|
|
30
|
+
complete_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
31
|
remove_action: z.ZodOptional<z.ZodString>;
|
|
32
|
+
remove_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
29
33
|
clear_actions: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
+
parent: z.ZodOptional<z.ZodString>;
|
|
30
35
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
31
36
|
resource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
32
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
37
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
33
38
|
confidence: z.ZodOptional<z.ZodEnum<{
|
|
34
39
|
[x: string]: string;
|
|
35
40
|
}>>;
|
|
36
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
37
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
41
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
42
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
38
43
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
39
44
|
insight: z.ZodOptional<z.ZodString>;
|
|
40
45
|
add_evidence: z.ZodOptional<z.ZodString>;
|
|
@@ -47,10 +52,10 @@ export declare const CreateEntityFieldsSchema: z.ZodObject<{
|
|
|
47
52
|
purpose: z.ZodOptional<z.ZodString>;
|
|
48
53
|
description: z.ZodOptional<z.ZodString>;
|
|
49
54
|
resource: z.ZodOptional<z.ZodString>;
|
|
50
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
55
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
51
56
|
parent: z.ZodOptional<z.ZodString>;
|
|
52
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
53
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
57
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
58
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
54
59
|
brief: z.ZodOptional<z.ZodString>;
|
|
55
60
|
add_action: z.ZodOptional<z.ZodString>;
|
|
56
61
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -62,23 +67,30 @@ export declare const CreateKnowledgeFieldsSchema: z.ZodObject<{
|
|
|
62
67
|
status: z.ZodOptional<z.ZodEnum<{
|
|
63
68
|
[x: string]: string;
|
|
64
69
|
}>>;
|
|
70
|
+
confidence: z.ZodOptional<z.ZodEnum<{
|
|
71
|
+
[x: string]: string;
|
|
72
|
+
}>>;
|
|
65
73
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
66
74
|
insight: z.ZodOptional<z.ZodString>;
|
|
67
75
|
description: z.ZodOptional<z.ZodString>;
|
|
68
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
69
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
70
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
76
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
77
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
78
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
71
79
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
72
80
|
}, z.core.$strict>;
|
|
73
81
|
export declare const CreateSessionFieldsSchema: z.ZodObject<{
|
|
74
82
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
75
83
|
insight: z.ZodOptional<z.ZodString>;
|
|
76
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
84
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
77
85
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
78
86
|
}, z.core.$strict>;
|
|
79
87
|
/** What `digest` may put in a session-create op — typechecked against the
|
|
80
88
|
* schema so the two can never silently diverge again. */
|
|
81
89
|
export type CreateSessionFieldsInput = z.infer<typeof CreateSessionFieldsSchema>;
|
|
90
|
+
/** The fields an update's mechanised session log entry names — empty when
|
|
91
|
+
* the update earns none. Shared by the update check (which rejects an
|
|
92
|
+
* `agent` with no entry to name) and the Apply update arm (which writes it). */
|
|
93
|
+
export declare function loggedFields(fields: UpdateFields): (keyof UpdateFields)[];
|
|
82
94
|
export declare const BatchOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
83
95
|
op: z.ZodLiteral<"update">;
|
|
84
96
|
slug: z.ZodString;
|
|
@@ -95,20 +107,24 @@ export declare const BatchOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
95
107
|
body: z.ZodOptional<z.ZodString>;
|
|
96
108
|
agent: z.ZodString;
|
|
97
109
|
}, z.core.$strict>>;
|
|
110
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
98
111
|
brief: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
99
112
|
add_action: z.ZodOptional<z.ZodString>;
|
|
100
113
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
101
114
|
complete_action: z.ZodOptional<z.ZodString>;
|
|
115
|
+
complete_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
102
116
|
remove_action: z.ZodOptional<z.ZodString>;
|
|
117
|
+
remove_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
103
118
|
clear_actions: z.ZodOptional<z.ZodBoolean>;
|
|
119
|
+
parent: z.ZodOptional<z.ZodString>;
|
|
104
120
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
105
121
|
resource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
106
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
122
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
107
123
|
confidence: z.ZodOptional<z.ZodEnum<{
|
|
108
124
|
[x: string]: string;
|
|
109
125
|
}>>;
|
|
110
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
111
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
126
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
127
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
112
128
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
113
129
|
insight: z.ZodOptional<z.ZodString>;
|
|
114
130
|
add_evidence: z.ZodOptional<z.ZodString>;
|
|
@@ -144,6 +160,9 @@ export declare const BatchOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
144
160
|
project: "project";
|
|
145
161
|
knowledge: "knowledge";
|
|
146
162
|
}>;
|
|
163
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
164
|
+
op: z.ZodLiteral<"delete">;
|
|
165
|
+
slug: z.ZodString;
|
|
147
166
|
}, z.core.$strict>], "op">;
|
|
148
167
|
/** Canonicalize an operation's declared aliases.
|
|
149
168
|
*
|
|
@@ -161,6 +180,12 @@ export interface ResolvedTarget {
|
|
|
161
180
|
type: string;
|
|
162
181
|
subtype?: KnowledgeSubtype;
|
|
163
182
|
}
|
|
183
|
+
/** The frontmatter links `validate` checks: what makes one file a referrer
|
|
184
|
+
* of another. */
|
|
185
|
+
export interface FileLinks {
|
|
186
|
+
parent?: string;
|
|
187
|
+
related?: string[];
|
|
188
|
+
}
|
|
164
189
|
/** Vault context handed to each spec's semantic check by validateBatchCore.
|
|
165
190
|
* Simulation-aware: creates earlier in the batch count as existing. */
|
|
166
191
|
export interface OpCheckContext {
|
|
@@ -169,6 +194,15 @@ export interface OpCheckContext {
|
|
|
169
194
|
sessionExists(slug: string): boolean;
|
|
170
195
|
simulate(slug: string, info: ResolvedTarget): void;
|
|
171
196
|
simulateSession(slug: string): void;
|
|
197
|
+
/** The parent/related a file carries after the earlier ops in the batch. */
|
|
198
|
+
linksOf(slug: string): FileLinks;
|
|
199
|
+
/** Record the parent/related a file will carry after this op. Every op
|
|
200
|
+
* that changes a link calls it, so `referrers` sees the batch so far. */
|
|
201
|
+
simulateLinks(slug: string, links: FileLinks): void;
|
|
202
|
+
/** Entities and knowledge notes whose parent or related names `slug`,
|
|
203
|
+
* after the earlier ops in the batch. A file never refers to itself. */
|
|
204
|
+
referrers(slug: string): string[];
|
|
205
|
+
simulateDelete(slug: string, kind: DeletableKind): void;
|
|
172
206
|
/** Kind gating is subtype-aware for knowledge: a plan carries `status`,
|
|
173
207
|
* an insight does not (see contracts.SUBTYPE_STATUSES). */
|
|
174
208
|
validateKindFields(kind: 'entity' | 'knowledge', fields: UpdateFields, subtype?: KnowledgeSubtype): void;
|
|
@@ -206,6 +240,11 @@ export declare function batchInvalidHeader(invalidCount: number, total: number):
|
|
|
206
240
|
* dry-run (spec warning) and apply (BatchResult.warning) report it with this
|
|
207
241
|
* same sentence so the two can never drift. */
|
|
208
242
|
export declare function duplicateSessionWarning(slug: string): string;
|
|
243
|
+
/** The parent chain `slug → parent → … → slug` that giving `slug` this
|
|
244
|
+
* parent closes, or null when the chain ends or loops without `slug`.
|
|
245
|
+
* Shared by the Op Spec checks (against the simulated batch) and
|
|
246
|
+
* `validateVault` (against the files). */
|
|
247
|
+
export declare function parentCycle(slug: string, parent: string | undefined, parentOf: (s: string) => string | undefined): string[] | null;
|
|
209
248
|
export interface OpSpec {
|
|
210
249
|
/** Stable identifier used by `schema <name>` and the op catalog. */
|
|
211
250
|
name: string;
|
|
@@ -215,6 +254,9 @@ export interface OpSpec {
|
|
|
215
254
|
description: string;
|
|
216
255
|
/** Optional fields force-included in the generated example payload. */
|
|
217
256
|
exampleInclude: readonly string[];
|
|
257
|
+
/** The op's `slug` may name a session note as well as an entity or
|
|
258
|
+
* knowledge note. */
|
|
259
|
+
addressesSessions?: boolean;
|
|
218
260
|
check(op: any, ctx: OpCheckContext): OpCheckResult;
|
|
219
261
|
}
|
|
220
262
|
declare const OP_SPEC_ROWS: readonly [{
|
|
@@ -236,27 +278,31 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
236
278
|
body: z.ZodOptional<z.ZodString>;
|
|
237
279
|
agent: z.ZodString;
|
|
238
280
|
}, z.core.$strict>>;
|
|
281
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
239
282
|
brief: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
240
283
|
add_action: z.ZodOptional<z.ZodString>;
|
|
241
284
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
242
285
|
complete_action: z.ZodOptional<z.ZodString>;
|
|
286
|
+
complete_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
243
287
|
remove_action: z.ZodOptional<z.ZodString>;
|
|
288
|
+
remove_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
244
289
|
clear_actions: z.ZodOptional<z.ZodBoolean>;
|
|
290
|
+
parent: z.ZodOptional<z.ZodString>;
|
|
245
291
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
246
292
|
resource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
247
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
293
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
248
294
|
confidence: z.ZodOptional<z.ZodEnum<{
|
|
249
295
|
[x: string]: string;
|
|
250
296
|
}>>;
|
|
251
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
252
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
297
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
298
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
253
299
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
254
300
|
insight: z.ZodOptional<z.ZodString>;
|
|
255
301
|
add_evidence: z.ZodOptional<z.ZodString>;
|
|
256
302
|
add_source: z.ZodOptional<z.ZodString>;
|
|
257
303
|
}, z.core.$strict>;
|
|
258
304
|
}, z.core.$strict>;
|
|
259
|
-
readonly description: "Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …). `insight` is an alias for `synthesis` (knowledge-only; ignored when `synthesis` is also given). On knowledge notes `status` is plan-subtype only (active|done); insights reject it.";
|
|
305
|
+
readonly description: "Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …). Next Actions fields compose within one op as prune, tick, add: `clear_actions` → `remove_action(s)` → `complete_action(s)` → `add_action(s)` — queries match the list as it was before the op (two queries naming one line is an error), added lines land last, so `clear_actions` + `add_actions` rebuilds the list; `clear_actions` with a query field is rejected. Lines leaving the list (removed, cleared, pruned with an emptied section, ticked past nine, oldest first) move to the entity's archive log. A Handle of an open action (as `read` shows it, or any longer or shorter prefix of four or more hex that names only that line) names that line; anything else is a case-insensitive substring of the text. `parent` sets the parent link (entity-only; the slug must exist, and the parent chain must not lead back to the entity). `insight` is an alias for `synthesis` (knowledge-only; ignored when `synthesis` is also given). An entity update with no `session_log` gets a `progress` entry naming the fields written, unless its only fields are action fields; `agent` names the agent on it (entity-only, detected from the Harness when absent, rejected alongside `session_log`). On knowledge notes `status` is plan-subtype only (active|done); insights reject it.";
|
|
260
306
|
readonly exampleInclude: readonly ["status"];
|
|
261
307
|
readonly check: (op: Extract<BatchOperation, {
|
|
262
308
|
op: "update";
|
|
@@ -280,10 +326,10 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
280
326
|
purpose: z.ZodOptional<z.ZodString>;
|
|
281
327
|
description: z.ZodOptional<z.ZodString>;
|
|
282
328
|
resource: z.ZodOptional<z.ZodString>;
|
|
283
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
329
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
284
330
|
parent: z.ZodOptional<z.ZodString>;
|
|
285
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
286
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
331
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
332
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
287
333
|
brief: z.ZodOptional<z.ZodString>;
|
|
288
334
|
add_action: z.ZodOptional<z.ZodString>;
|
|
289
335
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -315,16 +361,19 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
315
361
|
status: z.ZodOptional<z.ZodEnum<{
|
|
316
362
|
[x: string]: string;
|
|
317
363
|
}>>;
|
|
364
|
+
confidence: z.ZodOptional<z.ZodEnum<{
|
|
365
|
+
[x: string]: string;
|
|
366
|
+
}>>;
|
|
318
367
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
319
368
|
insight: z.ZodOptional<z.ZodString>;
|
|
320
369
|
description: z.ZodOptional<z.ZodString>;
|
|
321
|
-
domain: z.ZodOptional<z.ZodArray<z.ZodString
|
|
322
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
323
|
-
tags: z.ZodOptional<z.ZodArray<z.ZodString
|
|
370
|
+
domain: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
371
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
372
|
+
tags: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
324
373
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
325
374
|
}, z.core.$strict>>;
|
|
326
375
|
}, z.core.$strict>;
|
|
327
|
-
readonly description: "Create a new knowledge note (insight / plan subtype). `insight` is an alias for `synthesis` (ignored when `synthesis` is also given). `status` (active|done) is plan-only; `synthesis` is insight-only — plan bodies are Goal/Steps/Dependencies, and a field whose section the subtype lacks is rejected, never dropped.";
|
|
376
|
+
readonly description: "Create a new knowledge note (insight / plan subtype). `confidence` defaults to observation. `insight` is an alias for `synthesis` (ignored when `synthesis` is also given). `status` (active|done) is plan-only; `synthesis` is insight-only — plan bodies are Goal/Steps/Dependencies, and a field whose section the subtype lacks is rejected, never dropped.";
|
|
328
377
|
readonly exampleInclude: readonly ["subtype", "synthesis"];
|
|
329
378
|
readonly check: (op: Extract<BatchOperation, {
|
|
330
379
|
op: "create";
|
|
@@ -346,7 +395,7 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
346
395
|
fields: z.ZodOptional<z.ZodObject<{
|
|
347
396
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
348
397
|
insight: z.ZodOptional<z.ZodString>;
|
|
349
|
-
related: z.ZodOptional<z.ZodArray<z.ZodString
|
|
398
|
+
related: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<z.ZodString>>>;
|
|
350
399
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
351
400
|
}, z.core.$strict>>;
|
|
352
401
|
}, z.core.$strict>;
|
|
@@ -447,6 +496,23 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
447
496
|
errors: string[];
|
|
448
497
|
primaryCode: "SLUG_EXISTS" | "NOT_FOUND" | undefined;
|
|
449
498
|
};
|
|
499
|
+
}, {
|
|
500
|
+
readonly name: "delete";
|
|
501
|
+
readonly op: "delete";
|
|
502
|
+
readonly schema: z.ZodObject<{
|
|
503
|
+
op: z.ZodLiteral<"delete">;
|
|
504
|
+
slug: z.ZodString;
|
|
505
|
+
}, z.core.$strict>;
|
|
506
|
+
readonly description: "Delete a knowledge or session note. An entity is never deleted — retire it with `hafez update <slug> --status done`. Rejected while another entity or knowledge note links to the note (parent/related): unlink or re-point those first — an earlier op in the same batch counts.";
|
|
507
|
+
readonly exampleInclude: readonly [];
|
|
508
|
+
readonly addressesSessions: true;
|
|
509
|
+
readonly check: (op: Extract<BatchOperation, {
|
|
510
|
+
op: "delete";
|
|
511
|
+
}>, ctx: OpCheckContext) => {
|
|
512
|
+
slug: string;
|
|
513
|
+
errors: string[];
|
|
514
|
+
primaryCode: "SLUG_EXISTS" | "NOT_FOUND" | undefined;
|
|
515
|
+
};
|
|
450
516
|
}];
|
|
451
517
|
/** Every op the table defines. The Apply table (src/apply.ts) is keyed by this
|
|
452
518
|
* union, so a spec row added here without an apply arm fails to build. */
|
|
@@ -465,6 +531,9 @@ export declare function validOpNames(): string[];
|
|
|
465
531
|
* NOT_FOUND precedence on shape failure in validateBatchCore) without a
|
|
466
532
|
* hand-kept list. */
|
|
467
533
|
export declare function slugBearingOpNames(): string[];
|
|
534
|
+
/** Slug-bearing ops whose slug may name a session note — a missing entity
|
|
535
|
+
* or knowledge note alone is not NOT_FOUND for them. */
|
|
536
|
+
export declare function sessionAddressingOpNames(): string[];
|
|
468
537
|
/** Create kinds for error text — derived from the table. */
|
|
469
538
|
export declare function validCreateKinds(): string[];
|
|
470
539
|
export {};
|
package/dist/batch-ops.js
CHANGED
|
@@ -17,12 +17,6 @@ import { HafezError } from './types.js';
|
|
|
17
17
|
// Every z.object(...) is .strict() — unknown keys become `unrecognized_keys` issues
|
|
18
18
|
// (not silently dropped), which is load-bearing for the error-formatter's did-you-mean
|
|
19
19
|
// hints and for catching typos like `{"kind": "parent"}` on a link op.
|
|
20
|
-
export const SessionLogSchema = z.object({
|
|
21
|
-
type: z.enum([...SESSION_LOG_TYPES]),
|
|
22
|
-
summary: z.string(),
|
|
23
|
-
body: z.string().optional(),
|
|
24
|
-
agent: z.string(),
|
|
25
|
-
}).strict();
|
|
26
20
|
// Body-section text must have text after trimming. An empty or whitespace-only
|
|
27
21
|
// value (an agent expanding an unset variable) used to sail through the shape —
|
|
28
22
|
// on the action/evidence fields it was dropped by a truthiness guard in apply
|
|
@@ -37,23 +31,39 @@ export const SessionLogSchema = z.object({
|
|
|
37
31
|
// CLI maps `--brief ""` to it. Trimming also keeps accidental padding out of
|
|
38
32
|
// stored text.
|
|
39
33
|
const nonEmptyText = z.string().trim().min(1, 'must contain text — an empty value is rejected, never silently dropped');
|
|
34
|
+
// A list field (domain, tags, related) takes a string[] or, like the CLI's
|
|
35
|
+
// list flags (`--domain "a,b"`), one comma string. The split is the one rule
|
|
36
|
+
// for both paths — the CLI's `splitList` calls it — so `"a, b"` is the same
|
|
37
|
+
// two values everywhere (#33).
|
|
38
|
+
export const splitCommaList = (value) => value.split(',').map(s => s.trim()).filter(Boolean);
|
|
39
|
+
const stringList = z.preprocess(v => typeof v === 'string' ? splitCommaList(v) : v, z.array(z.string()));
|
|
40
|
+
export const SessionLogSchema = z.object({
|
|
41
|
+
type: z.enum([...SESSION_LOG_TYPES]),
|
|
42
|
+
summary: z.string(),
|
|
43
|
+
body: nonEmptyText.optional(),
|
|
44
|
+
agent: z.string(),
|
|
45
|
+
}).strict();
|
|
40
46
|
export const UpdateFieldsSchema = z.object({
|
|
41
47
|
status: z.enum([...ENTITY_STATUSES]).optional(),
|
|
42
48
|
current_state: nonEmptyText.optional(),
|
|
43
49
|
session_log: SessionLogSchema.optional(),
|
|
50
|
+
agent: nonEmptyText.optional(), // entity-only; names the agent on the added session log entry
|
|
44
51
|
brief: z.string().nullable().optional(),
|
|
45
52
|
add_action: nonEmptyText.optional(),
|
|
46
53
|
add_actions: z.array(nonEmptyText).optional(),
|
|
47
54
|
complete_action: nonEmptyText.optional(),
|
|
55
|
+
complete_actions: z.array(nonEmptyText).min(1).optional(),
|
|
48
56
|
remove_action: nonEmptyText.optional(),
|
|
57
|
+
remove_actions: z.array(nonEmptyText).min(1).optional(),
|
|
49
58
|
clear_actions: z.boolean().optional(),
|
|
59
|
+
parent: z.string().optional(), // entity-only; the slug must exist, no cycle
|
|
50
60
|
// Knowledge metadata
|
|
51
61
|
description: z.string().nullable().optional(),
|
|
52
62
|
resource: z.string().nullable().optional(),
|
|
53
|
-
domain:
|
|
63
|
+
domain: stringList.optional(),
|
|
54
64
|
confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
|
|
55
|
-
tags:
|
|
56
|
-
related:
|
|
65
|
+
tags: stringList.optional(),
|
|
66
|
+
related: stringList.optional(),
|
|
57
67
|
synthesis: nonEmptyText.optional(),
|
|
58
68
|
insight: nonEmptyText.optional(), // alias for synthesis (normalized by normalizeAliases)
|
|
59
69
|
add_evidence: nonEmptyText.optional(),
|
|
@@ -64,10 +74,10 @@ export const CreateEntityFieldsSchema = z.object({
|
|
|
64
74
|
purpose: nonEmptyText.optional(),
|
|
65
75
|
description: z.string().optional(),
|
|
66
76
|
resource: z.string().optional(),
|
|
67
|
-
domain:
|
|
77
|
+
domain: stringList.optional(),
|
|
68
78
|
parent: z.string().optional(),
|
|
69
|
-
related:
|
|
70
|
-
tags:
|
|
79
|
+
related: stringList.optional(),
|
|
80
|
+
tags: stringList.optional(),
|
|
71
81
|
brief: nonEmptyText.optional(),
|
|
72
82
|
add_action: nonEmptyText.optional(),
|
|
73
83
|
add_actions: z.array(nonEmptyText).optional(),
|
|
@@ -77,18 +87,19 @@ export const CreateKnowledgeFieldsSchema = z.object({
|
|
|
77
87
|
// Plan-only: insights have no lifecycle, so the semantic check rejects
|
|
78
88
|
// status on any other subtype (the shape only pins the allowed values).
|
|
79
89
|
status: z.enum([...PLAN_STATUSES]).optional(),
|
|
90
|
+
confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
|
|
80
91
|
synthesis: nonEmptyText.optional(),
|
|
81
92
|
insight: nonEmptyText.optional(),
|
|
82
93
|
description: z.string().optional(),
|
|
83
|
-
domain:
|
|
84
|
-
related:
|
|
85
|
-
tags:
|
|
94
|
+
domain: stringList.optional(),
|
|
95
|
+
related: stringList.optional(),
|
|
96
|
+
tags: stringList.optional(),
|
|
86
97
|
'session-date': z.string().optional(),
|
|
87
98
|
}).strict();
|
|
88
99
|
export const CreateSessionFieldsSchema = z.object({
|
|
89
100
|
synthesis: nonEmptyText.optional(),
|
|
90
101
|
insight: nonEmptyText.optional(),
|
|
91
|
-
related:
|
|
102
|
+
related: stringList.optional(),
|
|
92
103
|
'session-date': z.string().optional(),
|
|
93
104
|
}).strict();
|
|
94
105
|
const CreateEntityBranch = z.object({
|
|
@@ -109,6 +120,16 @@ const CreateSessionBranch = z.object({
|
|
|
109
120
|
name: z.string(),
|
|
110
121
|
fields: CreateSessionFieldsSchema.optional(),
|
|
111
122
|
}).strict();
|
|
123
|
+
/** Update fields the mechanised session log neither counts nor names:
|
|
124
|
+
* ticking a box is not history worth an entry, and `agent` only names it. */
|
|
125
|
+
const UNLOGGED_FIELDS = new Set(['add_action', 'add_actions', 'complete_action', 'complete_actions', 'remove_action', 'remove_actions', 'clear_actions', 'agent']);
|
|
126
|
+
/** The fields an update's mechanised session log entry names — empty when
|
|
127
|
+
* the update earns none. Shared by the update check (which rejects an
|
|
128
|
+
* `agent` with no entry to name) and the Apply update arm (which writes it). */
|
|
129
|
+
export function loggedFields(fields) {
|
|
130
|
+
return Object.keys(fields)
|
|
131
|
+
.filter(f => fields[f] !== undefined && !UNLOGGED_FIELDS.has(f));
|
|
132
|
+
}
|
|
112
133
|
const UpdateBranch = z.object({
|
|
113
134
|
op: z.literal('update'),
|
|
114
135
|
slug: z.string(),
|
|
@@ -136,6 +157,12 @@ const PromoteBranch = z.object({
|
|
|
136
157
|
slug: z.string(),
|
|
137
158
|
target: z.enum(['entity', 'project', 'knowledge']),
|
|
138
159
|
}).strict();
|
|
160
|
+
// No `kind`: the slug alone says what is being deleted, the same as
|
|
161
|
+
// `hafez delete <slug>`. The check resolves it and rejects an entity.
|
|
162
|
+
const DeleteBranch = z.object({
|
|
163
|
+
op: z.literal('delete'),
|
|
164
|
+
slug: z.string(),
|
|
165
|
+
}).strict();
|
|
139
166
|
// Nested discriminated union: outer on `op`, inner on `kind` for create branch.
|
|
140
167
|
// A flat `discriminatedUnion('op', [...])` cannot work because three branches share
|
|
141
168
|
// op=create — Zod requires unique discriminator values per branch and throws at
|
|
@@ -154,6 +181,7 @@ export const BatchOperationSchema = z.discriminatedUnion('op', [
|
|
|
154
181
|
LinkBranch,
|
|
155
182
|
UnlinkBranch,
|
|
156
183
|
PromoteBranch,
|
|
184
|
+
DeleteBranch,
|
|
157
185
|
]);
|
|
158
186
|
// --- Alias normalization ---
|
|
159
187
|
// The one place a declared alias (contracts.ts ALIAS_KEYS) becomes its canonical
|
|
@@ -251,6 +279,36 @@ function claimNew(ctx, errors, slug, info, build) {
|
|
|
251
279
|
ctx.simulate(slug, info);
|
|
252
280
|
return taken ? 'SLUG_EXISTS' : undefined;
|
|
253
281
|
}
|
|
282
|
+
/** The link fields an update payload sets, or null when it sets none. */
|
|
283
|
+
function linkFieldsOf(fields) {
|
|
284
|
+
const links = {};
|
|
285
|
+
if (fields.parent !== undefined)
|
|
286
|
+
links.parent = fields.parent;
|
|
287
|
+
if (fields.related !== undefined)
|
|
288
|
+
links.related = fields.related;
|
|
289
|
+
return Object.keys(links).length > 0 ? links : null;
|
|
290
|
+
}
|
|
291
|
+
/** The parent chain `slug → parent → … → slug` that giving `slug` this
|
|
292
|
+
* parent closes, or null when the chain ends or loops without `slug`.
|
|
293
|
+
* Shared by the Op Spec checks (against the simulated batch) and
|
|
294
|
+
* `validateVault` (against the files). */
|
|
295
|
+
export function parentCycle(slug, parent, parentOf) {
|
|
296
|
+
const path = [slug];
|
|
297
|
+
const seen = new Set(path);
|
|
298
|
+
for (let p = parent; p !== undefined; p = parentOf(p)) {
|
|
299
|
+
path.push(p);
|
|
300
|
+
if (p === slug)
|
|
301
|
+
return path;
|
|
302
|
+
if (seen.has(p))
|
|
303
|
+
return null;
|
|
304
|
+
seen.add(p);
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
function parentCycleError(slug, parent, ctx) {
|
|
309
|
+
const cycle = parentCycle(slug, parent, s => ctx.linksOf(s).parent);
|
|
310
|
+
return cycle ? `Parent '${parent}' would make a cycle: ${cycle.join(' → ')}` : null;
|
|
311
|
+
}
|
|
254
312
|
// `as const satisfies` rather than a `: OpSpec[]` annotation: it keeps each
|
|
255
313
|
// row's `name` a literal, which is what OpSpecName (and through it the Apply
|
|
256
314
|
// table's key type) is derived from. OP_SPECS below re-exports the rows under
|
|
@@ -260,7 +318,7 @@ const OP_SPEC_ROWS = [
|
|
|
260
318
|
name: 'update',
|
|
261
319
|
op: 'update',
|
|
262
320
|
schema: UpdateBranch,
|
|
263
|
-
description: 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …). `insight` is an alias for `synthesis` (knowledge-only; ignored when `synthesis` is also given). On knowledge notes `status` is plan-subtype only (active|done); insights reject it.',
|
|
321
|
+
description: 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …). Next Actions fields compose within one op as prune, tick, add: `clear_actions` → `remove_action(s)` → `complete_action(s)` → `add_action(s)` — queries match the list as it was before the op (two queries naming one line is an error), added lines land last, so `clear_actions` + `add_actions` rebuilds the list; `clear_actions` with a query field is rejected. Lines leaving the list (removed, cleared, pruned with an emptied section, ticked past nine, oldest first) move to the entity\'s archive log. A Handle of an open action (as `read` shows it, or any longer or shorter prefix of four or more hex that names only that line) names that line; anything else is a case-insensitive substring of the text. `parent` sets the parent link (entity-only; the slug must exist, and the parent chain must not lead back to the entity). `insight` is an alias for `synthesis` (knowledge-only; ignored when `synthesis` is also given). An entity update with no `session_log` gets a `progress` entry naming the fields written, unless its only fields are action fields; `agent` names the agent on it (entity-only, detected from the Harness when absent, rejected alongside `session_log`). On knowledge notes `status` is plan-subtype only (active|done); insights reject it.',
|
|
264
322
|
exampleInclude: ['status'],
|
|
265
323
|
check(op, ctx) {
|
|
266
324
|
const errors = [];
|
|
@@ -278,6 +336,29 @@ const OP_SPEC_ROWS = [
|
|
|
278
336
|
pushErr(errors, err);
|
|
279
337
|
}
|
|
280
338
|
}
|
|
339
|
+
// Statically contradictory: clear_actions runs first and empties the
|
|
340
|
+
// list, so any query in the same op would fail at apply time with a
|
|
341
|
+
// misleading "No matching action". Reject here, where the message can
|
|
342
|
+
// say why and land beside the batch's other errors. Entity targets
|
|
343
|
+
// only — on a knowledge note validateKindFields has already rejected
|
|
344
|
+
// the fields themselves, and a second error about their order is noise.
|
|
345
|
+
if (target?.kind === 'entity' && op.fields.clear_actions) {
|
|
346
|
+
const queries = ['remove_action', 'remove_actions', 'complete_action', 'complete_actions'].filter(f => op.fields[f] !== undefined);
|
|
347
|
+
if (queries.length > 0) {
|
|
348
|
+
errors.push(`clear_actions empties the list before ${queries.slice(0, -1).join(', ')}${queries.length > 1 ? ' and ' : ''}${queries.at(-1)} would run — drop the clear or use a separate op`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// `agent` names the entry the Spine adds; with nothing added it would be
|
|
352
|
+
// dropped. Entity targets only — on a knowledge note validateKindFields
|
|
353
|
+
// has already rejected it as entity-only.
|
|
354
|
+
if (target?.kind === 'entity' && op.fields.agent !== undefined) {
|
|
355
|
+
if (op.fields.session_log) {
|
|
356
|
+
errors.push('agent names the session log entry added when session_log is absent — with session_log, set session_log.agent instead');
|
|
357
|
+
}
|
|
358
|
+
else if (loggedFields(op.fields).length === 0) {
|
|
359
|
+
errors.push('agent names the session log entry, and an update of action fields alone writes none — drop agent');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
281
362
|
if (op.fields.session_log) {
|
|
282
363
|
const logErrors = ctx.validateSessionLogEntry(op.fields.session_log);
|
|
283
364
|
if (logErrors.length > 0)
|
|
@@ -289,6 +370,20 @@ const OP_SPEC_ROWS = [
|
|
|
289
370
|
errors.push(`Related slug '${r}' does not exist`);
|
|
290
371
|
}
|
|
291
372
|
}
|
|
373
|
+
if (op.fields.parent !== undefined) {
|
|
374
|
+
if (!ctx.existsAnywhere(op.fields.parent))
|
|
375
|
+
errors.push(`Parent '${op.fields.parent}' does not exist`);
|
|
376
|
+
else if (target?.kind === 'entity') {
|
|
377
|
+
const cycle = parentCycleError(op.slug, op.fields.parent, ctx);
|
|
378
|
+
if (cycle)
|
|
379
|
+
errors.push(cycle);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (target) {
|
|
383
|
+
const links = linkFieldsOf(op.fields);
|
|
384
|
+
if (links)
|
|
385
|
+
ctx.simulateLinks(op.slug, { ...ctx.linksOf(op.slug), ...links });
|
|
386
|
+
}
|
|
292
387
|
return { slug: op.slug, errors, primaryCode };
|
|
293
388
|
},
|
|
294
389
|
},
|
|
@@ -303,6 +398,8 @@ const OP_SPEC_ROWS = [
|
|
|
303
398
|
const errors = [];
|
|
304
399
|
const slug = slugify(op.name);
|
|
305
400
|
const primaryCode = claimNew(ctx, errors, slug, { kind: 'entity', type: op.fields.type }, () => ctx.buildEntityFrontmatter(op.name, op.fields));
|
|
401
|
+
if (!primaryCode)
|
|
402
|
+
ctx.simulateLinks(slug, { parent: op.fields.parent, related: op.fields.related });
|
|
306
403
|
return { slug, errors, created: errors.length === 0, primaryCode };
|
|
307
404
|
},
|
|
308
405
|
},
|
|
@@ -311,12 +408,14 @@ const OP_SPEC_ROWS = [
|
|
|
311
408
|
op: 'create',
|
|
312
409
|
kind: 'knowledge',
|
|
313
410
|
schema: CreateKnowledgeBranch,
|
|
314
|
-
description: 'Create a new knowledge note (insight / plan subtype). `insight` is an alias for `synthesis` (ignored when `synthesis` is also given). `status` (active|done) is plan-only; `synthesis` is insight-only — plan bodies are Goal/Steps/Dependencies, and a field whose section the subtype lacks is rejected, never dropped.',
|
|
411
|
+
description: 'Create a new knowledge note (insight / plan subtype). `confidence` defaults to observation. `insight` is an alias for `synthesis` (ignored when `synthesis` is also given). `status` (active|done) is plan-only; `synthesis` is insight-only — plan bodies are Goal/Steps/Dependencies, and a field whose section the subtype lacks is rejected, never dropped.',
|
|
315
412
|
exampleInclude: ['subtype', 'synthesis'],
|
|
316
413
|
check(op, ctx) {
|
|
317
414
|
const errors = [];
|
|
318
415
|
const slug = slugify(op.name);
|
|
319
416
|
const primaryCode = claimNew(ctx, errors, slug, { kind: 'knowledge', type: 'knowledge', subtype: op.fields?.subtype }, () => ctx.buildKnowledgeFrontmatter(op.name, (op.fields || {})));
|
|
417
|
+
if (!primaryCode)
|
|
418
|
+
ctx.simulateLinks(slug, { related: op.fields?.related });
|
|
320
419
|
return { slug, errors, created: errors.length === 0, primaryCode };
|
|
321
420
|
},
|
|
322
421
|
},
|
|
@@ -364,6 +463,17 @@ const OP_SPEC_ROWS = [
|
|
|
364
463
|
}
|
|
365
464
|
if (!ctx.existsAnywhere(op.target))
|
|
366
465
|
errors.push(`Target '${op.target}' does not exist`);
|
|
466
|
+
else if (!primaryCode && op.relation === 'parent') {
|
|
467
|
+
const cycle = parentCycleError(op.slug, op.target, ctx);
|
|
468
|
+
if (cycle)
|
|
469
|
+
errors.push(cycle);
|
|
470
|
+
}
|
|
471
|
+
if (!primaryCode) {
|
|
472
|
+
const links = ctx.linksOf(op.slug);
|
|
473
|
+
ctx.simulateLinks(op.slug, op.relation === 'parent'
|
|
474
|
+
? { ...links, parent: op.target }
|
|
475
|
+
: { ...links, related: [...(links.related ?? []).filter(r => r !== op.target), op.target] });
|
|
476
|
+
}
|
|
367
477
|
return { slug: op.slug, errors, primaryCode };
|
|
368
478
|
},
|
|
369
479
|
},
|
|
@@ -380,6 +490,12 @@ const OP_SPEC_ROWS = [
|
|
|
380
490
|
errors.push(notFoundError(op.slug));
|
|
381
491
|
primaryCode = 'NOT_FOUND';
|
|
382
492
|
}
|
|
493
|
+
else {
|
|
494
|
+
const links = ctx.linksOf(op.slug);
|
|
495
|
+
ctx.simulateLinks(op.slug, op.relation === 'parent'
|
|
496
|
+
? { ...links, parent: links.parent === op.target ? undefined : links.parent }
|
|
497
|
+
: { ...links, related: links.related?.filter(r => r !== op.target) });
|
|
498
|
+
}
|
|
383
499
|
return { slug: op.slug, errors, primaryCode };
|
|
384
500
|
},
|
|
385
501
|
},
|
|
@@ -406,6 +522,9 @@ const OP_SPEC_ROWS = [
|
|
|
406
522
|
}
|
|
407
523
|
else {
|
|
408
524
|
ctx.simulate(op.slug, op.target === 'knowledge' ? { kind: 'knowledge', type: 'knowledge' } : { kind: 'entity', type: op.target });
|
|
525
|
+
// A promoted knowledge note keeps related but not parent.
|
|
526
|
+
if (op.target === 'knowledge')
|
|
527
|
+
ctx.simulateLinks(op.slug, { related: ctx.linksOf(op.slug).related });
|
|
409
528
|
}
|
|
410
529
|
}
|
|
411
530
|
catch (err) {
|
|
@@ -415,6 +534,42 @@ const OP_SPEC_ROWS = [
|
|
|
415
534
|
return { slug: op.slug, errors, primaryCode };
|
|
416
535
|
},
|
|
417
536
|
},
|
|
537
|
+
{
|
|
538
|
+
name: 'delete',
|
|
539
|
+
op: 'delete',
|
|
540
|
+
schema: DeleteBranch,
|
|
541
|
+
description: 'Delete a knowledge or session note. An entity is never deleted — retire it with `hafez update <slug> --status done`. Rejected while another entity or knowledge note links to the note (parent/related): unlink or re-point those first — an earlier op in the same batch counts.',
|
|
542
|
+
exampleInclude: [],
|
|
543
|
+
addressesSessions: true,
|
|
544
|
+
check(op, ctx) {
|
|
545
|
+
const errors = [];
|
|
546
|
+
let primaryCode;
|
|
547
|
+
const target = ctx.resolve(op.slug);
|
|
548
|
+
const session = ctx.sessionExists(op.slug);
|
|
549
|
+
if (target?.kind === 'entity') {
|
|
550
|
+
errors.push(`'${op.slug}' is an entity (${target.type}) — an entity is never deleted; retire it with \`hafez update ${op.slug} --status done\``);
|
|
551
|
+
}
|
|
552
|
+
else if (target && session) {
|
|
553
|
+
errors.push(`'${op.slug}' is ambiguous — it names both a knowledge note and a session note, and delete will not guess which`);
|
|
554
|
+
}
|
|
555
|
+
else if (!target && !session) {
|
|
556
|
+
errors.push(notFoundError(op.slug));
|
|
557
|
+
primaryCode = 'NOT_FOUND';
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Only knowledge can be linked to: a parent/related slug must name an
|
|
561
|
+
// entity or knowledge note, never a session.
|
|
562
|
+
const referrers = target ? ctx.referrers(op.slug) : [];
|
|
563
|
+
if (referrers.length > 0) {
|
|
564
|
+
errors.push(`'${op.slug}' is still linked from ${referrers.join(', ')} (parent/related) — unlink or re-point those first; an earlier op in the same batch counts`);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
ctx.simulateDelete(op.slug, target ? 'knowledge' : 'session');
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { slug: op.slug, errors, primaryCode };
|
|
571
|
+
},
|
|
572
|
+
},
|
|
418
573
|
];
|
|
419
574
|
export const OP_SPECS = OP_SPEC_ROWS;
|
|
420
575
|
/** Find the spec for a (shape-valid) operation. */
|
|
@@ -440,6 +595,11 @@ export function slugBearingOpNames() {
|
|
|
440
595
|
})
|
|
441
596
|
.map(s => s.op))];
|
|
442
597
|
}
|
|
598
|
+
/** Slug-bearing ops whose slug may name a session note — a missing entity
|
|
599
|
+
* or knowledge note alone is not NOT_FOUND for them. */
|
|
600
|
+
export function sessionAddressingOpNames() {
|
|
601
|
+
return OP_SPECS.filter(s => s.addressesSessions).map(s => s.op);
|
|
602
|
+
}
|
|
443
603
|
/** Create kinds for error text — derived from the table. */
|
|
444
604
|
export function validCreateKinds() {
|
|
445
605
|
return OP_SPECS.filter(s => s.op === 'create' && s.kind).map(s => s.kind);
|