wendkeep 0.42.0 → 0.44.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,32 @@ 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.44.0] — 2026-07-17
8
+
9
+ ### Changed
10
+
11
+ - `wendkeep change new` não cria mais o placeholder `specs/exemplo/spec.md` (nem a pasta
12
+ `specs/`). Era ruído — sempre deletado à mão, e `discoverSpecDeltas` já o filtrava. Quando
13
+ a change resolve `spec_impact: required`, o autor escreve `specs/<capability>/spec.md`
14
+ direto; o formato do delta vive na skill wk-workflow. O filtro de `exemplo` fica (compat
15
+ com changes antigas em voo).
16
+ - README gerado do `07-Specs` reescrito para explicar o ponto mais confundido: specs são
17
+ **por capability, não por mudança** (N changes promovem no mesmo arquivo; o histórico
18
+ por-change vive em `_arquivo`). `promoteSpecs` passa a garantir/atualizar esse README a
19
+ cada archive (`ensureSpecsReadme`), então vaults existentes recebem o texto novo no
20
+ próximo archive — não só os criados via `init`.
21
+
22
+ ## [0.43.0] — 2026-07-17
23
+
24
+ ### Fixed
25
+
26
+ - Dedup de nota derivada era assimétrico: aprendizado deduplicava recursivamente (vault
27
+ inteiro), mas **bug e decisão só olhavam a pasta do mês** (`existingKeysForSession`, scan
28
+ não-recursivo). Uma nota da sessão numa subpasta `DIA` legada não era vista, então um
29
+ re-import/re-captura da mesma sessão criava uma duplicata de bug/decisão. Agora
30
+ `existingKeysForSession` varre a pasta derivada recursivamente (como o de aprendizado),
31
+ mantendo a semântica per-sessão (só notas que referenciam a sessão contam).
32
+
7
33
  ## [0.42.0] — 2026-07-17
8
34
 
9
35
  ### Changed
@@ -110,14 +110,12 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
110
110
  write('proposta.md', files.proposta);
111
111
  write('tarefas.md', files.tarefas);
112
112
  if (!existed) write('.spec-impact-v1', '1\n');
113
- // Auto-sizing (Wave B): a --simple change skips the design + spec-delta scaffold.
113
+ // Auto-sizing (Wave B): a --simple change skips the design scaffold.
114
+ // No `specs/exemplo` placeholder: it was pure noise (always hand-deleted). When a change
115
+ // resolves `spec_impact: required`, the author writes `specs/<capability>/spec.md` directly
116
+ // — the delta format lives in the wk-workflow skill (and `renderChangeScaffold().specDelta`).
114
117
  if (!simple) {
115
118
  write('design.md', files.design);
116
- const exampleDelta = join(dir, 'specs', 'exemplo', 'spec.md');
117
- if (!existsSync(exampleDelta)) {
118
- mkdirSync(join(dir, 'specs', 'exemplo'), { recursive: true });
119
- writeFileSync(exampleDelta, files.specDelta, 'utf8');
120
- }
121
119
  }
122
120
  if (!existed) captureSpecBaseline(vaultBase, dir);
123
121
  setActiveChange(vaultBase, slug);
@@ -553,10 +553,6 @@ ${L.learn.futureHint}
553
553
 
554
554
  const derivedFoldersFor = (vaultBase) => { const f = getLocale(vaultBase).folders; return { bugs: f.bugs, decisions: f.decisions, learnings: f.learnings }; };
555
555
 
556
- function listMd(dir) {
557
- try { return readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { return []; }
558
- }
559
-
560
556
  // Chaves content_key das derivadas já existentes que linkam esta sessão.
561
557
  // Vault-wide learning content_keys (recursive over the learnings folder). existingKeysForSession
562
558
  // only looks at the current session + month, so the same lesson re-extracted on a later day/
@@ -582,19 +578,31 @@ function collectLearningKeys(vaultBase) {
582
578
  return keys;
583
579
  }
584
580
 
585
- function existingKeysForSession(vaultBase, sessionRel, dateStr) {
581
+ // dateStr kept for call compatibility; no longer used to narrow the scan — a session's note may
582
+ // sit in a legacy `DIA` subfolder, not just the month folder, so we walk the whole derived tree
583
+ // (like collectLearningKeys). The per-session semantics stay: only notes referencing THIS session
584
+ // count, so bugs/decisions from other sessions never leak in.
585
+ export function existingKeysForSession(vaultBase, sessionRel, dateStr) { // eslint-disable-line no-unused-vars
586
586
  const wikilink = wikilinkFromRel(sessionRel);
587
587
  const out = { bugs: [], decisions: [], learnings: [] };
588
588
  for (const [type, folder] of Object.entries(derivedFoldersFor(vaultBase))) {
589
- const dir = join(vaultBase, monthFolderRelFromDateStr(folder, dateStr, vaultBase));
590
- for (const fileName of listMd(dir)) {
591
- try {
592
- const c = readFileSync(join(dir, fileName), 'utf-8');
593
- if (!c.includes(sessionRel) && !c.includes(wikilink)) continue;
594
- const m = c.match(/^content_key:\s*"?(.*?)"?\s*$/m);
595
- if (m && m[1]) out[type].push(m[1]);
596
- } catch { /* ignora nota ilegível */ }
597
- }
589
+ const root = join(vaultBase, folder);
590
+ const walk = (d) => {
591
+ let entries;
592
+ try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
593
+ for (const e of entries) {
594
+ const p = join(d, e.name);
595
+ if (e.isDirectory()) { walk(p); continue; }
596
+ if (!e.name.endsWith('.md')) continue;
597
+ try {
598
+ const c = readFileSync(p, 'utf-8');
599
+ if (!c.includes(sessionRel) && !c.includes(wikilink)) continue;
600
+ const m = c.match(/^content_key:\s*"?(.*?)"?\s*$/m);
601
+ if (m && m[1]) out[type].push(m[1]);
602
+ } catch { /* ignora nota ilegível */ }
603
+ }
604
+ };
605
+ walk(root);
598
606
  }
599
607
  return out;
600
608
  }
@@ -317,6 +317,47 @@ export function discoverSpecDeltas(changeDir) {
317
317
  return caps;
318
318
  }
319
319
 
320
+ // The 07-Specs README, explaining the most-confused point: specs are per-CAPABILITY, not
321
+ // per-change (many changes promote into the same file; the per-change record lives in
322
+ // _arquivo). Generated + read-only. Written on init and refreshed on every archive so
323
+ // existing vaults self-heal. Bilingual by vault locale.
324
+ export function ensureSpecsReadme(vaultBase) {
325
+ const loc = getLocale(vaultBase);
326
+ const en = loc.id === 'en';
327
+ const dir = join(vaultBase, loc.folders.specs);
328
+ ensureDir(dir);
329
+ const body = en
330
+ ? `# Specs — generated living contract
331
+
332
+ **One file per _capability_, not per change.** Each file is the current, cumulative contract
333
+ of a capability — the sum of every change's spec delta that was promoted here. Many changes
334
+ touch the same capability, so N changes collapse into far fewer spec files.
335
+
336
+ Think of it like source code vs commits: this folder is the *current code* of each capability;
337
+ the *commits* that built it are the archived changes in \`${loc.folders.changes}/_arquivo/\`.
338
+
339
+ - **Generated + read-only.** Never author here. Write deltas only in
340
+ \`${loc.folders.changes}/<slug>/specs/<capability>/spec.md\` (ADDED/MODIFIED/REMOVED);
341
+ \`wendkeep change archive\` promotes them into this folder.
342
+ - Per-change history → \`${loc.folders.changes}/_arquivo/\`. Current contract → here.
343
+ `
344
+ : `# Specs — contrato consolidado gerado
345
+
346
+ **Um arquivo por _capability_, não por mudança.** Cada arquivo é o contrato atual e acumulado
347
+ de uma capability — a soma de todos os deltas de spec promovidos aqui. Várias mudanças tocam a
348
+ mesma capability, então N mudanças colapsam em bem menos specs.
349
+
350
+ Pense como código-fonte vs commits: esta pasta é o *código atual* de cada capability; os
351
+ *commits* que a construíram são as mudanças arquivadas em \`${loc.folders.changes}/_arquivo/\`.
352
+
353
+ - **Gerado + somente leitura.** Nunca edite aqui. Escreva deltas apenas em
354
+ \`${loc.folders.changes}/<slug>/specs/<capability>/spec.md\` (ADDED/MODIFIED/REMOVED); o
355
+ \`wendkeep change archive\` promove para esta pasta.
356
+ - Histórico por mudança → \`${loc.folders.changes}/_arquivo/\`. Contrato atual → aqui.
357
+ `;
358
+ writeFileSync(join(dir, 'README.md'), body, 'utf8');
359
+ }
360
+
320
361
  // Merge each capability's delta (in the change) into the living spec in 07-Specs.
321
362
  export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, dateStr } = {}) {
322
363
  const loc = getLocale(vaultBase);
@@ -347,6 +388,7 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
347
388
  promoted.push(cap);
348
389
  }
349
390
  recordPromotedSpecs(vaultBase, promoted);
391
+ ensureSpecsReadme(vaultBase); // self-heal the explainer so existing vaults get it on archive
350
392
  return { promoted, warnings };
351
393
  }
352
394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.42.0",
3
+ "version": "0.44.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/init.mjs CHANGED
@@ -40,7 +40,7 @@ import { seedDefinitions, syncDefs } from './sync-defs.mjs';
40
40
  import { seedWkSkills } from './skills-seed.mjs';
41
41
  import { LOCALES, DEFAULT_LOCALE, getLocale, clearLocaleCache, vaultFolders } from '../hooks/locale.mjs';
42
42
  import { seedDotcontext, globalHasDotcontext, resolveDotcontextSkipMcp, renderSensorsJson } from './dotcontext-seed.mjs';
43
- import { adoptSpecsState, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
43
+ import { adoptSpecsState, ensureSpecsReadme, SPECS_STATE_FILE } from '../hooks/spec-core.mjs';
44
44
  import { bindProjectVault, readProjectBinding } from './project-vault.mjs';
45
45
 
46
46
  function parseArgs(argv) {
@@ -490,12 +490,7 @@ export async function runInit(argv) {
490
490
  seedWkSkills(brainDir, loc.id); // Pilar A: native process skills, in the vault locale.
491
491
  // Seed the change/spec layer starters (Pilar B) — non-destructive.
492
492
  const en = loc.id === 'en';
493
- const specsReadme = join(vaultPath, loc.folders.specs, 'README.md');
494
- if (!existsSync(specsReadme)) {
495
- writeFileSync(specsReadme, en
496
- ? `# Specs — generated living contract\n\nRead-only: do not author here. Write deltas only in \`${loc.folders.changes}/<slug>/specs/\`; archive promotes them here.\n`
497
- : `# Specs — contrato consolidado gerado\n\nSomente leitura: não edite aqui. Escreva deltas apenas em \`${loc.folders.changes}/<slug>/specs/\`; o archive promove para cá.\n`, 'utf8');
498
- }
493
+ ensureSpecsReadme(vaultPath); // same explainer promoteSpecs refreshes (per-capability, read-only)
499
494
  if (!existsSync(join(vaultPath, SPECS_STATE_FILE))) {
500
495
  const livingSpecs = readdirSync(join(vaultPath, loc.folders.specs)).filter((name) => name.endsWith('.md') && name !== 'README.md');
501
496
  if (!livingSpecs.length) adoptSpecsState(vaultPath);