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