hafez 2.0.0 → 2.2.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 +24 -4
- package/dist/apply.d.ts +2 -1
- package/dist/apply.js +61 -40
- package/dist/batch-ops.d.ts +7 -1
- package/dist/batch-ops.js +9 -5
- package/dist/cli/block-hook.js +24 -9
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +108 -50
- package/dist/cli/format.d.ts +8 -3
- package/dist/cli/format.js +42 -4
- package/dist/cli/index.js +32 -20
- package/dist/cli/install.d.ts +22 -1
- package/dist/cli/install.js +78 -21
- package/dist/cli/onboard.js +38 -9
- package/dist/cli/upgrade.d.ts +4 -2
- package/dist/cli/upgrade.js +21 -7
- package/dist/contracts.js +2 -0
- package/dist/dates.d.ts +4 -0
- package/dist/dates.js +12 -1
- package/dist/db.d.ts +18 -8
- package/dist/db.js +96 -52
- package/dist/git.js +74 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -10
- package/dist/knowledge-index.d.ts +1 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +11 -0
- package/dist/surfaces/items.d.ts +56 -1
- package/dist/surfaces/items.js +94 -11
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +31 -16
- package/dist/types.d.ts +67 -19
- package/dist/validate.js +44 -56
- package/dist/vault.d.ts +20 -7
- package/dist/vault.js +29 -8
- package/package.json +2 -2
- package/skill/hafez/SKILL.md +5 -2
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,25 @@
|
|
|
1
1
|
import type { ParsedVaultFile } from './types.js';
|
|
2
2
|
export declare function slugify(name: string): string;
|
|
3
3
|
export type VaultKind = 'entity' | 'knowledge' | 'session';
|
|
4
|
-
/** Kinds that live in the SQLite read index
|
|
5
|
-
|
|
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;
|
|
19
23
|
slug: string;
|
|
20
24
|
} | null;
|
|
21
25
|
/** All .md files directly in dir — non-recursive (archive/ subdirs are
|
|
@@ -25,9 +29,18 @@ export declare function scanVaultDir(dirPath: string): string[];
|
|
|
25
29
|
/** Where an entity's overflow session-log entries are archived. */
|
|
26
30
|
export declare function archiveLogPath(vaultPath: string, slug: string): string;
|
|
27
31
|
export declare function resolveFilePath(vaultPath: string, slug: string, kind: VaultKind): string;
|
|
28
|
-
/** The
|
|
29
|
-
*
|
|
32
|
+
/** The inverse of resolveFilePath / archiveLogPath: a vault file as the
|
|
33
|
+
* Journal names it — vault-relative and '/'-separated on every platform.
|
|
34
|
+
* Everything downstream compares these lists as POSIX paths: stageablePaths
|
|
35
|
+
* (src/git.ts) keeps an absent path only if `git ls-files` echoes it back,
|
|
36
|
+
* always with '/', so a '\' deletion was dropped and never staged; and the
|
|
37
|
+
* index.md gate (isMutableRelPath) never matched a '\' path (#187). */
|
|
38
|
+
export declare function vaultRel(vaultPath: string, filePath: string): string;
|
|
39
|
+
/** The mutable file a slug names, or null — entities are resolved before
|
|
40
|
+
* knowledge, the one resolution order every mutation sees. */
|
|
30
41
|
export declare function findVaultFile(vaultPath: string, slug: string): string | null;
|
|
42
|
+
/** The file `read` resolves: the mutable kinds first, then a session note. */
|
|
43
|
+
export declare function findReadableFile(vaultPath: string, slug: string): string | null;
|
|
31
44
|
/** Does this slug name a vault file? An invalid slug definitionally doesn't
|
|
32
45
|
* exist — reference checks (create parent/related, link targets, validate)
|
|
33
46
|
* must report "does not exist", not crash. validate() in particular exists to
|
package/dist/vault.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import matter from 'gray-matter';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
5
|
+
import { join, relative, sep } from 'path';
|
|
6
6
|
import { HafezError } from './types.js';
|
|
7
7
|
// Prevent gray-matter from auto-coercing YAML date strings into JS Date objects
|
|
8
8
|
const MATTER_OPTIONS = { engines: { yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) } };
|
|
@@ -17,17 +17,21 @@ export function slugify(name) {
|
|
|
17
17
|
.replace(/-{2,}/g, '-');
|
|
18
18
|
}
|
|
19
19
|
const KIND_DIRS = { entity: 'entities', knowledge: 'knowledge', session: 'sessions' };
|
|
20
|
-
/** Kinds that live in the SQLite read index
|
|
21
|
-
|
|
20
|
+
/** Kinds that live in the SQLite read index: every kind. Sessions joined in
|
|
21
|
+
* #35 — before that they were files-and-git only, for no recorded reason. */
|
|
22
|
+
export const INDEXED_KINDS = ['entity', 'knowledge', 'session'];
|
|
23
|
+
/** The two kinds a mutation can address. A session note is written once and
|
|
24
|
+
* read; `update`, `link`, `promote` never resolve one. */
|
|
25
|
+
export const MUTABLE_KINDS = ['entity', 'knowledge'];
|
|
22
26
|
/** Vault directory for a kind, relative to the vault root. */
|
|
23
27
|
export function kindDir(kind) {
|
|
24
28
|
return KIND_DIRS[kind];
|
|
25
29
|
}
|
|
26
30
|
/** A vault content file: anything a sync/merge must treat semantically. */
|
|
27
31
|
export const VAULT_FILE_RE = new RegExp(`^(?:${Object.values(KIND_DIRS).join('|')})/.+\\.md$`);
|
|
28
|
-
/** True for vault-relative paths
|
|
29
|
-
export function
|
|
30
|
-
return
|
|
32
|
+
/** True for vault-relative paths of a mutable kind — what index.md shows. */
|
|
33
|
+
export function isMutableRelPath(relPath) {
|
|
34
|
+
return MUTABLE_KINDS.some(k => relPath.startsWith(`${KIND_DIRS[k]}/`));
|
|
31
35
|
}
|
|
32
36
|
/** History pathspecs covering the indexed dirs (changelog, file times). */
|
|
33
37
|
export const INDEXED_DIR_PATHSPECS = INDEXED_KINDS.map(k => `${KIND_DIRS[k]}/`);
|
|
@@ -69,8 +73,17 @@ export function resolveFilePath(vaultPath, slug, kind) {
|
|
|
69
73
|
}
|
|
70
74
|
return join(vaultPath, KIND_DIRS[kind], `${slug}.md`);
|
|
71
75
|
}
|
|
72
|
-
/** The
|
|
73
|
-
*
|
|
76
|
+
/** The inverse of resolveFilePath / archiveLogPath: a vault file as the
|
|
77
|
+
* Journal names it — vault-relative and '/'-separated on every platform.
|
|
78
|
+
* Everything downstream compares these lists as POSIX paths: stageablePaths
|
|
79
|
+
* (src/git.ts) keeps an absent path only if `git ls-files` echoes it back,
|
|
80
|
+
* always with '/', so a '\' deletion was dropped and never staged; and the
|
|
81
|
+
* index.md gate (isMutableRelPath) never matched a '\' path (#187). */
|
|
82
|
+
export function vaultRel(vaultPath, filePath) {
|
|
83
|
+
return relative(vaultPath, filePath).split(sep).join('/');
|
|
84
|
+
}
|
|
85
|
+
/** The mutable file a slug names, or null — entities are resolved before
|
|
86
|
+
* knowledge, the one resolution order every mutation sees. */
|
|
74
87
|
export function findVaultFile(vaultPath, slug) {
|
|
75
88
|
const entity = resolveFilePath(vaultPath, slug, 'entity');
|
|
76
89
|
if (existsSync(entity))
|
|
@@ -80,6 +93,14 @@ export function findVaultFile(vaultPath, slug) {
|
|
|
80
93
|
return knowledge;
|
|
81
94
|
return null;
|
|
82
95
|
}
|
|
96
|
+
/** The file `read` resolves: the mutable kinds first, then a session note. */
|
|
97
|
+
export function findReadableFile(vaultPath, slug) {
|
|
98
|
+
const mutable = findVaultFile(vaultPath, slug);
|
|
99
|
+
if (mutable)
|
|
100
|
+
return mutable;
|
|
101
|
+
const session = resolveFilePath(vaultPath, slug, 'session');
|
|
102
|
+
return existsSync(session) ? session : null;
|
|
103
|
+
}
|
|
83
104
|
/** Does this slug name a vault file? An invalid slug definitionally doesn't
|
|
84
105
|
* exist — reference checks (create parent/related, link targets, validate)
|
|
85
106
|
* must report "does not exist", not crash. validate() in particular exists to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hafez",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"gray-matter": "^4.0.3",
|
|
31
31
|
"js-yaml": "^4.1.1",
|
|
32
32
|
"proper-lockfile": "^4.1.2",
|
|
33
|
-
"simple-git": "^
|
|
33
|
+
"simple-git": "^4.0.2",
|
|
34
34
|
"zod": "^4.3.6"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
package/skill/hafez/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ description: >
|
|
|
6
6
|
wants to promote, link, unlink, delete, change a status or validate, or
|
|
7
7
|
wants to update or reconfigure Hafez ("upgrade hafez", "set up hafez").
|
|
8
8
|
Also the reference to read when no Hafez block is in your instructions.
|
|
9
|
-
hash:
|
|
9
|
+
hash: 24f12671554e
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Hafez
|
|
@@ -32,10 +32,13 @@ A plan is `--subtype plan` without `--synthesis`. Cannot tell entity from knowle
|
|
|
32
32
|
|
|
33
33
|
```
|
|
34
34
|
hafez query --filter <active|paused|done|stale|capture>
|
|
35
|
-
hafez search "<terms>" --kind <entity|knowledge>
|
|
35
|
+
hafez search "<terms>" --kind <entity|knowledge|session|all>
|
|
36
|
+
hafez query --kind session --related-to <slug>
|
|
36
37
|
hafez stats
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
`search` and `query` reach entities and knowledge notes; `--kind all` adds the session notes, `--kind session` lists them alone, newest first. `hafez read <slug> --archive` prints an entity's archived log entries.
|
|
41
|
+
|
|
39
42
|
## Less common operations
|
|
40
43
|
|
|
41
44
|
```
|