wendkeep 0.47.0 → 0.48.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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to **wendkeep** are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.48.0] — 2026-07-23
8
+
9
+ ### Added
10
+
11
+ - **`wendkeep note relink [--apply]` — backfill de proveniência das notas derivadas
12
+ órfãs.** BUG/APR legadas (criadas por versão antiga, sem `source:` de sessão) ficam ilhas
13
+ no grafo — num vault real: 13 de 15 BUG e 3 de 8 APR sem nenhum link de entrada nem de
14
+ saída. A origem não está registrada no órfão, mas os irmãos não-órfãos do mesmo tipo
15
+ carregam a sessão-fonte real. O comando liga cada órfão herdando a sessão **modal** (mais
16
+ comum) dos irmãos do mesmo tipo e mês, injetando `source:` + `related:` no frontmatter.
17
+ Dry-run por default; `--apply` escreve; idempotente; pula e reporta o órfão sem nenhum
18
+ irmão-fonte pra inferir (nunca chuta). Documentado em `derived-notes` (DRV-9).
19
+
7
20
  ## [0.47.0] — 2026-07-23
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -184,6 +184,8 @@ wendkeep note new --type learning "a regex without /g only ever returns the firs
184
184
 
185
185
  It prints the created path, numbers from the current max (recursive scan), files it in the month folder for today (`--date YYYY-MM-DD` to override), and links the active session in `source:` so the graph stays connected. Agents get this rule injected at SessionStart — they call the command instead of guessing a filename.
186
186
 
187
+ **Reconnecting legacy notes.** Derived notes created by older versions carry no `source:` session and sit as islands in the graph. `wendkeep note relink` backfills them: each orphan inherits the modal source session of its type/month cohort (the session its non-orphan siblings already point to). Dry-run by default; `--apply` writes; notes with no sibling to infer from are skipped and reported.
188
+
187
189
  **Migrating an existing vault.** Notes created before `0.41.0` have date-prefixed names (`2026-07-16-bug-<slug>.md`) and may sit in legacy `DIA N` subfolders. One command per tree renumbers them chronologically, moves them up into the month folder, and rewrites every wikilink across the vault:
188
190
 
189
191
  ```bash
package/bin/wendkeep.mjs CHANGED
@@ -77,6 +77,9 @@ Usage:
77
77
  wendkeep renumber-learnings Same for 06-Aprendizados/06-Learnings with APR-<NNNN>-<slug>.
78
78
  wendkeep note new --type bug|learning "<título>" Create a numbered derived note (BUG-/APR-NNNN)
79
79
  in the month folder and print its vault path. --date YYYY-MM-DD · --vault P.
80
+ wendkeep note relink [--apply] Backfill orphan derived notes (BUG/APR without a source session),
81
+ linking each to the modal source session of its type/month cohort. Dry-run
82
+ by default; --apply writes; skips notes with no sibling to infer from.
80
83
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
81
84
  wendkeep validate-memory [path] Validate .brain/CORE.md against the compaction
82
85
  protocol (cap 25, 3 sections, no secrets/PII). Uses
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
3
- import { basename, join } from 'path';
3
+ import { basename, join, relative } from 'path';
4
4
  import {
5
5
  monthFolderRelFromDateStr,
6
6
  derivedContentKey,
@@ -63,6 +63,77 @@ function sessionYamlLinks(sessionRel) {
63
63
  ].join('\n');
64
64
  }
65
65
 
66
+ // --- note relink: backfill de proveniência das notas derivadas órfãs (DRV-9) -----
67
+ // Nota derivada legada (BUG/APR criada por wendkeep antigo) nasce sem `source:` de sessão —
68
+ // ilha no grafo. A origem não está registrada nela, mas os irmãos NÃO-órfãos do mesmo tipo
69
+ // carregam a sessão-fonte real: o órfão herda a sessão MODAL (mais comum) do seu (tipo, mês).
70
+
71
+ // Extrai a sessão do primeiro `source: - [[...]]` do frontmatter (vazio se não houver).
72
+ function sourceSessionOf(content) {
73
+ const m = content.match(/^source:\s*\n\s*-\s*"?\[\[([^\]"|]+)/m);
74
+ return m ? m[1].trim() : '';
75
+ }
76
+
77
+ // Injeta source+related antes do `---` de fechamento do frontmatter. Sem frontmatter, no-op.
78
+ function insertSourceLinks(content, sessionRel) {
79
+ const m = content.match(/^(---\n[\s\S]*?\n)(---\n)/);
80
+ if (!m) return content;
81
+ return `${m[1]}${sessionYamlLinks(sessionRel)}\n${m[2]}${content.slice(m[0].length)}`;
82
+ }
83
+
84
+ function modalKey(counts) {
85
+ const entries = Object.entries(counts || {});
86
+ if (!entries.length) return '';
87
+ entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
88
+ return entries[0][0];
89
+ }
90
+
91
+ export function relinkDerivedNotes(vaultBase, { apply = false } = {}) {
92
+ const loc = getLocale(vaultBase);
93
+ const monthOf = (abs) => relative(vaultBase, abs).replaceAll('\\', '/').split('/').slice(0, 3).join('/');
94
+ const linked = [];
95
+ const skipped = [];
96
+ for (const [folderKey, prefix] of [['bugs', 'BUG'], ['learnings', 'APR']]) {
97
+ const root = join(vaultBase, loc.folders[folderKey]);
98
+ const files = [];
99
+ const walk = (dir) => {
100
+ let entries;
101
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
102
+ for (const e of entries) {
103
+ const p = join(dir, e.name);
104
+ if (e.isDirectory()) walk(p);
105
+ else if (e.name.endsWith('.md') && e.name.startsWith(`${prefix}-`)) files.push(p);
106
+ }
107
+ };
108
+ walk(root);
109
+ const byMonth = {};
110
+ const typeWide = {};
111
+ const orphans = [];
112
+ for (const p of files) {
113
+ let c;
114
+ try { c = readFileSync(p, 'utf8'); } catch { continue; }
115
+ const src = sourceSessionOf(c);
116
+ if (src) {
117
+ (byMonth[monthOf(p)] ??= {})[src] = ((byMonth[monthOf(p)] || {})[src] || 0) + 1;
118
+ typeWide[src] = (typeWide[src] || 0) + 1;
119
+ } else {
120
+ orphans.push({ p, c });
121
+ }
122
+ }
123
+ for (const o of orphans) {
124
+ const rel = relative(vaultBase, o.p).replaceAll('\\', '/');
125
+ const session = modalKey(byMonth[monthOf(o.p)]) || modalKey(typeWide);
126
+ if (!session) { skipped.push({ file: rel, reason: 'sem irmão com source para inferir' }); continue; }
127
+ if (apply) {
128
+ try { writeFileSync(o.p, insertSourceLinks(o.c, session), 'utf8'); }
129
+ catch { skipped.push({ file: rel, reason: 'escrita falhou' }); continue; }
130
+ }
131
+ linked.push({ file: rel, session: basename(session) });
132
+ }
133
+ }
134
+ return { applied: apply, linked, skipped };
135
+ }
136
+
66
137
  function extractIssueRefs(tx) {
67
138
  const text = [
68
139
  assistantText(tx),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/note.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  toVaultRelative,
15
15
  } from '../hooks/obsidian-common.mjs';
16
16
  import { getLocale } from '../hooks/locale.mjs';
17
- import { buildManualBugNote, buildManualLearningNote } from '../hooks/linked-notes.mjs';
17
+ import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
18
18
 
19
19
  const TYPES = {
20
20
  bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
@@ -30,8 +30,23 @@ function opt(argv, name) {
30
30
 
31
31
  export function runNote(argv) {
32
32
  const [sub, ...rest] = argv;
33
+
34
+ if (sub === 'relink') {
35
+ const vaultRaw = opt(rest, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
36
+ if (!vaultRaw) { process.stderr.write('wendkeep note relink: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
37
+ const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
38
+ if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep note relink: vault not found: ${vaultBase}\n`); process.exit(2); }
39
+ const r = relinkDerivedNotes(vaultBase, { apply: rest.includes('--apply') });
40
+ if (rest.includes('--json')) { process.stdout.write(`${JSON.stringify(r, null, 2)}\n`); process.exit(0); }
41
+ process.stdout.write(`${r.linked.length} nota(s) derivada(s) órfã(s)${r.applied ? ' linkadas' : ' seriam linkadas'}\n`);
42
+ for (const l of r.linked) process.stdout.write(` ${l.file} -> ${l.session}\n`);
43
+ for (const s of r.skipped) process.stdout.write(` pulado: ${s.file} (${s.reason})\n`);
44
+ if (!r.applied && r.linked.length) process.stdout.write('\ndry-run — nada escrito. Rode com --apply para injetar os backlinks.\n');
45
+ process.exit(0);
46
+ }
47
+
33
48
  if (sub !== 'new') {
34
- process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"`).\n');
49
+ process.stderr.write('wendkeep note: subcomando desconhecido (use `note new --type bug|learning "<título>"` ou `note relink [--apply]`).\n');
35
50
  process.exit(2);
36
51
  }
37
52