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
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createHafez } from '../index.js';
|
|
6
6
|
import { HafezError } from '../types.js';
|
|
7
|
+
import { parseArgs, UsageError } from './args.js';
|
|
7
8
|
import { cmdRead, cmdQuery, cmdSearch, cmdCreate, cmdUpdate, cmdLink, cmdUnlink, cmdPromote, cmdValidate, cmdIndexRebuild, cmdBatch, cmdSync, cmdStats, cmdChangelog, cmdDigest, cmdCapture } from './commands.js';
|
|
8
9
|
import { cmdSchema } from './commands-schema.js';
|
|
9
10
|
import { renderAgentHelp } from './help-agent.js';
|
|
@@ -11,6 +12,7 @@ import { renderOnboard } from './onboard.js';
|
|
|
11
12
|
import { resolveVaultPath, noVaultMessage, cmdInit } from './resolve-vault.js';
|
|
12
13
|
const COMMANDS = {
|
|
13
14
|
read: cmdRead,
|
|
15
|
+
sync: cmdSync,
|
|
14
16
|
query: cmdQuery,
|
|
15
17
|
search: cmdSearch,
|
|
16
18
|
create: cmdCreate,
|
|
@@ -25,7 +27,7 @@ const COMMANDS = {
|
|
|
25
27
|
index: (os, args) => {
|
|
26
28
|
if (args[0] === 'rebuild')
|
|
27
29
|
return cmdIndexRebuild(os);
|
|
28
|
-
throw new
|
|
30
|
+
throw new UsageError('Usage: hafez index rebuild');
|
|
29
31
|
},
|
|
30
32
|
stats: cmdStats,
|
|
31
33
|
changelog: cmdChangelog,
|
|
@@ -76,6 +78,33 @@ Global flags:
|
|
|
76
78
|
|
|
77
79
|
Vault discovery: --vault flag > ~/.config/hafez/vault
|
|
78
80
|
`;
|
|
81
|
+
/** Exit codes, documented in `hafez help --agent`. 0 = success; 4 is the
|
|
82
|
+
* mutate-succeeded-sync-failed warning path; anything unmapped = 1. */
|
|
83
|
+
const EXIT_CODES = {
|
|
84
|
+
NOT_FOUND: 1,
|
|
85
|
+
VALIDATION_FAILED: 2,
|
|
86
|
+
GIT_PUSH_FAILED: 3,
|
|
87
|
+
SLUG_EXISTS: 5,
|
|
88
|
+
GIT_COMMIT_FAILED: 6,
|
|
89
|
+
VAULT_LOCKED: 7,
|
|
90
|
+
};
|
|
91
|
+
/** The one error → exit adapter. Usage errors (bad flags/args) are distinct
|
|
92
|
+
* from vault errors and from unknown crashes. */
|
|
93
|
+
function report(err) {
|
|
94
|
+
if (err instanceof UsageError) {
|
|
95
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
if (err instanceof HafezError) {
|
|
99
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
100
|
+
if (err.details?.length) {
|
|
101
|
+
for (const detail of err.details)
|
|
102
|
+
process.stderr.write(` - ${detail}\n`);
|
|
103
|
+
}
|
|
104
|
+
process.exit(EXIT_CODES[err.code] ?? 1);
|
|
105
|
+
}
|
|
106
|
+
failUnknown(err);
|
|
107
|
+
}
|
|
79
108
|
// Terminal handler for non-Hafez errors. Special-cases unbuilt better-sqlite3
|
|
80
109
|
// bindings: npm 11+ blocks install scripts by default, so `npm install -g
|
|
81
110
|
// hafez` reports success with the native module unbuilt and the first indexed
|
|
@@ -137,15 +166,7 @@ export async function main(argv) {
|
|
|
137
166
|
process.stdout.write(result + '\n');
|
|
138
167
|
}
|
|
139
168
|
catch (err) {
|
|
140
|
-
|
|
141
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
142
|
-
if (err.details?.length) {
|
|
143
|
-
for (const detail of err.details)
|
|
144
|
-
process.stderr.write(` - ${detail}\n`);
|
|
145
|
-
}
|
|
146
|
-
process.exit(2);
|
|
147
|
-
}
|
|
148
|
-
failUnknown(err);
|
|
169
|
+
report(err);
|
|
149
170
|
}
|
|
150
171
|
return;
|
|
151
172
|
}
|
|
@@ -167,59 +188,28 @@ export async function main(argv) {
|
|
|
167
188
|
const vaultPath = resolution;
|
|
168
189
|
// export runs before Hafez instance creation — read-only, no auto-sync
|
|
169
190
|
if (command === 'export') {
|
|
170
|
-
if (!args.includes('--okf')) {
|
|
171
|
-
process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}
|
|
174
|
-
const outIdx = args.indexOf('--out');
|
|
175
|
-
let outDir = resolve(process.cwd(), 'okf-export');
|
|
176
|
-
if (outIdx !== -1) {
|
|
177
|
-
const outValue = args[outIdx + 1];
|
|
178
|
-
if (!outValue) {
|
|
179
|
-
process.stderr.write('Usage: hafez export --okf [--out <dir>]\n');
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
outDir = resolve(outValue);
|
|
183
|
-
}
|
|
184
|
-
const { exportOkf } = await import('../export-okf.js');
|
|
185
191
|
try {
|
|
186
|
-
const
|
|
192
|
+
const EXPORT_USAGE = 'Usage: hafez export --okf [--out <dir>]';
|
|
193
|
+
const { flags } = parseArgs(args, { okf: 'boolean', out: 'string' }, EXPORT_USAGE);
|
|
194
|
+
if (flags.okf !== true)
|
|
195
|
+
throw new UsageError(EXPORT_USAGE);
|
|
196
|
+
const outDir = typeof flags.out === 'string' ? resolve(flags.out) : resolve(process.cwd(), 'okf-export');
|
|
197
|
+
const { exportOkf } = await import('../export-okf.js');
|
|
198
|
+
const exportReport = exportOkf(vaultPath, outDir);
|
|
187
199
|
if (json) {
|
|
188
|
-
process.stdout.write(JSON.stringify(
|
|
200
|
+
process.stdout.write(JSON.stringify(exportReport, null, 2) + '\n');
|
|
189
201
|
}
|
|
190
202
|
else {
|
|
191
|
-
const lines = [`Exported ${
|
|
192
|
-
for (const s of
|
|
203
|
+
const lines = [`Exported ${exportReport.entities} entities, ${exportReport.knowledge} knowledge, ${exportReport.sessions} sessions to ${exportReport.outDir}`];
|
|
204
|
+
for (const s of exportReport.skipped)
|
|
193
205
|
lines.push(` skipped ${s.file}: ${s.reason}`);
|
|
194
|
-
if (
|
|
195
|
-
lines.push(` unresolved links: ${
|
|
206
|
+
if (exportReport.unresolvedLinks > 0)
|
|
207
|
+
lines.push(` unresolved links: ${exportReport.unresolvedLinks}`);
|
|
196
208
|
process.stdout.write(lines.join('\n') + '\n');
|
|
197
209
|
}
|
|
198
210
|
}
|
|
199
211
|
catch (err) {
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
// sync is handled specially — always runs, not gated by MUTATING_COMMANDS
|
|
205
|
-
if (command === 'sync') {
|
|
206
|
-
const os = createHafez({ vaultPath, git: { push: false } });
|
|
207
|
-
try {
|
|
208
|
-
const result = await cmdSync(os, args);
|
|
209
|
-
process.stdout.write(result + '\n');
|
|
210
|
-
}
|
|
211
|
-
catch (err) {
|
|
212
|
-
if (err instanceof HafezError) {
|
|
213
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
214
|
-
if (err.details?.length) {
|
|
215
|
-
for (const detail of err.details) {
|
|
216
|
-
process.stderr.write(` - ${detail}\n`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
|
|
220
|
-
process.exit(exitCodes[err.code] ?? 1);
|
|
221
|
-
}
|
|
222
|
-
failUnknown(err);
|
|
212
|
+
report(err);
|
|
223
213
|
}
|
|
224
214
|
return;
|
|
225
215
|
}
|
|
@@ -246,16 +236,6 @@ export async function main(argv) {
|
|
|
246
236
|
process.stdout.write(result + '\n');
|
|
247
237
|
}
|
|
248
238
|
catch (err) {
|
|
249
|
-
|
|
250
|
-
process.stderr.write(`Error: ${err.message}\n`);
|
|
251
|
-
if (err.details?.length) {
|
|
252
|
-
for (const detail of err.details) {
|
|
253
|
-
process.stderr.write(` - ${detail}\n`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const exitCodes = { NOT_FOUND: 1, VALIDATION_FAILED: 2, GIT_PUSH_FAILED: 3 };
|
|
257
|
-
process.exit(exitCodes[err.code] ?? 1);
|
|
258
|
-
}
|
|
259
|
-
failUnknown(err);
|
|
239
|
+
report(err);
|
|
260
240
|
}
|
|
261
241
|
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs
|
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import { kindDir } from '../vault.js';
|
|
6
7
|
export function getConfigDir() {
|
|
7
8
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
8
9
|
return join(xdg || join(homedir(), '.config'), 'hafez');
|
|
@@ -28,7 +29,7 @@ export function saveVaultConfig(vaultPath) {
|
|
|
28
29
|
writeFileSync(join(dir, 'vault'), resolve(vaultPath) + '\n');
|
|
29
30
|
}
|
|
30
31
|
function isVault(dirPath) {
|
|
31
|
-
return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, '
|
|
32
|
+
return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, kindDir('entity')));
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Resolve vault path. Returns string or null.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// types actually used in BatchOperationSchema plus a few forward-looking ones
|
|
11
11
|
// (`default`, `effects`, `union`) so the walker does not explode when the
|
|
12
12
|
// schema is extended.
|
|
13
|
-
import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, } from '
|
|
13
|
+
import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, OP_SPECS, } from '../batch-ops.js';
|
|
14
14
|
import { DigestInputSchema } from '../digest.js';
|
|
15
15
|
import { ALIAS_KEYS } from '../contracts.js';
|
|
16
16
|
// --- Walker ---
|
|
@@ -155,16 +155,8 @@ export const DIGEST_EXAMPLE = {
|
|
|
155
155
|
session_date: '2026-01-15',
|
|
156
156
|
agent: 'claude',
|
|
157
157
|
};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
'create-entity': 'Create a new entity (capture / entity / project).',
|
|
161
|
-
'create-knowledge': 'Create a new knowledge note (insight / plan subtype).',
|
|
162
|
-
'create-session': 'Create a new session note linked to an entity.',
|
|
163
|
-
'capture': 'Quick capture — a raw inbox item with no structure required.',
|
|
164
|
-
'link': 'Add a parent or related link between two entities.',
|
|
165
|
-
'unlink': 'Remove a parent or related link between two entities.',
|
|
166
|
-
'promote': 'Promote a capture to entity/project/knowledge, or an entity to project.',
|
|
167
|
-
};
|
|
158
|
+
// Descriptions live in the Op Spec table (src/batch-ops.ts) — one row per op.
|
|
159
|
+
const OP_DESCRIPTIONS = Object.fromEntries(OP_SPECS.map(s => [s.name, s.description]));
|
|
168
160
|
export function listOps() {
|
|
169
161
|
return enumerateBranches().map(({ op, kind }) => {
|
|
170
162
|
const name = kind ? `${op}-${kind}` : op;
|
|
@@ -293,17 +285,9 @@ function shouldStrip(path, _key) {
|
|
|
293
285
|
return Object.prototype.hasOwnProperty.call(ALIAS_KEYS, path);
|
|
294
286
|
}
|
|
295
287
|
function isDefaultIncluded(op, kind, fieldKey) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return true;
|
|
300
|
-
if (op === 'create' && kind === 'session' && fieldKey === 'synthesis')
|
|
301
|
-
return true;
|
|
302
|
-
if (op === 'update' && fieldKey === 'status')
|
|
303
|
-
return true;
|
|
304
|
-
if (op === 'capture' && fieldKey === 'notes')
|
|
305
|
-
return true;
|
|
306
|
-
return false;
|
|
288
|
+
// Example-inclusion hints live in the Op Spec table, one row per op.
|
|
289
|
+
const spec = OP_SPECS.find(s => s.op === op && s.kind === kind);
|
|
290
|
+
return spec ? spec.exampleInclude.includes(fieldKey) : false;
|
|
307
291
|
}
|
|
308
292
|
function sampleValue(field) {
|
|
309
293
|
switch (field.type) {
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/contracts.d.ts
CHANGED
|
@@ -24,3 +24,9 @@ export declare const ALIAS_KEYS: {
|
|
|
24
24
|
readonly 'create.session.fields.insight': "synthesis";
|
|
25
25
|
};
|
|
26
26
|
export declare const SUBTYPE_SECTIONS: Record<KnowledgeSubtype, string[]>;
|
|
27
|
+
/** Sections whose content only ever grows — sync merges union their lines
|
|
28
|
+
* instead of letting the newer side replace the older (see merge.ts). */
|
|
29
|
+
export declare const APPEND_ONLY_SECTIONS: ReadonlySet<string>;
|
|
30
|
+
/** Session-file sections. Sessions have no TypeContract (they are not typed
|
|
31
|
+
* entities), but their headings are still structural for Document splitting. */
|
|
32
|
+
export declare const SESSION_SECTIONS: readonly ["Summary", "Entities Touched", "Decisions"];
|
package/dist/contracts.js
CHANGED
|
@@ -63,8 +63,8 @@ export function hasCapability(type, section) {
|
|
|
63
63
|
return contract.defaultSections.includes(section) || contract.optionalSections.includes(section);
|
|
64
64
|
}
|
|
65
65
|
// --- Canonical enum tuples (single source of truth) ---
|
|
66
|
-
// Every consumer (file validators,
|
|
67
|
-
//
|
|
66
|
+
// Every consumer (file validators, the Op Spec table, help renderer) imports
|
|
67
|
+
// these — no other file may declare local copies.
|
|
68
68
|
export const ENTITY_TYPES = ['capture', 'entity', 'project'];
|
|
69
69
|
export const ENTITY_STATUSES = ['active', 'paused', 'done'];
|
|
70
70
|
export const SESSION_LOG_TYPES = ['progress', 'decision', 'blocker', 'research'];
|
|
@@ -85,3 +85,9 @@ export const SUBTYPE_SECTIONS = {
|
|
|
85
85
|
insight: ['Synthesis', 'Evidence', 'Sources'],
|
|
86
86
|
plan: ['Goal', 'Steps', 'Dependencies'],
|
|
87
87
|
};
|
|
88
|
+
/** Sections whose content only ever grows — sync merges union their lines
|
|
89
|
+
* instead of letting the newer side replace the older (see merge.ts). */
|
|
90
|
+
export const APPEND_ONLY_SECTIONS = new Set(['Evidence', 'Sources']);
|
|
91
|
+
/** Session-file sections. Sessions have no TypeContract (they are not typed
|
|
92
|
+
* entities), but their headings are still structural for Document splitting. */
|
|
93
|
+
export const SESSION_SECTIONS = ['Summary', 'Entities Touched', 'Decisions'];
|
package/dist/db.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { readFileSync, statSync, existsSync, unlinkSync } from 'fs';
|
|
4
4
|
import { join, basename } from 'path';
|
|
5
|
-
import { parseFilePath } from './vault.js';
|
|
5
|
+
import { parseFilePath, scanVaultDir, kindDir, INDEXED_KINDS } from './vault.js';
|
|
6
6
|
import { ensureLocalExclude } from './knowledge-index.js';
|
|
7
|
-
import { getNextActions, getBrief, WIKI_LINK_RE, findSection } from './
|
|
8
|
-
import { parseSessionLog } from './
|
|
7
|
+
import { getNextActions, getBrief, WIKI_LINK_RE, findSection } from './document.js';
|
|
8
|
+
import { parseSessionLog } from './document.js';
|
|
9
9
|
// Bump this when indexing logic changes to force a full rebuild.
|
|
10
10
|
// v5: wiki link harvesting (mention relations), parent link indexing, relation filter on queries.
|
|
11
11
|
// v6: description column on items.
|
|
@@ -86,16 +86,6 @@ const STOPWORDS = new Set([
|
|
|
86
86
|
function hashContent(content) {
|
|
87
87
|
return createHash('sha256').update(content).digest('hex');
|
|
88
88
|
}
|
|
89
|
-
function scanDir(dirPath) {
|
|
90
|
-
try {
|
|
91
|
-
// Skip dotfiles: their slugs are rejected by resolveFilePath, so indexing
|
|
92
|
-
// them would list entities that read/update can never reach.
|
|
93
|
-
return readdirSync(dirPath).filter(f => f.endsWith('.md') && !f.startsWith('.')).map(f => join(dirPath, f));
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
89
|
// --- Shared query functions (used by both write and readonly paths) ---
|
|
100
90
|
function sanitizeFtsQuery(raw) {
|
|
101
91
|
const cleaned = raw.replace(/[^\w\s]/g, ' ');
|
|
@@ -583,8 +573,8 @@ export function createIndex(vaultPath, opts) {
|
|
|
583
573
|
while ((wikiMatch = linkRe.exec(authoredBody)) !== null) {
|
|
584
574
|
const targetSlug = wikiMatch[1];
|
|
585
575
|
// Check if target file exists on disk (not in index — target may not be indexed yet)
|
|
586
|
-
const targetEntityPath = join(vaultPath, '
|
|
587
|
-
const targetKnowledgePath = join(vaultPath, 'knowledge', `${targetSlug}.md`);
|
|
576
|
+
const targetEntityPath = join(vaultPath, kindDir('entity'), `${targetSlug}.md`);
|
|
577
|
+
const targetKnowledgePath = join(vaultPath, kindDir('knowledge'), `${targetSlug}.md`);
|
|
588
578
|
const targetExists = knownSlugs
|
|
589
579
|
? knownSlugs.has(targetSlug)
|
|
590
580
|
: existsSync(targetEntityPath) || existsSync(targetKnowledgePath);
|
|
@@ -598,16 +588,16 @@ export function createIndex(vaultPath, opts) {
|
|
|
598
588
|
db.exec('DELETE FROM links');
|
|
599
589
|
// Collect all vault slugs for wiki link target validation
|
|
600
590
|
const allVaultSlugs = new Set();
|
|
601
|
-
for (const file of
|
|
591
|
+
for (const file of scanVaultDir(join(vaultPath, kindDir('entity')))) {
|
|
602
592
|
allVaultSlugs.add(basename(file, '.md'));
|
|
603
593
|
}
|
|
604
|
-
for (const file of
|
|
594
|
+
for (const file of scanVaultDir(join(vaultPath, kindDir('knowledge')))) {
|
|
605
595
|
allVaultSlugs.add(basename(file, '.md'));
|
|
606
596
|
}
|
|
607
|
-
for (const file of
|
|
597
|
+
for (const file of scanVaultDir(join(vaultPath, kindDir('entity')))) {
|
|
608
598
|
indexFile(file, 'entity', allVaultSlugs);
|
|
609
599
|
}
|
|
610
|
-
for (const file of
|
|
600
|
+
for (const file of scanVaultDir(join(vaultPath, kindDir('knowledge')))) {
|
|
611
601
|
indexFile(file, 'knowledge', allVaultSlugs);
|
|
612
602
|
}
|
|
613
603
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_sync', ?)")
|
|
@@ -624,12 +614,9 @@ export function createIndex(vaultPath, opts) {
|
|
|
624
614
|
}
|
|
625
615
|
const lastSyncTime = parseInt(lastSync.value, 10);
|
|
626
616
|
let changed = false;
|
|
627
|
-
const dirs =
|
|
628
|
-
{ dir: 'entities', kind: 'entity' },
|
|
629
|
-
{ dir: 'knowledge', kind: 'knowledge' },
|
|
630
|
-
];
|
|
617
|
+
const dirs = INDEXED_KINDS.map(kind => ({ dir: kindDir(kind), kind }));
|
|
631
618
|
for (const { dir, kind } of dirs) {
|
|
632
|
-
for (const file of
|
|
619
|
+
for (const file of scanVaultDir(join(vaultPath, dir))) {
|
|
633
620
|
if (statSync(file).mtimeMs > lastSyncTime) {
|
|
634
621
|
indexFile(file, kind);
|
|
635
622
|
changed = true;
|
|
@@ -640,7 +627,7 @@ export function createIndex(vaultPath, opts) {
|
|
|
640
627
|
const dbSlugs = db.prepare('SELECT slug FROM items').all().map(r => r.slug);
|
|
641
628
|
const fileSlugs = new Set();
|
|
642
629
|
for (const { dir } of dirs) {
|
|
643
|
-
for (const file of
|
|
630
|
+
for (const file of scanVaultDir(join(vaultPath, dir))) {
|
|
644
631
|
fileSlugs.add(basename(file, '.md'));
|
|
645
632
|
}
|
|
646
633
|
}
|
|
@@ -656,8 +643,7 @@ export function createIndex(vaultPath, opts) {
|
|
|
656
643
|
}
|
|
657
644
|
}
|
|
658
645
|
function upsertFromFile(slug, kind) {
|
|
659
|
-
const
|
|
660
|
-
const filePath = join(vaultPath, dir, `${slug}.md`);
|
|
646
|
+
const filePath = join(vaultPath, kindDir(kind), `${slug}.md`);
|
|
661
647
|
if (existsSync(filePath)) {
|
|
662
648
|
// Force re-index by clearing cached hash
|
|
663
649
|
deleteItemStmt.run(slug);
|
package/dist/digest.js
CHANGED
|
@@ -60,6 +60,8 @@ export function digest(input, existingSlugs, now = new Date()) {
|
|
|
60
60
|
// leak into the note body (the schema promises they are omitted).
|
|
61
61
|
const noteBody = buildSessionNoteBody(input, knownTouched);
|
|
62
62
|
const related = knownTouched;
|
|
63
|
+
// Typechecked against the Op Spec's session-create schema so digest output
|
|
64
|
+
// can never silently diverge from what batch validation accepts.
|
|
63
65
|
const noteFields = {
|
|
64
66
|
synthesis: noteBody,
|
|
65
67
|
'session-date': input.session_date,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { SessionLogEntry, SessionLogType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Known structural section headings in Hafez entities, knowledge notes, and
|
|
4
|
+
* session files. Derived from contracts.ts (defaultSections + optionalSections
|
|
5
|
+
* + session sections) plus auto-generated sections not tracked in contracts
|
|
6
|
+
* (Related). Only these are treated as section boundaries. Any other `## `
|
|
7
|
+
* heading inside section content (e.g. in a brief or session log) is
|
|
8
|
+
* preserved as-is.
|
|
9
|
+
*/
|
|
10
|
+
export declare const STRUCTURAL_SECTIONS: Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Find the next structural `## Heading` boundary after a given position.
|
|
13
|
+
* Skips `## ` headings that are not known structural sections (content headings).
|
|
14
|
+
* Returns the index of the `\n` before the `## `, or -1 if no structural heading found.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findNextStructuralHeading(body: string, from: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Find a ## Heading section and its content.
|
|
19
|
+
* Returns { start, headingEnd, end, content } or null if not found.
|
|
20
|
+
* Matches heading exactly (e.g. "## Brief" won't match "## Briefing").
|
|
21
|
+
* Trailing whitespace on the heading line is tolerated — the splitter's
|
|
22
|
+
* boundary tokenizer trims headings, so anything it treats as a boundary
|
|
23
|
+
* must also be findable here (a hand-edited `## Session Log ` otherwise
|
|
24
|
+
* gets a duplicate section and archival never fires).
|
|
25
|
+
* Only treats known structural headings as section boundaries -- content
|
|
26
|
+
* inside a section may contain arbitrary `## ` headings without corruption.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findSection(body: string, heading: string): {
|
|
29
|
+
start: number;
|
|
30
|
+
headingEnd: number;
|
|
31
|
+
end: number;
|
|
32
|
+
content: string;
|
|
33
|
+
} | null;
|
|
34
|
+
export interface DocSection {
|
|
35
|
+
heading: string;
|
|
36
|
+
content: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Split a body into preamble + structural sections. Non-structural `## `
|
|
40
|
+
* headings stay inside the enclosing section's content — sections are
|
|
41
|
+
* atomic units of authored content, never split at user headings.
|
|
42
|
+
* Headings and content come back trimmed; reassemble with
|
|
43
|
+
* `## ${heading}\n\n${content}` joined by blank lines.
|
|
44
|
+
*/
|
|
45
|
+
export declare function splitStructural(body: string): {
|
|
46
|
+
preamble: string;
|
|
47
|
+
sections: DocSection[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Replace a structural section's content, or create the section: before
|
|
51
|
+
* `insertBefore` when that section exists, at EOF otherwise.
|
|
52
|
+
*/
|
|
53
|
+
export declare function setSection(body: string, heading: string, content: string, insertBefore?: string): string;
|
|
54
|
+
/** Append content at the end of a structural section (creating it at EOF if absent). */
|
|
55
|
+
export declare function appendToSection(body: string, heading: string, content: string): string;
|
|
56
|
+
/** Insert a formatted session-log entry newest-first (creating the section at EOF if absent). */
|
|
57
|
+
export declare function prependSessionLogEntry(body: string, entry: string): string;
|
|
58
|
+
/** Body up to the real Session Log section — the `summary` read depth. */
|
|
59
|
+
export declare function bodyBeforeSessionLog(body: string): string;
|
|
60
|
+
export declare function parseSessionLogHeading(line: string): {
|
|
61
|
+
date: string;
|
|
62
|
+
agent: string;
|
|
63
|
+
type: SessionLogType;
|
|
64
|
+
} | null;
|
|
65
|
+
export declare function formatSessionLogEntry(entry: SessionLogEntry): string;
|
|
66
|
+
/** Entries inside the real Session Log section only — entry-shaped lines
|
|
67
|
+
* elsewhere in the body (e.g. quoted in a Brief) do not count. */
|
|
68
|
+
export declare function countSessionLogEntries(body: string): number;
|
|
69
|
+
export declare function parseSessionLog(body: string): Array<{
|
|
70
|
+
date: string;
|
|
71
|
+
agent: string;
|
|
72
|
+
type: SessionLogType;
|
|
73
|
+
summary: string;
|
|
74
|
+
}>;
|
|
75
|
+
export declare function extractOldestSessionLogEntry(body: string): {
|
|
76
|
+
entry: string;
|
|
77
|
+
remaining: string;
|
|
78
|
+
} | null;
|
|
79
|
+
export declare function getBrief(body: string): string | null;
|
|
80
|
+
export declare function setBrief(body: string, content: string): string;
|
|
81
|
+
export declare function removeBrief(body: string): string;
|
|
82
|
+
export declare function getNextActions(body: string): string[];
|
|
83
|
+
export declare function addNextAction(body: string, action: string): string;
|
|
84
|
+
export declare function completeNextAction(body: string, query: string): {
|
|
85
|
+
matched: string;
|
|
86
|
+
body: string;
|
|
87
|
+
};
|
|
88
|
+
export declare function removeNextAction(body: string, query: string): {
|
|
89
|
+
matched: string;
|
|
90
|
+
body: string;
|
|
91
|
+
};
|
|
92
|
+
export declare function clearNextActions(body: string): string;
|
|
93
|
+
export declare function firstNonEmptyLine(s: string | null | undefined): string;
|
|
94
|
+
/**
|
|
95
|
+
* One-line summary for a doc: frontmatter description first, then the first
|
|
96
|
+
* non-empty line of the kind's primary section (entity: Brief; knowledge:
|
|
97
|
+
* Synthesis, falling back to Goal for plans; session: Summary).
|
|
98
|
+
* Shared by the vault index and the OKF export so the two surfaces agree.
|
|
99
|
+
*/
|
|
100
|
+
export declare function deriveSummary(kind: 'entity' | 'knowledge' | 'session', fm: Record<string, any>, body: string): string;
|
|
101
|
+
/** The one wiki-link syntax the vault recognizes: [[slug]] or
|
|
102
|
+
* [[slug|display text]] — slug in group 1, display text in group 2.
|
|
103
|
+
* Non-global — wrap with `new RegExp(WIKI_LINK_RE.source, 'g')` for iteration. */
|
|
104
|
+
export declare const WIKI_LINK_RE: RegExp;
|
|
105
|
+
export declare function syncRelatedSection(body: string, frontmatter: {
|
|
106
|
+
parent?: string;
|
|
107
|
+
related?: string[];
|
|
108
|
+
}): string;
|