hafez 2.1.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/dist/validate.js CHANGED
@@ -1,29 +1,42 @@
1
1
  import { join, basename } from 'path';
2
- import { parseFilePath, scanVaultDir, kindDir, vaultFileExists } from './vault.js';
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
- const exists = (slug) => vaultFileExists(vaultPath, slug);
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 knowledgeSlugs = new Set();
32
+ const knowledgeRelated = new Map();
20
33
  const referencedKnowledge = new Set();
21
34
  const parents = new Map();
22
- // Scan entities
23
- const entDir = join(vaultPath, kindDir('entity'));
24
- {
25
- const files = scanVaultDir(entDir);
26
- report.total_entities = files.length;
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 errors = validateEntityFrontmatter(fm);
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
- // Scan knowledge
68
- const knDir = join(vaultPath, kindDir('knowledge'));
69
- {
70
- const files = scanVaultDir(knDir);
71
- report.total_knowledge = files.length;
72
- for (const filePath of files) {
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 knowledgeSlugs) {
96
- let fm;
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 (sessions are files-and-git only). */
5
- export declare const INDEXED_KINDS: ReadonlyArray<'entity' | 'knowledge'>;
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 whose kind lives in the read index. */
11
- export declare function isIndexedRelPath(relPath: string): boolean;
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: 'entity' | 'knowledge';
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 file a slug names, or null — entities are resolved before knowledge,
29
- * the one resolution order every operation sees. */
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 (sessions are files-and-git only). */
21
- export const INDEXED_KINDS = ['entity', 'knowledge'];
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 whose kind lives in the read index. */
29
- export function isIndexedRelPath(relPath) {
30
- return INDEXED_KINDS.some(k => relPath.startsWith(`${KIND_DIRS[k]}/`));
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 file a slug names, or null — entities are resolved before knowledge,
73
- * the one resolution order every operation sees. */
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.1.0",
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": "^3.33.0",
33
+ "simple-git": "^4.0.2",
34
34
  "zod": "^4.3.6"
35
35
  },
36
36
  "devDependencies": {
@@ -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: 01a30ae1e209
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
  ```