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/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Hafez
|
|
2
2
|
|
|
3
|
+
[](https://github.com/shrvn-r/hafez/actions/workflows/public.yml)
|
|
4
|
+
|
|
3
5
|
**A knowledge and project vault shared between you and your agents.** *Hafez* (حافظ) is Persian for "the keeper — the one who memorizes," after the poet. Plain markdown files you own, backed by SQLite full-text search and git.
|
|
4
6
|
|
|
5
7
|
## Why
|
|
@@ -77,6 +79,23 @@ plus `--allow-remote=root`, which npm 12+ requires for a remote URL:
|
|
|
77
79
|
npm install -g https://github.com/shrvn-r/hafez/releases/latest/download/hafez.tgz --allow-scripts=better-sqlite3 --allow-remote=root
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
### Cloud sessions
|
|
83
|
+
|
|
84
|
+
A Claude Code cloud session (claude.ai/code, the mobile app) starts in a fresh
|
|
85
|
+
container: no `hafez`, no vault, no Block. Keep the vault in a private GitHub
|
|
86
|
+
repo and print the environment's setup script for it:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
hafez onboard --cloud --vault-repo <owner>/<vault-repo>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Paste the script into the cloud environment's Setup script; the Trusted network
|
|
93
|
+
policy is enough. It installs Hafez, registers the vault when it is attached to
|
|
94
|
+
the session, and writes a Block with one rule more: a session started without
|
|
95
|
+
the vault attached attaches it, clones it and registers it, with one approval
|
|
96
|
+
from you. The script never clones the vault itself, because it runs without
|
|
97
|
+
GitHub credentials. Writes push to the vault's `main` like anywhere else.
|
|
98
|
+
|
|
80
99
|
## Updating
|
|
81
100
|
|
|
82
101
|
Tell your agent: **update hafez**. Or paste: "Run `hafez upgrade` and follow
|
|
@@ -148,6 +167,9 @@ hafez query --kind knowledge --domain security
|
|
|
148
167
|
# Full-text search across the whole vault
|
|
149
168
|
hafez search "token rotation"
|
|
150
169
|
|
|
170
|
+
# See it in context: a live graph of the vault in your browser
|
|
171
|
+
hafez graph auth-refactor
|
|
172
|
+
|
|
151
173
|
# Record progress
|
|
152
174
|
hafez update auth-refactor --log "progress: JWT flow implemented"
|
|
153
175
|
hafez update auth-refactor --complete-action "migration plan"
|
|
@@ -161,7 +183,7 @@ Three kinds of documents, all markdown + YAML frontmatter:
|
|
|
161
183
|
|
|
162
184
|
- **Entities** (`entities/*.md`) — the things you're working on. Types: `capture` (inbox item), `entity` (anything worth tracking), `project` (has outcomes). Statuses: `active | paused | done`. Bodies start from a per-type template (capture: `## Notes`; entity: `## Context`; project: `## Purpose` + `## Goals`) plus a `## Session Log` of dated agent entries; `## Brief` (freeform handoff context) and `## Next Actions` (checkbox list) appear when first written to.
|
|
163
185
|
- **Knowledge** (`knowledge/*.md`) — durable insights that outlive any one project. Subtypes `insight | plan`; confidence levels `observation → pattern → principle` (promoted manually). Bodies hold `## Synthesis`, `## Evidence`, and `## Sources` — evidence accumulates over time via `--add-evidence`.
|
|
164
|
-
- **Sessions** (`sessions/*.md`) — session summaries written
|
|
186
|
+
- **Sessions** (`sessions/*.md`) — session summaries written at save time, never updated. Readable (`hafez read <slug>`), listed newest first (`hafez query --kind session`), and the look back from an entity (`hafez query --related-to <slug>`); search and query reach them only under `--kind session` or `--kind all`.
|
|
165
187
|
|
|
166
188
|
Frontmatter at a glance:
|
|
167
189
|
|
|
@@ -175,15 +197,57 @@ Frontmatter at a glance:
|
|
|
175
197
|
| `resource` (canonical URI) | optional | — |
|
|
176
198
|
| `subtype`, `confidence` | — | optional |
|
|
177
199
|
|
|
178
|
-
Links (`parent`, `related`) are validated — they must point at slugs that exist. `hafez validate` checks vault integrity.
|
|
200
|
+
Links (`parent`, `related`) are validated — they must point at slugs that exist. A `related` link between two entities, or two knowledge notes, is kept on both files. `hafez validate` checks vault integrity.
|
|
201
|
+
|
|
202
|
+
## Where your judgement matters
|
|
203
|
+
|
|
204
|
+
The agent writes to your vault as work lands, without asking. Behind a few of those writes is a judgement no rule in the tool can make, and the Health pass's proposals wait for your yes. Each choice is below as the question, the test, and what happens once it is made, so you can make it, or check the one the agent made.
|
|
205
|
+
|
|
206
|
+
### 1. Child or related?
|
|
207
|
+
|
|
208
|
+
**Question:** one piece of work bears on another. Is it part of it, or beside it?
|
|
209
|
+
|
|
210
|
+
**Test:** would it carry on if the other were finished or dropped? If not, it is a **Child**. If so, the two are **Related**.
|
|
211
|
+
|
|
212
|
+
**Then:** a Child carries `parent`, the one mark of childhood, and the parent's `read` lists it. Related between two entities, or two knowledge notes, is one edge the tool keeps on both files. A pair is never both: a `related` naming a parent or a child is rejected, and so is a `--parent` naming a related entity. A parent whose own work is coordination only is a **Hub**.
|
|
213
|
+
|
|
214
|
+
### 2. Split a Workstream or not?
|
|
215
|
+
|
|
216
|
+
**Question:** an entity's `read` shows a `workstream:` line: its actions share an ALL-CAPS prefix that no Child or related entity carries in its name. Is it a real thread of work, or a habit of phrasing?
|
|
217
|
+
|
|
218
|
+
**Test:** would you track it, brief it and finish it apart from the parent?
|
|
219
|
+
|
|
220
|
+
**Then:** split, and the agent does it with ordinary writes: a Child is created under the parent, the open actions move to it, the log entries stay where they are, and one `decision` entry on the parent records the split. Don't, and nothing is written: the line is a fact, not a request, and it goes once those actions are done.
|
|
221
|
+
|
|
222
|
+
### 3. Decision or lesson?
|
|
223
|
+
|
|
224
|
+
**Question:** something was settled. Where does it live?
|
|
225
|
+
|
|
226
|
+
**Test:** is it still true with this entity gone, no slug, path or "we chose" needed? If not, it is a **Decision**; if so, a lesson.
|
|
227
|
+
|
|
228
|
+
**Then:** a decision is one `decision` entry on the log of the entity that owns the work (or an entry pointing at the repo's own record). A lesson is a knowledge note, related to where it came from. One fact, one place: a knowledge note that only repeats a decision is the Health pass's "restated decision".
|
|
229
|
+
|
|
230
|
+
### 4. Retire or delete?
|
|
231
|
+
|
|
232
|
+
**Question:** something in the vault is no longer wanted.
|
|
233
|
+
|
|
234
|
+
**Test:** is it an entity? Then it is retired. A knowledge or session note can be deleted.
|
|
235
|
+
|
|
236
|
+
**Then:** retiring sets `--status done`: the entity stays, readable and linked, and leaves the live views unless a Child of it is not yet done. Deleting removes the note in one commit, together with the back-edges it owns (the notes its own `related` names that name it back); any other entity or knowledge note naming it as `parent` or `related` refuses the delete until that link goes. Every write is a git commit, so either can be undone.
|
|
237
|
+
|
|
238
|
+
### 5. What `hafez health` lists
|
|
239
|
+
|
|
240
|
+
`hafez health` is the **Health pass**: it reads the vault and lists what placement would change, applying nothing. Run it, or ask the agent to check the vault; a sample of its output is [`health.md`](tests/fixtures/surfaces/health.md).
|
|
241
|
+
|
|
242
|
+
Its six groups take three kinds of answer. **One-way edges** (a `related` kept on one end only) and **unlinked mentions** (a `[[slug]]` with no edge either way) are mechanical: each comes with the `link` line that adds the edge the vault already implies, so one yes can cover the group. **Workstreams** are question 2, one entity at a time. **Leakage** (a parent's, or a related entity's, log naming an entity since that entity was created: was that its work?), **restated decisions** (question 3) and **recurring names** (a name that keeps coming back in one entity's log and actions) are judged one by one: nothing is proposed for them.
|
|
179
243
|
|
|
180
244
|
## CLI overview
|
|
181
245
|
|
|
182
246
|
| Command | What it does |
|
|
183
247
|
|---|---|
|
|
184
|
-
| `hafez read <slug> [--depth frontmatter\|summary\|full]` | Read an entity or
|
|
185
|
-
| `hafez query [--filter active\|paused\|done\|stale\|capture\|all] [--type] [--kind entity\|knowledge\|all] [--domain] [--tag] [--parent] [--related-to] [--limit]` | Filter and list |
|
|
186
|
-
| `hafez search <terms> [--kind
|
|
248
|
+
| `hafez read <slug> [--depth frontmatter\|summary\|full] [--archive]` | Read an entity, knowledge or session note; `--archive` an entity's archived log entries |
|
|
249
|
+
| `hafez query [--filter active\|paused\|done\|stale\|capture\|all] [--type] [--kind entity\|knowledge\|session\|all] [--domain] [--tag] [--parent] [--related-to] [--limit]` | Filter and list |
|
|
250
|
+
| `hafez search <terms> [--kind entity\|knowledge\|session\|all]` | FTS5 full-text search (default: entities and knowledge) |
|
|
187
251
|
| `hafez create <entity\|knowledge> <name> [flags]` | Create a document |
|
|
188
252
|
| `hafez capture <name> [--notes]` | Quick inbox capture |
|
|
189
253
|
| `hafez promote <slug> <entity\|project\|knowledge>` | Promote a capture upward |
|
|
@@ -193,10 +257,13 @@ Links (`parent`, `related`) are validated — they must point at slugs that exis
|
|
|
193
257
|
| `hafez batch [--dry-run] [--data '<json>' \| --file payload.json]` | JSON ops (stdin, `--data` or `--file`) → atomic multi-op, single git commit, rollback on failure; dry-run runs the same validation as apply and reports derived slugs |
|
|
194
258
|
| `hafez stats` | Vault summary: counts, stale items, recents |
|
|
195
259
|
| `hafez changelog --since 7.days.ago` | Git-derived change history |
|
|
260
|
+
| `hafez graph [slug] [--port N] [--no-open]` | Interactive, live graph of the vault in the browser (served on 127.0.0.1); a slug opens it focused on that note; `--json [--layers tag,domain,session]` prints `{nodes, edges}` |
|
|
196
261
|
| `hafez sync` | Pull remote, push local commits (semantic merge on conflicts) |
|
|
197
262
|
| `hafez validate` / `hafez index rebuild` | Integrity check / rebuild the read index |
|
|
263
|
+
| `hafez health` | What placement would change: unsplit workstreams, recurring names, leakage, one-way `related` edges and unlinked `[[slug]]` mentions (each with its `hafez link` line), restated decisions; applies nothing |
|
|
198
264
|
| `hafez onboard` | Setup status, then the script your agent follows: Mode, install, permissions, seeding |
|
|
199
265
|
| `hafez onboard --install --mode <always-on\|invoke-only>` / `--remove` | Write (or remove) the two skills and, always-on, the marked Block in the harness's instructions file |
|
|
266
|
+
| `hafez onboard --cloud --vault-repo <owner>/<repo>` | Print the setup script for Claude Code cloud sessions |
|
|
200
267
|
| `hafez schema [op] [--examples]` | Machine-readable JSON schema for every operation |
|
|
201
268
|
| `hafez export --okf [--out <dir>]` | Read-only OKF v0.1 bundle export |
|
|
202
269
|
|
package/dist/apply.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VaultKind } from './vault.js';
|
|
1
2
|
import type { BatchOperation, BatchResult, CreateEntityFields, DeletableKind, CreateKnowledgeFields, KnowledgeSubtype, UpdateFields, UpdateResult } from './types.js';
|
|
2
3
|
export interface AppliedOp {
|
|
3
4
|
filesWritten: string[];
|
|
@@ -7,7 +8,7 @@ export interface AppliedOp {
|
|
|
7
8
|
indexRemovals: string[];
|
|
8
9
|
indexUpserts: Array<{
|
|
9
10
|
slug: string;
|
|
10
|
-
kind:
|
|
11
|
+
kind: VaultKind;
|
|
11
12
|
}>;
|
|
12
13
|
/** Set by the update arm: the line each Next Actions query matched. */
|
|
13
14
|
updateResult?: UpdateResult;
|
|
@@ -25,6 +26,18 @@ export interface ApplyContext {
|
|
|
25
26
|
export declare function validateKindFields(kind: 'entity' | 'knowledge', fields: UpdateFields, subtype?: KnowledgeSubtype): void;
|
|
26
27
|
export declare function buildEntityFrontmatter(name: string, ef: CreateEntityFields, existsFn: (slug: string) => boolean): Record<string, any>;
|
|
27
28
|
export declare function buildKnowledgeFrontmatter(name: string, kf: CreateKnowledgeFields, existsFn: (slug: string) => boolean): Record<string, any>;
|
|
29
|
+
/** The mirror file an edge op touched: what the arm appends to its
|
|
30
|
+
* filesWritten and indexUpserts. */
|
|
31
|
+
export interface EdgeTouch {
|
|
32
|
+
file: string;
|
|
33
|
+
upsert: {
|
|
34
|
+
slug: string;
|
|
35
|
+
kind: VaultKind;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Drop `a` from `b`'s related when the edge is same-kind. Exported for the
|
|
39
|
+
* delete arm (#243) as well. */
|
|
40
|
+
export declare function dropBackEdge(ctx: ApplyContext, a: string, aKind: VaultKind, b: string): EdgeTouch | null;
|
|
28
41
|
/** The single apply path for every mutation. Writes vault files but never
|
|
29
42
|
* touches git or SQLite: the Spine commits [...filesWritten, ...filesDeleted]
|
|
30
43
|
* under commitMessage (single ops) or one composed batch message, then runs
|
package/dist/apply.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// pure (no filesystem access); the frontmatter builders live here and are
|
|
19
19
|
// exported so the batch validate phase can run them against a simulated vault.
|
|
20
20
|
import { writeFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync } from 'fs';
|
|
21
|
-
import { join,
|
|
22
|
-
import { parseFilePath, serializeFile, slugify, resolveFilePath, kindFromPath, kindDir, archiveLogPath, findVaultFile, vaultFileExists } from './vault.js';
|
|
21
|
+
import { join, dirname } from 'path';
|
|
22
|
+
import { parseFilePath, serializeFile, slugify, resolveFilePath, vaultRel, kindFromPath, kindDir, archiveLogPath, findVaultFile, vaultFileExists } from './vault.js';
|
|
23
23
|
import { validateEntityFrontmatter, validateKnowledgeFrontmatter } from './schema.js';
|
|
24
24
|
import { bodyTemplate, knowledgeBodyTemplate, sessionBodyTemplate } from './templates.js';
|
|
25
25
|
import { formatSessionLogEntry, stampAction, documentFor, documentForFile } from './document.js';
|
|
@@ -369,6 +369,42 @@ function withMechanisedLog(kind, fields) {
|
|
|
369
369
|
},
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
|
+
function mirrorEdge(ctx, a, aKind, b, mode) {
|
|
373
|
+
if (a === b)
|
|
374
|
+
return null;
|
|
375
|
+
const bPath = findVaultFile(ctx.vaultPath, b);
|
|
376
|
+
if (!bPath || kindFromPath(bPath) !== aKind)
|
|
377
|
+
return null;
|
|
378
|
+
const parsed = parseFilePath(bPath);
|
|
379
|
+
const fm = parsed.frontmatter;
|
|
380
|
+
const related = fm.related ?? [];
|
|
381
|
+
if (mode === 'write' ? related.includes(a) : !related.includes(a))
|
|
382
|
+
return null;
|
|
383
|
+
ctx.captureOriginal(bPath);
|
|
384
|
+
if (mode === 'write')
|
|
385
|
+
fm.related = [...related, a];
|
|
386
|
+
else {
|
|
387
|
+
fm.related = related.filter(r => r !== a);
|
|
388
|
+
if (fm.related.length === 0)
|
|
389
|
+
delete fm.related;
|
|
390
|
+
}
|
|
391
|
+
const body = documentForFile(parsed).syncRelatedSection(parsed.body, fm);
|
|
392
|
+
writeFileSync(bPath, serializeFile(fm, body));
|
|
393
|
+
return { file: vaultRel(ctx.vaultPath, bPath), upsert: { slug: b, kind: aKind } };
|
|
394
|
+
}
|
|
395
|
+
/** Write `a` into `b`'s related when the edge is same-kind. */
|
|
396
|
+
function writeBackEdge(ctx, a, aKind, b) {
|
|
397
|
+
return mirrorEdge(ctx, a, aKind, b, 'write');
|
|
398
|
+
}
|
|
399
|
+
/** Write `a` into every same-kind target's related. */
|
|
400
|
+
function backEdges(ctx, a, aKind, targets) {
|
|
401
|
+
return targets.flatMap(b => writeBackEdge(ctx, a, aKind, b) ?? []);
|
|
402
|
+
}
|
|
403
|
+
/** Drop `a` from `b`'s related when the edge is same-kind. Exported for the
|
|
404
|
+
* delete arm (#243) as well. */
|
|
405
|
+
export function dropBackEdge(ctx, a, aKind, b) {
|
|
406
|
+
return mirrorEdge(ctx, a, aKind, b, 'drop');
|
|
407
|
+
}
|
|
372
408
|
// --- The arms ---
|
|
373
409
|
function applyUpdate(op, ctx) {
|
|
374
410
|
const { vaultPath } = ctx;
|
|
@@ -379,6 +415,9 @@ function applyUpdate(op, ctx) {
|
|
|
379
415
|
const doc = documentForFile(parsedFile);
|
|
380
416
|
let fm = parsedFile.frontmatter;
|
|
381
417
|
let body = parsedFile.body;
|
|
418
|
+
// The related list before the fields replace it: the diff against the new
|
|
419
|
+
// one is what the edge writer and dropper mirror on the targets.
|
|
420
|
+
const relatedBefore = fm.related ?? [];
|
|
382
421
|
const applied = applyUpdateFields(doc, fm, body, withMechanisedLog(kind, op.fields));
|
|
383
422
|
fm = applied.fm;
|
|
384
423
|
body = applied.body;
|
|
@@ -400,19 +439,30 @@ function applyUpdate(op, ctx) {
|
|
|
400
439
|
}
|
|
401
440
|
body = doc.syncRelatedSection(body, fm);
|
|
402
441
|
writeFileSync(filePath, serializeFile(fm, body));
|
|
442
|
+
const touches = [];
|
|
443
|
+
if (op.fields.related !== undefined) {
|
|
444
|
+
const relatedAfter = fm.related ?? [];
|
|
445
|
+
touches.push(...backEdges(ctx, op.slug, kind, relatedAfter.filter(r => !relatedBefore.includes(r))));
|
|
446
|
+
for (const r of relatedBefore.filter(r => !relatedAfter.includes(r))) {
|
|
447
|
+
const t = dropBackEdge(ctx, op.slug, kind, r);
|
|
448
|
+
if (t)
|
|
449
|
+
touches.push(t);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
403
452
|
// Only the files THIS update wrote. Keying off the archive file merely
|
|
404
453
|
// existing swept any external hand-edit sitting in entities/archive/
|
|
405
454
|
// into an unrelated commit.
|
|
406
|
-
const files = [
|
|
455
|
+
const files = [vaultRel(vaultPath, filePath)];
|
|
407
456
|
if (wroteArchive)
|
|
408
|
-
files.push(
|
|
457
|
+
files.push(vaultRel(vaultPath, archivePath));
|
|
458
|
+
files.push(...touches.map(t => t.file));
|
|
409
459
|
return {
|
|
410
460
|
filesWritten: files,
|
|
411
461
|
filesDeleted: [],
|
|
412
462
|
result: { op: 'update', slug: op.slug, status: 'ok' },
|
|
413
463
|
commitMessage: `update: ${op.slug}`,
|
|
414
464
|
indexRemovals: [],
|
|
415
|
-
indexUpserts: [{ slug: op.slug, kind }],
|
|
465
|
+
indexUpserts: [{ slug: op.slug, kind }, ...touches.map(t => t.upsert)],
|
|
416
466
|
updateResult: applied.updateResult,
|
|
417
467
|
};
|
|
418
468
|
}
|
|
@@ -432,13 +482,14 @@ function applyCreateEntity(op, ctx) {
|
|
|
432
482
|
mkdirSync(join(vaultPath, kindDir('entity')), { recursive: true });
|
|
433
483
|
body = documentFor({ kind: 'entity' }).syncRelatedSection(body, fm);
|
|
434
484
|
writeFileSync(filePath, serializeFile(fm, body));
|
|
485
|
+
const touches = backEdges(ctx, slug, 'entity', fm.related ?? []);
|
|
435
486
|
return {
|
|
436
|
-
filesWritten: [
|
|
487
|
+
filesWritten: [vaultRel(vaultPath, filePath), ...touches.map(t => t.file)],
|
|
437
488
|
filesDeleted: [],
|
|
438
489
|
result: { op: 'create', slug, status: 'ok', created: true },
|
|
439
490
|
commitMessage: `create: ${op.name}`,
|
|
440
491
|
indexRemovals: [],
|
|
441
|
-
indexUpserts: [{ slug, kind: 'entity' }],
|
|
492
|
+
indexUpserts: [{ slug, kind: 'entity' }, ...touches.map(t => t.upsert)],
|
|
442
493
|
};
|
|
443
494
|
}
|
|
444
495
|
function applyCreateKnowledge(op, ctx) {
|
|
@@ -454,13 +505,14 @@ function applyCreateKnowledge(op, ctx) {
|
|
|
454
505
|
mkdirSync(join(vaultPath, kindDir('knowledge')), { recursive: true });
|
|
455
506
|
body = documentFor({ kind: 'knowledge' }).syncRelatedSection(body, fm);
|
|
456
507
|
writeFileSync(filePath, serializeFile(fm, body));
|
|
508
|
+
const touches = backEdges(ctx, slug, 'knowledge', fm.related ?? []);
|
|
457
509
|
return {
|
|
458
|
-
filesWritten: [
|
|
510
|
+
filesWritten: [vaultRel(vaultPath, filePath), ...touches.map(t => t.file)],
|
|
459
511
|
filesDeleted: [],
|
|
460
512
|
result: { op: 'create', slug, status: 'ok', created: true },
|
|
461
513
|
commitMessage: `create: ${op.name}`,
|
|
462
514
|
indexRemovals: [],
|
|
463
|
-
indexUpserts: [{ slug, kind: 'knowledge' }],
|
|
515
|
+
indexUpserts: [{ slug, kind: 'knowledge' }, ...touches.map(t => t.upsert)],
|
|
464
516
|
};
|
|
465
517
|
}
|
|
466
518
|
function applyCreateSession(op, ctx) {
|
|
@@ -480,25 +532,23 @@ function applyCreateSession(op, ctx) {
|
|
|
480
532
|
appeared(slug);
|
|
481
533
|
ctx.captureOriginal(filePath);
|
|
482
534
|
const fm = { name: op.name, created: today() };
|
|
483
|
-
const kf =
|
|
535
|
+
const kf = op.fields ?? {};
|
|
484
536
|
if (kf['session-date'])
|
|
485
537
|
fm['session-date'] = kf['session-date'];
|
|
486
538
|
if (kf.related?.length)
|
|
487
539
|
fm.related = kf.related;
|
|
488
540
|
// Session sections come from contracts.SESSION_SECTIONS — the same list
|
|
489
541
|
// Document splits on, so the written body and the structural set agree.
|
|
490
|
-
let sessionBody = sessionBodyTemplate({ synthesis: kf.synthesis });
|
|
542
|
+
let sessionBody = sessionBodyTemplate({ synthesis: kf.synthesis, decisions: kf.decisions, entities_touched: kf.entities_touched });
|
|
491
543
|
sessionBody = documentFor({ kind: 'session' }).syncRelatedSection(sessionBody, fm);
|
|
492
544
|
writeFileSync(filePath, serializeFile(fm, sessionBody));
|
|
493
|
-
// Sessions are not indexed (db.ts only scans entities/ and knowledge/).
|
|
494
|
-
// They ARE included in git semantic merge (git.ts VAULT_FILE_RE).
|
|
495
545
|
return {
|
|
496
|
-
filesWritten: [
|
|
546
|
+
filesWritten: [vaultRel(vaultPath, filePath)],
|
|
497
547
|
filesDeleted: [],
|
|
498
548
|
result: { op: 'create', slug, status: 'ok', created: true },
|
|
499
549
|
commitMessage: `create: ${op.name}`,
|
|
500
550
|
indexRemovals: [],
|
|
501
|
-
indexUpserts: [],
|
|
551
|
+
indexUpserts: [{ slug, kind: 'session' }],
|
|
502
552
|
};
|
|
503
553
|
}
|
|
504
554
|
function applyCapture(op, ctx) {
|
|
@@ -518,7 +568,7 @@ function applyCapture(op, ctx) {
|
|
|
518
568
|
mkdirSync(join(vaultPath, kindDir('entity')), { recursive: true });
|
|
519
569
|
writeFileSync(filePath, serializeFile(fm, bodyTemplate('capture', { notes: op.notes })));
|
|
520
570
|
return {
|
|
521
|
-
filesWritten: [
|
|
571
|
+
filesWritten: [vaultRel(vaultPath, filePath)],
|
|
522
572
|
filesDeleted: [],
|
|
523
573
|
result: { op: 'capture', slug, status: 'ok', created: true },
|
|
524
574
|
commitMessage: `capture: ${op.name}`,
|
|
@@ -533,6 +583,8 @@ function applyLink(op, ctx) {
|
|
|
533
583
|
const parsedFile = parseFilePath(filePath);
|
|
534
584
|
const fm = parsedFile.frontmatter;
|
|
535
585
|
let body = parsedFile.body;
|
|
586
|
+
const kind = kindFromPath(filePath);
|
|
587
|
+
let touch = null;
|
|
536
588
|
if (op.relation === 'parent') {
|
|
537
589
|
fm.parent = op.target;
|
|
538
590
|
}
|
|
@@ -541,16 +593,17 @@ function applyLink(op, ctx) {
|
|
|
541
593
|
if (!related.includes(op.target))
|
|
542
594
|
related.push(op.target);
|
|
543
595
|
fm.related = related;
|
|
596
|
+
touch = writeBackEdge(ctx, op.slug, kind, op.target);
|
|
544
597
|
}
|
|
545
598
|
body = documentForFile(parsedFile).syncRelatedSection(body, fm);
|
|
546
599
|
writeFileSync(filePath, serializeFile(fm, body));
|
|
547
600
|
return {
|
|
548
|
-
filesWritten: [
|
|
601
|
+
filesWritten: [vaultRel(vaultPath, filePath), ...(touch ? [touch.file] : [])],
|
|
549
602
|
filesDeleted: [],
|
|
550
603
|
result: { op: 'link', slug: op.slug, status: 'ok' },
|
|
551
604
|
commitMessage: `link: ${op.slug} → ${op.target} (${op.relation})`,
|
|
552
605
|
indexRemovals: [],
|
|
553
|
-
indexUpserts: [{ slug: op.slug, kind
|
|
606
|
+
indexUpserts: [{ slug: op.slug, kind }, ...(touch ? [touch.upsert] : [])],
|
|
554
607
|
};
|
|
555
608
|
}
|
|
556
609
|
function applyUnlink(op, ctx) {
|
|
@@ -560,23 +613,30 @@ function applyUnlink(op, ctx) {
|
|
|
560
613
|
const parsedFile = parseFilePath(filePath);
|
|
561
614
|
const fm = parsedFile.frontmatter;
|
|
562
615
|
let body = parsedFile.body;
|
|
616
|
+
const kind = kindFromPath(filePath);
|
|
617
|
+
let touch = null;
|
|
563
618
|
if (op.relation === 'parent' && fm.parent === op.target) {
|
|
564
619
|
delete fm.parent;
|
|
565
620
|
}
|
|
566
|
-
else if (op.relation === 'related'
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
621
|
+
else if (op.relation === 'related') {
|
|
622
|
+
if (fm.related) {
|
|
623
|
+
fm.related = fm.related.filter((r) => r !== op.target);
|
|
624
|
+
if (fm.related.length === 0)
|
|
625
|
+
delete fm.related;
|
|
626
|
+
}
|
|
627
|
+
// The target's end goes too, even when this file never carried the edge
|
|
628
|
+
// (a legacy one-way edge).
|
|
629
|
+
touch = dropBackEdge(ctx, op.slug, kind, op.target);
|
|
570
630
|
}
|
|
571
631
|
body = documentForFile(parsedFile).syncRelatedSection(body, fm);
|
|
572
632
|
writeFileSync(filePath, serializeFile(fm, body));
|
|
573
633
|
return {
|
|
574
|
-
filesWritten: [
|
|
634
|
+
filesWritten: [vaultRel(vaultPath, filePath), ...(touch ? [touch.file] : [])],
|
|
575
635
|
filesDeleted: [],
|
|
576
636
|
result: { op: 'unlink', slug: op.slug, status: 'ok' },
|
|
577
637
|
commitMessage: `unlink: ${op.slug} ✕ ${op.target} (${op.relation})`,
|
|
578
638
|
indexRemovals: [],
|
|
579
|
-
indexUpserts: [{ slug: op.slug, kind
|
|
639
|
+
indexUpserts: [{ slug: op.slug, kind }, ...(touch ? [touch.upsert] : [])],
|
|
580
640
|
};
|
|
581
641
|
}
|
|
582
642
|
function applyPromote(op, ctx) {
|
|
@@ -631,10 +691,10 @@ function applyPromote(op, ctx) {
|
|
|
631
691
|
catch (err) {
|
|
632
692
|
throw new HafezError('VALIDATION_FAILED', `Failed to promote '${op.slug}' to knowledge: ${err instanceof Error ? err.message : String(err)}`);
|
|
633
693
|
}
|
|
634
|
-
modifiedFiles.push(
|
|
694
|
+
modifiedFiles.push(vaultRel(vaultPath, newPath));
|
|
635
695
|
// The deletion must be staged too — `git add` of a removed path stages
|
|
636
696
|
// the removal; omitting it leaves the vault permanently dirty.
|
|
637
|
-
deletedFiles.push(
|
|
697
|
+
deletedFiles.push(vaultRel(vaultPath, filePath));
|
|
638
698
|
}
|
|
639
699
|
else {
|
|
640
700
|
// Same-kind: capture→entity, capture→project, entity→project
|
|
@@ -661,7 +721,7 @@ function applyPromote(op, ctx) {
|
|
|
661
721
|
}));
|
|
662
722
|
newBody = doc.syncRelatedSection(newBody, fm);
|
|
663
723
|
writeFileSync(filePath, serializeFile(fm, newBody));
|
|
664
|
-
modifiedFiles.push(
|
|
724
|
+
modifiedFiles.push(vaultRel(vaultPath, filePath));
|
|
665
725
|
}
|
|
666
726
|
return {
|
|
667
727
|
filesWritten: modifiedFiles,
|
|
@@ -683,15 +743,17 @@ function applyDelete(op, ctx) {
|
|
|
683
743
|
vanished(op.slug);
|
|
684
744
|
// The snapshot is the rollback: a commit-stage failure writes it back.
|
|
685
745
|
ctx.captureOriginal(filePath);
|
|
746
|
+
// The other end of every same-kind edge the note owns goes with it (#243).
|
|
747
|
+
const own = kind === 'knowledge' ? parseFilePath(filePath).frontmatter.related ?? [] : [];
|
|
686
748
|
unlinkSync(filePath);
|
|
749
|
+
const touches = own.flatMap(r => dropBackEdge(ctx, op.slug, 'knowledge', r) ?? []);
|
|
687
750
|
return {
|
|
688
|
-
filesWritten:
|
|
689
|
-
filesDeleted: [
|
|
751
|
+
filesWritten: touches.map(t => t.file),
|
|
752
|
+
filesDeleted: [vaultRel(vaultPath, filePath)],
|
|
690
753
|
result: { op: 'delete', slug: op.slug, status: 'ok' },
|
|
691
754
|
commitMessage: `delete: ${op.slug} (${kind})`,
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
indexUpserts: [],
|
|
755
|
+
indexRemovals: [op.slug],
|
|
756
|
+
indexUpserts: touches.map(t => t.upsert),
|
|
695
757
|
deletedKind: kind,
|
|
696
758
|
};
|
|
697
759
|
}
|
package/dist/batch-ops.d.ts
CHANGED
|
@@ -34,12 +34,12 @@ export declare const UpdateFieldsSchema: z.ZodObject<{
|
|
|
34
34
|
parent: z.ZodOptional<z.ZodString>;
|
|
35
35
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
36
36
|
resource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
-
domain: z.ZodOptional<z.
|
|
37
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
38
38
|
confidence: z.ZodOptional<z.ZodEnum<{
|
|
39
39
|
[x: string]: string;
|
|
40
40
|
}>>;
|
|
41
|
-
tags: z.ZodOptional<z.
|
|
42
|
-
related: z.ZodOptional<z.
|
|
41
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
42
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
43
43
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
44
44
|
insight: z.ZodOptional<z.ZodString>;
|
|
45
45
|
add_evidence: z.ZodOptional<z.ZodString>;
|
|
@@ -52,10 +52,10 @@ export declare const CreateEntityFieldsSchema: z.ZodObject<{
|
|
|
52
52
|
purpose: z.ZodOptional<z.ZodString>;
|
|
53
53
|
description: z.ZodOptional<z.ZodString>;
|
|
54
54
|
resource: z.ZodOptional<z.ZodString>;
|
|
55
|
-
domain: z.ZodOptional<z.
|
|
55
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
56
56
|
parent: z.ZodOptional<z.ZodString>;
|
|
57
|
-
related: z.ZodOptional<z.
|
|
58
|
-
tags: z.ZodOptional<z.
|
|
57
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
58
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
59
59
|
brief: z.ZodOptional<z.ZodString>;
|
|
60
60
|
add_action: z.ZodOptional<z.ZodString>;
|
|
61
61
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -73,16 +73,18 @@ export declare const CreateKnowledgeFieldsSchema: z.ZodObject<{
|
|
|
73
73
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
74
74
|
insight: z.ZodOptional<z.ZodString>;
|
|
75
75
|
description: z.ZodOptional<z.ZodString>;
|
|
76
|
-
domain: z.ZodOptional<z.
|
|
77
|
-
related: z.ZodOptional<z.
|
|
78
|
-
tags: z.ZodOptional<z.
|
|
76
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
77
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
78
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
79
79
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
80
80
|
}, z.core.$strict>;
|
|
81
81
|
export declare const CreateSessionFieldsSchema: z.ZodObject<{
|
|
82
82
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
83
83
|
insight: z.ZodOptional<z.ZodString>;
|
|
84
|
-
related: z.ZodOptional<z.
|
|
84
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
85
85
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
86
|
+
decisions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
87
|
+
entities_touched: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
86
88
|
}, z.core.$strict>;
|
|
87
89
|
/** What `digest` may put in a session-create op — typechecked against the
|
|
88
90
|
* schema so the two can never silently diverge again. */
|
|
@@ -119,12 +121,12 @@ export declare const BatchOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
119
121
|
parent: z.ZodOptional<z.ZodString>;
|
|
120
122
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
121
123
|
resource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
122
|
-
domain: z.ZodOptional<z.
|
|
124
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
123
125
|
confidence: z.ZodOptional<z.ZodEnum<{
|
|
124
126
|
[x: string]: string;
|
|
125
127
|
}>>;
|
|
126
|
-
tags: z.ZodOptional<z.
|
|
127
|
-
related: z.ZodOptional<z.
|
|
128
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
129
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
128
130
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
129
131
|
insight: z.ZodOptional<z.ZodString>;
|
|
130
132
|
add_evidence: z.ZodOptional<z.ZodString>;
|
|
@@ -290,12 +292,12 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
290
292
|
parent: z.ZodOptional<z.ZodString>;
|
|
291
293
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
292
294
|
resource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
293
|
-
domain: z.ZodOptional<z.
|
|
295
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
294
296
|
confidence: z.ZodOptional<z.ZodEnum<{
|
|
295
297
|
[x: string]: string;
|
|
296
298
|
}>>;
|
|
297
|
-
tags: z.ZodOptional<z.
|
|
298
|
-
related: z.ZodOptional<z.
|
|
299
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
300
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
299
301
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
300
302
|
insight: z.ZodOptional<z.ZodString>;
|
|
301
303
|
add_evidence: z.ZodOptional<z.ZodString>;
|
|
@@ -326,10 +328,10 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
326
328
|
purpose: z.ZodOptional<z.ZodString>;
|
|
327
329
|
description: z.ZodOptional<z.ZodString>;
|
|
328
330
|
resource: z.ZodOptional<z.ZodString>;
|
|
329
|
-
domain: z.ZodOptional<z.
|
|
331
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
330
332
|
parent: z.ZodOptional<z.ZodString>;
|
|
331
|
-
related: z.ZodOptional<z.
|
|
332
|
-
tags: z.ZodOptional<z.
|
|
333
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
334
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
333
335
|
brief: z.ZodOptional<z.ZodString>;
|
|
334
336
|
add_action: z.ZodOptional<z.ZodString>;
|
|
335
337
|
add_actions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -367,9 +369,9 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
367
369
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
368
370
|
insight: z.ZodOptional<z.ZodString>;
|
|
369
371
|
description: z.ZodOptional<z.ZodString>;
|
|
370
|
-
domain: z.ZodOptional<z.
|
|
371
|
-
related: z.ZodOptional<z.
|
|
372
|
-
tags: z.ZodOptional<z.
|
|
372
|
+
domain: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
373
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
374
|
+
tags: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
373
375
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
374
376
|
}, z.core.$strict>>;
|
|
375
377
|
}, z.core.$strict>;
|
|
@@ -395,11 +397,13 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
395
397
|
fields: z.ZodOptional<z.ZodObject<{
|
|
396
398
|
synthesis: z.ZodOptional<z.ZodString>;
|
|
397
399
|
insight: z.ZodOptional<z.ZodString>;
|
|
398
|
-
related: z.ZodOptional<z.
|
|
400
|
+
related: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
399
401
|
'session-date': z.ZodOptional<z.ZodString>;
|
|
402
|
+
decisions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
403
|
+
entities_touched: z.ZodOptional<z.ZodPreprocess<z.ZodArray<z.ZodString>, unknown>>;
|
|
400
404
|
}, z.core.$strict>>;
|
|
401
405
|
}, z.core.$strict>;
|
|
402
|
-
readonly description: "Create a new session note linked to an entity. `insight` is an alias for `synthesis` (ignored when `synthesis` is also given).";
|
|
406
|
+
readonly description: "Create a new session note linked to an entity. `synthesis` writes Summary, `decisions` one bullet each under Decisions, `entities_touched` one `[[slug]]` bullet each under Entities Touched. `insight` is an alias for `synthesis` (ignored when `synthesis` is also given).";
|
|
403
407
|
readonly exampleInclude: readonly ["synthesis"];
|
|
404
408
|
readonly check: (op: Extract<BatchOperation, {
|
|
405
409
|
op: "create";
|
|
@@ -445,7 +449,7 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
445
449
|
related: "related";
|
|
446
450
|
}>;
|
|
447
451
|
}, z.core.$strict>;
|
|
448
|
-
readonly description: "Add a parent or related link
|
|
452
|
+
readonly description: "Add a parent or related link. A same-kind related link (entity↔entity, knowledge↔knowledge) is written on both files; a related naming the entity's parent or a child is rejected (parent subsumes related).";
|
|
449
453
|
readonly exampleInclude: readonly [];
|
|
450
454
|
readonly check: (op: Extract<BatchOperation, {
|
|
451
455
|
op: "link";
|
|
@@ -466,7 +470,7 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
466
470
|
related: "related";
|
|
467
471
|
}>;
|
|
468
472
|
}, z.core.$strict>;
|
|
469
|
-
readonly description: "Remove a parent or related link
|
|
473
|
+
readonly description: "Remove a parent or related link. A same-kind related link is dropped from both files, a legacy one-way edge from whichever file carries it.";
|
|
470
474
|
readonly exampleInclude: readonly [];
|
|
471
475
|
readonly check: (op: Extract<BatchOperation, {
|
|
472
476
|
op: "unlink";
|
|
@@ -503,7 +507,7 @@ declare const OP_SPEC_ROWS: readonly [{
|
|
|
503
507
|
op: z.ZodLiteral<"delete">;
|
|
504
508
|
slug: z.ZodString;
|
|
505
509
|
}, z.core.$strict>;
|
|
506
|
-
readonly description: "Delete a knowledge or session note. An entity is never deleted — retire it with `hafez update <slug> --status done`. Rejected while
|
|
510
|
+
readonly description: "Delete a knowledge or session note. An entity is never deleted — retire it with `hafez update <slug> --status done`. The back-edges the note owns (a knowledge note in its own related that names it back) are dropped with it, in the same commit. Rejected while any other entity or knowledge note links to the note (parent/related): unlink or re-point those first — an earlier op in the same batch counts.";
|
|
507
511
|
readonly exampleInclude: readonly [];
|
|
508
512
|
readonly addressesSessions: true;
|
|
509
513
|
readonly check: (op: Extract<BatchOperation, {
|