hafez 1.2.0 → 1.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/dist/batch-ops.d.ts +187 -0
- package/dist/batch-ops.js +322 -0
- package/dist/cli/args.d.ts +15 -0
- package/dist/cli/args.js +49 -0
- package/dist/cli/batch-errors.js +6 -3
- package/dist/cli/commands.d.ts +0 -108
- package/dist/cli/commands.js +154 -252
- package/dist/cli/help-agent.d.ts +0 -5
- package/dist/cli/help-agent.js +20 -5
- package/dist/cli/index.js +45 -65
- package/dist/cli/resolve-vault.js +2 -1
- package/dist/cli/schema-introspect.d.ts +1 -1
- package/dist/cli/schema-introspect.js +6 -22
- package/dist/cli.js +0 -0
- package/dist/contracts.d.ts +6 -0
- package/dist/contracts.js +8 -2
- package/dist/db.js +14 -28
- package/dist/digest.js +2 -0
- package/dist/document.d.ts +108 -0
- package/dist/{sections.js → document.js} +173 -22
- package/dist/export-okf.js +10 -22
- package/dist/git.d.ts +7 -8
- package/dist/git.js +39 -21
- package/dist/index.d.ts +3 -4
- package/dist/index.js +419 -517
- package/dist/knowledge-index.js +9 -7
- package/dist/merge.js +13 -25
- package/dist/query.d.ts +1 -1
- package/dist/query.js +3 -1
- package/dist/types.d.ts +48 -1
- package/dist/vault.d.ts +33 -6
- package/dist/vault.js +65 -8
- package/package.json +2 -2
- package/dist/parser.d.ts +0 -18
- package/dist/parser.js +0 -68
- package/dist/sections.d.ts +0 -54
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// src/document.ts
|
|
2
|
+
// The Document module (see CONTEXT.md): owns the body of a vault file —
|
|
3
|
+
// structural sections, session-log entries, wiki-links. Absorbs the former
|
|
4
|
+
// sections.ts and parser.ts; every body read or mutation goes through the
|
|
5
|
+
// single structural-heading model defined here. Pure string→string.
|
|
6
|
+
import { ALL_CONTRACTS, SUBTYPE_SECTIONS, SESSION_SECTIONS } from './contracts.js';
|
|
4
7
|
const UNCHECKED_RE = /^- \[ \] (.+)$/gm;
|
|
5
8
|
/**
|
|
6
|
-
* Known structural section headings in Hafez entities
|
|
7
|
-
* Derived from contracts.ts (defaultSections + optionalSections
|
|
8
|
-
* auto-generated sections not tracked in contracts
|
|
9
|
-
* Only these are treated as section boundaries. Any other `## `
|
|
10
|
-
* inside section content (e.g. in a brief or session log) is
|
|
9
|
+
* Known structural section headings in Hafez entities, knowledge notes, and
|
|
10
|
+
* session files. Derived from contracts.ts (defaultSections + optionalSections
|
|
11
|
+
* + session sections) plus auto-generated sections not tracked in contracts
|
|
12
|
+
* (Related). Only these are treated as section boundaries. Any other `## `
|
|
13
|
+
* heading inside section content (e.g. in a brief or session log) is
|
|
14
|
+
* preserved as-is.
|
|
11
15
|
*/
|
|
12
16
|
export const STRUCTURAL_SECTIONS = new Set([
|
|
13
17
|
...ALL_CONTRACTS.flatMap(c => [...c.defaultSections, ...c.optionalSections]),
|
|
14
18
|
...Object.values(SUBTYPE_SECTIONS).flat(),
|
|
19
|
+
...SESSION_SECTIONS,
|
|
15
20
|
'Related', // auto-generated by syncRelatedSection
|
|
16
|
-
'Summary', // used in session files
|
|
17
21
|
]);
|
|
18
22
|
/**
|
|
19
23
|
* Find the next structural `## Heading` boundary after a given position.
|
|
@@ -32,31 +36,176 @@ export function findNextStructuralHeading(body, from) {
|
|
|
32
36
|
}
|
|
33
37
|
return -1;
|
|
34
38
|
}
|
|
35
|
-
// --- Internal helpers ---
|
|
36
39
|
/**
|
|
37
40
|
* Find a ## Heading section and its content.
|
|
38
|
-
* Returns { start, end, content } or null if not found.
|
|
41
|
+
* Returns { start, headingEnd, end, content } or null if not found.
|
|
39
42
|
* Matches heading exactly (e.g. "## Brief" won't match "## Briefing").
|
|
43
|
+
* Trailing whitespace on the heading line is tolerated — the splitter's
|
|
44
|
+
* boundary tokenizer trims headings, so anything it treats as a boundary
|
|
45
|
+
* must also be findable here (a hand-edited `## Session Log ` otherwise
|
|
46
|
+
* gets a duplicate section and archival never fires).
|
|
40
47
|
* Only treats known structural headings as section boundaries -- content
|
|
41
48
|
* inside a section may contain arbitrary `## ` headings without corruption.
|
|
42
49
|
*/
|
|
43
50
|
export function findSection(body, heading) {
|
|
44
|
-
const re = new RegExp(`(^|\\n)(## ${escapeRegex(heading)})(?:\\n|$)`);
|
|
51
|
+
const re = new RegExp(`(^|\\n)(## ${escapeRegex(heading)}[ \\t]*)(?:\\n|$)`);
|
|
45
52
|
const match = re.exec(body);
|
|
46
53
|
if (!match)
|
|
47
54
|
return null;
|
|
48
55
|
const start = match.index + match[1].length;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
const headingEnd = start + match[2].length;
|
|
57
|
+
const afterHeading = headingEnd + 1;
|
|
58
|
+
// Find the next structural ## heading or EOF. Scan from the heading's own
|
|
59
|
+
// trailing newline (afterHeading - 1), not past it — otherwise a structural
|
|
60
|
+
// heading on the immediately following line (no blank line between, as in
|
|
61
|
+
// hand-edited files) is invisible and mutators swallow the next section.
|
|
62
|
+
const nextHeading = findNextStructuralHeading(body, Math.max(0, afterHeading - 1));
|
|
52
63
|
const end = nextHeading === -1 ? body.length : nextHeading;
|
|
53
64
|
const rawContent = body.slice(afterHeading, end);
|
|
54
65
|
const content = rawContent.replace(/^\n+/, '').replace(/\n+$/, '');
|
|
55
|
-
return { start, end, content };
|
|
66
|
+
return { start, headingEnd, end, content };
|
|
56
67
|
}
|
|
57
68
|
function escapeRegex(s) {
|
|
58
69
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
59
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Split a body into preamble + structural sections. Non-structural `## `
|
|
73
|
+
* headings stay inside the enclosing section's content — sections are
|
|
74
|
+
* atomic units of authored content, never split at user headings.
|
|
75
|
+
* Headings and content come back trimmed; reassemble with
|
|
76
|
+
* `## ${heading}\n\n${content}` joined by blank lines.
|
|
77
|
+
*/
|
|
78
|
+
export function splitStructural(body) {
|
|
79
|
+
const marks = [];
|
|
80
|
+
const re = /(^|\n)## ([^\n]*)/g;
|
|
81
|
+
let m;
|
|
82
|
+
while ((m = re.exec(body)) !== null) {
|
|
83
|
+
const heading = m[2].trim();
|
|
84
|
+
if (!STRUCTURAL_SECTIONS.has(heading))
|
|
85
|
+
continue;
|
|
86
|
+
marks.push({ start: m.index + m[1].length, contentStart: m.index + m[0].length, heading });
|
|
87
|
+
}
|
|
88
|
+
if (marks.length === 0)
|
|
89
|
+
return { preamble: body.trim(), sections: [] };
|
|
90
|
+
const preamble = body.slice(0, marks[0].start).trim();
|
|
91
|
+
const sections = marks.map((mk, i) => ({
|
|
92
|
+
heading: mk.heading,
|
|
93
|
+
content: body.slice(mk.contentStart, i + 1 < marks.length ? marks[i + 1].start : body.length).trim(),
|
|
94
|
+
}));
|
|
95
|
+
return { preamble, sections };
|
|
96
|
+
}
|
|
97
|
+
// --- Generic section mutation ---
|
|
98
|
+
/**
|
|
99
|
+
* Replace a structural section's content, or create the section: before
|
|
100
|
+
* `insertBefore` when that section exists, at EOF otherwise.
|
|
101
|
+
*/
|
|
102
|
+
export function setSection(body, heading, content, insertBefore) {
|
|
103
|
+
const existing = findSection(body, heading);
|
|
104
|
+
if (existing) {
|
|
105
|
+
return body.slice(0, existing.start) + `## ${heading}\n\n${content}\n` + body.slice(existing.end);
|
|
106
|
+
}
|
|
107
|
+
if (insertBefore) {
|
|
108
|
+
const target = findSection(body, insertBefore);
|
|
109
|
+
if (target) {
|
|
110
|
+
return body.slice(0, target.start) + `## ${heading}\n\n${content}\n\n` + body.slice(target.start);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return body + `\n\n## ${heading}\n\n${content}\n`;
|
|
114
|
+
}
|
|
115
|
+
/** Append content at the end of a structural section (creating it at EOF if absent). */
|
|
116
|
+
export function appendToSection(body, heading, content) {
|
|
117
|
+
const section = findSection(body, heading);
|
|
118
|
+
if (!section)
|
|
119
|
+
return body + `\n\n## ${heading}\n\n${content}\n`;
|
|
120
|
+
if (section.end === body.length)
|
|
121
|
+
return body + '\n' + content + '\n';
|
|
122
|
+
return body.slice(0, section.end) + '\n' + content + body.slice(section.end);
|
|
123
|
+
}
|
|
124
|
+
/** Insert a formatted session-log entry newest-first (creating the section at EOF if absent). */
|
|
125
|
+
export function prependSessionLogEntry(body, entry) {
|
|
126
|
+
const section = findSection(body, 'Session Log');
|
|
127
|
+
if (!section)
|
|
128
|
+
return body + `\n\n## Session Log\n\n${entry}\n`;
|
|
129
|
+
const insertPos = section.headingEnd;
|
|
130
|
+
return body.slice(0, insertPos) + '\n\n' + entry + '\n' + body.slice(insertPos);
|
|
131
|
+
}
|
|
132
|
+
/** Body up to the real Session Log section — the `summary` read depth. */
|
|
133
|
+
export function bodyBeforeSessionLog(body) {
|
|
134
|
+
const section = findSection(body, 'Session Log');
|
|
135
|
+
return section ? body.slice(0, section.start).trim() : body;
|
|
136
|
+
}
|
|
137
|
+
// --- Session log parsing ---
|
|
138
|
+
const HEADING_RE = /^### (\d{4}-\d{2}-\d{2}) (?:—|--) (.+?)(?:\s+\[(\w+)\])?$/;
|
|
139
|
+
export function parseSessionLogHeading(line) {
|
|
140
|
+
const match = line.match(HEADING_RE);
|
|
141
|
+
if (!match)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
date: match[1],
|
|
145
|
+
agent: match[2],
|
|
146
|
+
type: match[3] || 'progress'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function formatSessionLogEntry(entry) {
|
|
150
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
151
|
+
const lines = [`### ${today} — ${entry.agent} [${entry.type}]`, `Summary: ${entry.summary}`];
|
|
152
|
+
if (entry.body)
|
|
153
|
+
lines.push(entry.body);
|
|
154
|
+
return lines.join('\n');
|
|
155
|
+
}
|
|
156
|
+
/** Entries inside the real Session Log section only — entry-shaped lines
|
|
157
|
+
* elsewhere in the body (e.g. quoted in a Brief) do not count. */
|
|
158
|
+
export function countSessionLogEntries(body) {
|
|
159
|
+
const section = findSection(body, 'Session Log');
|
|
160
|
+
if (!section)
|
|
161
|
+
return 0;
|
|
162
|
+
return (section.content.match(/^### \d{4}-\d{2}-\d{2} (?:—|--)/gm) || []).length;
|
|
163
|
+
}
|
|
164
|
+
export function parseSessionLog(body) {
|
|
165
|
+
const section = findSection(body, 'Session Log');
|
|
166
|
+
if (!section)
|
|
167
|
+
return [];
|
|
168
|
+
const entries = [];
|
|
169
|
+
const lines = section.content.split('\n');
|
|
170
|
+
let current = null;
|
|
171
|
+
let summaryLine = null;
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const heading = parseSessionLogHeading(line);
|
|
174
|
+
if (heading) {
|
|
175
|
+
if (current && summaryLine) {
|
|
176
|
+
entries.push({ ...current, summary: summaryLine });
|
|
177
|
+
}
|
|
178
|
+
current = heading;
|
|
179
|
+
summaryLine = null;
|
|
180
|
+
}
|
|
181
|
+
else if (current && !summaryLine && line.startsWith('Summary: ')) {
|
|
182
|
+
summaryLine = line.slice('Summary: '.length);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (current && summaryLine) {
|
|
186
|
+
entries.push({ ...current, summary: summaryLine });
|
|
187
|
+
}
|
|
188
|
+
return entries;
|
|
189
|
+
}
|
|
190
|
+
export function extractOldestSessionLogEntry(body) {
|
|
191
|
+
const section = findSection(body, 'Session Log');
|
|
192
|
+
if (!section)
|
|
193
|
+
return null;
|
|
194
|
+
// Work on the raw slice between the heading and the section's structural
|
|
195
|
+
// end, so the extracted entry can never swallow a following section
|
|
196
|
+
// (e.g. an auto-appended Related) into the archive.
|
|
197
|
+
const contentStart = section.headingEnd;
|
|
198
|
+
const region = body.slice(contentStart, section.end);
|
|
199
|
+
const entryStarts = [...region.matchAll(/^### \d{4}-\d{2}-\d{2} (?:—|--)/gm)];
|
|
200
|
+
if (entryStarts.length < 2)
|
|
201
|
+
return null;
|
|
202
|
+
// Entries are newest-first in the file, so the oldest is the last match
|
|
203
|
+
const oldestStart = entryStarts[entryStarts.length - 1].index;
|
|
204
|
+
const entry = region.slice(oldestStart).trim();
|
|
205
|
+
const remaining = body.slice(0, contentStart) + region.slice(0, oldestStart) + body.slice(section.end);
|
|
206
|
+
return { entry, remaining };
|
|
207
|
+
}
|
|
208
|
+
// --- Section removal / insertion helpers (Brief, Next Actions) ---
|
|
60
209
|
/**
|
|
61
210
|
* Remove a section (heading + content) from body, cleaning up extra whitespace.
|
|
62
211
|
*/
|
|
@@ -121,9 +270,9 @@ function insertSectionAfter(body, heading, content, after, beforeFallbacks) {
|
|
|
121
270
|
function normalizeForMatch(s) {
|
|
122
271
|
return s
|
|
123
272
|
.trim()
|
|
124
|
-
.replace(/[
|
|
125
|
-
.replace(/[
|
|
126
|
-
.replace(
|
|
273
|
+
.replace(/[‘’]/g, "'")
|
|
274
|
+
.replace(/[“”]/g, '"')
|
|
275
|
+
.replace(/—/g, '-');
|
|
127
276
|
}
|
|
128
277
|
/**
|
|
129
278
|
* Match an action by case-insensitive substring against unchecked items.
|
|
@@ -251,9 +400,11 @@ export function deriveSummary(kind, fm, body) {
|
|
|
251
400
|
}
|
|
252
401
|
return firstNonEmptyLine(findSection(body, 'Summary')?.content);
|
|
253
402
|
}
|
|
254
|
-
// ---
|
|
255
|
-
/**
|
|
256
|
-
|
|
403
|
+
// --- Wiki-links ---
|
|
404
|
+
/** The one wiki-link syntax the vault recognizes: [[slug]] or
|
|
405
|
+
* [[slug|display text]] — slug in group 1, display text in group 2.
|
|
406
|
+
* Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
|
|
407
|
+
export const WIKI_LINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/;
|
|
257
408
|
export function syncRelatedSection(body, frontmatter) {
|
|
258
409
|
const targets = [];
|
|
259
410
|
if (frontmatter.parent)
|
package/dist/export-okf.js
CHANGED
|
@@ -7,24 +7,12 @@
|
|
|
7
7
|
// regenerated indexes) until the bundle directory is deleted and re-exported.
|
|
8
8
|
import { readdirSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
9
9
|
import { join, resolve, sep, basename } from 'path';
|
|
10
|
-
import { parseFilePath, serializeFile } from './vault.js';
|
|
11
|
-
import { deriveSummary } from './
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
];
|
|
17
|
-
// Local capturing variant of the shared WIKI_LINK_RE — captures display text in
|
|
18
|
-
// group 2, which the shared regex's consumers don't need.
|
|
19
|
-
const OKF_LINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
20
|
-
function scanDir(dirPath) {
|
|
21
|
-
try {
|
|
22
|
-
return readdirSync(dirPath).filter(f => f.endsWith('.md')).map(f => join(dirPath, f));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
10
|
+
import { parseFilePath, serializeFile, scanVaultDir, kindDir } from './vault.js';
|
|
11
|
+
import { deriveSummary, WIKI_LINK_RE } from './document.js';
|
|
12
|
+
const SOURCE_KINDS = ['entity', 'knowledge', 'session'];
|
|
13
|
+
const KIND_DIRS = SOURCE_KINDS.map(kind => ({ dir: kindDir(kind), kind }));
|
|
14
|
+
// The shared wiki-link syntax (document.ts) — group 1 slug, group 2 display.
|
|
15
|
+
const OKF_LINK_RE = new RegExp(WIKI_LINK_RE.source, 'g');
|
|
28
16
|
function mapFrontmatter(doc) {
|
|
29
17
|
const { fm, kind, body } = doc;
|
|
30
18
|
const mapped = {};
|
|
@@ -88,7 +76,7 @@ export function exportOkf(vaultPath, outDir) {
|
|
|
88
76
|
// Enumerate and parse. Sessions are not in SQLite — disk enumeration is required.
|
|
89
77
|
const docs = [];
|
|
90
78
|
for (const { dir, kind } of KIND_DIRS) {
|
|
91
|
-
for (const filePath of
|
|
79
|
+
for (const filePath of scanVaultDir(join(resolvedVault, dir))) {
|
|
92
80
|
const slug = basename(filePath, '.md');
|
|
93
81
|
const relFile = `${dir}/${slug}.md`;
|
|
94
82
|
let fm, body;
|
|
@@ -160,9 +148,9 @@ export function exportOkf(vaultPath, outDir) {
|
|
|
160
148
|
}
|
|
161
149
|
// Root index.md — the only place okf_version may appear
|
|
162
150
|
const counts = [
|
|
163
|
-
['
|
|
164
|
-
['knowledge', report.knowledge],
|
|
165
|
-
['
|
|
151
|
+
[kindDir('entity'), report.entities],
|
|
152
|
+
[kindDir('knowledge'), report.knowledge],
|
|
153
|
+
[kindDir('session'), report.sessions],
|
|
166
154
|
];
|
|
167
155
|
const rootLines = ['# Vault Export', ''];
|
|
168
156
|
for (const [dir, count] of counts) {
|
package/dist/git.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GitConfig, type ChangelogEntry } from './types.js';
|
|
1
|
+
import { type GitConfig, type ChangelogEntry, type Journal } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Bidirectional sync: pull remote changes, push local commits.
|
|
4
4
|
* All work must be committed before calling — no file save/restore needed.
|
|
@@ -8,13 +8,6 @@ export declare function gitSync(vaultPath: string): Promise<{
|
|
|
8
8
|
pushed: boolean;
|
|
9
9
|
remote: boolean;
|
|
10
10
|
}>;
|
|
11
|
-
/**
|
|
12
|
-
* Unstage files (best effort). Used by batch rollback: a commit that fails
|
|
13
|
-
* after `git add` leaves the new blobs staged in .git/index, and the next
|
|
14
|
-
* successful commit would publish the rolled-back content under the wrong
|
|
15
|
-
* message. Clearing the index entries restores worktree/HEAD/index agreement.
|
|
16
|
-
*/
|
|
17
|
-
export declare function gitUnstage(vaultPath: string, files: string[]): Promise<void>;
|
|
18
11
|
export declare function gitChangelog(vaultPath: string, since: string): Promise<ChangelogEntry[]>;
|
|
19
12
|
/**
|
|
20
13
|
* Latest git time per file (repo-relative, forward-slash paths). Used to
|
|
@@ -24,3 +17,9 @@ export declare function gitChangelog(vaultPath: string, since: string): Promise<
|
|
|
24
17
|
*/
|
|
25
18
|
export declare function gitFileTimes(vaultPath: string, mode: 'modified' | 'added'): Promise<Map<string, string>>;
|
|
26
19
|
export declare function gitCommitAndPush(vaultPath: string, files: string[], message: string, config?: GitConfig): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* The production Journal (see CONTEXT.md): vault persistence backed by git.
|
|
22
|
+
* Local-only vaults (no origin remote) commit silently without pushing —
|
|
23
|
+
* that behavior lives in gitCommitAndPush/gitSync, inside this adapter.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createGitJournal(vaultPath: string, config?: GitConfig): Journal;
|
package/dist/git.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { simpleGit } from 'simple-git';
|
|
5
5
|
import { mergeVaultContent } from './merge.js';
|
|
6
|
+
import { VAULT_FILE_RE, INDEXED_DIR_PATHSPECS, classifyIndexedRelPath } from './vault.js';
|
|
6
7
|
import { HafezError } from './types.js';
|
|
7
8
|
const logMerge = (msg) => process.stderr.write(`[hafez-merge] ${msg}\n`);
|
|
8
9
|
const MAX_PUSH_RETRIES = 3;
|
|
@@ -40,7 +41,6 @@ async function ensureCleanState(git, vaultPath) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
const VAULT_FILE_RE = /^(?:entities|knowledge|sessions)\/.+\.md$/;
|
|
44
44
|
/**
|
|
45
45
|
* True if the vault repo has an `origin` remote. A local-only vault (the
|
|
46
46
|
* default the README setup produces) has none — every pull/push must be
|
|
@@ -253,12 +253,13 @@ export async function gitSync(vaultPath) {
|
|
|
253
253
|
return { pulled, pushed: true, remote: true };
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
|
-
* Unstage files (best effort). Used by
|
|
257
|
-
* after `git add` leaves the new blobs staged in
|
|
258
|
-
* successful commit would publish
|
|
259
|
-
* message. Clearing the index entries restores
|
|
256
|
+
* Unstage files (best effort). Used by the journal adapter's commit-failure
|
|
257
|
+
* repair: a commit that fails after `git add` leaves the new blobs staged in
|
|
258
|
+
* .git/index, and the next successful commit would publish rolled-back
|
|
259
|
+
* content under the wrong message. Clearing the index entries restores
|
|
260
|
+
* worktree/HEAD/index agreement.
|
|
260
261
|
*/
|
|
261
|
-
|
|
262
|
+
async function gitUnstage(vaultPath, files) {
|
|
262
263
|
try {
|
|
263
264
|
await simpleGit(vaultPath).raw(['reset', '-q', 'HEAD', '--', ...files]);
|
|
264
265
|
}
|
|
@@ -290,7 +291,7 @@ export async function gitChangelog(vaultPath, since) {
|
|
|
290
291
|
try {
|
|
291
292
|
output = await git.raw([
|
|
292
293
|
'log', `--since=${normalizedSince}`, '--name-status',
|
|
293
|
-
'--pretty=format:%H|%aI|%s', '--',
|
|
294
|
+
'--pretty=format:%H|%aI|%s', '--', ...INDEXED_DIR_PATHSPECS,
|
|
294
295
|
]);
|
|
295
296
|
}
|
|
296
297
|
catch {
|
|
@@ -320,20 +321,10 @@ export async function gitChangelog(vaultPath, since) {
|
|
|
320
321
|
// dirs and would otherwise surface as changelog rows.
|
|
321
322
|
if (!filePath.endsWith('.md'))
|
|
322
323
|
continue;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (filePath.startsWith('entities/')) {
|
|
326
|
-
kind = 'entity';
|
|
327
|
-
slug = filePath.replace('entities/', '').replace('.md', '');
|
|
328
|
-
}
|
|
329
|
-
else if (filePath.startsWith('knowledge/')) {
|
|
330
|
-
kind = 'knowledge';
|
|
331
|
-
slug = filePath.replace('knowledge/', '').replace('.md', '');
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
324
|
+
const classified = classifyIndexedRelPath(filePath);
|
|
325
|
+
if (!classified)
|
|
334
326
|
continue;
|
|
335
|
-
}
|
|
336
|
-
entries.push({ slug, kind, operation, timestamp, commit_message });
|
|
327
|
+
entries.push({ slug: classified.slug, kind: classified.kind, operation, timestamp, commit_message });
|
|
337
328
|
}
|
|
338
329
|
}
|
|
339
330
|
return entries;
|
|
@@ -348,7 +339,7 @@ export async function gitFileTimes(vaultPath, mode) {
|
|
|
348
339
|
const git = simpleGit(vaultPath);
|
|
349
340
|
let output;
|
|
350
341
|
try {
|
|
351
|
-
const args = ['log', '--pretty=format:%x00%aI', '--name-only', '--',
|
|
342
|
+
const args = ['log', '--pretty=format:%x00%aI', '--name-only', '--', ...INDEXED_DIR_PATHSPECS];
|
|
352
343
|
if (mode === 'added')
|
|
353
344
|
args.splice(1, 0, '--diff-filter=A');
|
|
354
345
|
output = await git.raw(args);
|
|
@@ -405,3 +396,30 @@ export async function gitCommitAndPush(vaultPath, files, message, config = {}) {
|
|
|
405
396
|
return;
|
|
406
397
|
await pushResolving(git, vaultPath, (conflictFiles) => `Git conflict with remote (files: ${conflictFiles.join(', ')}). Changes saved locally.`, `Git push failed after ${MAX_PUSH_RETRIES} attempts. Changes saved locally.`);
|
|
407
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* The production Journal (see CONTEXT.md): vault persistence backed by git.
|
|
401
|
+
* Local-only vaults (no origin remote) commit silently without pushing —
|
|
402
|
+
* that behavior lives in gitCommitAndPush/gitSync, inside this adapter.
|
|
403
|
+
*/
|
|
404
|
+
export function createGitJournal(vaultPath, config = {}) {
|
|
405
|
+
return {
|
|
406
|
+
async commit(written, deleted, message) {
|
|
407
|
+
const files = [...written, ...deleted];
|
|
408
|
+
try {
|
|
409
|
+
await gitCommitAndPush(vaultPath, files, message, config);
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
// Commit-stage failure: nothing landed, but `git add` may have staged
|
|
413
|
+
// the new blobs — clean up our own partial state before rethrowing.
|
|
414
|
+
// Push-stage failure keeps the local commit (GIT_PUSH_FAILED).
|
|
415
|
+
if (!(err instanceof HafezError && err.code === 'GIT_PUSH_FAILED')) {
|
|
416
|
+
await gitUnstage(vaultPath, files);
|
|
417
|
+
}
|
|
418
|
+
throw err;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
sync: () => gitSync(vaultPath),
|
|
422
|
+
changelog: (since) => gitChangelog(vaultPath, since),
|
|
423
|
+
fileTimes: (mode) => gitFileTimes(vaultPath, mode),
|
|
424
|
+
};
|
|
425
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Hafez, HafezConfig } from './types.js';
|
|
2
2
|
export declare function createHafez(config: HafezConfig): Hafez;
|
|
3
3
|
export { HafezError } from './types.js';
|
|
4
|
-
export type { Hafez, HafezConfig, HafezErrorCode, EntityType, EntityStatus, EntityQueryOpts, KnowledgeQueryOpts, EntityFrontmatter, KnowledgeFrontmatter, ParsedFile, SessionLogEntry, UpdateFields, UpdateResult, CreateEntityFields, CreateKnowledgeFields, QueryFilter, QueryResult, KnowledgeQueryResult, UnifiedResult, ValidationReport, BatchOperation, BatchResult, ReadDepth, ConfidenceLevel, LinkRelation, GitConfig, SearchResult, VaultStats, ChangelogEntry, KnowledgeSubtype } from './types.js';
|
|
5
|
-
export { getBrief, getNextActions, findSection } from './
|
|
6
|
-
export { parseSessionLogHeading, parseSessionLog } from './
|
|
7
|
-
export { formatStats, formatChangelog, formatValidation } from './cli/format.js';
|
|
4
|
+
export type { Hafez, HafezConfig, HafezErrorCode, EntityType, EntityStatus, EntityQueryOpts, KnowledgeQueryOpts, EntityFrontmatter, KnowledgeFrontmatter, ParsedFile, SessionLogEntry, UpdateFields, UpdateResult, CreateEntityFields, CreateKnowledgeFields, QueryFilter, QueryResult, KnowledgeQueryResult, UnifiedResult, ValidationReport, BatchOperation, BatchResult, ReadDepth, ConfidenceLevel, LinkRelation, GitConfig, Journal, SearchResult, VaultStats, ChangelogEntry, KnowledgeSubtype } from './types.js';
|
|
5
|
+
export { getBrief, getNextActions, findSection } from './document.js';
|
|
6
|
+
export { parseSessionLogHeading, parseSessionLog } from './document.js';
|