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.
Files changed (75) hide show
  1. package/README.md +72 -5
  2. package/dist/apply.d.ts +14 -1
  3. package/dist/apply.js +94 -32
  4. package/dist/batch-ops.d.ts +31 -27
  5. package/dist/batch-ops.js +83 -16
  6. package/dist/cli/block-hook.d.ts +6 -0
  7. package/dist/cli/block-hook.js +27 -12
  8. package/dist/cli/commands.d.ts +3 -1
  9. package/dist/cli/commands.js +183 -73
  10. package/dist/cli/format.d.ts +11 -3
  11. package/dist/cli/format.js +61 -4
  12. package/dist/cli/index.js +40 -19
  13. package/dist/cli/install.d.ts +9 -0
  14. package/dist/cli/install.js +36 -15
  15. package/dist/cli/onboard.js +30 -9
  16. package/dist/cli/upgrade.js +5 -4
  17. package/dist/contracts.d.ts +9 -0
  18. package/dist/contracts.js +9 -0
  19. package/dist/dates.d.ts +7 -0
  20. package/dist/dates.js +21 -1
  21. package/dist/db.d.ts +50 -10
  22. package/dist/db.js +251 -65
  23. package/dist/digest.js +14 -7
  24. package/dist/document.d.ts +2 -0
  25. package/dist/document.js +2 -2
  26. package/dist/git.d.ts +13 -2
  27. package/dist/git.js +121 -14
  28. package/dist/graph/activity.d.ts +5 -0
  29. package/dist/graph/activity.js +70 -0
  30. package/dist/graph/client.js +326 -0
  31. package/dist/graph/copy-fonts.d.ts +1 -0
  32. package/dist/graph/copy-fonts.js +13 -0
  33. package/dist/graph/fields.d.ts +8 -0
  34. package/dist/graph/fields.js +12 -0
  35. package/dist/graph/fonts/geist-OFL.txt +93 -0
  36. package/dist/graph/fonts/geist-mono-OFL.txt +93 -0
  37. package/dist/graph/fonts/geist-mono.woff2 +0 -0
  38. package/dist/graph/fonts/geist.woff2 +0 -0
  39. package/dist/graph/fonts/outfit-OFL.txt +93 -0
  40. package/dist/graph/fonts/outfit.woff2 +0 -0
  41. package/dist/graph/fonts.d.ts +14 -0
  42. package/dist/graph/fonts.js +17 -0
  43. package/dist/graph/live.d.ts +16 -0
  44. package/dist/graph/live.js +99 -0
  45. package/dist/graph/open.d.ts +1 -0
  46. package/dist/graph/open.js +20 -0
  47. package/dist/graph/server.d.ts +17 -0
  48. package/dist/graph/server.js +189 -0
  49. package/dist/health.d.ts +6 -0
  50. package/dist/health.js +310 -0
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +66 -18
  53. package/dist/internal.d.ts +21 -0
  54. package/dist/internal.js +9 -0
  55. package/dist/knowledge-index.d.ts +1 -1
  56. package/dist/read-json.d.ts +26 -0
  57. package/dist/read-json.js +53 -0
  58. package/dist/schema.d.ts +2 -0
  59. package/dist/schema.js +11 -0
  60. package/dist/stale.d.ts +2 -2
  61. package/dist/stale.js +4 -9
  62. package/dist/surfaces/items.d.ts +100 -1
  63. package/dist/surfaces/items.js +148 -10
  64. package/dist/surfaces/surfaces.d.ts +8 -5
  65. package/dist/surfaces/surfaces.js +34 -17
  66. package/dist/templates.d.ts +2 -0
  67. package/dist/templates.js +6 -1
  68. package/dist/types.d.ts +239 -25
  69. package/dist/validate.js +44 -56
  70. package/dist/vault.d.ts +29 -9
  71. package/dist/vault.js +39 -8
  72. package/dist/workstream.d.ts +17 -0
  73. package/dist/workstream.js +78 -0
  74. package/package.json +10 -4
  75. package/skill/hafez/SKILL.md +16 -7
@@ -14,6 +14,7 @@ import { dirname, join } from 'path';
14
14
  import { HafezError } from '../types.js';
15
15
  import { HARNESSES } from '../harness.js';
16
16
  import { contentHash, renderBlock, renderHafezSaveSkill, renderHafezSkill } from '../surfaces/surfaces.js';
17
+ import { isVaultRepo } from '../surfaces/items.js';
17
18
  export const MODES = ['always-on', 'invoke-only'];
18
19
  export const isMode = (value) => MODES.includes(value);
19
20
  export const SKILL_DIRS = ['.claude/skills', '.agents/skills'];
@@ -36,7 +37,8 @@ function hashState(found, rendered) {
36
37
  return 'stale';
37
38
  }
38
39
  // --- The Block region ---
39
- const BEGIN = /^<!-- hafez:begin ([0-9a-f]{12}) -->$/;
40
+ /** The hash, then a cloud Block's vault repo (#160). */
41
+ const BEGIN = /^<!-- hafez:begin ([0-9a-f]{12})(?: vault-repo=(\S+))? -->$/;
40
42
  const END = '<!-- hafez:end -->';
41
43
  /** What broken markers are, for a refusal or a report about `path`. */
42
44
  const brokenMarkers = (path) => `The Hafez markers in ${path} are broken: expected one "<!-- hafez:begin <hash> -->" line followed by one "${END}" line.`;
@@ -62,8 +64,9 @@ function findRegion(file, displayPath) {
62
64
  return null;
63
65
  const [begin] = begins;
64
66
  const [end] = ends;
65
- const hash = begin ? BEGIN.exec(begin.line)?.[1] : undefined;
66
- if (begins.length !== 1 || ends.length !== 1 || end.start < begin.start || !hash || end.line !== END) {
67
+ const [, hash, vaultRepo] = (begin && BEGIN.exec(begin.line)) || [];
68
+ if (begins.length !== 1 || ends.length !== 1 || end.start < begin.start || !hash || end.line !== END
69
+ || (vaultRepo !== undefined && !isVaultRepo(vaultRepo))) {
67
70
  throw new HafezError('VALIDATION_FAILED', `${brokenMarkers(displayPath)} Fix or delete them by hand, then re-run.`);
68
71
  }
69
72
  return {
@@ -73,8 +76,11 @@ function findRegion(file, displayPath) {
73
76
  hash,
74
77
  body: file.slice(begin.next, end.start),
75
78
  text: file.slice(begin.start, end.start + END.length),
79
+ ...(vaultRepo !== undefined && { vaultRepo }),
76
80
  };
77
81
  }
82
+ /** The flag that keeps an install a cloud one, for every printed install line. */
83
+ export const vaultRepoFlag = (vaultRepo) => (vaultRepo === undefined ? '' : ` --vault-repo ${vaultRepo}`);
78
84
  // --- The 1.x section ---
79
85
  /** The heading 1.2–1.4 `onboard` had the agent paste, with no markers (#157). */
80
86
  const LEGACY_HEADING = '## Hafez — Session Memory';
@@ -169,14 +175,22 @@ function skillSteps(home, want) {
169
175
  return { line: `~/${rel}: ${state === 'absent' ? 'written' : `replaced (was ${state})`}`, write: () => write(path, rendered) };
170
176
  });
171
177
  }
172
- const RENDERED = findRegion(renderBlock(), 'the render');
173
- function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
178
+ const RENDERS = new Map();
179
+ /** The Block Hafez writes, as a region; with `vaultRepo`, the cloud Block. A
180
+ * found Block is checked against the render for its own marker's repo. */
181
+ function rendered(vaultRepo) {
182
+ if (!RENDERS.has(vaultRepo))
183
+ RENDERS.set(vaultRepo, findRegion(renderBlock(vaultRepo), 'the render'));
184
+ return RENDERS.get(vaultRepo);
185
+ }
186
+ function blockStep({ home, rel, want, confirm, rerun, suffix = '', vaultRepo }) {
174
187
  const path = join(home, rel);
175
188
  const label = `Block in ~/${rel}`;
176
189
  const file = readInstalled(path);
177
190
  const text = file ?? '';
178
191
  const region = findRegion(text, `~/${rel}`);
179
- const state = hashState(region, RENDERED.text);
192
+ const target = rendered(want === 'write' ? vaultRepo : region?.vaultRepo);
193
+ const state = hashState(region, target.text);
180
194
  if (region && state === 'hand-edited' && !confirm) {
181
195
  return {
182
196
  line: `${label}: hand-edited, left alone`,
@@ -186,7 +200,7 @@ function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
186
200
  rerun,
187
201
  '',
188
202
  '````diff',
189
- lineDiff(region.body, RENDERED.body),
203
+ lineDiff(region.body, target.body),
190
204
  '````',
191
205
  ].join('\n'),
192
206
  };
@@ -200,11 +214,11 @@ function blockStep({ home, rel, want, confirm, rerun, suffix = '' }) {
200
214
  return { line: `${label}: current` };
201
215
  if (!region) {
202
216
  const before = text === '' || text.endsWith('\n') ? text : `${text}\n`;
203
- return { line: `${label}: written`, write: () => write(path, before + renderBlock()) };
217
+ return { line: `${label}: written`, write: () => write(path, before + renderBlock(vaultRepo)) };
204
218
  }
205
219
  return {
206
220
  line: `${label}: replaced (was ${state})`,
207
- write: () => write(path, text.slice(0, region.start) + RENDERED.text + text.slice(region.end)),
221
+ write: () => write(path, text.slice(0, region.start) + target.text + text.slice(region.end)),
208
222
  };
209
223
  }
210
224
  /** Codex reads an `AGENTS.override.md` in place of `AGENTS.md` when one
@@ -228,13 +242,15 @@ export function installState(home, harness) {
228
242
  const { instructions } = HARNESSES[harness];
229
243
  let block = null;
230
244
  let legacy = null;
245
+ let vaultRepo;
231
246
  if (instructions !== null) {
232
247
  const rel = instructionsFile(home, instructions);
233
248
  const text = readInstalled(join(home, rel)) ?? '';
234
249
  let region;
235
250
  try {
236
251
  region = findRegion(text, `~/${rel}`);
237
- block = hashState(region, RENDERED.text);
252
+ vaultRepo = region?.vaultRepo;
253
+ block = hashState(region, rendered(vaultRepo).text);
238
254
  }
239
255
  catch {
240
256
  block = 'broken';
@@ -249,11 +265,13 @@ export function installState(home, harness) {
249
265
  const states = new Set(skills.map(s => s.state));
250
266
  const [only] = states;
251
267
  const merged = states.size === 1 ? only : states.has('hand-edited') ? 'hand-edited' : 'stale';
252
- return { block, skills: merged, onboarded: skills.some(s => s.hashed), legacy };
268
+ return { block, skills: merged, onboarded: skills.some(s => s.hashed), legacy, ...(vaultRepo !== undefined && { vaultRepo }) };
253
269
  }
254
270
  // --- The two commands ---
271
+ /** `vaultRepo`: the install is for a cloud container, whose Block carries
272
+ * the rule that attaches that repo as the vault (#160). */
255
273
  export function install(opts) {
256
- const { home, harness, mode, confirm } = opts;
274
+ const { home, harness, mode, confirm, vaultRepo } = opts;
257
275
  const { label, instructions } = HARNESSES[harness];
258
276
  const steps = skillSteps(home, 'write');
259
277
  const tail = [];
@@ -273,8 +291,9 @@ export function install(opts) {
273
291
  rel,
274
292
  want: mode === 'always-on' ? 'write' : 'remove',
275
293
  confirm,
276
- rerun: `hafez onboard --install --harness ${harness} --mode ${mode} --confirm`,
294
+ rerun: `hafez onboard --install --harness ${harness} --mode ${mode}${vaultRepoFlag(vaultRepo)} --confirm`,
277
295
  suffix: mode === 'invoke-only' ? ' (invoke-only)' : '',
296
+ vaultRepo,
278
297
  }));
279
298
  }
280
299
  const out = run({ header: `${label}, ${mode}:`, steps, tail, unchanged: 'Nothing changed: the install is current.', listAll: true });
@@ -316,8 +335,10 @@ export function checkInstall(home) {
316
335
  legacy.push({ harness, path, line });
317
336
  if (!region)
318
337
  continue;
319
- const state = hashState(region, RENDERED.text);
320
- blocks.push({ harness, path, state, renderChanged: region.hash !== RENDERED.hash });
338
+ const { vaultRepo } = region;
339
+ const render = rendered(vaultRepo);
340
+ const state = hashState(region, render.text);
341
+ blocks.push({ harness, path, state, renderChanged: region.hash !== render.hash, ...(vaultRepo !== undefined && { vaultRepo }) });
321
342
  }
322
343
  return { skills, blocks, legacy };
323
344
  }
@@ -9,33 +9,36 @@
9
9
  // `--install` and `--remove` write the files Hafez owns outside the vault
10
10
  // (install.ts). The binary has no interactive prompts: the agent asks, then
11
11
  // passes the answer as a flag. `--block` is the SessionStart hook's render
12
- // (block-hook.ts) and writes nothing.
12
+ // (block-hook.ts) and writes nothing. `--cloud` prints the setup script for a
13
+ // Claude Code cloud environment, whose `--install --vault-repo` writes the
14
+ // Block with the rule that attaches the vault repo (#160).
13
15
  import { existsSync, readFileSync } from 'fs';
14
16
  import { homedir } from 'os';
15
17
  import { join } from 'path';
16
18
  import { UsageError, parseArgs, str } from './args.js';
17
19
  import { HARNESSES, ONBOARD_HARNESSES, detectAgent, isHarness, isOnboardHarness } from '../harness.js';
18
- import { MODES, SKILL_DIRS, fixBrokenMarkers, install, installState, instructionsFile, isMode, legacyLine, remove, removeLegacy } from './install.js';
20
+ import { MODES, SKILL_DIRS, fixBrokenMarkers, install, installState, instructionsFile, isMode, legacyLine, remove, removeLegacy, vaultRepoFlag } from './install.js';
19
21
  import { noVaultMessage, resolveVaultPath } from './resolve-vault.js';
20
22
  import { HafezError } from '../types.js';
21
23
  import { kindDir, scanVaultDir } from '../vault.js';
22
- import { ITEMS } from '../surfaces/items.js';
24
+ import { ITEMS, isVaultRepo } from '../surfaces/items.js';
23
25
  import { renderBlock } from '../surfaces/surfaces.js';
24
26
  import { blockHook } from './block-hook.js';
25
27
  const HARNESS_IDS = Object.keys(HARNESSES).join('|');
26
28
  const MODE_IDS = MODES.join('|');
27
29
  const ONBOARD_USAGE = `Usage:
28
30
  hafez onboard
29
- hafez onboard --install --mode <${MODE_IDS}> [--harness <${HARNESS_IDS}>] [--confirm]
31
+ hafez onboard --install --mode <${MODE_IDS}> [--harness <${HARNESS_IDS}>] [--vault-repo <owner>/<repo>] [--confirm]
30
32
  hafez onboard --remove [--confirm]
31
- hafez onboard --block [--harness <${HARNESS_IDS}>]`;
33
+ hafez onboard --block [--harness <${HARNESS_IDS}>]
34
+ hafez onboard --cloud --vault-repo <owner>/<repo>`;
32
35
  /** `ctx` is the temp-home seam: tests pass a temporary home and environment;
33
36
  * the install functions are given the home, never read it. `vaultFlag` is the
34
37
  * global `--vault`; without it the bare form reports on the registered vault.
35
38
  * `--block` returns a promise of its render, or of null when it must print nothing. */
36
39
  export function cmdOnboard(args, ctx = {}) {
37
40
  const { home = homedir(), env = process.env, cwd, vaultFlag } = ctx;
38
- const { flags, positionals } = parseArgs(args, { install: 'boolean', remove: 'boolean', block: 'boolean', mode: 'string', harness: 'string', confirm: 'boolean' }, ONBOARD_USAGE);
41
+ const { flags, positionals } = parseArgs(args, { install: 'boolean', remove: 'boolean', block: 'boolean', cloud: 'boolean', mode: 'string', harness: 'string', 'vault-repo': 'string', confirm: 'boolean' }, ONBOARD_USAGE);
39
42
  const fail = (why) => {
40
43
  throw new UsageError(`${why}\n${ONBOARD_USAGE}`);
41
44
  };
@@ -43,9 +46,22 @@ export function cmdOnboard(args, ctx = {}) {
43
46
  fail(`Unexpected argument: ${positionals[0]}`);
44
47
  const mode = str(flags.mode);
45
48
  const harnessFlag = str(flags.harness);
49
+ const vaultRepo = str(flags['vault-repo']);
46
50
  const confirm = flags.confirm === true;
47
51
  if (flags.install && flags.remove)
48
52
  fail('--install and --remove are separate commands');
53
+ if (vaultRepo !== undefined && !isVaultRepo(vaultRepo))
54
+ fail(`--vault-repo takes the vault's GitHub repo as <owner>/<repo>, got: ${vaultRepo}`);
55
+ if (vaultRepo !== undefined && !flags.cloud && !flags.install)
56
+ fail('--vault-repo goes with --cloud or --install');
57
+ if (flags.cloud) {
58
+ if (flags.install || flags.remove || flags.block || mode !== undefined || harnessFlag !== undefined || confirm) {
59
+ fail('--cloud takes only --vault-repo: it prints the setup script and writes nothing');
60
+ }
61
+ if (vaultRepo === undefined)
62
+ return fail('--cloud needs --vault-repo <owner>/<repo>, the vault\'s GitHub repo');
63
+ return ITEMS.cloudSetup.render({ vaultRepo });
64
+ }
49
65
  if (flags.block) {
50
66
  if (flags.install || flags.remove || mode !== undefined || confirm)
51
67
  fail('--block takes only --harness: it prints the Block and writes nothing');
@@ -67,7 +83,11 @@ export function cmdOnboard(args, ctx = {}) {
67
83
  if (!isHarness(harness)) {
68
84
  return fail(harnessFlag === undefined ? `No harness detected: pass --harness <${HARNESS_IDS}>` : `Unknown harness: ${harnessFlag}`);
69
85
  }
70
- return install({ home, harness, mode, confirm });
86
+ // The cloud Block's rule is Claude Code's: cloud sessions attach repos there.
87
+ if (vaultRepo !== undefined && (mode !== 'always-on' || harness !== 'claude-code')) {
88
+ fail('--vault-repo is for a Claude Code cloud container: --mode always-on --harness claude-code');
89
+ }
90
+ return install({ home, harness, mode, confirm, vaultRepo });
71
91
  }
72
92
  if (mode !== undefined || harnessFlag !== undefined || confirm)
73
93
  fail('--mode, --harness and --confirm go with --install, --remove or --block');
@@ -149,7 +169,8 @@ export function renderOnboard({ home, harness, vault }) {
149
169
  const teaching = (m) => ITEMS.teachingLines.render({ harness, mode: m });
150
170
  const line2 = (m) => ITEMS.teachingLines.render({ harness, mode: m, line2Only: true });
151
171
  const instructionsRel = `~/${instructionsFile(home, instructions)}`;
152
- const block = `\`\`\`\`\n${renderBlock()}\`\`\`\``;
172
+ // A cloud Block stays one: its install line and Block keep the vault repo.
173
+ const block = `\`\`\`\`\n${renderBlock(state.vaultRepo)}\`\`\`\``;
153
174
  const showBlock = `show the user the Block below word for word, not a summary, say it goes into ${instructionsRel} between two marker lines and nothing else in that file changes, and get a yes. Then run:`;
154
175
  const status = [
155
176
  `Vault: ${vault} (${plural(entities, 'entity', 'entities')}, ${plural(knowledge, 'knowledge note', 'knowledge notes')})`,
@@ -194,7 +215,7 @@ export function renderOnboard({ home, harness, vault }) {
194
215
  ...(state.legacy === null ? [] : [`${removeLegacy(instructionsRel, state.legacy)} Do this before either install below.`, '']),
195
216
  `Always-on: ${showBlock}`,
196
217
  '',
197
- 'hafez onboard --install --mode always-on',
218
+ `hafez onboard --install --mode always-on${vaultRepoFlag(state.vaultRepo)}`,
198
219
  '',
199
220
  'Invoke-only: no file of theirs is edited. Run:',
200
221
  '',
@@ -29,12 +29,12 @@ import { dirname, join } from 'path';
29
29
  import { fileURLToPath } from 'url';
30
30
  import { UsageError, helpIfAsked } from './args.js';
31
31
  import { HARNESSES, detectAgent, isHarness } from '../harness.js';
32
- import { checkInstall, fixBrokenMarkers, legacyLine, removeLegacy } from './install.js';
32
+ import { checkInstall, fixBrokenMarkers, legacyLine, removeLegacy, vaultRepoFlag } from './install.js';
33
33
  const PLAIN_SEMVER = /^\d+\.\d+\.\d+$/;
34
34
  const REGISTRY_TIMEOUT_MS = 3000;
35
35
  const NPM_PROBE_TIMEOUT_MS = 3000;
36
36
  const DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org/hafez/latest';
37
- const DEV_CHECKOUT_WORK = 'git pull && npm run build && npm run install:local';
37
+ const DEV_CHECKOUT_WORK = 'git pull && npm ci && npm run build && npm run install:local';
38
38
  const NPM_PROBE_NOTE = 'The local npm version could not be determined, so every install flag is included above. ' +
39
39
  'npm majors that predate a flag print `npm warn Unknown cli config` and proceed — that warning is harmless.';
40
40
  const ETARGET_NOTE = 'If the install fails with ETARGET, your configured registry (a mirror or proxy) does not have the pinned version — ' +
@@ -179,7 +179,7 @@ function blockSteps({ blocks, skills }, upgrading, env) {
179
179
  const targets = upgrading ? blocks : blocks.filter(blockDrift);
180
180
  return (targets.length > 0 ? targets : skillsOnly).map(b => ({
181
181
  kind: 'command',
182
- text: `hafez onboard --install --mode always-on --harness ${b.harness}`,
182
+ text: `hafez onboard --install --mode always-on --harness ${b.harness}${vaultRepoFlag(b.vaultRepo)}`,
183
183
  }));
184
184
  }
185
185
  const harness = detectAgent(env);
@@ -192,7 +192,8 @@ function blockSteps({ blocks, skills }, upgrading, env) {
192
192
  }
193
193
  /** The action list, in execution order. Every command is built from static
194
194
  * constants; the only interpolated values are the regex-validated version,
195
- * the quoted package root, and harness ids from the HARNESSES table. */
195
+ * the quoted package root, harness ids from the HARNESSES table, and a cloud
196
+ * Block's vault repo, which `findRegion` reads only when it passes `isVaultRepo`. */
196
197
  function deriveSteps(state, ctx) {
197
198
  const steps = [];
198
199
  let alternativeInstall = null;
@@ -4,6 +4,15 @@ export declare const ENTITY_STATUSES: readonly ["active", "paused", "done"];
4
4
  export declare const SESSION_LOG_TYPES: readonly ["progress", "decision", "blocker", "research"];
5
5
  export declare const CONFIDENCE_LEVELS: readonly ["observation", "pattern", "principle"];
6
6
  export declare const VALID_SUBTYPES: readonly ["insight", "plan"];
7
+ /** The Graph's layers whose nodes are synthetic, one per distinct value
8
+ * (`<layer>:<value>`); each adds the edge relation of the same name. */
9
+ export declare const SYNTHETIC_LAYERS: readonly ["tag", "domain"];
10
+ /** The Graph's optional layers (#253): tag and domain nodes, Session nodes. */
11
+ export declare const GRAPH_LAYERS: readonly ["tag", "domain", "session"];
12
+ /** The Graph's edges between notes; the synthetic layers add their own. */
13
+ export declare const GRAPH_NOTE_RELATIONS: readonly ["parent", "related", "mention"];
14
+ /** What a write did to a note, as history tells it (changesSince, #258). */
15
+ export declare const CHANGE_OPS: readonly ["create", "update", "delete"];
7
16
  export interface TypeContract {
8
17
  type: string;
9
18
  description: string;
package/dist/contracts.js CHANGED
@@ -10,6 +10,15 @@ export const CONFIDENCE_LEVELS = ['observation', 'pattern', 'principle'];
10
10
  // 'insight' is the subtype (frontmatter classification), 'Synthesis' is the section heading.
11
11
  // The rename was insight→synthesis for the section/field only; the subtype stays 'insight'.
12
12
  export const VALID_SUBTYPES = ['insight', 'plan'];
13
+ /** The Graph's layers whose nodes are synthetic, one per distinct value
14
+ * (`<layer>:<value>`); each adds the edge relation of the same name. */
15
+ export const SYNTHETIC_LAYERS = ['tag', 'domain'];
16
+ /** The Graph's optional layers (#253): tag and domain nodes, Session nodes. */
17
+ export const GRAPH_LAYERS = [...SYNTHETIC_LAYERS, 'session'];
18
+ /** The Graph's edges between notes; the synthetic layers add their own. */
19
+ export const GRAPH_NOTE_RELATIONS = ['parent', 'related', 'mention'];
20
+ /** What a write did to a note, as history tells it (changesSince, #258). */
21
+ export const CHANGE_OPS = ['create', 'update', 'delete'];
13
22
  /** The item frontmatter contract, shared by all three item types — they
14
23
  * differ in sections and lifecycle, never in which fields are mandatory.
15
24
  * schema.validateEntityFrontmatter enforces exactly this list (parity test in
package/dist/dates.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  /** The local calendar day as YYYY-MM-DD. The single source of "today". */
2
2
  export declare function localDay(d?: Date): string;
3
+ /** Whole days from one YYYY-MM-DD day to another, as calendar days: a
4
+ * difference that ignores DST. */
5
+ export declare function daysBetween(from: string, to: string): number;
3
6
  /**
4
7
  * Bare YYYY-MM-DD → local midnight, for git's approxidate. A date-only
5
8
  * `--since` is otherwise filled with the *current* time of day ("since right
6
9
  * now"), and any explicit instant must anchor to the same local day boundary
7
10
  * the stamps and query comparisons use. Relative forms (`7.days.ago`) are
8
11
  * already local and pass through untouched.
12
+ *
13
+ * The offset is spelled out: without one git applies its own idea of the
14
+ * local zone, and Git for Windows ignores an IANA `TZ` that Node honours,
15
+ * so the two disagreed about where the day starts.
9
16
  */
10
17
  export declare function normalizeSinceDate(since: string): string;
package/dist/dates.js CHANGED
@@ -15,13 +15,33 @@ function pad(n) {
15
15
  export function localDay(d = new Date()) {
16
16
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
17
17
  }
18
+ /** Whole days from one YYYY-MM-DD day to another, as calendar days: a
19
+ * difference that ignores DST. */
20
+ export function daysBetween(from, to) {
21
+ return dayNumber(to) - dayNumber(from);
22
+ }
23
+ function dayNumber(day) {
24
+ const [y, m, d] = day.split('-').map(Number);
25
+ return Date.UTC(y, m - 1, d) / 86_400_000;
26
+ }
18
27
  /**
19
28
  * Bare YYYY-MM-DD → local midnight, for git's approxidate. A date-only
20
29
  * `--since` is otherwise filled with the *current* time of day ("since right
21
30
  * now"), and any explicit instant must anchor to the same local day boundary
22
31
  * the stamps and query comparisons use. Relative forms (`7.days.ago`) are
23
32
  * already local and pass through untouched.
33
+ *
34
+ * The offset is spelled out: without one git applies its own idea of the
35
+ * local zone, and Git for Windows ignores an IANA `TZ` that Node honours,
36
+ * so the two disagreed about where the day starts.
24
37
  */
25
38
  export function normalizeSinceDate(since) {
26
- return /^\d{4}-\d{2}-\d{2}$/.test(since) ? `${since}T00:00:00` : since;
39
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(since);
40
+ if (!m)
41
+ return since;
42
+ // The day's first instant: 01:00 where a DST jump skips midnight.
43
+ const start = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
44
+ const offset = -start.getTimezoneOffset();
45
+ const zone = `${offset < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(offset) / 60))}${pad(Math.abs(offset) % 60)}`;
46
+ return `${localDay(start)}T${pad(start.getHours())}:${pad(start.getMinutes())}:00${zone}`;
27
47
  }
package/dist/db.d.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import { type VaultKind } from './vault.js';
1
2
  import { type VaultIndexRow } from './knowledge-index.js';
2
- import type { SearchResult, VaultStats, QueryResult, KnowledgeQueryResult, UnifiedResult, EntityQueryOpts, KnowledgeQueryOpts, UnifiedQueryOpts, SortField } from './types.js';
3
- export declare const SCHEMA_VERSION = "11";
3
+ import type { SearchResult, SearchKind, VaultStats, QueryResult, KnowledgeQueryResult, SessionQueryResult, UnifiedResult, EntityQueryOpts, KnowledgeQueryOpts, SessionQueryOpts, UnifiedQueryOpts, SortField, Graph, GraphOpts } from './types.js';
4
+ export declare const SCHEMA_VERSION = "13";
4
5
  interface IndexItem {
5
6
  slug: string;
6
- kind: 'entity' | 'knowledge';
7
+ kind: VaultKind;
7
8
  name: string;
8
9
  type: string | null;
9
10
  status: string | null;
@@ -26,6 +27,9 @@ interface IndexItem {
26
27
  last_session_date: string | null;
27
28
  last_session_type: string | null;
28
29
  last_session_summary: string | null;
30
+ session_date: string | null;
31
+ /** An entity's open actions' added days as a JSON array, null for an undated line. */
32
+ action_added: string | null;
29
33
  }
30
34
  export interface IndexQueryResult {
31
35
  items: IndexItem[];
@@ -33,14 +37,20 @@ export interface IndexQueryResult {
33
37
  }
34
38
  export interface HafezIndex {
35
39
  rebuild(): void;
40
+ /** Catch the index up with the files, whatever the mode: a stat per note
41
+ * (read-only: reopen a database another process rewrote). */
36
42
  syncIfStale(): void;
37
- upsertFromFile(slug: string, kind: 'entity' | 'knowledge'): void;
43
+ /** What a read runs first, by the index's `sync` mode (`IndexOpts`). */
44
+ syncForRead(): void;
45
+ upsertFromFile(slug: string, kind: VaultKind): void;
38
46
  removeItem(slug: string): void;
39
47
  queryItems(opts: QueryOpts): IndexQueryResult;
40
- search(query: string, kind?: 'entity' | 'knowledge' | 'all'): SearchResult[];
48
+ search(query: string, kind?: SearchKind): SearchResult[];
41
49
  getStats(): VaultStats;
42
50
  /** Every indexed document, in the shape index.md is rendered from. */
43
51
  indexEntries(): VaultIndexRow[];
52
+ /** The rows the Graph is shaped from: every item, every link. */
53
+ graphRows(): GraphRows;
44
54
  close(): void;
45
55
  queryEntities(opts?: EntityQueryOpts): {
46
56
  items: QueryResult[];
@@ -58,13 +68,30 @@ export interface HafezIndex {
58
68
  items: KnowledgeQueryResult[];
59
69
  total: number;
60
70
  };
71
+ querySessions(opts?: SessionQueryOpts): {
72
+ items: SessionQueryResult[];
73
+ total: number;
74
+ };
61
75
  queryUnified(opts?: UnifiedQueryOpts): {
62
76
  items: UnifiedResult[];
63
77
  total: number;
64
78
  };
79
+ queryGraph(opts?: GraphOpts): Graph;
80
+ }
81
+ export interface GraphRows {
82
+ items: Array<Pick<IndexItem, 'slug' | 'kind' | 'name' | 'type' | 'status' | 'subtype' | 'confidence' | 'last_touched' | 'description' | 'next_action_count' | 'staleness_days' | 'action_added'> & {
83
+ /** Entity-stale by the index's stale rule, 1 or 0; null for a note with no last-touched. */
84
+ stale: number | null;
85
+ }>;
86
+ links: Array<{
87
+ source: string;
88
+ target: string;
89
+ relation: string;
90
+ }>;
65
91
  }
92
+ /** `kind` unset is the working set, entity + knowledge; `all` is literal. */
66
93
  export interface QueryOpts {
67
- kind?: 'entity' | 'knowledge' | 'all';
94
+ kind?: VaultKind | 'all';
68
95
  status?: string;
69
96
  type?: string;
70
97
  subtype?: string;
@@ -79,15 +106,25 @@ export interface QueryOpts {
79
106
  createdSince?: string;
80
107
  createdBefore?: string;
81
108
  tags?: string[];
82
- sort_by?: 'last_touched' | 'created' | 'name' | 'staleness' | 'last_reinforced' | 'reinforcement_count';
109
+ sort_by?: SortField;
83
110
  sort_order?: 'asc' | 'desc';
84
111
  limit?: number;
85
112
  offset?: number;
86
113
  }
87
114
  /** @internal */
88
115
  export declare function __setAfterScanHook(fn: (() => void) | null): void;
89
- export declare function createIndex(vaultPath: string): HafezIndex;
90
- export declare function createIndex(vaultPath: string, opts: {
116
+ /** `sync`: what `syncForRead`, run first by every read-surface query, does.
117
+ * 'read' (default): runs `syncIfStale`. 'once': runs it until a sync has
118
+ * caught the index up, then trusts the index (#269). 'manual': trusts the
119
+ * index, and the caller syncs it. */
120
+ export type IndexSync = 'read' | 'once' | 'manual';
121
+ export interface IndexOpts {
122
+ sync?: IndexSync;
123
+ }
124
+ export declare function createIndex(vaultPath: string, opts?: IndexOpts & {
125
+ readonly?: false;
126
+ }): HafezIndex;
127
+ export declare function createIndex(vaultPath: string, opts: IndexOpts & {
91
128
  readonly: true;
92
129
  }): HafezIndex | null;
93
130
  /** Opts a branch cannot map into SQL. Declared per branch and rejected on
@@ -96,11 +133,14 @@ export declare function createIndex(vaultPath: string, opts: {
96
133
  * Asserted against the CLI's flag matrix in query-surface.test.ts. */
97
134
  export declare const ENTITY_ONLY_OPTS: readonly ["filter", "type", "parent"];
98
135
  export declare const KNOWLEDGE_ONLY_OPTS: readonly ["confidence", "subtype"];
136
+ /** What a session query cannot map: both sides' opts, and the two every
137
+ * other kind shares — a session note carries no domain and no tags. */
138
+ export declare const SESSION_REJECTED_OPTS: readonly ["filter", "type", "parent", "confidence", "subtype", "domain", "tags"];
99
139
  /** Every opt any branch might carry, flattened. Only the unified read uses it,
100
140
  * and only to check: the checks must see an opt the caller's own type forbids,
101
141
  * because an untyped caller can still send it. */
102
142
  export type UnmappableCheckOpts = Omit<EntityQueryOpts, 'sort_by'> & Omit<KnowledgeQueryOpts, 'sort_by'> & {
103
- kind?: 'entity' | 'knowledge' | 'all';
143
+ kind?: VaultKind | 'all';
104
144
  sort_by?: SortField;
105
145
  };
106
146
  export {};