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/README.md +24 -4
- package/dist/apply.d.ts +2 -1
- package/dist/apply.js +16 -19
- package/dist/batch-ops.js +3 -1
- package/dist/cli/block-hook.js +24 -9
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +106 -48
- package/dist/cli/format.d.ts +8 -3
- package/dist/cli/format.js +42 -4
- package/dist/cli/index.js +29 -17
- package/dist/cli/install.d.ts +9 -0
- package/dist/cli/install.js +36 -15
- package/dist/cli/onboard.js +30 -9
- package/dist/cli/upgrade.js +4 -3
- 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 +84 -5
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +31 -16
- package/dist/types.d.ts +63 -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/surfaces/items.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ import type { Mode } from '../cli/install.js';
|
|
|
9
9
|
export type SurfaceId = 'block' | 'hafezSkillDescription' | 'hafezSkillBody' | 'hafezSaveDescription' | 'hafezSaveBody' | 'read' | 'readHeader' | 'readFooter' | 'readMiss' | 'updateSuccess' | 'updateMisuse' | 'createKnowledge' | 'digestError' | 'batchMisuse' | 'mechanised' | 'schema' | 'onboard';
|
|
10
10
|
/** Places another Item inside this one's text, counted against the same Surface. */
|
|
11
11
|
export type Place = (id: string) => string;
|
|
12
|
+
/** What a static Surface is composed for. `vaultRepo`: the Block for a cloud
|
|
13
|
+
* container, whose vault is that GitHub repo (#160). */
|
|
14
|
+
export interface SurfaceCtx {
|
|
15
|
+
vaultRepo?: string;
|
|
16
|
+
}
|
|
12
17
|
export interface Item {
|
|
13
18
|
/** The Item's row text in the owner table. */
|
|
14
19
|
label: string;
|
|
@@ -21,7 +26,7 @@ export interface Item {
|
|
|
21
26
|
evidence: string;
|
|
22
27
|
/** Static text. A function form may place nested Items, and may give the
|
|
23
28
|
* fallback a shorter form than the Owner. */
|
|
24
|
-
text?: string | ((place: Place, at: 'owner' | 'fallback') => string);
|
|
29
|
+
text?: string | ((place: Place, at: 'owner' | 'fallback', ctx: SurfaceCtx) => string);
|
|
25
30
|
/** What the binary computes: for Items owned by command output, and for a
|
|
26
31
|
* static Item a binary Surface places filled in (the create line in the
|
|
27
32
|
* `read` miss). */
|
|
@@ -94,6 +99,19 @@ declare function renderUpdateSummary({ slug, fields, result }: {
|
|
|
94
99
|
/** The suffix a Stale open line carries on `read`: its age, or that it is
|
|
95
100
|
* undated. A fresh line carries none. */
|
|
96
101
|
declare function renderStaleMarker({ stale, age_days }: ActionAge): string;
|
|
102
|
+
/** The `read` header line naming the sessions that touched an entity, with
|
|
103
|
+
* the look back from it. None: no line. */
|
|
104
|
+
declare function renderSessionsLine({ slug, count, latest }: {
|
|
105
|
+
slug: string;
|
|
106
|
+
count: number;
|
|
107
|
+
latest: string | null;
|
|
108
|
+
}): string;
|
|
109
|
+
/** The `read` header line naming an entity's archived log entries, with the
|
|
110
|
+
* command that reads them. No archive: no line. */
|
|
111
|
+
declare function renderArchivePointer({ slug, entries }: {
|
|
112
|
+
slug: string;
|
|
113
|
+
entries: number;
|
|
114
|
+
}): string;
|
|
97
115
|
/** The line after an update's success while Stale lines remain on the entity. */
|
|
98
116
|
declare function renderStaleReminder({ handles }: {
|
|
99
117
|
handles: string[];
|
|
@@ -129,6 +147,15 @@ declare function renderTeachingLines({ harness, mode, line2Only }: {
|
|
|
129
147
|
mode: Mode;
|
|
130
148
|
line2Only?: boolean;
|
|
131
149
|
}): string;
|
|
150
|
+
/** A GitHub `<owner>/<repo>`, as one shell word safe to print in a command
|
|
151
|
+
* and to name a directory by: not `.` or `..`, not a clone URL's `.git`. */
|
|
152
|
+
export declare const isVaultRepo: (value: string) => boolean;
|
|
153
|
+
/** `onboard --cloud`: the cloud environment's setup script. It cannot clone
|
|
154
|
+
* (it runs without GitHub credentials) and no step may fail session start: a
|
|
155
|
+
* failed install exits 0, leaving a session without Hafez rather than none. */
|
|
156
|
+
declare function renderCloudSetup({ vaultRepo }: {
|
|
157
|
+
vaultRepo: string;
|
|
158
|
+
}): string;
|
|
132
159
|
export declare const ITEMS: {
|
|
133
160
|
loadTriggers: {
|
|
134
161
|
label: string;
|
|
@@ -159,6 +186,20 @@ export declare const ITEMS: {
|
|
|
159
186
|
description: string | null;
|
|
160
187
|
}) => string;
|
|
161
188
|
};
|
|
189
|
+
sessionsLine: {
|
|
190
|
+
label: string;
|
|
191
|
+
owner: "readHeader";
|
|
192
|
+
invokeOnly: string;
|
|
193
|
+
evidence: string;
|
|
194
|
+
render: typeof renderSessionsLine;
|
|
195
|
+
};
|
|
196
|
+
archivePointer: {
|
|
197
|
+
label: string;
|
|
198
|
+
owner: "readHeader";
|
|
199
|
+
invokeOnly: string;
|
|
200
|
+
evidence: string;
|
|
201
|
+
render: typeof renderArchivePointer;
|
|
202
|
+
};
|
|
162
203
|
writeTriggers: {
|
|
163
204
|
label: string;
|
|
164
205
|
owner: "block";
|
|
@@ -354,6 +395,20 @@ export declare const ITEMS: {
|
|
|
354
395
|
evidence: string;
|
|
355
396
|
render: typeof renderHarnessPermissions;
|
|
356
397
|
};
|
|
398
|
+
cloudVault: {
|
|
399
|
+
label: string;
|
|
400
|
+
owner: "block";
|
|
401
|
+
invokeOnly: string;
|
|
402
|
+
evidence: string;
|
|
403
|
+
text: (_place: Place, _at: "owner" | "fallback", { vaultRepo }: SurfaceCtx) => string;
|
|
404
|
+
};
|
|
405
|
+
cloudSetup: {
|
|
406
|
+
label: string;
|
|
407
|
+
owner: "onboard";
|
|
408
|
+
invokeOnly: string;
|
|
409
|
+
evidence: string;
|
|
410
|
+
render: typeof renderCloudSetup;
|
|
411
|
+
};
|
|
357
412
|
teachingLines: {
|
|
358
413
|
label: string;
|
|
359
414
|
owner: "onboard";
|
package/dist/surfaces/items.js
CHANGED
|
@@ -162,6 +162,20 @@ function renderStaleMarker({ stale, age_days }) {
|
|
|
162
162
|
return '';
|
|
163
163
|
return age_days === null ? ' · stale, undated' : ` · stale ${age_days}d`;
|
|
164
164
|
}
|
|
165
|
+
/** The `read` header line naming the sessions that touched an entity, with
|
|
166
|
+
* the look back from it. None: no line. */
|
|
167
|
+
function renderSessionsLine({ slug, count, latest }) {
|
|
168
|
+
if (count === 0)
|
|
169
|
+
return '';
|
|
170
|
+
return `sessions: ${count}${latest ? ` · latest ${latest}` : ''} · hafez query --kind session --related-to ${slug}`;
|
|
171
|
+
}
|
|
172
|
+
/** The `read` header line naming an entity's archived log entries, with the
|
|
173
|
+
* command that reads them. No archive: no line. */
|
|
174
|
+
function renderArchivePointer({ slug, entries }) {
|
|
175
|
+
if (entries === 0)
|
|
176
|
+
return '';
|
|
177
|
+
return `archive: ${entries} older log ${entries === 1 ? 'entry' : 'entries'} · hafez read ${slug} --archive`;
|
|
178
|
+
}
|
|
165
179
|
/** The line after an update's success while Stale lines remain on the entity. */
|
|
166
180
|
function renderStaleReminder({ handles }) {
|
|
167
181
|
const n = handles.length;
|
|
@@ -221,6 +235,38 @@ function renderTeachingLines({ harness, mode, line2Only = false }) {
|
|
|
221
235
|
'4. If a hafez command ever asks for permission, setup is unfinished: run `hafez onboard` again.',
|
|
222
236
|
].join('\n');
|
|
223
237
|
}
|
|
238
|
+
/** A GitHub `<owner>/<repo>`, as one shell word safe to print in a command
|
|
239
|
+
* and to name a directory by: not `.` or `..`, not a clone URL's `.git`. */
|
|
240
|
+
export const isVaultRepo = (value) => /^[A-Za-z0-9-]+\/[\w.-]+$/.test(value) && !/\/\.\.?$/.test(value) && !value.endsWith('.git');
|
|
241
|
+
/** Where a cloud container clones a repo attached to the session. */
|
|
242
|
+
const CLOUD_HOME = '/home/user';
|
|
243
|
+
const repoName = (vaultRepo) => vaultRepo.slice(vaultRepo.indexOf('/') + 1);
|
|
244
|
+
/** The Block's self-heal rule in a cloud container: a session started without
|
|
245
|
+
* the vault attached attaches, clones and registers it (verified, #160). */
|
|
246
|
+
function renderCloudVault(vaultRepo) {
|
|
247
|
+
const path = `${CLOUD_HOME}/${repoName(vaultRepo)}`;
|
|
248
|
+
return `If \`hafez init\` shows no registered vault: attach \`${vaultRepo}\` to this session (add_repo, push access), clone it to ${path}, run \`hafez init --register ${path}\`, then continue. Never create a throwaway vault.`;
|
|
249
|
+
}
|
|
250
|
+
/** `onboard --cloud`: the cloud environment's setup script. It cannot clone
|
|
251
|
+
* (it runs without GitHub credentials) and no step may fail session start: a
|
|
252
|
+
* failed install exits 0, leaving a session without Hafez rather than none. */
|
|
253
|
+
function renderCloudSetup({ vaultRepo }) {
|
|
254
|
+
return [
|
|
255
|
+
`Each Claude Code cloud session is a fresh container: no \`hafez\`, no vault, no Block. Paste this into the cloud environment's Setup script; the Trusted network policy is enough.`,
|
|
256
|
+
'',
|
|
257
|
+
fence([
|
|
258
|
+
'#!/bin/bash',
|
|
259
|
+
'npm install -g hafez --allow-scripts=better-sqlite3 || exit 0',
|
|
260
|
+
`VAULT=${CLOUD_HOME}/${repoName(vaultRepo)}`,
|
|
261
|
+
'if [ -d "$VAULT/.git" ]; then',
|
|
262
|
+
' hafez init --register "$VAULT"',
|
|
263
|
+
'fi',
|
|
264
|
+
`hafez onboard --install --mode always-on --harness claude-code --vault-repo ${vaultRepo} || true`,
|
|
265
|
+
].join('\n')),
|
|
266
|
+
'',
|
|
267
|
+
`Attach ${vaultRepo} when you start a session, with push access, and the script registers it. The script cannot clone it: it runs without GitHub credentials. A session started without it attached is told by the Block to attach, clone and register it.`,
|
|
268
|
+
].join('\n');
|
|
269
|
+
}
|
|
224
270
|
const BATCH_EXAMPLE_OP = '{"op":"create","kind":"entity","name":"Token Rotation","fields":{"type":"entity"}}';
|
|
225
271
|
export const ITEMS = {
|
|
226
272
|
loadTriggers: {
|
|
@@ -259,13 +305,28 @@ export const ITEMS = {
|
|
|
259
305
|
* in brackets when it has one. */
|
|
260
306
|
render: ({ slug, description }) => description ? `${slug} (${description})` : slug,
|
|
261
307
|
},
|
|
308
|
+
sessionsLine: {
|
|
309
|
+
label: 'Sessions line: the look back from an entity',
|
|
310
|
+
owner: 'readHeader',
|
|
311
|
+
invokeOnly: 'same',
|
|
312
|
+
evidence: '#35',
|
|
313
|
+
render: renderSessionsLine,
|
|
314
|
+
},
|
|
315
|
+
archivePointer: {
|
|
316
|
+
label: 'Archive pointer',
|
|
317
|
+
owner: 'readHeader',
|
|
318
|
+
invokeOnly: 'same',
|
|
319
|
+
evidence: '#34',
|
|
320
|
+
render: renderArchivePointer,
|
|
321
|
+
},
|
|
262
322
|
writeTriggers: {
|
|
263
|
-
label: 'Write triggers: work landed, insight, weighing, "note this"',
|
|
323
|
+
label: 'Write triggers: work landed, decision made, insight, weighing, "note this"',
|
|
264
324
|
owner: 'block',
|
|
265
325
|
invokeOnly: 'absent',
|
|
266
|
-
evidence: 'M11, M2, never-ask decision #42',
|
|
326
|
+
evidence: 'M11, M2, never-ask decision #42, decision-record miss #159',
|
|
267
327
|
text: place => [
|
|
268
|
-
'- Work landed (a commit, a merged PR, CI green): update the entity you read. The end of its `read` output shows the command.',
|
|
328
|
+
'- Work landed (a commit, a merged PR, CI green): update the entity that owns the work. That is the entity you read, unless the work belongs to another: read that one first. The end of its `read` output shows the command.',
|
|
329
|
+
'- A decision was made: if the repo keeps its own decisions record, write it there now and give Hafez one line that points at it (`hafez update <slug> --log "decision: <one line>, see decisions.md"`). Otherwise it goes in the save\'s `decisions`.',
|
|
269
330
|
`- An insight emerged that is useful beyond this repo: create a knowledge note. ${place('knowledgeBoundary')}`,
|
|
270
331
|
'- The user says "note this" or "record that": write it.',
|
|
271
332
|
`- You are weighing whether something is worth writing: write it, as knowledge with \`--confidence ${CONFIDENCE_LEVELS[0]}\`.`,
|
|
@@ -438,9 +499,11 @@ export const ITEMS = {
|
|
|
438
499
|
evidence: 'help decision #49',
|
|
439
500
|
text: fence([
|
|
440
501
|
`hafez query --filter ${oneOf([...ENTITY_STATUSES, 'stale', 'capture'])}`,
|
|
441
|
-
'hafez search "<terms>" --kind <entity|knowledge>',
|
|
502
|
+
'hafez search "<terms>" --kind <entity|knowledge|session|all>',
|
|
503
|
+
'hafez query --kind session --related-to <slug>',
|
|
442
504
|
'hafez stats',
|
|
443
|
-
].join('\n'))
|
|
505
|
+
].join('\n'))
|
|
506
|
+
+ '\n\n`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.',
|
|
444
507
|
},
|
|
445
508
|
rareOps: {
|
|
446
509
|
label: 'Rare ops: promote, link, unlink, status, delete, validate',
|
|
@@ -517,6 +580,22 @@ export const ITEMS = {
|
|
|
517
580
|
evidence: 'never-ask decision #42; onboard decision #58',
|
|
518
581
|
render: renderHarnessPermissions,
|
|
519
582
|
},
|
|
583
|
+
cloudVault: {
|
|
584
|
+
label: 'Cloud session: no vault registered, attach the vault repo, clone, register',
|
|
585
|
+
owner: 'block',
|
|
586
|
+
invokeOnly: 'absent',
|
|
587
|
+
evidence: '#160, verified 2026-10-16',
|
|
588
|
+
// Placed only in a Block composed for a cloud container, whose begin
|
|
589
|
+
// marker names the repo (renderBlock).
|
|
590
|
+
text: (_place, _at, { vaultRepo = '<owner>/<repo>' }) => renderCloudVault(vaultRepo),
|
|
591
|
+
},
|
|
592
|
+
cloudSetup: {
|
|
593
|
+
label: 'The cloud environment\'s setup script',
|
|
594
|
+
owner: 'onboard',
|
|
595
|
+
invokeOnly: 'not offered',
|
|
596
|
+
evidence: '#160, verified 2026-10-16',
|
|
597
|
+
render: renderCloudSetup,
|
|
598
|
+
},
|
|
520
599
|
teachingLines: {
|
|
521
600
|
label: 'The four teaching lines (to the user, not the agent)',
|
|
522
601
|
owner: 'onboard',
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import { type FolderMatch, type Item, type ItemId, type SurfaceId } from './items.js';
|
|
1
|
+
import { type FolderMatch, type Item, type ItemId, type SurfaceCtx, type SurfaceId } from './items.js';
|
|
2
2
|
export interface Surface<Id extends string = ItemId> {
|
|
3
3
|
/** How the owner table names this Surface. */
|
|
4
4
|
label: string;
|
|
5
5
|
/** Static Surfaces only: the text, built by placing Items. */
|
|
6
|
-
compose?: (place: (id: Id) => string) => string;
|
|
6
|
+
compose?: (place: (id: Id) => string, ctx: SurfaceCtx) => string;
|
|
7
7
|
}
|
|
8
8
|
export declare const SURFACES: Record<SurfaceId, Surface>;
|
|
9
9
|
export declare const contentHash: (text: string) => string;
|
|
10
10
|
/** An Item's text as a binary Surface places it at run time, in the form its
|
|
11
11
|
* role there (Owner or fallback) calls for. */
|
|
12
12
|
export declare function placeIn(surfaceId: SurfaceId, id: ItemId): string;
|
|
13
|
-
|
|
13
|
+
/** The Block; with `vaultRepo`, the Block for a cloud container whose vault
|
|
14
|
+
* is that repo. Its begin marker names the repo, so a check reads it from
|
|
15
|
+
* there and never from the Block's wording, which a release may change. */
|
|
16
|
+
export declare function renderBlock(vaultRepo?: string): string;
|
|
14
17
|
/** `onboard --block`, what the SessionStart hook prints: the Block, then the
|
|
15
18
|
* read line for each entity matching the folder. */
|
|
16
19
|
export declare function renderBlockHook(matches: FolderMatch[]): string;
|
|
@@ -26,7 +29,7 @@ export declare function renderOwnerTable(): string;
|
|
|
26
29
|
/** Replace the marked owner-table region of `doc` with a fresh render. */
|
|
27
30
|
export declare function spliceOwnerTable(doc: string): string;
|
|
28
31
|
/** Every problem with the Item table: an Item with no Owner or no evidence
|
|
29
|
-
* pointer, an Item its Owner or static fallback never places, or one placed more
|
|
30
|
-
* Surface or in a Surface that is neither its Owner nor its declared fallback.
|
|
32
|
+
* pointer, an Item its Owner or static fallback never places in any context, or one placed more
|
|
33
|
+
* than once in a Surface or in a Surface that is neither its Owner nor its declared fallback.
|
|
31
34
|
* Placement is checked for static Surfaces only; binary ones place at run time. */
|
|
32
35
|
export declare function itemProblems(items: Record<string, Item>, surfaces: Record<string, Surface<string>>): string[];
|
|
@@ -13,11 +13,12 @@ import { ITEMS } from './items.js';
|
|
|
13
13
|
export const SURFACES = {
|
|
14
14
|
block: {
|
|
15
15
|
label: 'Block',
|
|
16
|
-
compose: place => [
|
|
16
|
+
compose: (place, { vaultRepo }) => [
|
|
17
17
|
'## Hafez',
|
|
18
18
|
'',
|
|
19
19
|
'Hafez is this user\'s vault: what they are working on (entities) and what they have learned (knowledge). You reach it only through the `hafez` command.',
|
|
20
20
|
'',
|
|
21
|
+
...(vaultRepo === undefined ? [] : [`**Cloud session.** ${place('cloudVault')}`, '']),
|
|
21
22
|
`**Load.** ${place('loadTriggers')}`,
|
|
22
23
|
'',
|
|
23
24
|
'**Write as it happens.** Each of these is due the moment it occurs:',
|
|
@@ -96,11 +97,14 @@ export const SURFACES = {
|
|
|
96
97
|
schema: { label: '`schema`' },
|
|
97
98
|
onboard: { label: '`onboard`' },
|
|
98
99
|
};
|
|
100
|
+
/** Every context a static Surface is composed in: the default, and the
|
|
101
|
+
* Block for a cloud container. */
|
|
102
|
+
const CONTEXTS = [{}, { vaultRepo: '<owner>/<repo>' }];
|
|
99
103
|
export const contentHash = (text) => createHash('sha256').update(text).digest('hex').slice(0, 12);
|
|
100
104
|
/** Compose a static Surface, placing each Item's text in the form its role
|
|
101
105
|
* (Owner or fallback) calls for. `onPlace` sees every placement, nested ones
|
|
102
106
|
* included. */
|
|
103
|
-
function composeWith(items, surfaceId, surface, onPlace = () => { }) {
|
|
107
|
+
function composeWith(items, surfaceId, surface, ctx = {}, onPlace = () => { }) {
|
|
104
108
|
const place = (id) => {
|
|
105
109
|
onPlace(id);
|
|
106
110
|
const item = items[id];
|
|
@@ -108,16 +112,16 @@ function composeWith(items, surfaceId, surface, onPlace = () => { }) {
|
|
|
108
112
|
throw new Error(`${surfaceId} places unknown Item ${id}`);
|
|
109
113
|
const text = item.text;
|
|
110
114
|
if (typeof text === 'function')
|
|
111
|
-
return text(place, item.owner === surfaceId ? 'owner' : 'fallback');
|
|
115
|
+
return text(place, item.owner === surfaceId ? 'owner' : 'fallback', ctx);
|
|
112
116
|
if (text === undefined)
|
|
113
117
|
throw new Error(`${surfaceId} places ${id}, which has no text`);
|
|
114
118
|
return text;
|
|
115
119
|
};
|
|
116
120
|
if (!surface.compose)
|
|
117
121
|
throw new Error(`${surfaceId} is not a static surface`);
|
|
118
|
-
return surface.compose(place);
|
|
122
|
+
return surface.compose(place, ctx);
|
|
119
123
|
}
|
|
120
|
-
const compose = (id) => composeWith(ITEMS, id, SURFACES[id]);
|
|
124
|
+
const compose = (id, ctx = {}) => composeWith(ITEMS, id, SURFACES[id], ctx);
|
|
121
125
|
/** An Item's text as a binary Surface places it at run time, in the form its
|
|
122
126
|
* role there (Owner or fallback) calls for. */
|
|
123
127
|
export function placeIn(surfaceId, id) {
|
|
@@ -147,9 +151,13 @@ function skillFile(name, description, body) {
|
|
|
147
151
|
const text = compose(body);
|
|
148
152
|
return `---\nname: ${name}\ndescription: >\n${fold(compose(description))}\nhash: ${contentHash(text)}\n---\n${text}`;
|
|
149
153
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
/** The Block; with `vaultRepo`, the Block for a cloud container whose vault
|
|
155
|
+
* is that repo. Its begin marker names the repo, so a check reads it from
|
|
156
|
+
* there and never from the Block's wording, which a release may change. */
|
|
157
|
+
export function renderBlock(vaultRepo) {
|
|
158
|
+
const inner = compose('block', { vaultRepo });
|
|
159
|
+
const repo = vaultRepo === undefined ? '' : ` vault-repo=${vaultRepo}`;
|
|
160
|
+
return `<!-- hafez:begin ${contentHash(inner)}${repo} -->\n${inner}<!-- hafez:end -->\n`;
|
|
153
161
|
}
|
|
154
162
|
/** `onboard --block`, what the SessionStart hook prints: the Block, then the
|
|
155
163
|
* read line for each entity matching the folder. */
|
|
@@ -185,21 +193,28 @@ export function spliceOwnerTable(doc) {
|
|
|
185
193
|
}
|
|
186
194
|
// --- Drift check 2: the typed Item table ---
|
|
187
195
|
/** Every problem with the Item table: an Item with no Owner or no evidence
|
|
188
|
-
* pointer, an Item its Owner or static fallback never places, or one placed more
|
|
189
|
-
* Surface or in a Surface that is neither its Owner nor its declared fallback.
|
|
196
|
+
* pointer, an Item its Owner or static fallback never places in any context, or one placed more
|
|
197
|
+
* than once in a Surface or in a Surface that is neither its Owner nor its declared fallback.
|
|
190
198
|
* Placement is checked for static Surfaces only; binary ones place at run time. */
|
|
191
199
|
export function itemProblems(items, surfaces) {
|
|
192
200
|
const problems = [];
|
|
201
|
+
// Per Surface, the most times each Item is placed in one of its contexts.
|
|
193
202
|
const placed = {};
|
|
194
203
|
for (const [sid, surface] of Object.entries(surfaces)) {
|
|
195
204
|
if (!surface.compose)
|
|
196
205
|
continue;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
const most = (placed[sid] = {});
|
|
207
|
+
for (const ctx of CONTEXTS) {
|
|
208
|
+
const counts = {};
|
|
209
|
+
try {
|
|
210
|
+
composeWith(items, sid, surface, ctx, id => { counts[id] = (counts[id] ?? 0) + 1; });
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
if (!problems.includes(err.message))
|
|
214
|
+
problems.push(err.message);
|
|
215
|
+
}
|
|
216
|
+
for (const [id, n] of Object.entries(counts))
|
|
217
|
+
most[id] = Math.max(most[id] ?? 0, n);
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
for (const [id, item] of Object.entries(items)) {
|
package/dist/types.d.ts
CHANGED
|
@@ -13,8 +13,14 @@ export type PlanStatus = typeof PLAN_STATUSES[number];
|
|
|
13
13
|
* lives on knowledge notes only. Asking for one on the entity branch is a
|
|
14
14
|
* validation error, not a silent fall back to recency, so the entity opts
|
|
15
15
|
* type declares only what that branch can actually sort by. */
|
|
16
|
-
export type SortField = 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count';
|
|
17
|
-
export type EntitySortField = Exclude<SortField, 'last_reinforced' | 'reinforcement_count'>;
|
|
16
|
+
export type SortField = 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count' | 'session_date';
|
|
17
|
+
export type EntitySortField = Exclude<SortField, 'last_reinforced' | 'reinforcement_count' | 'session_date'>;
|
|
18
|
+
export type KnowledgeSortField = Exclude<SortField, 'session_date'>;
|
|
19
|
+
/** A session note carries only its dates and its name. */
|
|
20
|
+
export type SessionSortField = 'session_date' | 'created' | 'name';
|
|
21
|
+
/** What `search` reaches. Unset is the working set, entity + knowledge;
|
|
22
|
+
* `all` is literal, sessions included. */
|
|
23
|
+
export type SearchKind = 'entity' | 'knowledge' | 'session' | 'all';
|
|
18
24
|
export interface EntityQueryOpts {
|
|
19
25
|
filter?: QueryFilter;
|
|
20
26
|
type?: EntityType;
|
|
@@ -39,14 +45,28 @@ export interface KnowledgeQueryOpts {
|
|
|
39
45
|
createdSince?: string;
|
|
40
46
|
createdBefore?: string;
|
|
41
47
|
tags?: string[];
|
|
42
|
-
sort_by?:
|
|
48
|
+
sort_by?: KnowledgeSortField;
|
|
49
|
+
sort_order?: 'asc' | 'desc';
|
|
50
|
+
limit?: number;
|
|
51
|
+
offset?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Sessions carry no status, domain or tags: the time window (`since`/`before`
|
|
54
|
+
* against `session-date`), sort and paging are all a session query takes. */
|
|
55
|
+
export interface SessionQueryOpts {
|
|
56
|
+
since?: string;
|
|
57
|
+
before?: string;
|
|
58
|
+
createdSince?: string;
|
|
59
|
+
createdBefore?: string;
|
|
60
|
+
sort_by?: SessionSortField;
|
|
43
61
|
sort_order?: 'asc' | 'desc';
|
|
44
62
|
limit?: number;
|
|
45
63
|
offset?: number;
|
|
46
64
|
}
|
|
47
65
|
/** `kind: 'all'` (or unset): reaches both sides, so it takes opts from both
|
|
48
66
|
* and can honour every sort field. Status filters narrow the entity side only
|
|
49
|
-
* — knowledge notes carry no status and are never excluded by one.
|
|
67
|
+
* — knowledge notes carry no status and are never excluded by one. Unset
|
|
68
|
+
* is the working set, entity + knowledge; `all` is literal and reaches the
|
|
69
|
+
* sessions too. */
|
|
50
70
|
export interface AllQueryOpts extends Omit<EntityQueryOpts, 'sort_by'> {
|
|
51
71
|
kind?: 'all';
|
|
52
72
|
confidence?: ConfidenceLevel;
|
|
@@ -63,6 +83,8 @@ export type UnifiedQueryOpts = AllQueryOpts | (EntityQueryOpts & {
|
|
|
63
83
|
kind: 'entity';
|
|
64
84
|
}) | (KnowledgeQueryOpts & {
|
|
65
85
|
kind: 'knowledge';
|
|
86
|
+
}) | (SessionQueryOpts & {
|
|
87
|
+
kind: 'session';
|
|
66
88
|
});
|
|
67
89
|
export interface EntityFrontmatter {
|
|
68
90
|
name: string;
|
|
@@ -114,11 +136,8 @@ export type ParsedVaultFile = {
|
|
|
114
136
|
frontmatter: SessionFrontmatter;
|
|
115
137
|
body: string;
|
|
116
138
|
};
|
|
117
|
-
/** What read() returns:
|
|
118
|
-
|
|
119
|
-
export type ParsedFile = Extract<ParsedVaultFile, {
|
|
120
|
-
kind: 'entity' | 'knowledge';
|
|
121
|
-
}>;
|
|
139
|
+
/** What read() returns: any vault file, so consumers narrow on `kind`. */
|
|
140
|
+
export type ParsedFile = ParsedVaultFile;
|
|
122
141
|
export interface SessionLogEntry {
|
|
123
142
|
type: SessionLogType;
|
|
124
143
|
summary: string;
|
|
@@ -221,10 +240,19 @@ export interface KnowledgeQueryResult {
|
|
|
221
240
|
last_reinforced: string | null;
|
|
222
241
|
last_touched: string | null;
|
|
223
242
|
}
|
|
243
|
+
export interface SessionQueryResult {
|
|
244
|
+
slug: string;
|
|
245
|
+
name: string;
|
|
246
|
+
session_date: string;
|
|
247
|
+
created: string;
|
|
248
|
+
related: string[];
|
|
249
|
+
}
|
|
224
250
|
export type UnifiedResult = (QueryResult & {
|
|
225
251
|
kind: 'entity';
|
|
226
252
|
}) | (KnowledgeQueryResult & {
|
|
227
253
|
kind: 'knowledge';
|
|
254
|
+
}) | (SessionQueryResult & {
|
|
255
|
+
kind: 'session';
|
|
228
256
|
});
|
|
229
257
|
export interface ValidationIssue {
|
|
230
258
|
slug: string;
|
|
@@ -237,10 +265,11 @@ export interface ValidationReport {
|
|
|
237
265
|
missing_fields: ValidationIssue[];
|
|
238
266
|
total_entities: number;
|
|
239
267
|
total_knowledge: number;
|
|
268
|
+
total_sessions: number;
|
|
240
269
|
}
|
|
241
270
|
export interface SearchResult {
|
|
242
271
|
slug: string;
|
|
243
|
-
kind: 'entity' | 'knowledge';
|
|
272
|
+
kind: 'entity' | 'knowledge' | 'session';
|
|
244
273
|
name: string;
|
|
245
274
|
snippet: string;
|
|
246
275
|
type?: EntityType;
|
|
@@ -275,7 +304,7 @@ export interface VaultStats {
|
|
|
275
304
|
}
|
|
276
305
|
export interface ChangelogEntry {
|
|
277
306
|
slug: string;
|
|
278
|
-
kind: 'entity' | 'knowledge';
|
|
307
|
+
kind: 'entity' | 'knowledge' | 'session';
|
|
279
308
|
operation: 'created' | 'updated' | 'deleted';
|
|
280
309
|
timestamp: string;
|
|
281
310
|
commit_message: string;
|
|
@@ -355,7 +384,7 @@ export interface BatchValidationReport {
|
|
|
355
384
|
valid: boolean;
|
|
356
385
|
operations: BatchOpValidation[];
|
|
357
386
|
}
|
|
358
|
-
export type HafezErrorCode = 'NOT_FOUND' | 'SLUG_EXISTS' | 'VALIDATION_FAILED' | 'GIT_PUSH_FAILED' | 'GIT_COMMIT_FAILED' | 'VAULT_LOCKED';
|
|
387
|
+
export type HafezErrorCode = 'NOT_FOUND' | 'SLUG_EXISTS' | 'VALIDATION_FAILED' | 'GIT_PUSH_FAILED' | 'GIT_COMMIT_FAILED' | 'GIT_READ_FAILED' | 'VAULT_LOCKED';
|
|
359
388
|
export declare class HafezError extends Error {
|
|
360
389
|
code: HafezErrorCode;
|
|
361
390
|
details?: string[] | undefined;
|
|
@@ -370,10 +399,11 @@ export interface GitConfig {
|
|
|
370
399
|
* production (src/git.ts); tests may inject an in-memory adapter.
|
|
371
400
|
*/
|
|
372
401
|
export interface Journal {
|
|
373
|
-
/** Persist one logical change: stage written+deleted paths (vault-relative
|
|
374
|
-
* and commit under message. Must clean up
|
|
375
|
-
* staged files) before rethrowing on a
|
|
376
|
-
* failure surfaces as GIT_PUSH_FAILED
|
|
402
|
+
/** Persist one logical change: stage written+deleted paths (vault-relative,
|
|
403
|
+
* '/'-separated on every platform) and commit under message. Must clean up
|
|
404
|
+
* its own partial state (e.g. staged files) before rethrowing on a
|
|
405
|
+
* commit-stage failure; a push-stage failure surfaces as GIT_PUSH_FAILED
|
|
406
|
+
* with the commit kept. */
|
|
377
407
|
commit(written: string[], deleted: string[], message: string): Promise<void>;
|
|
378
408
|
/** Bidirectional sync with the remote, if any. */
|
|
379
409
|
sync(): Promise<{
|
|
@@ -381,9 +411,11 @@ export interface Journal {
|
|
|
381
411
|
pushed: boolean;
|
|
382
412
|
remote: boolean;
|
|
383
413
|
}>;
|
|
384
|
-
/** History-derived changelog for
|
|
414
|
+
/** History-derived changelog for the indexed dirs since a date. Empty when
|
|
415
|
+
* the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
|
|
385
416
|
changelog(since: string): Promise<ChangelogEntry[]>;
|
|
386
|
-
/** Newest history timestamp per vault file (modified = any change, added = creation).
|
|
417
|
+
/** Newest history timestamp per vault file (modified = any change, added = creation).
|
|
418
|
+
* Empty when the repo has no commits; any other git failure throws `GIT_READ_FAILED`. */
|
|
387
419
|
fileTimes(mode: 'modified' | 'added'): Promise<Map<string, string>>;
|
|
388
420
|
}
|
|
389
421
|
export interface HafezConfig {
|
|
@@ -432,9 +464,21 @@ export interface Hafez {
|
|
|
432
464
|
items: KnowledgeQueryResult[];
|
|
433
465
|
total: number;
|
|
434
466
|
}>;
|
|
467
|
+
query_sessions(opts?: SessionQueryOpts): Promise<{
|
|
468
|
+
items: SessionQueryResult[];
|
|
469
|
+
total: number;
|
|
470
|
+
}>;
|
|
471
|
+
/** An entity's archived log entries (`entities/archive/<slug>-log.md`), as
|
|
472
|
+
* written; null when it has none. Read from disk, never indexed: the
|
|
473
|
+
* archive is reached from its entity on purpose, never found by search. */
|
|
474
|
+
archive(slug: string): Promise<string | null>;
|
|
435
475
|
validate(): Promise<ValidationReport>;
|
|
436
|
-
search(query: string, kind?:
|
|
476
|
+
search(query: string, kind?: SearchKind): Promise<SearchResult[]>;
|
|
437
477
|
rebuildIndex(): Promise<void>;
|
|
438
478
|
stats(): Promise<VaultStats>;
|
|
439
479
|
changelog(since: string): Promise<ChangelogEntry[]>;
|
|
480
|
+
/** Release the SQLite index handle. Idempotent; the next call that needs
|
|
481
|
+
* the index reopens it. Call before deleting the vault: Windows cannot
|
|
482
|
+
* remove a directory holding an open file. */
|
|
483
|
+
close(): void;
|
|
440
484
|
}
|