hafez 2.2.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 +48 -1
- package/dist/apply.d.ts +12 -0
- package/dist/apply.js +82 -17
- package/dist/batch-ops.d.ts +31 -27
- package/dist/batch-ops.js +80 -15
- package/dist/cli/block-hook.d.ts +6 -0
- package/dist/cli/block-hook.js +3 -3
- package/dist/cli/commands.d.ts +2 -0
- package/dist/cli/commands.js +98 -46
- package/dist/cli/format.d.ts +4 -1
- package/dist/cli/format.js +19 -0
- package/dist/cli/index.js +11 -2
- package/dist/cli/upgrade.js +1 -1
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.js +9 -0
- package/dist/dates.d.ts +3 -0
- package/dist/dates.js +9 -0
- package/dist/db.d.ts +34 -4
- package/dist/db.js +163 -21
- 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 +47 -5
- 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 +37 -9
- package/dist/internal.d.ts +21 -0
- package/dist/internal.js +9 -0
- package/dist/read-json.d.ts +26 -0
- package/dist/read-json.js +53 -0
- package/dist/stale.d.ts +2 -2
- package/dist/stale.js +4 -9
- package/dist/surfaces/items.d.ts +44 -0
- package/dist/surfaces/items.js +68 -9
- package/dist/surfaces/surfaces.js +3 -1
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +6 -1
- package/dist/types.d.ts +178 -8
- package/dist/vault.d.ts +9 -2
- package/dist/vault.js +10 -0
- package/dist/workstream.d.ts +17 -0
- package/dist/workstream.js +78 -0
- package/package.json +9 -3
- package/skill/hafez/SKILL.md +12 -6
package/dist/cli/block-hook.d.ts
CHANGED
|
@@ -7,3 +7,9 @@ export interface BlockHookCtx extends ConfigHome {
|
|
|
7
7
|
}
|
|
8
8
|
/** The render, or null for any failure: the caller prints nothing. */
|
|
9
9
|
export declare function blockHook(ctx: BlockHookCtx): Promise<string | null>;
|
|
10
|
+
/** The working directory, its git root, and in a linked worktree the main
|
|
11
|
+
* repository's root. */
|
|
12
|
+
export declare function foldersOf(cwd: string): string[];
|
|
13
|
+
/** Every remote's URL, normalised. */
|
|
14
|
+
export declare function remoteUrls(cwd: string): string[];
|
|
15
|
+
export declare function resourceMatches(resource: string, folders: string[], remotes: string[]): boolean;
|
package/dist/cli/block-hook.js
CHANGED
|
@@ -58,7 +58,7 @@ function git(cwd, ...args) {
|
|
|
58
58
|
}
|
|
59
59
|
/** The working directory, its git root, and in a linked worktree the main
|
|
60
60
|
* repository's root. */
|
|
61
|
-
function foldersOf(cwd) {
|
|
61
|
+
export function foldersOf(cwd) {
|
|
62
62
|
const folders = [cwd];
|
|
63
63
|
const out = git(cwd, 'rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir');
|
|
64
64
|
const [top, common] = out?.split('\n') ?? [];
|
|
@@ -84,7 +84,7 @@ function canonical(path) {
|
|
|
84
84
|
return existsSync(abs) ? realpathSync.native(abs) : abs;
|
|
85
85
|
}
|
|
86
86
|
/** Every remote's URL, normalised. */
|
|
87
|
-
function remoteUrls(cwd) {
|
|
87
|
+
export function remoteUrls(cwd) {
|
|
88
88
|
const out = git(cwd, 'config', '--get-regexp', '^remote\\..*\\.url$');
|
|
89
89
|
return out ? out.split('\n').map(line => normaliseUrl(line.slice(line.indexOf(' ') + 1))) : [];
|
|
90
90
|
}
|
|
@@ -98,7 +98,7 @@ function normaliseUrl(url) {
|
|
|
98
98
|
.replace(/\/+$/, '')
|
|
99
99
|
.replace(/\.git$/, '');
|
|
100
100
|
}
|
|
101
|
-
function resourceMatches(resource, folders, remotes) {
|
|
101
|
+
export function resourceMatches(resource, folders, remotes) {
|
|
102
102
|
if (isAbsolute(resource))
|
|
103
103
|
return folders.includes(canonical(resource));
|
|
104
104
|
return remotes.includes(normaliseUrl(resource));
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare function cmdCapture(os: Hafez, args: string[], opts?: CommandOpts
|
|
|
36
36
|
export declare function cmdPromote(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
37
37
|
export declare function cmdDelete(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
38
38
|
export declare function cmdValidate(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
39
|
+
export declare function cmdHealth(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
40
|
+
export declare function cmdGraph(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
|
39
41
|
export declare function cmdIndexRebuild(os: Hafez): Promise<string>;
|
|
40
42
|
export declare function cmdSync(os: Hafez, args: string[]): Promise<string>;
|
|
41
43
|
export declare function cmdStats(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -7,13 +7,14 @@ import { BatchOperationSchema, normalizeAliases, opErrorLine, batchInvalidHeader
|
|
|
7
7
|
import { formatBatchError } from './batch-errors.js';
|
|
8
8
|
import { parseDigestInput, digest } from '../digest.js';
|
|
9
9
|
import { detectAgent } from '../harness.js';
|
|
10
|
-
import { getActionItems, parseSessionLogHeading } from '../document.js';
|
|
11
10
|
import { resemblances } from '../near-match.js';
|
|
12
|
-
import { actionAge,
|
|
11
|
+
import { actionAge, staleHandles, stalenessDaysOf } from '../stale.js';
|
|
13
12
|
import { slugify } from '../vault.js';
|
|
14
|
-
import {
|
|
13
|
+
import { GRAPH_LAYERS } from '../contracts.js';
|
|
14
|
+
import { ITEMS } from '../surfaces/items.js';
|
|
15
|
+
import { footerFor, pointersFor, readJsonOf } from '../read-json.js';
|
|
15
16
|
import { placeIn } from '../surfaces/surfaces.js';
|
|
16
|
-
import { formatEntityHeader, formatKnowledgeHeader, formatSessionHeader, formatQueryTable, formatKnowledgeTable, formatSessionTable, formatSearchResults, formatValidation, formatStats, formatChangelog } from './format.js';
|
|
17
|
+
import { formatEntityHeader, formatKnowledgeHeader, formatSessionHeader, formatQueryTable, formatKnowledgeTable, formatSessionTable, formatSearchResults, formatValidation, formatHealth, formatStats, formatChangelog } from './format.js';
|
|
17
18
|
function jsonOut(data) {
|
|
18
19
|
return JSON.stringify(data, null, 2);
|
|
19
20
|
}
|
|
@@ -25,14 +26,7 @@ export async function cmdRead(os, args, opts = {}) {
|
|
|
25
26
|
if (!slug)
|
|
26
27
|
throw new UsageError(READ_USAGE);
|
|
27
28
|
const depth = (str(flags.depth) ?? 'summary');
|
|
28
|
-
const result = await os.read(slug, depth).catch(err => {
|
|
29
|
-
if (!(err instanceof HafezError) || err.code !== 'NOT_FOUND')
|
|
30
|
-
throw err;
|
|
31
|
-
const closest = err.details ?? [];
|
|
32
|
-
throw opts.json
|
|
33
|
-
? new RenderedError('NOT_FOUND', jsonOut({ slug, closest }), 'stdout')
|
|
34
|
-
: new RenderedError('NOT_FOUND', ITEMS.slugMissRecovery.render({ slug, closest }), 'stderr');
|
|
35
|
-
});
|
|
29
|
+
const result = await os.read(slug, depth).catch(err => { throw readMiss(err, slug, opts); });
|
|
36
30
|
if (flags.archive) {
|
|
37
31
|
// The archive is an entity's: reached from it on purpose (#34).
|
|
38
32
|
if (result.kind !== 'entity')
|
|
@@ -42,26 +36,23 @@ export async function cmdRead(os, args, opts = {}) {
|
|
|
42
36
|
return jsonOut({ slug, archive });
|
|
43
37
|
return archive ?? `no archive for ${slug}`;
|
|
44
38
|
}
|
|
39
|
+
if (opts.json)
|
|
40
|
+
return jsonOut(await readJsonOf(os, slug, depth, result));
|
|
45
41
|
// A session note is read, never written: no Footer (#35).
|
|
46
|
-
if (result.kind === 'session')
|
|
47
|
-
if (opts.json)
|
|
48
|
-
return jsonOut(result);
|
|
42
|
+
if (result.kind === 'session')
|
|
49
43
|
return formatSessionHeader(result.frontmatter, result.body);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// A frontmatter-depth read carries no body, so it has no actions to list.
|
|
56
|
-
if (result.kind !== 'entity' || depth === 'frontmatter')
|
|
57
|
-
return jsonOut({ ...result, ...pointers, writes });
|
|
58
|
-
return jsonOut({ ...result, actions: agedActions(result.body, result.frontmatter), ...pointers, writes });
|
|
59
|
-
}
|
|
44
|
+
// The Footer and the pointers are the same at every depth: they read the
|
|
45
|
+
// whole file, not the body this depth shows.
|
|
46
|
+
const whole = depth === 'full' ? result : await os.read(slug, 'full');
|
|
47
|
+
const footer = footerFor(slug, whole);
|
|
48
|
+
const pointers = whole.kind === 'entity' ? await pointersFor(os, slug, whole.body) : null;
|
|
60
49
|
const fm = result.frontmatter;
|
|
61
50
|
const days = stalenessDaysOf(fm);
|
|
62
51
|
const staleMark = (added) => ITEMS.staleMarker.render(actionAge(added, days));
|
|
63
52
|
const text = result.kind === 'entity'
|
|
64
53
|
? formatEntityHeader(fm, ITEMS.handles.render(result.body, staleMark), await parentLine(os, fm.parent), [
|
|
54
|
+
ITEMS.childrenBlock.render({ slug, children: pointers.children }),
|
|
55
|
+
...pointers.workstreams.map(w => ITEMS.workstreamLine.render(w)),
|
|
65
56
|
ITEMS.sessionsLine.render({ slug, ...pointers.sessions }),
|
|
66
57
|
ITEMS.archivePointer.render({ slug, entries: pointers.archive_entries }),
|
|
67
58
|
].filter(Boolean))
|
|
@@ -69,15 +60,15 @@ export async function cmdRead(os, args, opts = {}) {
|
|
|
69
60
|
// Trailing newlines only: the Footer brings its own blank line.
|
|
70
61
|
return `${text.replace(/\n+$/, '')}\n${ITEMS.writeLine.render(footer)}`;
|
|
71
62
|
}
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
63
|
+
/** A `read` miss as the CLI renders it: the closest slugs on stderr (`--json`:
|
|
64
|
+
* `{slug, closest}` on stdout), exit 1. Any other error passes through. */
|
|
65
|
+
function readMiss(err, slug, opts) {
|
|
66
|
+
if (!(err instanceof HafezError) || err.code !== 'NOT_FOUND')
|
|
67
|
+
return err;
|
|
68
|
+
const closest = err.details ?? [];
|
|
69
|
+
return opts.json
|
|
70
|
+
? new RenderedError('NOT_FOUND', jsonOut({ slug, closest }), 'stdout')
|
|
71
|
+
: new RenderedError('NOT_FOUND', ITEMS.slugMissRecovery.render({ slug, closest }), 'stderr');
|
|
81
72
|
}
|
|
82
73
|
/** The `parent:` line's value: the slug with the parent's description. The
|
|
83
74
|
* one thing `read` takes from another file; a parent that cannot be read
|
|
@@ -102,17 +93,6 @@ async function staleAfterWrite(os, slug) {
|
|
|
102
93
|
return [];
|
|
103
94
|
}
|
|
104
95
|
}
|
|
105
|
-
/** The Footer for a file `read` returned at `depth`. The Footer is the same
|
|
106
|
-
* at every depth, so its Handle comes from the whole file, not from the body
|
|
107
|
-
* this depth shows. A session note has none: it is never written to. */
|
|
108
|
-
async function footerFor(os, slug, result, depth) {
|
|
109
|
-
return { slug, agent: detectAgent(), ...(result.kind === 'entity'
|
|
110
|
-
? {
|
|
111
|
-
target: { kind: 'entity', type: result.frontmatter.type },
|
|
112
|
-
handle: getActionItems(depth === 'full' ? result.body : (await os.read(slug, 'full')).body).find(a => a.handle)?.handle,
|
|
113
|
-
}
|
|
114
|
-
: { target: { kind: 'knowledge', subtype: result.frontmatter.subtype } }) };
|
|
115
|
-
}
|
|
116
96
|
const QUERY_USAGE = `Usage: hafez query [--kind entity|knowledge|session|all] [--filter ...] [--type t] [--parent slug] [--related-to slug] [--domain d] [--confidence c] [--subtype s] [--tag t] [--since d] [--before d] [--created-since d] [--created-before d] [--sort-by f] [--sort-order asc|desc] [--limit n] [--offset n]
|
|
117
97
|
|
|
118
98
|
--filter active|paused|done matches entity status; knowledge and session
|
|
@@ -520,7 +500,7 @@ async function updateFailure(os, slug, err, opts) {
|
|
|
520
500
|
if (file.kind === 'session') {
|
|
521
501
|
return new RenderedError('VALIDATION_FAILED', `'${slug}' is a session note: written once and read, never updated. What matters from it belongs on the entity it touched.`, 'stderr');
|
|
522
502
|
}
|
|
523
|
-
retry = { footer:
|
|
503
|
+
retry = { footer: footerFor(slug, file) };
|
|
524
504
|
}
|
|
525
505
|
catch (readErr) {
|
|
526
506
|
if (!(readErr instanceof HafezError) || readErr.code !== 'NOT_FOUND')
|
|
@@ -600,6 +580,78 @@ export async function cmdValidate(os, args, opts = {}) {
|
|
|
600
580
|
return jsonOut(report);
|
|
601
581
|
return formatValidation(report);
|
|
602
582
|
}
|
|
583
|
+
export async function cmdHealth(os, args, opts = {}) {
|
|
584
|
+
helpIfAsked(args, 'Usage: hafez health (what placement would change; applies nothing)');
|
|
585
|
+
const report = await os.health();
|
|
586
|
+
if (opts.json)
|
|
587
|
+
return jsonOut(report);
|
|
588
|
+
return formatHealth(report);
|
|
589
|
+
}
|
|
590
|
+
const GRAPH_USAGE = "Usage: hafez graph [slug] [--port N] [--no-open] (serve the vault's graph on 127.0.0.1 and open it in the browser; a slug opens it focused on that note)\n hafez graph --json [--layers tag,domain,session] (the vault's structure as {nodes, edges})";
|
|
591
|
+
const GRAPH_FLAGS = { layers: 'string', port: 'string', 'no-open': 'boolean' };
|
|
592
|
+
/** The local-graph depth `hafez graph <slug>` opens at (#256). */
|
|
593
|
+
const GRAPH_FOCUS_DEPTH = 2;
|
|
594
|
+
export async function cmdGraph(os, args, opts = {}) {
|
|
595
|
+
const { flags, positionals } = parseArgs(args, GRAPH_FLAGS, GRAPH_USAGE);
|
|
596
|
+
if (positionals.length > 1)
|
|
597
|
+
throw new UsageError(GRAPH_USAGE);
|
|
598
|
+
const slug = positionals[0];
|
|
599
|
+
if (!opts.json) {
|
|
600
|
+
if (flags.layers !== undefined)
|
|
601
|
+
throw new UsageError('--layers requires --json', GRAPH_USAGE);
|
|
602
|
+
// A miss fails as `read` does, before any server starts.
|
|
603
|
+
if (slug !== undefined)
|
|
604
|
+
await os.read(slug, 'frontmatter').catch(err => { throw readMiss(err, slug, opts); });
|
|
605
|
+
return serveGraph(opts.vaultPath, flags, slug);
|
|
606
|
+
}
|
|
607
|
+
if (slug !== undefined)
|
|
608
|
+
throw new UsageError('a slug opens the page focused on it; --json prints the whole Graph', GRAPH_USAGE);
|
|
609
|
+
if (flags.port !== undefined || flags['no-open'])
|
|
610
|
+
throw new UsageError('--port and --no-open serve the page; --json prints the Graph and exits', GRAPH_USAGE);
|
|
611
|
+
const layers = splitList(flags.layers);
|
|
612
|
+
const unknown = layers?.find(l => !GRAPH_LAYERS.includes(l));
|
|
613
|
+
if (unknown !== undefined)
|
|
614
|
+
throw new UsageError(`Unknown layer: ${unknown}. Valid layers: ${GRAPH_LAYERS.join(', ')}`, GRAPH_USAGE);
|
|
615
|
+
return jsonOut(await os.graph(layers ? { layers: layers } : undefined));
|
|
616
|
+
}
|
|
617
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
618
|
+
/** `hafez graph` without --json: serve the page until Ctrl-C or SIGTERM, then close the
|
|
619
|
+
* server and the index. The server gets its own instance whose reads trust the
|
|
620
|
+
* index: it syncs the index when its poll sees a commit (#271). */
|
|
621
|
+
async function serveGraph(vaultPath, flags, focus) {
|
|
622
|
+
const portFlag = str(flags.port);
|
|
623
|
+
if (portFlag !== undefined && !(/^\d+$/.test(portFlag) && Number(portFlag) <= 65535)) {
|
|
624
|
+
throw new UsageError(`--port takes a port number (0-65535), got "${portFlag}"`, GRAPH_USAGE);
|
|
625
|
+
}
|
|
626
|
+
const port = portFlag === undefined ? undefined : Number(portFlag);
|
|
627
|
+
const { startGraphServer, DEFAULT_PORT } = await import('../graph/server.js');
|
|
628
|
+
const { createInternalHafez } = await import('../internal.js');
|
|
629
|
+
const os = createInternalHafez({ vaultPath, git: { push: false }, indexSync: 'manual' });
|
|
630
|
+
const server = await startGraphServer({ os, port }).catch(err => {
|
|
631
|
+
os.close();
|
|
632
|
+
if (err.code !== 'EADDRINUSE' || port === undefined)
|
|
633
|
+
throw err;
|
|
634
|
+
throw new UsageError(`port ${port} is in use: pick another with --port, or leave it off for the first free port from ${DEFAULT_PORT}`);
|
|
635
|
+
});
|
|
636
|
+
// The page reads its view from the URL: a focus opens local-graph mode around it.
|
|
637
|
+
const url = focus === undefined ? server.url : `${server.url}/?focus=${encodeURIComponent(focus)}&depth=${GRAPH_FOCUS_DEPTH}`;
|
|
638
|
+
process.stdout.write(`Serving the graph at ${url} (Ctrl-C to stop)\n`);
|
|
639
|
+
if (!flags['no-open'])
|
|
640
|
+
(await import('../graph/open.js')).openBrowser(url);
|
|
641
|
+
// SIGTERM (a supervisor's kill) stops it as Ctrl-C does, so the index is
|
|
642
|
+
// closed either way (#280). A second signal during the close is not caught.
|
|
643
|
+
await new Promise(resolve => {
|
|
644
|
+
const stop = () => {
|
|
645
|
+
for (const signal of STOP_SIGNALS)
|
|
646
|
+
process.off(signal, stop);
|
|
647
|
+
resolve();
|
|
648
|
+
};
|
|
649
|
+
for (const signal of STOP_SIGNALS)
|
|
650
|
+
process.on(signal, stop);
|
|
651
|
+
});
|
|
652
|
+
await server.close();
|
|
653
|
+
return 'Graph server stopped.';
|
|
654
|
+
}
|
|
603
655
|
export async function cmdIndexRebuild(os) {
|
|
604
656
|
await os.rebuildIndex();
|
|
605
657
|
return 'Index rebuilt.';
|
package/dist/cli/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueryResult, KnowledgeQueryResult, SessionQueryResult, ValidationReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
1
|
+
import type { QueryResult, KnowledgeQueryResult, SessionQueryResult, ValidationReport, HealthReport, SearchResult, VaultStats, ChangelogEntry } from '../types.js';
|
|
2
2
|
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
3
3
|
* parent's description); absent, the slug alone. `pointers` are the lines
|
|
4
4
|
* naming what hangs off the entity elsewhere (its sessions, its archive). */
|
|
@@ -15,4 +15,7 @@ export declare function formatSessionTable(results: SessionTableRow[], total?: n
|
|
|
15
15
|
export declare function formatSearchResults(results: SearchResult[]): string;
|
|
16
16
|
export declare function formatStats(stats: VaultStats): string;
|
|
17
17
|
export declare function formatChangelog(entries: ChangelogEntry[]): string;
|
|
18
|
+
/** The Health pass as markdown: one section per check, every section
|
|
19
|
+
* printed, so an empty report still shows what was checked. */
|
|
20
|
+
export declare function formatHealth(report: HealthReport): string;
|
|
18
21
|
export declare function formatValidation(report: ValidationReport): string;
|
package/dist/cli/format.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ITEMS } from '../surfaces/items.js';
|
|
1
2
|
/** `parent` is the rendered value of the `parent:` line (the slug with the
|
|
2
3
|
* parent's description); absent, the slug alone. `pointers` are the lines
|
|
3
4
|
* naming what hangs off the entity elsewhere (its sessions, its archive). */
|
|
@@ -196,6 +197,24 @@ export function formatChangelog(entries) {
|
|
|
196
197
|
const rows = entries.map(e => `| ${e.slug} | ${e.kind} | ${e.operation} | ${e.timestamp.slice(0, 10)} | ${e.commit_message} |`);
|
|
197
198
|
return [header, sep, ...rows].join('\n');
|
|
198
199
|
}
|
|
200
|
+
/** The Health pass as markdown: one section per check, every section
|
|
201
|
+
* printed, so an empty report still shows what was checked. */
|
|
202
|
+
export function formatHealth(report) {
|
|
203
|
+
const out = ['## Health'];
|
|
204
|
+
const section = (title, items) => {
|
|
205
|
+
out.push('', `### ${title} (${items.length})`);
|
|
206
|
+
for (const item of items)
|
|
207
|
+
out.push(`- ${item}`);
|
|
208
|
+
};
|
|
209
|
+
// The workstream Item's own text (ADR 0001), prefixed with the entity.
|
|
210
|
+
section('Workstreams', report.workstreams.map(w => `${w.entity} ${ITEMS.workstreamLine.render(w)}`));
|
|
211
|
+
section('Recurring names', report.recurring_names.map(r => `${r.entity}: \`${r.term}\` in ${r.units} units across ${r.days.length} days (${r.days[0]} .. ${r.days[r.days.length - 1]})`));
|
|
212
|
+
section('Leakage', report.leakage.map(l => `${l.parent} → ${l.child} (${l.relation}): ${l.hits} of ${l.entries} entries (${Math.round(100 * l.hits / l.entries)}%)`));
|
|
213
|
+
section('One-way edges', report.one_way_edges.map(e => `${e.from} → ${e.to}: \`hafez link ${e.to} ${e.from} --relation related\``));
|
|
214
|
+
section('Unlinked mentions', report.unlinked_mentions.map(e => `${e.from} mentions [[${e.to}]]: \`hafez link ${e.from} ${e.to} --relation related\``));
|
|
215
|
+
section('Restated decisions', report.restated_decisions.map(r => `${r.day} ${r.entity} → ${r.note} (${Math.round(100 * r.share)}%): ${r.summary}`));
|
|
216
|
+
return out.join('\n');
|
|
217
|
+
}
|
|
199
218
|
export function formatValidation(report) {
|
|
200
219
|
const issues = report.broken_slugs.length + report.orphaned_knowledge.length +
|
|
201
220
|
report.missing_fields.length;
|
package/dist/cli/index.js
CHANGED
|
@@ -41,6 +41,8 @@ export const COMMANDS = {
|
|
|
41
41
|
delete: { phase: 'vault', mutating: true, run: lazy(m => m.cmdDelete) },
|
|
42
42
|
batch: { phase: 'vault', mutating: true, run: lazy(m => m.cmdBatch) },
|
|
43
43
|
validate: { phase: 'vault', run: lazy(m => m.cmdValidate) },
|
|
44
|
+
health: { phase: 'vault', run: lazy(m => m.cmdHealth) },
|
|
45
|
+
graph: { phase: 'vault', run: lazy(m => m.cmdGraph) },
|
|
44
46
|
index: {
|
|
45
47
|
phase: 'vault',
|
|
46
48
|
run: async (os, args) => {
|
|
@@ -109,6 +111,12 @@ Commands:
|
|
|
109
111
|
changelog --since <when> Git-derived change history (e.g. --since 7.days.ago)
|
|
110
112
|
schema [op] [--examples] Machine-readable JSON schema for batch operations
|
|
111
113
|
validate Check vault integrity
|
|
114
|
+
health The Health pass: what placement would change (applies nothing)
|
|
115
|
+
graph --json The vault's structure: entities and knowledge, parent/related/wiki-link edges
|
|
116
|
+
--layers tag,domain,session Add tag and domain nodes, and sessions
|
|
117
|
+
graph [slug] [--port N] [--no-open]
|
|
118
|
+
Serve the graph on 127.0.0.1 (first free port from 4747) and open the browser;
|
|
119
|
+
a slug opens it focused on that note, local graph at depth 2
|
|
112
120
|
index rebuild Rebuild the SQLite index
|
|
113
121
|
init --register <path> Register an existing vault
|
|
114
122
|
init Show current vault resolution
|
|
@@ -285,8 +293,9 @@ export async function main(argv) {
|
|
|
285
293
|
// First touch of the core — and so of the native SQLite binding. A
|
|
286
294
|
// failure to load it lands in the catch below and is recognised by
|
|
287
295
|
// isBindingsFailure, exactly as it was when this import was static.
|
|
288
|
-
const {
|
|
289
|
-
|
|
296
|
+
const { createInternalHafez } = await import('../internal.js');
|
|
297
|
+
// One index scan per process (#269): the first read syncs, later reads trust it.
|
|
298
|
+
const os = createInternalHafez({ vaultPath, git: { push: false }, indexSync: 'once' });
|
|
290
299
|
try {
|
|
291
300
|
const result = await spec.run(os, args, { json, vaultPath });
|
|
292
301
|
// Auto-sync after mutating commands
|
package/dist/cli/upgrade.js
CHANGED
|
@@ -34,7 +34,7 @@ const PLAIN_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
|
34
34
|
const REGISTRY_TIMEOUT_MS = 3000;
|
|
35
35
|
const NPM_PROBE_TIMEOUT_MS = 3000;
|
|
36
36
|
const DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org/hafez/latest';
|
|
37
|
-
const DEV_CHECKOUT_WORK = 'git pull && npm run build && npm run install:local';
|
|
37
|
+
const DEV_CHECKOUT_WORK = 'git pull && npm ci && npm run build && npm run install:local';
|
|
38
38
|
const NPM_PROBE_NOTE = 'The local npm version could not be determined, so every install flag is included above. ' +
|
|
39
39
|
'npm majors that predate a flag print `npm warn Unknown cli config` and proceed — that warning is harmless.';
|
|
40
40
|
const ETARGET_NOTE = 'If the install fails with ETARGET, your configured registry (a mirror or proxy) does not have the pinned version — ' +
|
package/dist/contracts.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ export declare const ENTITY_STATUSES: readonly ["active", "paused", "done"];
|
|
|
4
4
|
export declare const SESSION_LOG_TYPES: readonly ["progress", "decision", "blocker", "research"];
|
|
5
5
|
export declare const CONFIDENCE_LEVELS: readonly ["observation", "pattern", "principle"];
|
|
6
6
|
export declare const VALID_SUBTYPES: readonly ["insight", "plan"];
|
|
7
|
+
/** The Graph's layers whose nodes are synthetic, one per distinct value
|
|
8
|
+
* (`<layer>:<value>`); each adds the edge relation of the same name. */
|
|
9
|
+
export declare const SYNTHETIC_LAYERS: readonly ["tag", "domain"];
|
|
10
|
+
/** The Graph's optional layers (#253): tag and domain nodes, Session nodes. */
|
|
11
|
+
export declare const GRAPH_LAYERS: readonly ["tag", "domain", "session"];
|
|
12
|
+
/** The Graph's edges between notes; the synthetic layers add their own. */
|
|
13
|
+
export declare const GRAPH_NOTE_RELATIONS: readonly ["parent", "related", "mention"];
|
|
14
|
+
/** What a write did to a note, as history tells it (changesSince, #258). */
|
|
15
|
+
export declare const CHANGE_OPS: readonly ["create", "update", "delete"];
|
|
7
16
|
export interface TypeContract {
|
|
8
17
|
type: string;
|
|
9
18
|
description: string;
|
package/dist/contracts.js
CHANGED
|
@@ -10,6 +10,15 @@ export const CONFIDENCE_LEVELS = ['observation', 'pattern', 'principle'];
|
|
|
10
10
|
// 'insight' is the subtype (frontmatter classification), 'Synthesis' is the section heading.
|
|
11
11
|
// The rename was insight→synthesis for the section/field only; the subtype stays 'insight'.
|
|
12
12
|
export const VALID_SUBTYPES = ['insight', 'plan'];
|
|
13
|
+
/** The Graph's layers whose nodes are synthetic, one per distinct value
|
|
14
|
+
* (`<layer>:<value>`); each adds the edge relation of the same name. */
|
|
15
|
+
export const SYNTHETIC_LAYERS = ['tag', 'domain'];
|
|
16
|
+
/** The Graph's optional layers (#253): tag and domain nodes, Session nodes. */
|
|
17
|
+
export const GRAPH_LAYERS = [...SYNTHETIC_LAYERS, 'session'];
|
|
18
|
+
/** The Graph's edges between notes; the synthetic layers add their own. */
|
|
19
|
+
export const GRAPH_NOTE_RELATIONS = ['parent', 'related', 'mention'];
|
|
20
|
+
/** What a write did to a note, as history tells it (changesSince, #258). */
|
|
21
|
+
export const CHANGE_OPS = ['create', 'update', 'delete'];
|
|
13
22
|
/** The item frontmatter contract, shared by all three item types — they
|
|
14
23
|
* differ in sections and lifecycle, never in which fields are mandatory.
|
|
15
24
|
* schema.validateEntityFrontmatter enforces exactly this list (parity test in
|
package/dist/dates.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** The local calendar day as YYYY-MM-DD. The single source of "today". */
|
|
2
2
|
export declare function localDay(d?: Date): string;
|
|
3
|
+
/** Whole days from one YYYY-MM-DD day to another, as calendar days: a
|
|
4
|
+
* difference that ignores DST. */
|
|
5
|
+
export declare function daysBetween(from: string, to: string): number;
|
|
3
6
|
/**
|
|
4
7
|
* Bare YYYY-MM-DD → local midnight, for git's approxidate. A date-only
|
|
5
8
|
* `--since` is otherwise filled with the *current* time of day ("since right
|
package/dist/dates.js
CHANGED
|
@@ -15,6 +15,15 @@ function pad(n) {
|
|
|
15
15
|
export function localDay(d = new Date()) {
|
|
16
16
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
17
17
|
}
|
|
18
|
+
/** Whole days from one YYYY-MM-DD day to another, as calendar days: a
|
|
19
|
+
* difference that ignores DST. */
|
|
20
|
+
export function daysBetween(from, to) {
|
|
21
|
+
return dayNumber(to) - dayNumber(from);
|
|
22
|
+
}
|
|
23
|
+
function dayNumber(day) {
|
|
24
|
+
const [y, m, d] = day.split('-').map(Number);
|
|
25
|
+
return Date.UTC(y, m - 1, d) / 86_400_000;
|
|
26
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Bare YYYY-MM-DD → local midnight, for git's approxidate. A date-only
|
|
20
29
|
* `--since` is otherwise filled with the *current* time of day ("since right
|
package/dist/db.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type VaultKind } from './vault.js';
|
|
2
2
|
import { type VaultIndexRow } from './knowledge-index.js';
|
|
3
|
-
import type { SearchResult, SearchKind, VaultStats, QueryResult, KnowledgeQueryResult, SessionQueryResult, UnifiedResult, EntityQueryOpts, KnowledgeQueryOpts, SessionQueryOpts, UnifiedQueryOpts, SortField } from './types.js';
|
|
4
|
-
export declare const SCHEMA_VERSION = "
|
|
3
|
+
import type { SearchResult, SearchKind, VaultStats, QueryResult, KnowledgeQueryResult, SessionQueryResult, UnifiedResult, EntityQueryOpts, KnowledgeQueryOpts, SessionQueryOpts, UnifiedQueryOpts, SortField, Graph, GraphOpts } from './types.js';
|
|
4
|
+
export declare const SCHEMA_VERSION = "13";
|
|
5
5
|
interface IndexItem {
|
|
6
6
|
slug: string;
|
|
7
7
|
kind: VaultKind;
|
|
@@ -28,6 +28,8 @@ interface IndexItem {
|
|
|
28
28
|
last_session_type: string | null;
|
|
29
29
|
last_session_summary: string | null;
|
|
30
30
|
session_date: string | null;
|
|
31
|
+
/** An entity's open actions' added days as a JSON array, null for an undated line. */
|
|
32
|
+
action_added: string | null;
|
|
31
33
|
}
|
|
32
34
|
export interface IndexQueryResult {
|
|
33
35
|
items: IndexItem[];
|
|
@@ -35,7 +37,11 @@ export interface IndexQueryResult {
|
|
|
35
37
|
}
|
|
36
38
|
export interface HafezIndex {
|
|
37
39
|
rebuild(): void;
|
|
40
|
+
/** Catch the index up with the files, whatever the mode: a stat per note
|
|
41
|
+
* (read-only: reopen a database another process rewrote). */
|
|
38
42
|
syncIfStale(): void;
|
|
43
|
+
/** What a read runs first, by the index's `sync` mode (`IndexOpts`). */
|
|
44
|
+
syncForRead(): void;
|
|
39
45
|
upsertFromFile(slug: string, kind: VaultKind): void;
|
|
40
46
|
removeItem(slug: string): void;
|
|
41
47
|
queryItems(opts: QueryOpts): IndexQueryResult;
|
|
@@ -43,6 +49,8 @@ export interface HafezIndex {
|
|
|
43
49
|
getStats(): VaultStats;
|
|
44
50
|
/** Every indexed document, in the shape index.md is rendered from. */
|
|
45
51
|
indexEntries(): VaultIndexRow[];
|
|
52
|
+
/** The rows the Graph is shaped from: every item, every link. */
|
|
53
|
+
graphRows(): GraphRows;
|
|
46
54
|
close(): void;
|
|
47
55
|
queryEntities(opts?: EntityQueryOpts): {
|
|
48
56
|
items: QueryResult[];
|
|
@@ -68,6 +76,18 @@ export interface HafezIndex {
|
|
|
68
76
|
items: UnifiedResult[];
|
|
69
77
|
total: number;
|
|
70
78
|
};
|
|
79
|
+
queryGraph(opts?: GraphOpts): Graph;
|
|
80
|
+
}
|
|
81
|
+
export interface GraphRows {
|
|
82
|
+
items: Array<Pick<IndexItem, 'slug' | 'kind' | 'name' | 'type' | 'status' | 'subtype' | 'confidence' | 'last_touched' | 'description' | 'next_action_count' | 'staleness_days' | 'action_added'> & {
|
|
83
|
+
/** Entity-stale by the index's stale rule, 1 or 0; null for a note with no last-touched. */
|
|
84
|
+
stale: number | null;
|
|
85
|
+
}>;
|
|
86
|
+
links: Array<{
|
|
87
|
+
source: string;
|
|
88
|
+
target: string;
|
|
89
|
+
relation: string;
|
|
90
|
+
}>;
|
|
71
91
|
}
|
|
72
92
|
/** `kind` unset is the working set, entity + knowledge; `all` is literal. */
|
|
73
93
|
export interface QueryOpts {
|
|
@@ -93,8 +113,18 @@ export interface QueryOpts {
|
|
|
93
113
|
}
|
|
94
114
|
/** @internal */
|
|
95
115
|
export declare function __setAfterScanHook(fn: (() => void) | null): void;
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
/** `sync`: what `syncForRead`, run first by every read-surface query, does.
|
|
117
|
+
* 'read' (default): runs `syncIfStale`. 'once': runs it until a sync has
|
|
118
|
+
* caught the index up, then trusts the index (#269). 'manual': trusts the
|
|
119
|
+
* index, and the caller syncs it. */
|
|
120
|
+
export type IndexSync = 'read' | 'once' | 'manual';
|
|
121
|
+
export interface IndexOpts {
|
|
122
|
+
sync?: IndexSync;
|
|
123
|
+
}
|
|
124
|
+
export declare function createIndex(vaultPath: string, opts?: IndexOpts & {
|
|
125
|
+
readonly?: false;
|
|
126
|
+
}): HafezIndex;
|
|
127
|
+
export declare function createIndex(vaultPath: string, opts: IndexOpts & {
|
|
98
128
|
readonly: true;
|
|
99
129
|
}): HafezIndex | null;
|
|
100
130
|
/** Opts a branch cannot map into SQL. Declared per branch and rejected on
|