hafez 2.1.0 → 2.3.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 +72 -5
- package/dist/apply.d.ts +14 -1
- package/dist/apply.js +94 -32
- package/dist/batch-ops.d.ts +31 -27
- package/dist/batch-ops.js +83 -16
- package/dist/cli/block-hook.d.ts +6 -0
- package/dist/cli/block-hook.js +27 -12
- package/dist/cli/commands.d.ts +3 -1
- package/dist/cli/commands.js +183 -73
- package/dist/cli/format.d.ts +11 -3
- package/dist/cli/format.js +61 -4
- package/dist/cli/index.js +40 -19
- package/dist/cli/install.d.ts +9 -0
- package/dist/cli/install.js +36 -15
- package/dist/cli/onboard.js +30 -9
- package/dist/cli/upgrade.js +5 -4
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.js +9 -0
- package/dist/dates.d.ts +7 -0
- package/dist/dates.js +21 -1
- package/dist/db.d.ts +50 -10
- package/dist/db.js +251 -65
- package/dist/digest.js +14 -7
- package/dist/document.d.ts +2 -0
- package/dist/document.js +2 -2
- package/dist/git.d.ts +13 -2
- package/dist/git.js +121 -14
- package/dist/graph/activity.d.ts +5 -0
- package/dist/graph/activity.js +70 -0
- package/dist/graph/client.js +326 -0
- package/dist/graph/copy-fonts.d.ts +1 -0
- package/dist/graph/copy-fonts.js +13 -0
- package/dist/graph/fields.d.ts +8 -0
- package/dist/graph/fields.js +12 -0
- package/dist/graph/fonts/geist-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono.woff2 +0 -0
- package/dist/graph/fonts/geist.woff2 +0 -0
- package/dist/graph/fonts/outfit-OFL.txt +93 -0
- package/dist/graph/fonts/outfit.woff2 +0 -0
- package/dist/graph/fonts.d.ts +14 -0
- package/dist/graph/fonts.js +17 -0
- package/dist/graph/live.d.ts +16 -0
- package/dist/graph/live.js +99 -0
- package/dist/graph/open.d.ts +1 -0
- package/dist/graph/open.js +20 -0
- package/dist/graph/server.d.ts +17 -0
- package/dist/graph/server.js +189 -0
- package/dist/health.d.ts +6 -0
- package/dist/health.js +310 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +66 -18
- package/dist/internal.d.ts +21 -0
- package/dist/internal.js +9 -0
- package/dist/knowledge-index.d.ts +1 -1
- package/dist/read-json.d.ts +26 -0
- package/dist/read-json.js +53 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +11 -0
- package/dist/stale.d.ts +2 -2
- package/dist/stale.js +4 -9
- package/dist/surfaces/items.d.ts +100 -1
- package/dist/surfaces/items.js +148 -10
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +34 -17
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +6 -1
- package/dist/types.d.ts +239 -25
- package/dist/validate.js +44 -56
- package/dist/vault.d.ts +29 -9
- package/dist/vault.js +39 -8
- package/dist/workstream.d.ts +17 -0
- package/dist/workstream.js +78 -0
- package/package.json +10 -4
- package/skill/hafez/SKILL.md +16 -7
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ENTITY_TYPES, ENTITY_STATUSES, CONFIDENCE_LEVELS, SESSION_LOG_TYPES, VALID_SUBTYPES, PLAN_STATUSES } from './contracts.js';
|
|
1
|
+
import type { ENTITY_TYPES, ENTITY_STATUSES, CONFIDENCE_LEVELS, SESSION_LOG_TYPES, VALID_SUBTYPES, PLAN_STATUSES, GRAPH_LAYERS, SYNTHETIC_LAYERS, GRAPH_NOTE_RELATIONS, CHANGE_OPS } from './contracts.js';
|
|
2
2
|
export type EntityType = typeof ENTITY_TYPES[number];
|
|
3
3
|
export type EntityStatus = typeof ENTITY_STATUSES[number];
|
|
4
4
|
export type ConfidenceLevel = typeof CONFIDENCE_LEVELS[number];
|
|
@@ -9,12 +9,22 @@ export type LinkRelation = 'parent' | 'related';
|
|
|
9
9
|
export type KnowledgeSubtype = typeof VALID_SUBTYPES[number];
|
|
10
10
|
/** Plans are the one knowledge subtype with a lifecycle (see PLAN_STATUSES). */
|
|
11
11
|
export type PlanStatus = typeof PLAN_STATUSES[number];
|
|
12
|
+
/** The kinds of note the Vault holds, one directory each (the Layout,
|
|
13
|
+
* vault.ts, re-exports it). Declared here so this file imports no module
|
|
14
|
+
* with runtime dependencies: the graph page's bundle reads its types. */
|
|
15
|
+
export type VaultKind = 'entity' | 'knowledge' | 'session';
|
|
12
16
|
/** Sort fields, and the two an entity query cannot honour — reinforcement
|
|
13
17
|
* lives on knowledge notes only. Asking for one on the entity branch is a
|
|
14
18
|
* validation error, not a silent fall back to recency, so the entity opts
|
|
15
19
|
* type declares only what that branch can actually sort by. */
|
|
16
|
-
export type SortField = 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count';
|
|
17
|
-
export type EntitySortField = Exclude<SortField, 'last_reinforced' | 'reinforcement_count'>;
|
|
20
|
+
export type SortField = 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count' | 'session_date';
|
|
21
|
+
export type EntitySortField = Exclude<SortField, 'last_reinforced' | 'reinforcement_count' | 'session_date'>;
|
|
22
|
+
export type KnowledgeSortField = Exclude<SortField, 'session_date'>;
|
|
23
|
+
/** A session note carries only its dates and its name. */
|
|
24
|
+
export type SessionSortField = 'session_date' | 'created' | 'name';
|
|
25
|
+
/** What `search` reaches. Unset is the working set, entity + knowledge;
|
|
26
|
+
* `all` is literal, sessions included. */
|
|
27
|
+
export type SearchKind = 'entity' | 'knowledge' | 'session' | 'all';
|
|
18
28
|
export interface EntityQueryOpts {
|
|
19
29
|
filter?: QueryFilter;
|
|
20
30
|
type?: EntityType;
|
|
@@ -39,14 +49,28 @@ export interface KnowledgeQueryOpts {
|
|
|
39
49
|
createdSince?: string;
|
|
40
50
|
createdBefore?: string;
|
|
41
51
|
tags?: string[];
|
|
42
|
-
sort_by?:
|
|
52
|
+
sort_by?: KnowledgeSortField;
|
|
53
|
+
sort_order?: 'asc' | 'desc';
|
|
54
|
+
limit?: number;
|
|
55
|
+
offset?: number;
|
|
56
|
+
}
|
|
57
|
+
/** Sessions carry no status, domain or tags: the time window (`since`/`before`
|
|
58
|
+
* against `session-date`), sort and paging are all a session query takes. */
|
|
59
|
+
export interface SessionQueryOpts {
|
|
60
|
+
since?: string;
|
|
61
|
+
before?: string;
|
|
62
|
+
createdSince?: string;
|
|
63
|
+
createdBefore?: string;
|
|
64
|
+
sort_by?: SessionSortField;
|
|
43
65
|
sort_order?: 'asc' | 'desc';
|
|
44
66
|
limit?: number;
|
|
45
67
|
offset?: number;
|
|
46
68
|
}
|
|
47
69
|
/** `kind: 'all'` (or unset): reaches both sides, so it takes opts from both
|
|
48
70
|
* and can honour every sort field. Status filters narrow the entity side only
|
|
49
|
-
* — knowledge notes carry no status and are never excluded by one.
|
|
71
|
+
* — knowledge notes carry no status and are never excluded by one. Unset
|
|
72
|
+
* is the working set, entity + knowledge; `all` is literal and reaches the
|
|
73
|
+
* sessions too. */
|
|
50
74
|
export interface AllQueryOpts extends Omit<EntityQueryOpts, 'sort_by'> {
|
|
51
75
|
kind?: 'all';
|
|
52
76
|
confidence?: ConfidenceLevel;
|
|
@@ -63,6 +87,8 @@ export type UnifiedQueryOpts = AllQueryOpts | (EntityQueryOpts & {
|
|
|
63
87
|
kind: 'entity';
|
|
64
88
|
}) | (KnowledgeQueryOpts & {
|
|
65
89
|
kind: 'knowledge';
|
|
90
|
+
}) | (SessionQueryOpts & {
|
|
91
|
+
kind: 'session';
|
|
66
92
|
});
|
|
67
93
|
export interface EntityFrontmatter {
|
|
68
94
|
name: string;
|
|
@@ -114,11 +140,8 @@ export type ParsedVaultFile = {
|
|
|
114
140
|
frontmatter: SessionFrontmatter;
|
|
115
141
|
body: string;
|
|
116
142
|
};
|
|
117
|
-
/** What read() returns:
|
|
118
|
-
|
|
119
|
-
export type ParsedFile = Extract<ParsedVaultFile, {
|
|
120
|
-
kind: 'entity' | 'knowledge';
|
|
121
|
-
}>;
|
|
143
|
+
/** What read() returns: any vault file, so consumers narrow on `kind`. */
|
|
144
|
+
export type ParsedFile = ParsedVaultFile;
|
|
122
145
|
export interface SessionLogEntry {
|
|
123
146
|
type: SessionLogType;
|
|
124
147
|
summary: string;
|
|
@@ -189,6 +212,14 @@ export interface CreateKnowledgeFields {
|
|
|
189
212
|
tags?: string[];
|
|
190
213
|
'session-date'?: string;
|
|
191
214
|
}
|
|
215
|
+
export interface CreateSessionFields {
|
|
216
|
+
synthesis?: string;
|
|
217
|
+
insight?: string;
|
|
218
|
+
related?: string[];
|
|
219
|
+
'session-date'?: string;
|
|
220
|
+
decisions?: string[];
|
|
221
|
+
entities_touched?: string[];
|
|
222
|
+
}
|
|
192
223
|
export interface QueryResult {
|
|
193
224
|
slug: string;
|
|
194
225
|
name: string;
|
|
@@ -221,10 +252,19 @@ export interface KnowledgeQueryResult {
|
|
|
221
252
|
last_reinforced: string | null;
|
|
222
253
|
last_touched: string | null;
|
|
223
254
|
}
|
|
255
|
+
export interface SessionQueryResult {
|
|
256
|
+
slug: string;
|
|
257
|
+
name: string;
|
|
258
|
+
session_date: string;
|
|
259
|
+
created: string;
|
|
260
|
+
related: string[];
|
|
261
|
+
}
|
|
224
262
|
export type UnifiedResult = (QueryResult & {
|
|
225
263
|
kind: 'entity';
|
|
226
264
|
}) | (KnowledgeQueryResult & {
|
|
227
265
|
kind: 'knowledge';
|
|
266
|
+
}) | (SessionQueryResult & {
|
|
267
|
+
kind: 'session';
|
|
228
268
|
});
|
|
229
269
|
export interface ValidationIssue {
|
|
230
270
|
slug: string;
|
|
@@ -237,10 +277,100 @@ export interface ValidationReport {
|
|
|
237
277
|
missing_fields: ValidationIssue[];
|
|
238
278
|
total_entities: number;
|
|
239
279
|
total_knowledge: number;
|
|
280
|
+
total_sessions: number;
|
|
281
|
+
}
|
|
282
|
+
/** Check (i): an unsplit prefix family on an entity, from the family module. */
|
|
283
|
+
export interface HealthWorkstream {
|
|
284
|
+
entity: string;
|
|
285
|
+
prefix: string;
|
|
286
|
+
open_actions: number;
|
|
287
|
+
}
|
|
288
|
+
/** Check (ii): a name recurring in `units` units of one entity across `days`. */
|
|
289
|
+
export interface HealthRecurringName {
|
|
290
|
+
entity: string;
|
|
291
|
+
term: string;
|
|
292
|
+
units: number;
|
|
293
|
+
days: string[];
|
|
294
|
+
}
|
|
295
|
+
/** Check (iv): `hits` of the parent's log entries dated on or after the
|
|
296
|
+
* child's `created` (`entries` of them) name the child. */
|
|
297
|
+
export interface HealthLeak {
|
|
298
|
+
parent: string;
|
|
299
|
+
child: string;
|
|
300
|
+
relation: 'parent' | 'related';
|
|
301
|
+
hits: number;
|
|
302
|
+
entries: number;
|
|
303
|
+
}
|
|
304
|
+
/** Check (iii): `from`'s `related` names `to`, a note of the same kind,
|
|
305
|
+
* whose `related` does not name `from`. Check (v): `from`'s body mentions
|
|
306
|
+
* `[[to]]`, a note of the same kind, with no `related` edge either way.
|
|
307
|
+
* Neither lists a parent and its child: parent subsumes related. */
|
|
308
|
+
export interface HealthEdge {
|
|
309
|
+
from: string;
|
|
310
|
+
to: string;
|
|
311
|
+
}
|
|
312
|
+
/** Check (vi): the knowledge note `note`, created on `day` and related to
|
|
313
|
+
* `entity`, shares `share` (0..1) of the summary tokens of a `decision`
|
|
314
|
+
* entry of that day on the entity. */
|
|
315
|
+
export interface HealthRestatedDecision {
|
|
316
|
+
day: string;
|
|
317
|
+
entity: string;
|
|
318
|
+
note: string;
|
|
319
|
+
share: number;
|
|
320
|
+
summary: string;
|
|
321
|
+
}
|
|
322
|
+
export interface HealthReport {
|
|
323
|
+
workstreams: HealthWorkstream[];
|
|
324
|
+
recurring_names: HealthRecurringName[];
|
|
325
|
+
leakage: HealthLeak[];
|
|
326
|
+
one_way_edges: HealthEdge[];
|
|
327
|
+
unlinked_mentions: HealthEdge[];
|
|
328
|
+
restated_decisions: HealthRestatedDecision[];
|
|
329
|
+
}
|
|
330
|
+
export type GraphLayer = typeof GRAPH_LAYERS[number];
|
|
331
|
+
export type SyntheticLayer = typeof SYNTHETIC_LAYERS[number];
|
|
332
|
+
export interface GraphNode {
|
|
333
|
+
/** The slug; `tag:<t>` / `domain:<d>` for a layer node (a slug never holds `:`). */
|
|
334
|
+
id: string;
|
|
335
|
+
kind: VaultKind | SyntheticLayer;
|
|
336
|
+
name: string;
|
|
337
|
+
type?: EntityType;
|
|
338
|
+
status?: EntityStatus;
|
|
339
|
+
subtype?: KnowledgeSubtype;
|
|
340
|
+
confidence?: ConfidenceLevel;
|
|
341
|
+
last_touched?: string;
|
|
342
|
+
description?: string;
|
|
343
|
+
/** An active or paused Entity's open Next Actions. */
|
|
344
|
+
open_actions?: number;
|
|
345
|
+
/** An active Entity untouched past its staleness-days: the index's stale rule. */
|
|
346
|
+
stale?: boolean;
|
|
347
|
+
/** An active or paused Entity's open actions the Stale rule marks, undated ones included. */
|
|
348
|
+
stale_actions?: number;
|
|
349
|
+
/** Every Entity's effective staleness-days: its own, else 14. */
|
|
350
|
+
staleness_days?: number;
|
|
351
|
+
/** An Entity or Knowledge note with no Parent, Related or Wiki-link edge either way. */
|
|
352
|
+
unlinked?: true;
|
|
353
|
+
/** On an Unlinked note: the closest other linked note by name and description,
|
|
354
|
+
* the same kind when one matches; absent when none does. */
|
|
355
|
+
best?: string;
|
|
356
|
+
}
|
|
357
|
+
export interface GraphEdge {
|
|
358
|
+
source: string;
|
|
359
|
+
target: string;
|
|
360
|
+
relation: typeof GRAPH_NOTE_RELATIONS[number] | SyntheticLayer;
|
|
361
|
+
}
|
|
362
|
+
export interface Graph {
|
|
363
|
+
nodes: GraphNode[];
|
|
364
|
+
edges: GraphEdge[];
|
|
365
|
+
}
|
|
366
|
+
/** Any other opt is rejected, never dropped. */
|
|
367
|
+
export interface GraphOpts {
|
|
368
|
+
/** Optional layers added to the default graph; they combine. */
|
|
369
|
+
layers?: GraphLayer[];
|
|
240
370
|
}
|
|
241
371
|
export interface SearchResult {
|
|
242
372
|
slug: string;
|
|
243
|
-
kind: 'entity' | 'knowledge';
|
|
373
|
+
kind: 'entity' | 'knowledge' | 'session';
|
|
244
374
|
name: string;
|
|
245
375
|
snippet: string;
|
|
246
376
|
type?: EntityType;
|
|
@@ -275,10 +405,52 @@ export interface VaultStats {
|
|
|
275
405
|
}
|
|
276
406
|
export interface ChangelogEntry {
|
|
277
407
|
slug: string;
|
|
278
|
-
kind: 'entity' | 'knowledge';
|
|
408
|
+
kind: 'entity' | 'knowledge' | 'session';
|
|
279
409
|
operation: 'created' | 'updated' | 'deleted';
|
|
280
410
|
timestamp: string;
|
|
281
411
|
commit_message: string;
|
|
412
|
+
/** The commit's hash: what tells the entries of one commit from another's. */
|
|
413
|
+
commit: string;
|
|
414
|
+
}
|
|
415
|
+
/** What a write did to a note, as history tells it. */
|
|
416
|
+
export type ChangeOp = typeof CHANGE_OPS[number];
|
|
417
|
+
/** A file history changed between two points, as the Journal names it:
|
|
418
|
+
* vault-relative and '/'-separated, any file (Layout decides which are notes). */
|
|
419
|
+
export interface FileChange {
|
|
420
|
+
path: string;
|
|
421
|
+
op: ChangeOp;
|
|
422
|
+
}
|
|
423
|
+
/** A note created, updated or deleted since a cursor (`changesSince`). */
|
|
424
|
+
export interface NoteChange {
|
|
425
|
+
slug: string;
|
|
426
|
+
kind: VaultKind;
|
|
427
|
+
op: ChangeOp;
|
|
428
|
+
}
|
|
429
|
+
/** What the graph server's `/events` pushes, one per poll that found
|
|
430
|
+
* changes (src/graph/live.ts); the page reads it too. */
|
|
431
|
+
export interface CommitEvent {
|
|
432
|
+
type: 'commit';
|
|
433
|
+
changes: NoteChange[];
|
|
434
|
+
}
|
|
435
|
+
/** One row of the graph server's `/activity` (#278): a note's run of
|
|
436
|
+
* consecutive writes, newest first. `op`, `kind` and `timestamp` are the
|
|
437
|
+
* newest write's; `name` is the slug once the note is gone; `agent` is the
|
|
438
|
+
* agent of the Entity's newest Session Log entry dated the write's local day,
|
|
439
|
+
* absent where the Vault records none. */
|
|
440
|
+
export interface ActivityRow {
|
|
441
|
+
slug: string;
|
|
442
|
+
kind: VaultKind;
|
|
443
|
+
op: ChangeOp;
|
|
444
|
+
timestamp: string;
|
|
445
|
+
name: string;
|
|
446
|
+
count: number;
|
|
447
|
+
agent?: string;
|
|
448
|
+
}
|
|
449
|
+
export interface ChangesSince {
|
|
450
|
+
/** The history point now: pass it back to get what changed after it.
|
|
451
|
+
* Empty when the Vault has no history yet. */
|
|
452
|
+
cursor: string;
|
|
453
|
+
changes: NoteChange[];
|
|
282
454
|
}
|
|
283
455
|
export interface BatchResult {
|
|
284
456
|
op: string;
|
|
@@ -313,7 +485,7 @@ export type BatchOperation = {
|
|
|
313
485
|
op: 'create';
|
|
314
486
|
kind: 'session';
|
|
315
487
|
name: string;
|
|
316
|
-
fields?:
|
|
488
|
+
fields?: CreateSessionFields;
|
|
317
489
|
} | {
|
|
318
490
|
op: 'capture';
|
|
319
491
|
name: string;
|
|
@@ -355,7 +527,7 @@ export interface BatchValidationReport {
|
|
|
355
527
|
valid: boolean;
|
|
356
528
|
operations: BatchOpValidation[];
|
|
357
529
|
}
|
|
358
|
-
export type HafezErrorCode = 'NOT_FOUND' | 'SLUG_EXISTS' | 'VALIDATION_FAILED' | 'GIT_PUSH_FAILED' | 'GIT_COMMIT_FAILED' | 'VAULT_LOCKED';
|
|
530
|
+
export type HafezErrorCode = 'NOT_FOUND' | 'SLUG_EXISTS' | 'VALIDATION_FAILED' | 'GIT_PUSH_FAILED' | 'GIT_COMMIT_FAILED' | 'GIT_READ_FAILED' | 'VAULT_LOCKED';
|
|
359
531
|
export declare class HafezError extends Error {
|
|
360
532
|
code: HafezErrorCode;
|
|
361
533
|
details?: string[] | undefined;
|
|
@@ -366,14 +538,17 @@ export interface GitConfig {
|
|
|
366
538
|
}
|
|
367
539
|
/**
|
|
368
540
|
* The persistence seam (see CONTEXT.md: Journal) — everything core needs
|
|
369
|
-
* from the history layer behind vault
|
|
370
|
-
*
|
|
541
|
+
* from the history layer: the commit behind every vault mutation, sync, and
|
|
542
|
+
* the history reads (changelog, file times, the changes since a cursor). Git
|
|
543
|
+
* satisfies it in production (src/git.ts); tests may inject an in-memory
|
|
544
|
+
* adapter.
|
|
371
545
|
*/
|
|
372
546
|
export interface Journal {
|
|
373
|
-
/** Persist one logical change: stage written+deleted paths (vault-relative
|
|
374
|
-
* and commit under message. Must clean up
|
|
375
|
-
* staged files) before rethrowing on a
|
|
376
|
-
* failure surfaces as GIT_PUSH_FAILED
|
|
547
|
+
/** Persist one logical change: stage written+deleted paths (vault-relative,
|
|
548
|
+
* '/'-separated on every platform) and commit under message. Must clean up
|
|
549
|
+
* its own partial state (e.g. staged files) before rethrowing on a
|
|
550
|
+
* commit-stage failure; a push-stage failure surfaces as GIT_PUSH_FAILED
|
|
551
|
+
* with the commit kept. */
|
|
377
552
|
commit(written: string[], deleted: string[], message: string): Promise<void>;
|
|
378
553
|
/** Bidirectional sync with the remote, if any. */
|
|
379
554
|
sync(): Promise<{
|
|
@@ -381,10 +556,23 @@ export interface Journal {
|
|
|
381
556
|
pushed: boolean;
|
|
382
557
|
remote: boolean;
|
|
383
558
|
}>;
|
|
384
|
-
/** History-derived changelog for
|
|
385
|
-
|
|
386
|
-
|
|
559
|
+
/** History-derived changelog for the indexed dirs since a date ('' = all
|
|
560
|
+
* history), newest first; `limit` reads only the newest that many commits.
|
|
561
|
+
* Empty when the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
|
|
562
|
+
changelog(since: string, limit?: number): Promise<ChangelogEntry[]>;
|
|
563
|
+
/** Newest history timestamp per vault file (modified = any change, added = creation).
|
|
564
|
+
* Empty when the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
|
|
387
565
|
fileTimes(mode: 'modified' | 'added'): Promise<Map<string, string>>;
|
|
566
|
+
/** The history point now (`cursor`, '' before the first commit) and the
|
|
567
|
+
* indexed-dir files whose content differs between `from` and it, each with
|
|
568
|
+
* its net status across the whole span (a rename is a delete and a
|
|
569
|
+
* create). `from` unset: the cursor only, no files; `from` '': every file
|
|
570
|
+
* in history now is a create. An unknown `from` or any other git failure
|
|
571
|
+
* throws `GIT_READ_FAILED`. */
|
|
572
|
+
changesSince(from?: string): Promise<{
|
|
573
|
+
cursor: string;
|
|
574
|
+
files: FileChange[];
|
|
575
|
+
}>;
|
|
388
576
|
}
|
|
389
577
|
export interface HafezConfig {
|
|
390
578
|
vaultPath: string;
|
|
@@ -432,9 +620,35 @@ export interface Hafez {
|
|
|
432
620
|
items: KnowledgeQueryResult[];
|
|
433
621
|
total: number;
|
|
434
622
|
}>;
|
|
623
|
+
query_sessions(opts?: SessionQueryOpts): Promise<{
|
|
624
|
+
items: SessionQueryResult[];
|
|
625
|
+
total: number;
|
|
626
|
+
}>;
|
|
627
|
+
/** An entity's archived log entries (`entities/archive/<slug>-log.md`), as
|
|
628
|
+
* written; null when it has none. Read from disk, never indexed: the
|
|
629
|
+
* archive is reached from its entity on purpose, never found by search. */
|
|
630
|
+
archive(slug: string): Promise<string | null>;
|
|
435
631
|
validate(): Promise<ValidationReport>;
|
|
436
|
-
|
|
632
|
+
/** The Health pass: what placement would change, from the files on disk
|
|
633
|
+
* (never the index); applies nothing. */
|
|
634
|
+
health(): Promise<HealthReport>;
|
|
635
|
+
search(query: string, kind?: SearchKind): Promise<SearchResult[]>;
|
|
636
|
+
/** The Vault's structure: Entity and Knowledge nodes, Parent, Related (one
|
|
637
|
+
* per unordered pair) and Wiki-link edges among them; `layers` adds tag,
|
|
638
|
+
* domain and Session nodes with their edges. */
|
|
639
|
+
graph(opts?: GraphOpts): Promise<Graph>;
|
|
437
640
|
rebuildIndex(): Promise<void>;
|
|
438
641
|
stats(): Promise<VaultStats>;
|
|
439
|
-
|
|
642
|
+
/** The changes since a date ('' = all history), newest first; `limit`
|
|
643
|
+
* reads only the newest that many commits, a positive whole number
|
|
644
|
+
* (anything else is VALIDATION_FAILED). */
|
|
645
|
+
changelog(since: string, limit?: number): Promise<ChangelogEntry[]>;
|
|
646
|
+
/** The notes created, updated or deleted since `cursor`, from history, and
|
|
647
|
+
* the cursor to pass next time. No cursor: the current cursor and no
|
|
648
|
+
* changes, so a follower starts from now. */
|
|
649
|
+
changesSince(cursor?: string): Promise<ChangesSince>;
|
|
650
|
+
/** Release the SQLite index handle. Idempotent; the next call that needs
|
|
651
|
+
* the index reopens it. Call before deleting the vault: Windows cannot
|
|
652
|
+
* remove a directory holding an open file. */
|
|
653
|
+
close(): void;
|
|
440
654
|
}
|
package/dist/validate.js
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
import { join, basename } from 'path';
|
|
2
|
-
import { parseFilePath, scanVaultDir, kindDir,
|
|
3
|
-
import { validateEntityFrontmatter, validateKnowledgeFrontmatter } from './schema.js';
|
|
2
|
+
import { parseFilePath, scanVaultDir, kindDir, findReadableFile } from './vault.js';
|
|
3
|
+
import { validateEntityFrontmatter, validateKnowledgeFrontmatter, validateSessionFrontmatter } from './schema.js';
|
|
4
4
|
import { parentCycle } from './batch-ops.js';
|
|
5
|
+
const FRONTMATTER_CHECKS = {
|
|
6
|
+
entity: validateEntityFrontmatter,
|
|
7
|
+
knowledge: validateKnowledgeFrontmatter,
|
|
8
|
+
session: validateSessionFrontmatter,
|
|
9
|
+
};
|
|
5
10
|
/** The vault integrity scan. Deliberately independent of the SQLite index:
|
|
6
11
|
* this is the one operation that does not trust the index, so it reads the
|
|
7
12
|
* files directly. Re-implementing it as an index query was considered and
|
|
8
13
|
* rejected — an integrity checker that reads the index is checking the
|
|
9
14
|
* index's model of the vault, not the vault. */
|
|
10
15
|
export function validateVault(vaultPath) {
|
|
11
|
-
|
|
16
|
+
// Any readable file satisfies a link: an invalid slug definitionally does
|
|
17
|
+
// not exist, and must report as missing rather than crash.
|
|
18
|
+
const exists = (slug) => { try {
|
|
19
|
+
return findReadableFile(vaultPath, slug) !== null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
} };
|
|
12
24
|
const report = {
|
|
13
25
|
broken_slugs: [],
|
|
14
26
|
orphaned_knowledge: [],
|
|
15
27
|
missing_fields: [],
|
|
16
28
|
total_entities: 0,
|
|
17
29
|
total_knowledge: 0,
|
|
30
|
+
total_sessions: 0,
|
|
18
31
|
};
|
|
19
|
-
const
|
|
32
|
+
const knowledgeRelated = new Map();
|
|
20
33
|
const referencedKnowledge = new Set();
|
|
21
34
|
const parents = new Map();
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
// One scan per kind: parse, the kind's frontmatter check, then every
|
|
36
|
+
// `related` must exist. What a kind adds on top (parent, the orphan
|
|
37
|
+
// bookkeeping) is in `each`.
|
|
38
|
+
function scan(kind, each) {
|
|
39
|
+
const files = scanVaultDir(join(vaultPath, kindDir(kind)));
|
|
27
40
|
for (const filePath of files) {
|
|
28
41
|
const slug = basename(filePath, '.md');
|
|
29
42
|
let fm;
|
|
@@ -34,24 +47,29 @@ export function validateVault(vaultPath) {
|
|
|
34
47
|
report.missing_fields.push({ slug, field: '', issue: 'failed to parse frontmatter' });
|
|
35
48
|
continue;
|
|
36
49
|
}
|
|
37
|
-
const
|
|
38
|
-
for (const e of errors)
|
|
50
|
+
for (const e of FRONTMATTER_CHECKS[kind](fm))
|
|
39
51
|
report.missing_fields.push({ slug, field: '', issue: e });
|
|
40
|
-
if (fm.parent) {
|
|
41
|
-
parents.set(slug, fm.parent);
|
|
42
|
-
if (!exists(fm.parent))
|
|
43
|
-
report.broken_slugs.push({ slug, field: 'parent', issue: `parent '${fm.parent}' does not exist` });
|
|
44
|
-
}
|
|
45
52
|
if (fm.related && Array.isArray(fm.related)) {
|
|
46
53
|
for (const r of fm.related) {
|
|
47
54
|
if (!exists(r))
|
|
48
55
|
report.broken_slugs.push({ slug, field: 'related', issue: `related '${r}' does not exist` });
|
|
49
|
-
// Track referenced knowledge
|
|
50
|
-
referencedKnowledge.add(r);
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
each(slug, fm);
|
|
53
59
|
}
|
|
60
|
+
return files.length;
|
|
54
61
|
}
|
|
62
|
+
report.total_entities = scan('entity', (slug, fm) => {
|
|
63
|
+
if (fm.parent) {
|
|
64
|
+
parents.set(slug, fm.parent);
|
|
65
|
+
if (!exists(fm.parent))
|
|
66
|
+
report.broken_slugs.push({ slug, field: 'parent', issue: `parent '${fm.parent}' does not exist` });
|
|
67
|
+
}
|
|
68
|
+
// Track referenced knowledge
|
|
69
|
+
if (Array.isArray(fm.related))
|
|
70
|
+
for (const r of fm.related)
|
|
71
|
+
referencedKnowledge.add(r);
|
|
72
|
+
});
|
|
55
73
|
// Parent cycles: reported once each, under the first member found
|
|
56
74
|
const inCycle = new Set();
|
|
57
75
|
for (const [slug, parent] of parents) {
|
|
@@ -64,46 +82,16 @@ export function validateVault(vaultPath) {
|
|
|
64
82
|
inCycle.add(s);
|
|
65
83
|
report.broken_slugs.push({ slug, field: 'parent', issue: `parent cycle: ${cycle.join(' → ')}` });
|
|
66
84
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const slug = basename(filePath, '.md');
|
|
74
|
-
knowledgeSlugs.add(slug);
|
|
75
|
-
let fm;
|
|
76
|
-
try {
|
|
77
|
-
fm = parseFilePath(filePath).frontmatter;
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
report.missing_fields.push({ slug, field: '', issue: 'failed to parse frontmatter' });
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const errors = validateKnowledgeFrontmatter(fm);
|
|
84
|
-
for (const e of errors)
|
|
85
|
-
report.missing_fields.push({ slug, field: '', issue: e });
|
|
86
|
-
if (fm.related && Array.isArray(fm.related)) {
|
|
87
|
-
for (const r of fm.related) {
|
|
88
|
-
if (!exists(r))
|
|
89
|
-
report.broken_slugs.push({ slug, field: 'related', issue: `related '${r}' does not exist` });
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
85
|
+
report.total_knowledge = scan('knowledge', (slug, fm) => {
|
|
86
|
+
knowledgeRelated.set(slug, Array.isArray(fm.related) ? fm.related.length : 0);
|
|
87
|
+
});
|
|
88
|
+
// A session with no related is never an orphan — a session that touched
|
|
89
|
+
// nothing is legitimate.
|
|
90
|
+
report.total_sessions = scan('session', () => { });
|
|
94
91
|
// Find orphaned knowledge: no related, and not referenced by any entity
|
|
95
|
-
for (const slug of
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
fm = parseFilePath(join(knDir, `${slug}.md`)).frontmatter;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
continue; // already reported above
|
|
102
|
-
}
|
|
103
|
-
const hasRelated = fm.related && Array.isArray(fm.related) && fm.related.length > 0;
|
|
104
|
-
if (!hasRelated && !referencedKnowledge.has(slug)) {
|
|
92
|
+
for (const [slug, related] of knowledgeRelated) {
|
|
93
|
+
if (related === 0 && !referencedKnowledge.has(slug))
|
|
105
94
|
report.orphaned_knowledge.push(slug);
|
|
106
|
-
}
|
|
107
95
|
}
|
|
108
96
|
return report;
|
|
109
97
|
}
|
package/dist/vault.d.ts
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
import type { ParsedVaultFile } from './types.js';
|
|
1
|
+
import type { ParsedVaultFile, VaultKind } from './types.js';
|
|
2
2
|
export declare function slugify(name: string): string;
|
|
3
|
-
export type VaultKind
|
|
4
|
-
/** Kinds that live in the SQLite read index
|
|
5
|
-
|
|
3
|
+
export type { VaultKind };
|
|
4
|
+
/** Kinds that live in the SQLite read index: every kind. Sessions joined in
|
|
5
|
+
* #35 — before that they were files-and-git only, for no recorded reason. */
|
|
6
|
+
export declare const INDEXED_KINDS: ReadonlyArray<VaultKind>;
|
|
7
|
+
/** The two kinds a mutation can address. A session note is written once and
|
|
8
|
+
* read; `update`, `link`, `promote` never resolve one. */
|
|
9
|
+
export declare const MUTABLE_KINDS: ReadonlyArray<'entity' | 'knowledge'>;
|
|
6
10
|
/** Vault directory for a kind, relative to the vault root. */
|
|
7
11
|
export declare function kindDir(kind: VaultKind): string;
|
|
8
12
|
/** A vault content file: anything a sync/merge must treat semantically. */
|
|
9
13
|
export declare const VAULT_FILE_RE: RegExp;
|
|
10
|
-
/** True for vault-relative paths
|
|
11
|
-
export declare function
|
|
14
|
+
/** True for vault-relative paths of a mutable kind — what index.md shows. */
|
|
15
|
+
export declare function isMutableRelPath(relPath: string): boolean;
|
|
12
16
|
/** History pathspecs covering the indexed dirs (changelog, file times). */
|
|
13
17
|
export declare const INDEXED_DIR_PATHSPECS: ReadonlyArray<string>;
|
|
14
18
|
/** Classify a vault-relative path into an indexed kind + slug; null outside
|
|
15
19
|
* the indexed dirs (sessions, non-vault files). Nested paths keep their
|
|
16
20
|
* historical shape: entities/archive/x-log.md → entity 'archive/x-log'. */
|
|
17
21
|
export declare function classifyIndexedRelPath(relPath: string): {
|
|
18
|
-
kind:
|
|
22
|
+
kind: VaultKind;
|
|
23
|
+
slug: string;
|
|
24
|
+
} | null;
|
|
25
|
+
/** The note a vault-relative ('/'-separated) path is, or null: the files
|
|
26
|
+
* scanVaultDir lists — a non-dot `.md` directly in a kind dir. Archive logs,
|
|
27
|
+
* scaffold files and anything outside the kind dirs are not notes. */
|
|
28
|
+
export declare function noteAtRelPath(relPath: string): {
|
|
29
|
+
kind: VaultKind;
|
|
19
30
|
slug: string;
|
|
20
31
|
} | null;
|
|
21
32
|
/** All .md files directly in dir — non-recursive (archive/ subdirs are
|
|
@@ -25,9 +36,18 @@ export declare function scanVaultDir(dirPath: string): string[];
|
|
|
25
36
|
/** Where an entity's overflow session-log entries are archived. */
|
|
26
37
|
export declare function archiveLogPath(vaultPath: string, slug: string): string;
|
|
27
38
|
export declare function resolveFilePath(vaultPath: string, slug: string, kind: VaultKind): string;
|
|
28
|
-
/** The
|
|
29
|
-
*
|
|
39
|
+
/** The inverse of resolveFilePath / archiveLogPath: a vault file as the
|
|
40
|
+
* Journal names it — vault-relative and '/'-separated on every platform.
|
|
41
|
+
* Everything downstream compares these lists as POSIX paths: stageablePaths
|
|
42
|
+
* (src/git.ts) keeps an absent path only if `git ls-files` echoes it back,
|
|
43
|
+
* always with '/', so a '\' deletion was dropped and never staged; and the
|
|
44
|
+
* index.md gate (isMutableRelPath) never matched a '\' path (#187). */
|
|
45
|
+
export declare function vaultRel(vaultPath: string, filePath: string): string;
|
|
46
|
+
/** The mutable file a slug names, or null — entities are resolved before
|
|
47
|
+
* knowledge, the one resolution order every mutation sees. */
|
|
30
48
|
export declare function findVaultFile(vaultPath: string, slug: string): string | null;
|
|
49
|
+
/** The file `read` resolves: the mutable kinds first, then a session note. */
|
|
50
|
+
export declare function findReadableFile(vaultPath: string, slug: string): string | null;
|
|
31
51
|
/** Does this slug name a vault file? An invalid slug definitionally doesn't
|
|
32
52
|
* exist — reference checks (create parent/related, link targets, validate)
|
|
33
53
|
* must report "does not exist", not crash. validate() in particular exists to
|