hafez 2.1.0 → 2.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/README.md +72 -5
- package/dist/apply.d.ts +14 -1
- package/dist/apply.js +94 -32
- package/dist/batch-ops.d.ts +31 -27
- package/dist/batch-ops.js +83 -16
- package/dist/cli/block-hook.d.ts +6 -0
- package/dist/cli/block-hook.js +27 -12
- package/dist/cli/commands.d.ts +3 -1
- package/dist/cli/commands.js +183 -73
- package/dist/cli/format.d.ts +11 -3
- package/dist/cli/format.js +61 -4
- package/dist/cli/index.js +40 -19
- 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 +5 -4
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.js +9 -0
- package/dist/dates.d.ts +7 -0
- package/dist/dates.js +21 -1
- package/dist/db.d.ts +50 -10
- package/dist/db.js +251 -65
- package/dist/digest.js +14 -7
- package/dist/document.d.ts +2 -0
- package/dist/document.js +2 -2
- package/dist/git.d.ts +13 -2
- package/dist/git.js +121 -14
- package/dist/graph/activity.d.ts +5 -0
- package/dist/graph/activity.js +70 -0
- package/dist/graph/client.js +326 -0
- package/dist/graph/copy-fonts.d.ts +1 -0
- package/dist/graph/copy-fonts.js +13 -0
- package/dist/graph/fields.d.ts +8 -0
- package/dist/graph/fields.js +12 -0
- package/dist/graph/fonts/geist-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono-OFL.txt +93 -0
- package/dist/graph/fonts/geist-mono.woff2 +0 -0
- package/dist/graph/fonts/geist.woff2 +0 -0
- package/dist/graph/fonts/outfit-OFL.txt +93 -0
- package/dist/graph/fonts/outfit.woff2 +0 -0
- package/dist/graph/fonts.d.ts +14 -0
- package/dist/graph/fonts.js +17 -0
- package/dist/graph/live.d.ts +16 -0
- package/dist/graph/live.js +99 -0
- package/dist/graph/open.d.ts +1 -0
- package/dist/graph/open.js +20 -0
- package/dist/graph/server.d.ts +17 -0
- package/dist/graph/server.js +189 -0
- package/dist/health.d.ts +6 -0
- package/dist/health.js +310 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +66 -18
- package/dist/internal.d.ts +21 -0
- package/dist/internal.js +9 -0
- package/dist/knowledge-index.d.ts +1 -1
- package/dist/read-json.d.ts +26 -0
- package/dist/read-json.js +53 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +11 -0
- package/dist/stale.d.ts +2 -2
- package/dist/stale.js +4 -9
- package/dist/surfaces/items.d.ts +100 -1
- package/dist/surfaces/items.js +148 -10
- package/dist/surfaces/surfaces.d.ts +8 -5
- package/dist/surfaces/surfaces.js +34 -17
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +6 -1
- package/dist/types.d.ts +239 -25
- package/dist/validate.js +44 -56
- package/dist/vault.d.ts +29 -9
- package/dist/vault.js +39 -8
- package/dist/workstream.d.ts +17 -0
- package/dist/workstream.js +78 -0
- package/package.json +10 -4
- package/skill/hafez/SKILL.md +16 -7
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { parseFilePath, slugify, kindFromPath, kindDir,
|
|
2
|
+
import { parseFilePath, slugify, kindFromPath, kindDir, isMutableRelPath, findVaultFile, findReadableFile, vaultFileExists, resolveFilePath, scanVaultDir, archiveLogPath, noteAtRelPath, MUTABLE_KINDS } from './vault.js';
|
|
3
3
|
import { createGitJournal } from './git.js';
|
|
4
4
|
import { validateSessionLogEntry } from './schema.js';
|
|
5
5
|
import { validateVault } from './validate.js';
|
|
6
|
+
import { healthReport } from './health.js';
|
|
6
7
|
import { documentForFile } from './document.js';
|
|
7
8
|
import { createIndex } from './db.js';
|
|
8
9
|
import { closestSlugs } from './closest.js';
|
|
@@ -14,7 +15,9 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
|
14
15
|
import { join, basename } from 'path';
|
|
15
16
|
import { lock as lockVault } from 'proper-lockfile';
|
|
16
17
|
export function createHafez(config) {
|
|
17
|
-
const { vaultPath, readOnly = false, git: gitConfig = {}, onWrite } = config;
|
|
18
|
+
const { vaultPath, readOnly = false, git: gitConfig = {}, onWrite, indexSync = 'read' } = config;
|
|
19
|
+
// When reads catch the index up with the files (src/internal.ts, off the
|
|
20
|
+
// public types): 'once' is the CLI's (#269), 'manual' the graph server's (#271).
|
|
18
21
|
// The persistence seam (CONTEXT.md: Journal). Everything below talks to
|
|
19
22
|
// history through this port; git is just the default adapter.
|
|
20
23
|
const journal = config.persistence ?? createGitJournal(vaultPath, gitConfig);
|
|
@@ -52,6 +55,7 @@ export function createHafez(config) {
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
// Layout answers where a slug lives; these bind the vault path once.
|
|
58
|
+
// Mutations resolve the mutable kinds; `read` reaches a session note too.
|
|
55
59
|
function findFile(slug) { return findVaultFile(vaultPath, slug); }
|
|
56
60
|
function exists(slug) { return vaultFileExists(vaultPath, slug); }
|
|
57
61
|
function regenerateIndexSafe() {
|
|
@@ -65,8 +69,12 @@ export function createHafez(config) {
|
|
|
65
69
|
// outside hafez — a hand edit, a git pull — are represented too; that
|
|
66
70
|
// costs a stat per file, where the old path paid a parse per file on
|
|
67
71
|
// every single mutation.
|
|
72
|
+
// Under 'once' the process's first read already caught up.
|
|
68
73
|
const idx = getIndex();
|
|
69
|
-
|
|
74
|
+
if (indexSync === 'once')
|
|
75
|
+
idx?.syncForRead();
|
|
76
|
+
else
|
|
77
|
+
idx?.syncIfStale();
|
|
70
78
|
generateVaultIndex(vaultPath, idx?.indexEntries());
|
|
71
79
|
}
|
|
72
80
|
catch (err) {
|
|
@@ -77,7 +85,7 @@ export function createHafez(config) {
|
|
|
77
85
|
let _indexInitialized = false;
|
|
78
86
|
function getIndex() {
|
|
79
87
|
if (!_indexInitialized) {
|
|
80
|
-
_index = readOnly ? createIndex(vaultPath, { readonly: true }) : createIndex(vaultPath);
|
|
88
|
+
_index = readOnly ? createIndex(vaultPath, { readonly: true, sync: indexSync }) : createIndex(vaultPath, { sync: indexSync });
|
|
81
89
|
// A null read-only open means the on-disk index is missing or from another
|
|
82
90
|
// schema version. Don't latch it: retry on the next call so a long-lived
|
|
83
91
|
// read-only instance starts serving once a write-capable process rebuilds.
|
|
@@ -92,17 +100,14 @@ export function createHafez(config) {
|
|
|
92
100
|
return idx ? fn(idx) : empty;
|
|
93
101
|
}
|
|
94
102
|
async function read(slug, depth = 'summary') {
|
|
95
|
-
const filePath =
|
|
103
|
+
const filePath = findReadableFile(vaultPath, slug);
|
|
96
104
|
if (!filePath) {
|
|
97
105
|
// A miss carries the closest slugs as its details: the recovery the
|
|
98
106
|
// `read` miss output renders (#91).
|
|
99
|
-
const closest = withIndex(idx => { idx.
|
|
107
|
+
const closest = withIndex(idx => { idx.syncForRead(); return closestSlugs(slug, idx.indexEntries()); }, []);
|
|
100
108
|
throw new HafezError('NOT_FOUND', `Entity or knowledge '${slug}' not found`, closest);
|
|
101
109
|
}
|
|
102
110
|
const parsed = parseFilePath(filePath);
|
|
103
|
-
// findFile only resolves the indexed kinds, so the session arm is unreachable
|
|
104
|
-
if (parsed.kind === 'session')
|
|
105
|
-
throw new HafezError('NOT_FOUND', `Entity or knowledge '${slug}' not found`);
|
|
106
111
|
if (depth === 'frontmatter')
|
|
107
112
|
return { ...parsed, body: '' };
|
|
108
113
|
if (depth === 'summary') {
|
|
@@ -110,6 +115,15 @@ export function createHafez(config) {
|
|
|
110
115
|
}
|
|
111
116
|
return parsed;
|
|
112
117
|
}
|
|
118
|
+
/** The archive is an attachment of its entity, reached from it on purpose
|
|
119
|
+
* (#34): a disk read behind the entity's own slug, never an index row. */
|
|
120
|
+
async function archive(slug) {
|
|
121
|
+
const entity = resolveFilePath(vaultPath, slug, 'entity');
|
|
122
|
+
if (!existsSync(entity))
|
|
123
|
+
throw new HafezError('NOT_FOUND', `Entity '${slug}' not found`);
|
|
124
|
+
const path = archiveLogPath(vaultPath, slug);
|
|
125
|
+
return existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
126
|
+
}
|
|
113
127
|
function rollbackFiles(originals) {
|
|
114
128
|
for (const [path, content] of originals) {
|
|
115
129
|
if (content === null) {
|
|
@@ -216,12 +230,10 @@ export function createHafez(config) {
|
|
|
216
230
|
const deletedAll = [...new Set(applied.flatMap(a => a.filesDeleted))];
|
|
217
231
|
const finalize = () => {
|
|
218
232
|
updateIndexPostCommit(applied);
|
|
219
|
-
// index.md
|
|
220
|
-
if ([...writtenAll, ...deletedAll].some(
|
|
233
|
+
// index.md shows the mutable kinds: a session-only write leaves it.
|
|
234
|
+
if ([...writtenAll, ...deletedAll].some(isMutableRelPath)) {
|
|
221
235
|
regenerateIndexSafe();
|
|
222
236
|
}
|
|
223
|
-
// Session creates carry no index upserts — they are not indexed
|
|
224
|
-
// entities and never notified.
|
|
225
237
|
for (const a of applied) {
|
|
226
238
|
if (a.indexUpserts.length > 0)
|
|
227
239
|
onWrite?.(a.result.slug, single ? single.action : 'batch');
|
|
@@ -390,9 +402,11 @@ export function createHafez(config) {
|
|
|
390
402
|
simulateLinks: (slug, links) => { simulatedLinks.set(slug, links); },
|
|
391
403
|
// Read from disk, not the index: the validate phase must see the vault
|
|
392
404
|
// as `validate` will, and a stale index row would let a linked note go.
|
|
405
|
+
// The mutable kinds only: a session note's related are a record of
|
|
406
|
+
// what it touched, and a record must not keep a note from being deleted.
|
|
393
407
|
referrers: (slug) => {
|
|
394
408
|
const candidates = new Set(simulated.keys());
|
|
395
|
-
for (const kind of
|
|
409
|
+
for (const kind of MUTABLE_KINDS) {
|
|
396
410
|
for (const fp of scanVaultDir(join(vaultPath, kindDir(kind))))
|
|
397
411
|
candidates.add(basename(fp, '.md'));
|
|
398
412
|
}
|
|
@@ -476,6 +490,9 @@ export function createHafez(config) {
|
|
|
476
490
|
async function validate() {
|
|
477
491
|
return validateVault(vaultPath);
|
|
478
492
|
}
|
|
493
|
+
async function health() {
|
|
494
|
+
return healthReport(vaultPath);
|
|
495
|
+
}
|
|
479
496
|
async function promote(slug, target) {
|
|
480
497
|
// Rollback safety rides the Spine: applyOperation's promote arm captures
|
|
481
498
|
// both the source file and (for cross-kind moves) the knowledge target
|
|
@@ -491,7 +508,8 @@ export function createHafez(config) {
|
|
|
491
508
|
knowledge_count: 0,
|
|
492
509
|
stale: [], no_next_action: [], recently_touched: [], recently_created: [],
|
|
493
510
|
};
|
|
494
|
-
|
|
511
|
+
// An InternalHafez: syncIndex() is here, off the public Hafez type.
|
|
512
|
+
const hafez = {
|
|
495
513
|
read,
|
|
496
514
|
update,
|
|
497
515
|
promote,
|
|
@@ -518,8 +536,13 @@ export function createHafez(config) {
|
|
|
518
536
|
children: async (slug) => withIndex(idx => idx.queryChildren(slug), { items: [], total: 0 }),
|
|
519
537
|
related_to: async (slug, relation) => withIndex(idx => idx.queryRelatedTo(slug, relation), { items: [], total: 0 }),
|
|
520
538
|
query_knowledge: async (opts) => withIndex(idx => idx.queryKnowledge(opts), { items: [], total: 0 }),
|
|
539
|
+
query_sessions: async (opts) => withIndex(idx => idx.querySessions(opts), { items: [], total: 0 }),
|
|
540
|
+
archive,
|
|
521
541
|
validate,
|
|
522
|
-
|
|
542
|
+
health,
|
|
543
|
+
search: async (query, kind) => withIndex(idx => { idx.syncForRead(); return idx.search(query, kind); }, []),
|
|
544
|
+
graph: async (opts) => withIndex(idx => idx.queryGraph(opts), { nodes: [], edges: [] }),
|
|
545
|
+
syncIndex: async () => { withIndex(idx => idx.syncIfStale(), undefined); },
|
|
523
546
|
rebuildIndex: async () => {
|
|
524
547
|
if (readOnly)
|
|
525
548
|
throw new HafezError('VALIDATION_FAILED', 'Hafez instance is read-only');
|
|
@@ -533,14 +556,39 @@ export function createHafez(config) {
|
|
|
533
556
|
const idx = getIndex();
|
|
534
557
|
if (!idx)
|
|
535
558
|
return emptyStats;
|
|
536
|
-
idx.
|
|
559
|
+
idx.syncForRead();
|
|
537
560
|
const s = idx.getStats();
|
|
538
561
|
s.recently_touched = await rankRecents(s.recently_touched, 'last_touched', 'modified');
|
|
539
562
|
s.recently_created = await rankRecents(s.recently_created, 'created', 'added');
|
|
540
563
|
return s;
|
|
541
564
|
},
|
|
542
|
-
changelog: (since) =>
|
|
565
|
+
changelog: async (since, limit) => {
|
|
566
|
+
if (limit !== undefined && !(Number.isInteger(limit) && limit > 0)) {
|
|
567
|
+
throw new HafezError('VALIDATION_FAILED', `changelog limit must be a positive whole number of commits, got ${limit}`);
|
|
568
|
+
}
|
|
569
|
+
return journal.changelog(since, limit);
|
|
570
|
+
},
|
|
571
|
+
changesSince: async (cursor) => {
|
|
572
|
+
const { cursor: next, files } = await journal.changesSince(cursor);
|
|
573
|
+
const changes = files.flatMap(({ path, op }) => {
|
|
574
|
+
const note = noteAtRelPath(path);
|
|
575
|
+
return note ? [{ ...note, op }] : [];
|
|
576
|
+
});
|
|
577
|
+
return { cursor: next, changes };
|
|
578
|
+
},
|
|
579
|
+
close: () => {
|
|
580
|
+
// Forget the handle even if closing it throws: a retry must not
|
|
581
|
+
// re-throw, and getIndex() must never hand out a closed handle.
|
|
582
|
+
try {
|
|
583
|
+
_index?.close();
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
_index = null;
|
|
587
|
+
_indexInitialized = false;
|
|
588
|
+
}
|
|
589
|
+
},
|
|
543
590
|
};
|
|
591
|
+
return hafez;
|
|
544
592
|
}
|
|
545
593
|
export { HafezError } from './types.js';
|
|
546
594
|
export { getBrief, getNextActions } from './document.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Hafez, HafezConfig } from './types.js';
|
|
2
|
+
import type { IndexSync } from './db.js';
|
|
3
|
+
export interface InternalHafezConfig extends HafezConfig {
|
|
4
|
+
/** When the index catches up with files changed outside hafez (a hand edit,
|
|
5
|
+
* a raw git pull). 'read' (default): before every index read, a stat per
|
|
6
|
+
* note, so a long-lived instance never reads a stale index. 'once': the
|
|
7
|
+
* first index read syncs, later reads trust the index — the CLI's mode, one
|
|
8
|
+
* scan per process (#269); a file changed outside hafez after that read is
|
|
9
|
+
* seen by the next process, or after `syncIndex()`. Writes through the
|
|
10
|
+
* instance are indexed as they commit in every mode. 'manual': reads trust
|
|
11
|
+
* the index, and `syncIndex()` syncs it — the graph server's mode, which
|
|
12
|
+
* syncs when its poll sees a commit (#271). */
|
|
13
|
+
indexSync?: IndexSync;
|
|
14
|
+
}
|
|
15
|
+
export interface InternalHafez extends Hafez {
|
|
16
|
+
/** Catch the index up with files changed outside hafez, in every mode:
|
|
17
|
+
* what a read does first under `indexSync` 'read', and the first read under 'once'. */
|
|
18
|
+
syncIndex(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** createHafez with the internal options: every instance it builds is an InternalHafez. */
|
|
21
|
+
export declare function createInternalHafez(config: InternalHafezConfig): InternalHafez;
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/internal.ts
|
|
2
|
+
// What hafez's own CLI and graph server reach that the package does not
|
|
3
|
+
// export: src/index.ts re-exports none of it, and package.json's `exports`
|
|
4
|
+
// map stops a deep import. Off the library surface by the owner's call (#270).
|
|
5
|
+
import { createHafez } from './index.js';
|
|
6
|
+
/** createHafez with the internal options: every instance it builds is an InternalHafez. */
|
|
7
|
+
export function createInternalHafez(config) {
|
|
8
|
+
return createHafez(config);
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Hafez, ParsedFile, ReadDepth } from './types.js';
|
|
2
|
+
import { type Workstream } from './workstream.js';
|
|
3
|
+
import { type ChildLine, type FooterInput } from './surfaces/items.js';
|
|
4
|
+
/** `read --json`'s object for a note read at `depth`: what the CLI prints and
|
|
5
|
+
* what the graph server's `/note/:slug` serves. `whole`, its full-depth read
|
|
6
|
+
* when the caller holds one, saves reading it again. */
|
|
7
|
+
export declare function readJsonOf(os: Hafez, slug: string, depth: ReadDepth, result: ParsedFile, whole?: ParsedFile): Promise<object>;
|
|
8
|
+
/** What hangs off an entity elsewhere: the sessions that touched it (the
|
|
9
|
+
* look back, #35), its archived log entries (#34), its children (#230) and
|
|
10
|
+
* the prefix families its own Next Actions carry unsplit against the
|
|
11
|
+
* entities whose `parent` or `related` names it (#230). Counted at read
|
|
12
|
+
* time, nothing written. */
|
|
13
|
+
export declare function pointersFor(os: Hafez, slug: string, body: string): Promise<{
|
|
14
|
+
sessions: {
|
|
15
|
+
count: number;
|
|
16
|
+
latest: string | null;
|
|
17
|
+
};
|
|
18
|
+
archive_entries: number;
|
|
19
|
+
children: ChildLine[];
|
|
20
|
+
workstreams: Workstream[];
|
|
21
|
+
}>;
|
|
22
|
+
/** The Footer for a file read at full depth: its Handle comes from the
|
|
23
|
+
* whole file. A session note has none: it is never written to. */
|
|
24
|
+
export declare function footerFor(slug: string, result: Exclude<ParsedFile, {
|
|
25
|
+
kind: 'session';
|
|
26
|
+
}>): FooterInput;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { detectAgent } from './harness.js';
|
|
2
|
+
import { getActionItems, parseSessionLogHeading } from './document.js';
|
|
3
|
+
import { agedActions } from './stale.js';
|
|
4
|
+
import { workstreams } from './workstream.js';
|
|
5
|
+
import { CHILDREN_SHOWN, footerOp } from './surfaces/items.js';
|
|
6
|
+
/** `read --json`'s object for a note read at `depth`: what the CLI prints and
|
|
7
|
+
* what the graph server's `/note/:slug` serves. `whole`, its full-depth read
|
|
8
|
+
* when the caller holds one, saves reading it again. */
|
|
9
|
+
export async function readJsonOf(os, slug, depth, result, whole) {
|
|
10
|
+
// A session note is read, never written: no Footer (#35).
|
|
11
|
+
if (result.kind === 'session')
|
|
12
|
+
return result;
|
|
13
|
+
// The Footer and the pointers are the same at every depth: they read the
|
|
14
|
+
// whole file, not the body this depth shows.
|
|
15
|
+
const full = (whole ?? (depth === 'full' ? result : await os.read(slug, 'full')));
|
|
16
|
+
const writes = footerOp(footerFor(slug, full));
|
|
17
|
+
const pointers = full.kind === 'entity' ? await pointersFor(os, slug, full.body) : null;
|
|
18
|
+
const json = pointers && { ...pointers, children: pointers.children.slice(0, CHILDREN_SHOWN), children_total: pointers.children.length };
|
|
19
|
+
// A frontmatter-depth read carries no body, so it has no actions to list.
|
|
20
|
+
if (result.kind !== 'entity' || depth === 'frontmatter')
|
|
21
|
+
return { ...result, ...json, writes };
|
|
22
|
+
return { ...result, actions: agedActions(result.body, result.frontmatter), ...json, writes };
|
|
23
|
+
}
|
|
24
|
+
/** What hangs off an entity elsewhere: the sessions that touched it (the
|
|
25
|
+
* look back, #35), its archived log entries (#34), its children (#230) and
|
|
26
|
+
* the prefix families its own Next Actions carry unsplit against the
|
|
27
|
+
* entities whose `parent` or `related` names it (#230). Counted at read
|
|
28
|
+
* time, nothing written. */
|
|
29
|
+
export async function pointersFor(os, slug, body) {
|
|
30
|
+
const relatedTo = (await os.related_to(slug)).items;
|
|
31
|
+
const sessions = relatedTo.filter((i) => i.kind === 'session');
|
|
32
|
+
const latest = sessions.reduce((max, s) => (max === null || s.session_date > max ? s.session_date : max), null);
|
|
33
|
+
const archive = await os.archive(slug);
|
|
34
|
+
const entries = archive ? archive.split('\n').filter(line => parseSessionLogHeading(line)).length : 0;
|
|
35
|
+
// The children block (#230): the index's children query, last-touched
|
|
36
|
+
// descending then slug for a stable order; the renders cap what is shown.
|
|
37
|
+
const childItems = (await os.children(slug)).items;
|
|
38
|
+
const children = [...childItems]
|
|
39
|
+
.sort((a, b) => b.last_touched.localeCompare(a.last_touched) || a.slug.localeCompare(b.slug))
|
|
40
|
+
.map(c => ({ slug: c.slug, description: c.description, open_actions: c.next_action_count, last_touched: c.last_touched }));
|
|
41
|
+
const naming = [...childItems, ...relatedTo.filter(i => i.kind === 'entity')];
|
|
42
|
+
return { sessions: { count: sessions.length, latest }, archive_entries: entries, children, workstreams: workstreams(body, naming) };
|
|
43
|
+
}
|
|
44
|
+
/** The Footer for a file read at full depth: its Handle comes from the
|
|
45
|
+
* whole file. A session note has none: it is never written to. */
|
|
46
|
+
export function footerFor(slug, result) {
|
|
47
|
+
return { slug, agent: detectAgent(), ...(result.kind === 'entity'
|
|
48
|
+
? {
|
|
49
|
+
target: { kind: 'entity', type: result.frontmatter.type },
|
|
50
|
+
handle: getActionItems(result.body).find(a => a.handle)?.handle,
|
|
51
|
+
}
|
|
52
|
+
: { target: { kind: 'knowledge', subtype: result.frontmatter.subtype } }) };
|
|
53
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare function validateEntityFrontmatter(fm: Record<string, any>): string[];
|
|
2
2
|
export declare function validateKnowledgeFrontmatter(fm: Record<string, any>): string[];
|
|
3
|
+
/** A session note: its name and created stamp, nothing else is required. */
|
|
4
|
+
export declare function validateSessionFrontmatter(fm: Record<string, any>): string[];
|
|
3
5
|
export declare function validateKnowledgeSubtype(fm: Record<string, any>): string[];
|
|
4
6
|
export declare function validateSlugReference(slug: string, field: string, exists: (s: string) => boolean): string | null;
|
|
5
7
|
export declare function validateSessionLogEntry(entry: {
|
package/dist/schema.js
CHANGED
|
@@ -30,6 +30,17 @@ export function validateKnowledgeFrontmatter(fm) {
|
|
|
30
30
|
errors.push(...validateKnowledgeSubtype(fm));
|
|
31
31
|
return errors;
|
|
32
32
|
}
|
|
33
|
+
/** A session note: its name and created stamp, nothing else is required. */
|
|
34
|
+
export function validateSessionFrontmatter(fm) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
if (!fm.name || typeof fm.name !== 'string')
|
|
37
|
+
errors.push('missing required field: name');
|
|
38
|
+
if (!fm.created)
|
|
39
|
+
errors.push('missing required field: created');
|
|
40
|
+
if (fm.related !== undefined && !Array.isArray(fm.related))
|
|
41
|
+
errors.push('session related must be an array');
|
|
42
|
+
return errors;
|
|
43
|
+
}
|
|
33
44
|
export function validateKnowledgeSubtype(fm) {
|
|
34
45
|
const errors = [];
|
|
35
46
|
if (fm.subtype && !VALID_SUBTYPES.includes(fm.subtype)) {
|
package/dist/stale.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export interface ActionAge {
|
|
|
9
9
|
}
|
|
10
10
|
/** The entity side of the rule, as SQL over an index row `i` with `@today`
|
|
11
11
|
* bound to localDay(): whole days from its last-touched day to today.
|
|
12
|
-
* julianday of two bare YYYY-MM-DD days differs by exactly `
|
|
13
|
-
*
|
|
12
|
+
* julianday of two bare YYYY-MM-DD days differs by exactly `daysBetween`
|
|
13
|
+
* (src/dates.ts), so entities and their actions count the same days. */
|
|
14
14
|
export declare const ENTITY_AGE_DAYS_SQL = "CAST(julianday(@today) - julianday(i.last_touched) AS INTEGER)";
|
|
15
15
|
/** An entity row is stale when its age is past its threshold, not equal to
|
|
16
16
|
* it — `actionAge`'s rule. */
|
package/dist/stale.js
CHANGED
|
@@ -2,19 +2,14 @@
|
|
|
2
2
|
// Stale (see CONTEXT.md): past the entity's `staleness-days` without
|
|
3
3
|
// movement. An open next action is stale when unticked that long since its
|
|
4
4
|
// added day; an undated one is stale. Ages count local days (src/dates.ts).
|
|
5
|
-
import { localDay } from './dates.js';
|
|
5
|
+
import { daysBetween, localDay } from './dates.js';
|
|
6
6
|
import { getActionItems } from './document.js';
|
|
7
7
|
/** The threshold an entity without `staleness-days` gets. */
|
|
8
8
|
export const DEFAULT_STALENESS_DAYS = 14;
|
|
9
|
-
/** A YYYY-MM-DD day as a whole day count, so differences ignore DST. */
|
|
10
|
-
const dayNumber = (day) => {
|
|
11
|
-
const [y, m, d] = day.split('-').map(Number);
|
|
12
|
-
return Date.UTC(y, m - 1, d) / 86_400_000;
|
|
13
|
-
};
|
|
14
9
|
/** The entity side of the rule, as SQL over an index row `i` with `@today`
|
|
15
10
|
* bound to localDay(): whole days from its last-touched day to today.
|
|
16
|
-
* julianday of two bare YYYY-MM-DD days differs by exactly `
|
|
17
|
-
*
|
|
11
|
+
* julianday of two bare YYYY-MM-DD days differs by exactly `daysBetween`
|
|
12
|
+
* (src/dates.ts), so entities and their actions count the same days. */
|
|
18
13
|
export const ENTITY_AGE_DAYS_SQL = 'CAST(julianday(@today) - julianday(i.last_touched) AS INTEGER)';
|
|
19
14
|
/** An entity row is stale when its age is past its threshold, not equal to
|
|
20
15
|
* it — `actionAge`'s rule. */
|
|
@@ -24,7 +19,7 @@ export const ENTITY_STALE_SQL = `${ENTITY_AGE_DAYS_SQL} > COALESCE(i.staleness_d
|
|
|
24
19
|
export function actionAge(added, stalenessDays, today = localDay()) {
|
|
25
20
|
if (added === null)
|
|
26
21
|
return { added, age_days: null, stale: true };
|
|
27
|
-
const age_days =
|
|
22
|
+
const age_days = daysBetween(added, today);
|
|
28
23
|
return { added, age_days, stale: age_days > stalenessDays };
|
|
29
24
|
}
|
|
30
25
|
/** An entity's threshold: its `staleness-days`, else the default. */
|
package/dist/surfaces/items.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type DocTarget, type WriteLineField } from '../contracts.js';
|
|
2
2
|
import { renderActionHandles } from '../document.js';
|
|
3
3
|
import type { ActionAge } from '../stale.js';
|
|
4
|
+
import type { Workstream } from '../workstream.js';
|
|
4
5
|
import type { KnowledgeSubtype, UpdateFields, UpdateResult } from '../types.js';
|
|
5
6
|
import { type OnboardHarness } from '../harness.js';
|
|
6
7
|
import type { Mode } from '../cli/install.js';
|
|
@@ -9,6 +10,11 @@ import type { Mode } from '../cli/install.js';
|
|
|
9
10
|
export type SurfaceId = 'block' | 'hafezSkillDescription' | 'hafezSkillBody' | 'hafezSaveDescription' | 'hafezSaveBody' | 'read' | 'readHeader' | 'readFooter' | 'readMiss' | 'updateSuccess' | 'updateMisuse' | 'createKnowledge' | 'digestError' | 'batchMisuse' | 'mechanised' | 'schema' | 'onboard';
|
|
10
11
|
/** Places another Item inside this one's text, counted against the same Surface. */
|
|
11
12
|
export type Place = (id: string) => string;
|
|
13
|
+
/** What a static Surface is composed for. `vaultRepo`: the Block for a cloud
|
|
14
|
+
* container, whose vault is that GitHub repo (#160). */
|
|
15
|
+
export interface SurfaceCtx {
|
|
16
|
+
vaultRepo?: string;
|
|
17
|
+
}
|
|
12
18
|
export interface Item {
|
|
13
19
|
/** The Item's row text in the owner table. */
|
|
14
20
|
label: string;
|
|
@@ -21,7 +27,7 @@ export interface Item {
|
|
|
21
27
|
evidence: string;
|
|
22
28
|
/** Static text. A function form may place nested Items, and may give the
|
|
23
29
|
* fallback a shorter form than the Owner. */
|
|
24
|
-
text?: string | ((place: Place, at: 'owner' | 'fallback') => string);
|
|
30
|
+
text?: string | ((place: Place, at: 'owner' | 'fallback', ctx: SurfaceCtx) => string);
|
|
25
31
|
/** What the binary computes: for Items owned by command output, and for a
|
|
26
32
|
* static Item a binary Surface places filled in (the create line in the
|
|
27
33
|
* `read` miss). */
|
|
@@ -94,6 +100,41 @@ declare function renderUpdateSummary({ slug, fields, result }: {
|
|
|
94
100
|
/** The suffix a Stale open line carries on `read`: its age, or that it is
|
|
95
101
|
* undated. A fresh line carries none. */
|
|
96
102
|
declare function renderStaleMarker({ stale, age_days }: ActionAge): string;
|
|
103
|
+
/** The `read` header line naming the sessions that touched an entity, with
|
|
104
|
+
* the look back from it. None: no line. */
|
|
105
|
+
declare function renderSessionsLine({ slug, count, latest }: {
|
|
106
|
+
slug: string;
|
|
107
|
+
count: number;
|
|
108
|
+
latest: string | null;
|
|
109
|
+
}): string;
|
|
110
|
+
/** The `read` header line naming an entity's archived log entries, with the
|
|
111
|
+
* command that reads them. No archive: no line. */
|
|
112
|
+
declare function renderArchivePointer({ slug, entries }: {
|
|
113
|
+
slug: string;
|
|
114
|
+
entries: number;
|
|
115
|
+
}): string;
|
|
116
|
+
/** A child as the `read` header shows it. */
|
|
117
|
+
export interface ChildLine {
|
|
118
|
+
slug: string;
|
|
119
|
+
description: string | null;
|
|
120
|
+
open_actions: number;
|
|
121
|
+
last_touched: string;
|
|
122
|
+
}
|
|
123
|
+
/** How many children the header lists; the rest are one overflow line. */
|
|
124
|
+
export declare const CHILDREN_SHOWN = 5;
|
|
125
|
+
/** The `read` header's children block: the count line (children, open
|
|
126
|
+
* actions across all of them), one `hafez read` line per child (description,
|
|
127
|
+
* open count, last-touched day) for the first `CHILDREN_SHOWN`, and the
|
|
128
|
+
* overflow line naming the query that lists the rest. Children arrive sorted
|
|
129
|
+
* by last-touched descending. No children: no block. */
|
|
130
|
+
declare function renderChildrenBlock({ slug, children }: {
|
|
131
|
+
slug: string;
|
|
132
|
+
children: ChildLine[];
|
|
133
|
+
}): string;
|
|
134
|
+
/** The `read` header's fact line for one unsplit prefix family (#230):
|
|
135
|
+
* its open-line count and the family, and that no child carries it. A
|
|
136
|
+
* fact, never a filled create line or a question. */
|
|
137
|
+
declare function renderWorkstreamLine({ prefix, open_actions }: Workstream): string;
|
|
97
138
|
/** The line after an update's success while Stale lines remain on the entity. */
|
|
98
139
|
declare function renderStaleReminder({ handles }: {
|
|
99
140
|
handles: string[];
|
|
@@ -129,6 +170,15 @@ declare function renderTeachingLines({ harness, mode, line2Only }: {
|
|
|
129
170
|
mode: Mode;
|
|
130
171
|
line2Only?: boolean;
|
|
131
172
|
}): string;
|
|
173
|
+
/** A GitHub `<owner>/<repo>`, as one shell word safe to print in a command
|
|
174
|
+
* and to name a directory by: not `.` or `..`, not a clone URL's `.git`. */
|
|
175
|
+
export declare const isVaultRepo: (value: string) => boolean;
|
|
176
|
+
/** `onboard --cloud`: the cloud environment's setup script. It cannot clone
|
|
177
|
+
* (it runs without GitHub credentials) and no step may fail session start: a
|
|
178
|
+
* failed install exits 0, leaving a session without Hafez rather than none. */
|
|
179
|
+
declare function renderCloudSetup({ vaultRepo }: {
|
|
180
|
+
vaultRepo: string;
|
|
181
|
+
}): string;
|
|
132
182
|
export declare const ITEMS: {
|
|
133
183
|
loadTriggers: {
|
|
134
184
|
label: string;
|
|
@@ -159,6 +209,34 @@ export declare const ITEMS: {
|
|
|
159
209
|
description: string | null;
|
|
160
210
|
}) => string;
|
|
161
211
|
};
|
|
212
|
+
childrenBlock: {
|
|
213
|
+
label: string;
|
|
214
|
+
owner: "readHeader";
|
|
215
|
+
invokeOnly: string;
|
|
216
|
+
evidence: string;
|
|
217
|
+
render: typeof renderChildrenBlock;
|
|
218
|
+
};
|
|
219
|
+
workstreamLine: {
|
|
220
|
+
label: string;
|
|
221
|
+
owner: "readHeader";
|
|
222
|
+
invokeOnly: string;
|
|
223
|
+
evidence: string;
|
|
224
|
+
render: typeof renderWorkstreamLine;
|
|
225
|
+
};
|
|
226
|
+
sessionsLine: {
|
|
227
|
+
label: string;
|
|
228
|
+
owner: "readHeader";
|
|
229
|
+
invokeOnly: string;
|
|
230
|
+
evidence: string;
|
|
231
|
+
render: typeof renderSessionsLine;
|
|
232
|
+
};
|
|
233
|
+
archivePointer: {
|
|
234
|
+
label: string;
|
|
235
|
+
owner: "readHeader";
|
|
236
|
+
invokeOnly: string;
|
|
237
|
+
evidence: string;
|
|
238
|
+
render: typeof renderArchivePointer;
|
|
239
|
+
};
|
|
162
240
|
writeTriggers: {
|
|
163
241
|
label: string;
|
|
164
242
|
owner: "block";
|
|
@@ -319,6 +397,13 @@ export declare const ITEMS: {
|
|
|
319
397
|
evidence: string;
|
|
320
398
|
text: string;
|
|
321
399
|
};
|
|
400
|
+
healthProcedure: {
|
|
401
|
+
label: string;
|
|
402
|
+
owner: "hafezSkillBody";
|
|
403
|
+
invokeOnly: string;
|
|
404
|
+
evidence: string;
|
|
405
|
+
text: string;
|
|
406
|
+
};
|
|
322
407
|
batchShape: {
|
|
323
408
|
label: string;
|
|
324
409
|
owner: "hafezSkillBody";
|
|
@@ -354,6 +439,20 @@ export declare const ITEMS: {
|
|
|
354
439
|
evidence: string;
|
|
355
440
|
render: typeof renderHarnessPermissions;
|
|
356
441
|
};
|
|
442
|
+
cloudVault: {
|
|
443
|
+
label: string;
|
|
444
|
+
owner: "block";
|
|
445
|
+
invokeOnly: string;
|
|
446
|
+
evidence: string;
|
|
447
|
+
text: (_place: Place, _at: "owner" | "fallback", { vaultRepo }: SurfaceCtx) => string;
|
|
448
|
+
};
|
|
449
|
+
cloudSetup: {
|
|
450
|
+
label: string;
|
|
451
|
+
owner: "onboard";
|
|
452
|
+
invokeOnly: string;
|
|
453
|
+
evidence: string;
|
|
454
|
+
render: typeof renderCloudSetup;
|
|
455
|
+
};
|
|
357
456
|
teachingLines: {
|
|
358
457
|
label: string;
|
|
359
458
|
owner: "onboard";
|