hafez 1.3.0 → 1.4.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 +67 -9
- package/dist/apply.d.ts +30 -0
- package/dist/apply.js +582 -0
- package/dist/batch-ops.d.ts +296 -13
- package/dist/batch-ops.js +186 -62
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.js +6 -2
- package/dist/cli/commands.d.ts +24 -0
- package/dist/cli/commands.js +197 -115
- package/dist/cli/help-agent.js +24 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.js +150 -130
- package/dist/cli/upgrade.d.ts +42 -0
- package/dist/cli/upgrade.js +305 -0
- package/dist/contracts.d.ts +49 -6
- package/dist/contracts.js +104 -23
- package/dist/dates.d.ts +10 -0
- package/dist/dates.js +27 -0
- package/dist/db.d.ts +27 -2
- package/dist/db.js +309 -33
- package/dist/document.d.ts +97 -88
- package/dist/document.js +512 -382
- package/dist/export-okf.js +41 -15
- package/dist/git.js +165 -48
- package/dist/index.d.ts +2 -2
- package/dist/index.js +212 -727
- package/dist/knowledge-index.d.ts +23 -1
- package/dist/knowledge-index.js +62 -6
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +96 -64
- package/dist/schema.js +4 -4
- package/dist/templates.d.ts +6 -0
- package/dist/templates.js +27 -27
- package/dist/types.d.ts +38 -11
- package/dist/vault.d.ts +18 -0
- package/dist/vault.js +50 -4
- package/package.json +1 -1
- package/skill/SKILL.md +51 -110
- package/dist/query.d.ts +0 -24
- package/dist/query.js +0 -169
package/dist/batch-ops.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
// The Op Spec (see CONTEXT.md): one table defining every batch operation —
|
|
3
3
|
// shape (Zod), semantic check, description, and example hints — from which
|
|
4
4
|
// validation, error text, and schema introspection all derive. Adding an op
|
|
5
|
-
// means
|
|
5
|
+
// means one entry here plus one arm in the Apply table (src/apply.ts), which
|
|
6
|
+
// the compiler enforces: the arm table is keyed by this table's names.
|
|
7
|
+
//
|
|
8
|
+
// Every semantic gate lives here and runs exactly once, in the validate phase.
|
|
9
|
+
// Apply assumes the payload it receives passed these checks.
|
|
6
10
|
import { z } from 'zod';
|
|
7
|
-
import { ENTITY_TYPES, ENTITY_STATUSES, SESSION_LOG_TYPES, CONFIDENCE_LEVELS, VALID_SUBTYPES, getContract, } from './contracts.js';
|
|
11
|
+
import { ALIAS_KEYS, ENTITY_TYPES, ENTITY_STATUSES, SESSION_LOG_TYPES, CONFIDENCE_LEVELS, VALID_SUBTYPES, PLAN_STATUSES, getContract, } from './contracts.js';
|
|
8
12
|
import { slugify } from './vault.js';
|
|
9
13
|
import { HafezError } from './types.js';
|
|
10
14
|
// --- Shapes ---
|
|
@@ -19,15 +23,29 @@ export const SessionLogSchema = z.object({
|
|
|
19
23
|
body: z.string().optional(),
|
|
20
24
|
agent: z.string(),
|
|
21
25
|
}).strict();
|
|
26
|
+
// Body-section text must have text after trimming. An empty or whitespace-only
|
|
27
|
+
// value (an agent expanding an unset variable) used to sail through the shape —
|
|
28
|
+
// on the action/evidence fields it was dropped by a truthiness guard in apply
|
|
29
|
+
// (exit 0, nothing on disk); on the section-setting fields below it was worse,
|
|
30
|
+
// because apply trusts the validate phase and `setSection(body, 'Synthesis',
|
|
31
|
+
// '')` wipes what was already there. Same false-success class the CLI loop
|
|
32
|
+
// closed for the enum flags, once silent and once destructive.
|
|
33
|
+
//
|
|
34
|
+
// The rule: every non-nullable string field that writes body-section text is
|
|
35
|
+
// nonEmptyText. The nullable ones (update's brief/description/resource) are
|
|
36
|
+
// excluded by construction — null is their documented clear signal, and the
|
|
37
|
+
// CLI maps `--brief ""` to it. Trimming also keeps accidental padding out of
|
|
38
|
+
// stored text.
|
|
39
|
+
const nonEmptyText = z.string().trim().min(1, 'must contain text — an empty value is rejected, never silently dropped');
|
|
22
40
|
export const UpdateFieldsSchema = z.object({
|
|
23
41
|
status: z.enum([...ENTITY_STATUSES]).optional(),
|
|
24
|
-
current_state:
|
|
42
|
+
current_state: nonEmptyText.optional(),
|
|
25
43
|
session_log: SessionLogSchema.optional(),
|
|
26
44
|
brief: z.string().nullable().optional(),
|
|
27
|
-
add_action:
|
|
28
|
-
add_actions: z.array(
|
|
29
|
-
complete_action:
|
|
30
|
-
remove_action:
|
|
45
|
+
add_action: nonEmptyText.optional(),
|
|
46
|
+
add_actions: z.array(nonEmptyText).optional(),
|
|
47
|
+
complete_action: nonEmptyText.optional(),
|
|
48
|
+
remove_action: nonEmptyText.optional(),
|
|
31
49
|
clear_actions: z.boolean().optional(),
|
|
32
50
|
// Knowledge metadata
|
|
33
51
|
description: z.string().nullable().optional(),
|
|
@@ -36,28 +54,31 @@ export const UpdateFieldsSchema = z.object({
|
|
|
36
54
|
confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
|
|
37
55
|
tags: z.array(z.string()).optional(),
|
|
38
56
|
related: z.array(z.string()).optional(),
|
|
39
|
-
synthesis:
|
|
40
|
-
insight:
|
|
41
|
-
add_evidence:
|
|
42
|
-
add_source:
|
|
57
|
+
synthesis: nonEmptyText.optional(),
|
|
58
|
+
insight: nonEmptyText.optional(), // alias for synthesis (normalized by normalizeAliases)
|
|
59
|
+
add_evidence: nonEmptyText.optional(),
|
|
60
|
+
add_source: nonEmptyText.optional(),
|
|
43
61
|
}).strict();
|
|
44
62
|
export const CreateEntityFieldsSchema = z.object({
|
|
45
63
|
type: z.enum([...ENTITY_TYPES]),
|
|
46
|
-
purpose:
|
|
64
|
+
purpose: nonEmptyText.optional(),
|
|
47
65
|
description: z.string().optional(),
|
|
48
66
|
resource: z.string().optional(),
|
|
49
67
|
domain: z.array(z.string()).optional(),
|
|
50
68
|
parent: z.string().optional(),
|
|
51
69
|
related: z.array(z.string()).optional(),
|
|
52
70
|
tags: z.array(z.string()).optional(),
|
|
53
|
-
brief:
|
|
54
|
-
add_action:
|
|
55
|
-
add_actions: z.array(
|
|
71
|
+
brief: nonEmptyText.optional(),
|
|
72
|
+
add_action: nonEmptyText.optional(),
|
|
73
|
+
add_actions: z.array(nonEmptyText).optional(),
|
|
56
74
|
}).strict();
|
|
57
75
|
export const CreateKnowledgeFieldsSchema = z.object({
|
|
58
76
|
subtype: z.enum([...VALID_SUBTYPES]).optional(),
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
// Plan-only: insights have no lifecycle, so the semantic check rejects
|
|
78
|
+
// status on any other subtype (the shape only pins the allowed values).
|
|
79
|
+
status: z.enum([...PLAN_STATUSES]).optional(),
|
|
80
|
+
synthesis: nonEmptyText.optional(),
|
|
81
|
+
insight: nonEmptyText.optional(),
|
|
61
82
|
description: z.string().optional(),
|
|
62
83
|
domain: z.array(z.string()).optional(),
|
|
63
84
|
related: z.array(z.string()).optional(),
|
|
@@ -65,8 +86,8 @@ export const CreateKnowledgeFieldsSchema = z.object({
|
|
|
65
86
|
'session-date': z.string().optional(),
|
|
66
87
|
}).strict();
|
|
67
88
|
export const CreateSessionFieldsSchema = z.object({
|
|
68
|
-
synthesis:
|
|
69
|
-
insight:
|
|
89
|
+
synthesis: nonEmptyText.optional(),
|
|
90
|
+
insight: nonEmptyText.optional(),
|
|
70
91
|
related: z.array(z.string()).optional(),
|
|
71
92
|
'session-date': z.string().optional(),
|
|
72
93
|
}).strict();
|
|
@@ -134,25 +155,124 @@ export const BatchOperationSchema = z.discriminatedUnion('op', [
|
|
|
134
155
|
UnlinkBranch,
|
|
135
156
|
PromoteBranch,
|
|
136
157
|
]);
|
|
158
|
+
// --- Alias normalization ---
|
|
159
|
+
// The one place a declared alias (contracts.ts ALIAS_KEYS) becomes its canonical
|
|
160
|
+
// field. It runs before validation, so validate and apply both see the canonical
|
|
161
|
+
// op on every entry point — library create/update/batch and CLI batch input
|
|
162
|
+
// alike. Normalizing anywhere else is what made the alias a silent drop: the
|
|
163
|
+
// strict shapes accepted `insight`, nothing rewrote it, and the apply step (which
|
|
164
|
+
// only reads `synthesis`) wrote an empty Synthesis section and reported ok.
|
|
165
|
+
/** ALIAS_KEYS re-keyed by op path prefix: 'update.fields' → [[alias, canonical]]. */
|
|
166
|
+
const ALIASES_BY_PATH = new Map();
|
|
167
|
+
for (const [path, canonical] of Object.entries(ALIAS_KEYS)) {
|
|
168
|
+
const cut = path.lastIndexOf('.');
|
|
169
|
+
const prefix = path.slice(0, cut);
|
|
170
|
+
const list = ALIASES_BY_PATH.get(prefix) ?? [];
|
|
171
|
+
list.push([path.slice(cut + 1), canonical]);
|
|
172
|
+
ALIASES_BY_PATH.set(prefix, list);
|
|
173
|
+
}
|
|
174
|
+
/** Canonicalize an operation's declared aliases.
|
|
175
|
+
*
|
|
176
|
+
* Precedence when both are present: the canonical field wins and the alias is
|
|
177
|
+
* dropped. Never mutates the caller's object — an op carrying no alias is
|
|
178
|
+
* returned as-is. */
|
|
179
|
+
export function normalizeAliases(op) {
|
|
180
|
+
const o = op;
|
|
181
|
+
if (!o || typeof o !== 'object' || !o.fields || typeof o.fields !== 'object')
|
|
182
|
+
return op;
|
|
183
|
+
const prefix = `${String(o.op)}${o.kind === undefined ? '' : `.${String(o.kind)}`}.fields`;
|
|
184
|
+
const aliases = ALIASES_BY_PATH.get(prefix);
|
|
185
|
+
if (!aliases)
|
|
186
|
+
return op;
|
|
187
|
+
let fields;
|
|
188
|
+
for (const [alias, canonical] of aliases) {
|
|
189
|
+
if (o.fields[alias] === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
fields ??= { ...o.fields };
|
|
192
|
+
if (fields[canonical] === undefined)
|
|
193
|
+
fields[canonical] = fields[alias];
|
|
194
|
+
delete fields[alias];
|
|
195
|
+
}
|
|
196
|
+
return fields ? { ...o, fields } : op;
|
|
197
|
+
}
|
|
198
|
+
/** normalizeAliases over a payload — the form the Spine and dry-run use. */
|
|
199
|
+
export function normalizeOperations(operations) {
|
|
200
|
+
return operations.map(op => normalizeAliases(op));
|
|
201
|
+
}
|
|
137
202
|
function pushErr(errors, err) {
|
|
138
203
|
const e = err;
|
|
139
204
|
errors.push(e.details?.length ? `${e.message}: ${e.details.join('; ')}` : e.message);
|
|
140
205
|
}
|
|
141
|
-
|
|
206
|
+
/** The one source of the missing-slug error text — specs and the Spine's
|
|
207
|
+
* shape-failure precedence rule (validateBatchCore) must not drift apart. */
|
|
208
|
+
export function notFoundError(slug) {
|
|
209
|
+
return `'${slug}' not found`;
|
|
210
|
+
}
|
|
211
|
+
/** The one op-error line format. Every surface that localises an error to its
|
|
212
|
+
* op — the validate phase's error list, apply's execution-time failures, and
|
|
213
|
+
* the CLI's dry-run report — renders it through here, so an agent parsing
|
|
214
|
+
* `op[i] (op slug): …` sees one shape whichever surface produced it. */
|
|
215
|
+
export function opErrorLine(index, op, slug, message) {
|
|
216
|
+
return `op[${index}] (${op} ${slug}): ${message}`;
|
|
217
|
+
}
|
|
218
|
+
/** The one "the batch was rejected, nothing landed" header — shared by the
|
|
219
|
+
* library's validate gate and the CLI's shape-parse gate, which reject the
|
|
220
|
+
* same payload at different depths and must say so identically. `batch
|
|
221
|
+
* --dry-run` deliberately words its own header differently: it reports a
|
|
222
|
+
* hypothetical ("would be applied"), not a refusal. */
|
|
223
|
+
export function batchInvalidHeader(invalidCount, total) {
|
|
224
|
+
return `Batch failed validation: ${invalidCount} invalid operation${invalidCount === 1 ? '' : 's'} (0 of ${total} applied)`;
|
|
225
|
+
}
|
|
226
|
+
/** The one source of the duplicate-session disposition text. A duplicate
|
|
227
|
+
* session is not an error — the op is skipped and the batch continues — and
|
|
228
|
+
* dry-run (spec warning) and apply (BatchResult.warning) report it with this
|
|
229
|
+
* same sentence so the two can never drift. */
|
|
230
|
+
export function duplicateSessionWarning(slug) {
|
|
231
|
+
return `session '${slug}' already exists — op skipped (batch continues)`;
|
|
232
|
+
}
|
|
233
|
+
/** The create-style gate, in one place so the invariant can't be re-broken:
|
|
234
|
+
* a taken slug is a SLUG_EXISTS error and must NOT be simulated (simulating
|
|
235
|
+
* over an existing slug shadows the real file's kind for later ops in the
|
|
236
|
+
* batch). The builder runs before the claim — it must see the pre-claim
|
|
237
|
+
* world so a self-referencing parent still fails. */
|
|
238
|
+
function claimNew(ctx, errors, slug, info, build) {
|
|
239
|
+
const taken = ctx.existsAnywhere(slug);
|
|
240
|
+
if (taken)
|
|
241
|
+
errors.push(`Slug '${slug}' already exists`);
|
|
242
|
+
if (build) {
|
|
243
|
+
try {
|
|
244
|
+
build();
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
pushErr(errors, err);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!taken)
|
|
251
|
+
ctx.simulate(slug, info);
|
|
252
|
+
return taken ? 'SLUG_EXISTS' : undefined;
|
|
253
|
+
}
|
|
254
|
+
// `as const satisfies` rather than a `: OpSpec[]` annotation: it keeps each
|
|
255
|
+
// row's `name` a literal, which is what OpSpecName (and through it the Apply
|
|
256
|
+
// table's key type) is derived from. OP_SPECS below re-exports the rows under
|
|
257
|
+
// the interface, so consumers still read optional fields (`kind`) off them.
|
|
258
|
+
const OP_SPEC_ROWS = [
|
|
142
259
|
{
|
|
143
260
|
name: 'update',
|
|
144
261
|
op: 'update',
|
|
145
262
|
schema: UpdateBranch,
|
|
146
|
-
description: 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …).',
|
|
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.',
|
|
147
264
|
exampleInclude: ['status'],
|
|
148
265
|
check(op, ctx) {
|
|
149
266
|
const errors = [];
|
|
267
|
+
let primaryCode;
|
|
150
268
|
const target = ctx.resolve(op.slug);
|
|
151
|
-
if (!target)
|
|
152
|
-
errors.push(
|
|
269
|
+
if (!target) {
|
|
270
|
+
errors.push(notFoundError(op.slug));
|
|
271
|
+
primaryCode = 'NOT_FOUND';
|
|
272
|
+
}
|
|
153
273
|
else {
|
|
154
274
|
try {
|
|
155
|
-
ctx.validateKindFields(target.kind, op.fields);
|
|
275
|
+
ctx.validateKindFields(target.kind, op.fields, target.subtype);
|
|
156
276
|
}
|
|
157
277
|
catch (err) {
|
|
158
278
|
pushErr(errors, err);
|
|
@@ -169,7 +289,7 @@ export const OP_SPECS = [
|
|
|
169
289
|
errors.push(`Related slug '${r}' does not exist`);
|
|
170
290
|
}
|
|
171
291
|
}
|
|
172
|
-
return { slug: op.slug, errors };
|
|
292
|
+
return { slug: op.slug, errors, primaryCode };
|
|
173
293
|
},
|
|
174
294
|
},
|
|
175
295
|
{
|
|
@@ -177,21 +297,13 @@ export const OP_SPECS = [
|
|
|
177
297
|
op: 'create',
|
|
178
298
|
kind: 'entity',
|
|
179
299
|
schema: CreateEntityBranch,
|
|
180
|
-
description: 'Create a new entity (capture / entity / project).',
|
|
300
|
+
description: 'Create a new entity (capture / entity / project). A field whose section the chosen type lacks is rejected, never dropped — `purpose` needs type project.',
|
|
181
301
|
exampleInclude: ['type'],
|
|
182
302
|
check(op, ctx) {
|
|
183
303
|
const errors = [];
|
|
184
304
|
const slug = slugify(op.name);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
ctx.buildEntityFrontmatter(op.name, op.fields);
|
|
189
|
-
}
|
|
190
|
-
catch (err) {
|
|
191
|
-
pushErr(errors, err);
|
|
192
|
-
}
|
|
193
|
-
ctx.simulate(slug, { kind: 'entity', type: op.fields.type });
|
|
194
|
-
return { slug, errors, created: errors.length === 0 };
|
|
305
|
+
const primaryCode = claimNew(ctx, errors, slug, { kind: 'entity', type: op.fields.type }, () => ctx.buildEntityFrontmatter(op.name, op.fields));
|
|
306
|
+
return { slug, errors, created: errors.length === 0, primaryCode };
|
|
195
307
|
},
|
|
196
308
|
},
|
|
197
309
|
{
|
|
@@ -199,21 +311,13 @@ export const OP_SPECS = [
|
|
|
199
311
|
op: 'create',
|
|
200
312
|
kind: 'knowledge',
|
|
201
313
|
schema: CreateKnowledgeBranch,
|
|
202
|
-
description: 'Create a new knowledge note (insight / plan subtype).',
|
|
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.',
|
|
203
315
|
exampleInclude: ['subtype', 'synthesis'],
|
|
204
316
|
check(op, ctx) {
|
|
205
317
|
const errors = [];
|
|
206
318
|
const slug = slugify(op.name);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
ctx.buildKnowledgeFrontmatter(op.name, (op.fields || {}));
|
|
211
|
-
}
|
|
212
|
-
catch (err) {
|
|
213
|
-
pushErr(errors, err);
|
|
214
|
-
}
|
|
215
|
-
ctx.simulate(slug, { kind: 'knowledge', type: 'knowledge' });
|
|
216
|
-
return { slug, errors, created: errors.length === 0 };
|
|
319
|
+
const primaryCode = claimNew(ctx, errors, slug, { kind: 'knowledge', type: 'knowledge', subtype: op.fields?.subtype }, () => ctx.buildKnowledgeFrontmatter(op.name, (op.fields || {})));
|
|
320
|
+
return { slug, errors, created: errors.length === 0, primaryCode };
|
|
217
321
|
},
|
|
218
322
|
},
|
|
219
323
|
{
|
|
@@ -221,12 +325,12 @@ export const OP_SPECS = [
|
|
|
221
325
|
op: 'create',
|
|
222
326
|
kind: 'session',
|
|
223
327
|
schema: CreateSessionBranch,
|
|
224
|
-
description: 'Create a new session note linked to an entity.',
|
|
328
|
+
description: 'Create a new session note linked to an entity. `insight` is an alias for `synthesis` (ignored when `synthesis` is also given).',
|
|
225
329
|
exampleInclude: ['synthesis'],
|
|
226
330
|
check(op, ctx) {
|
|
227
331
|
const slug = slugify(op.name);
|
|
228
332
|
if (ctx.existsAnywhere(slug) || ctx.sessionExists(slug)) {
|
|
229
|
-
return { slug, errors: [], warning:
|
|
333
|
+
return { slug, errors: [], warning: duplicateSessionWarning(slug) };
|
|
230
334
|
}
|
|
231
335
|
ctx.simulateSession(slug);
|
|
232
336
|
return { slug, errors: [], created: true };
|
|
@@ -241,10 +345,8 @@ export const OP_SPECS = [
|
|
|
241
345
|
check(op, ctx) {
|
|
242
346
|
const errors = [];
|
|
243
347
|
const slug = slugify(op.name);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
ctx.simulate(slug, { kind: 'entity', type: 'capture' });
|
|
247
|
-
return { slug, errors, created: errors.length === 0 };
|
|
348
|
+
const primaryCode = claimNew(ctx, errors, slug, { kind: 'entity', type: 'capture' });
|
|
349
|
+
return { slug, errors, created: errors.length === 0, primaryCode };
|
|
248
350
|
},
|
|
249
351
|
},
|
|
250
352
|
{
|
|
@@ -255,11 +357,14 @@ export const OP_SPECS = [
|
|
|
255
357
|
exampleInclude: [],
|
|
256
358
|
check(op, ctx) {
|
|
257
359
|
const errors = [];
|
|
258
|
-
|
|
259
|
-
|
|
360
|
+
let primaryCode;
|
|
361
|
+
if (!ctx.resolve(op.slug)) {
|
|
362
|
+
errors.push(notFoundError(op.slug));
|
|
363
|
+
primaryCode = 'NOT_FOUND';
|
|
364
|
+
}
|
|
260
365
|
if (!ctx.existsAnywhere(op.target))
|
|
261
366
|
errors.push(`Target '${op.target}' does not exist`);
|
|
262
|
-
return { slug: op.slug, errors };
|
|
367
|
+
return { slug: op.slug, errors, primaryCode };
|
|
263
368
|
},
|
|
264
369
|
},
|
|
265
370
|
{
|
|
@@ -270,9 +375,12 @@ export const OP_SPECS = [
|
|
|
270
375
|
exampleInclude: [],
|
|
271
376
|
check(op, ctx) {
|
|
272
377
|
const errors = [];
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
378
|
+
let primaryCode;
|
|
379
|
+
if (!ctx.resolve(op.slug)) {
|
|
380
|
+
errors.push(notFoundError(op.slug));
|
|
381
|
+
primaryCode = 'NOT_FOUND';
|
|
382
|
+
}
|
|
383
|
+
return { slug: op.slug, errors, primaryCode };
|
|
276
384
|
},
|
|
277
385
|
},
|
|
278
386
|
{
|
|
@@ -283,9 +391,12 @@ export const OP_SPECS = [
|
|
|
283
391
|
exampleInclude: [],
|
|
284
392
|
check(op, ctx) {
|
|
285
393
|
const errors = [];
|
|
394
|
+
let primaryCode;
|
|
286
395
|
const cur = ctx.resolve(op.slug);
|
|
287
|
-
if (!cur)
|
|
288
|
-
errors.push(
|
|
396
|
+
if (!cur) {
|
|
397
|
+
errors.push(notFoundError(op.slug));
|
|
398
|
+
primaryCode = 'NOT_FOUND';
|
|
399
|
+
}
|
|
289
400
|
else {
|
|
290
401
|
const currentType = cur.kind === 'knowledge' ? 'knowledge' : cur.type;
|
|
291
402
|
try {
|
|
@@ -301,10 +412,11 @@ export const OP_SPECS = [
|
|
|
301
412
|
pushErr(errors, err);
|
|
302
413
|
}
|
|
303
414
|
}
|
|
304
|
-
return { slug: op.slug, errors };
|
|
415
|
+
return { slug: op.slug, errors, primaryCode };
|
|
305
416
|
},
|
|
306
417
|
},
|
|
307
418
|
];
|
|
419
|
+
export const OP_SPECS = OP_SPEC_ROWS;
|
|
308
420
|
/** Find the spec for a (shape-valid) operation. */
|
|
309
421
|
export function specFor(op) {
|
|
310
422
|
const spec = OP_SPECS.find(s => s.op === op.op && (s.kind === undefined || s.kind === op.kind));
|
|
@@ -316,6 +428,18 @@ export function specFor(op) {
|
|
|
316
428
|
export function validOpNames() {
|
|
317
429
|
return [...new Set(OP_SPECS.map(s => s.op))];
|
|
318
430
|
}
|
|
431
|
+
/** Ops whose payload addresses an existing file by `slug` — derived from the
|
|
432
|
+
* Zod shapes, so a new slug-bearing op picks up slug-level rules (the
|
|
433
|
+
* NOT_FOUND precedence on shape failure in validateBatchCore) without a
|
|
434
|
+
* hand-kept list. */
|
|
435
|
+
export function slugBearingOpNames() {
|
|
436
|
+
return [...new Set(OP_SPECS
|
|
437
|
+
.filter(s => {
|
|
438
|
+
const shape = s.schema.shape;
|
|
439
|
+
return shape !== undefined && 'slug' in shape;
|
|
440
|
+
})
|
|
441
|
+
.map(s => s.op))];
|
|
442
|
+
}
|
|
319
443
|
/** Create kinds for error text — derived from the table. */
|
|
320
444
|
export function validCreateKinds() {
|
|
321
445
|
return OP_SPECS.filter(s => s.op === 'create' && s.kind).map(s => s.kind);
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export interface ParsedArgs {
|
|
|
10
10
|
positionals: string[];
|
|
11
11
|
}
|
|
12
12
|
export declare function parseArgs(args: string[], spec: FlagSpec, usage: string): ParsedArgs;
|
|
13
|
-
/**
|
|
13
|
+
/** The one comma-list parser — every list flag on every command goes through
|
|
14
|
+
* it. Trims each segment and drops the empty ones, so `"ai, ml"` is two
|
|
15
|
+
* domains (not `ai` and ` ml`, which no query matches) and `"eng,"` is one
|
|
16
|
+
* (not one plus an empty string persisted to frontmatter and the index).
|
|
17
|
+
* undefined stays undefined; an all-empty list is an empty array (a clear). */
|
|
14
18
|
export declare function splitList(value: string | boolean | undefined): string[] | undefined;
|
|
15
19
|
export declare function str(value: string | boolean | undefined): string | undefined;
|
package/dist/cli/args.js
CHANGED
|
@@ -38,11 +38,15 @@ export function parseArgs(args, spec, usage) {
|
|
|
38
38
|
}
|
|
39
39
|
return { flags, positionals };
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
41
|
+
/** The one comma-list parser — every list flag on every command goes through
|
|
42
|
+
* it. Trims each segment and drops the empty ones, so `"ai, ml"` is two
|
|
43
|
+
* domains (not `ai` and ` ml`, which no query matches) and `"eng,"` is one
|
|
44
|
+
* (not one plus an empty string persisted to frontmatter and the index).
|
|
45
|
+
* undefined stays undefined; an all-empty list is an empty array (a clear). */
|
|
42
46
|
export function splitList(value) {
|
|
43
47
|
if (typeof value !== 'string')
|
|
44
48
|
return undefined;
|
|
45
|
-
return value.split(',').map(s => s.trim());
|
|
49
|
+
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
46
50
|
}
|
|
47
51
|
export function str(value) {
|
|
48
52
|
return typeof value === 'string' ? value : undefined;
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
import { type FlagSpec } from './args.js';
|
|
1
2
|
import type { Hafez, BatchOperation } from '../types.js';
|
|
2
3
|
export interface CommandOpts {
|
|
3
4
|
json?: boolean;
|
|
5
|
+
/** Resolved vault path — for handlers that read the vault directly
|
|
6
|
+
* (export) rather than through the Hafez instance. */
|
|
7
|
+
vaultPath?: string;
|
|
4
8
|
}
|
|
5
9
|
export declare function cmdRead(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
10
|
+
/** Every flag the query parser accepts, for the branch-support matrix. */
|
|
11
|
+
export declare const QUERY_FLAG_NAMES: string[];
|
|
12
|
+
export type QueryBranch = 'entity' | 'knowledge' | 'all' | 'related-to';
|
|
13
|
+
/** Which flags each kind branch actually applies. A branch that cannot honour
|
|
14
|
+
* a flag rejects it (below) instead of dropping it: `--kind knowledge --filter
|
|
15
|
+
* stale` used to return every knowledge note with exit 0, and `--confidence`
|
|
16
|
+
* never reached an entity query at all. */
|
|
17
|
+
export declare const QUERY_FLAG_SUPPORT: Record<QueryBranch, readonly string[]>;
|
|
6
18
|
export declare function cmdQuery(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
7
19
|
export declare function cmdSearch(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
20
|
+
/** Exported so tests can assert the flag × kind matrix is exhaustive. */
|
|
21
|
+
export declare const CREATE_FLAGS: FlagSpec;
|
|
8
22
|
export declare function cmdCreate(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
23
|
+
/** Exported so tests can assert the table covers the update schema. */
|
|
24
|
+
export declare const UPDATE_FLAGS: FlagSpec;
|
|
25
|
+
/** Flags the generic loop skips because they are not a field of the update
|
|
26
|
+
* shape — each is hand-handled in cmdUpdate. */
|
|
27
|
+
export declare const UPDATE_SPECIAL_FLAGS: Set<string>;
|
|
28
|
+
/** Update-shape fields with no flag of their own, and why. The exhaustiveness
|
|
29
|
+
* test consults this, so a gap here is a stated decision rather than the
|
|
30
|
+
* silent hole `add_actions` was. */
|
|
31
|
+
export declare const UPDATE_FIELDS_WITHOUT_FLAGS: Record<string, string>;
|
|
9
32
|
export declare function cmdUpdate(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
10
33
|
export declare function cmdLink(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
11
34
|
export declare function cmdUnlink(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
@@ -19,3 +42,4 @@ export declare function cmdChangelog(os: Hafez, args: string[], opts?: CommandOp
|
|
|
19
42
|
export declare function parseBatchInput(json: string): BatchOperation[];
|
|
20
43
|
export declare function cmdBatch(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
21
44
|
export declare function cmdDigest(os: Hafez, args: string[], _opts?: CommandOpts): Promise<string>;
|
|
45
|
+
export declare function cmdExport(_os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|