wendkeep 0.48.0 → 0.49.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,21 @@ 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.49.0] — 2026-07-23
8
+
9
+ ### Added
10
+
11
+ - **`wendkeep doctor` agora surfaça os órfãos do grafo com o comando de reparo.** Nova seção
12
+ `[links]`: conta notas derivadas sem sessão-fonte (→ `note relink`), artefatos de change
13
+ sem backlink (→ `change backlink`) e o estado das cores do grafo (→ `theme sync`); quando
14
+ tudo conectado, diz `grafo conectado`. Reusa as funções de reparo em dry-run — zero lógica
15
+ nova de detecção. Antes o dono do vault só descobria os órfãos olhando o grafo no Obsidian
16
+ e não sabia qual comando rodava.
17
+ - **Sessão não mente "inativa" com atividade recente.** Nova seção `[sessão]`: quando o
18
+ control marca `inactive` mas a nota da sessão foi escrita há pouco, o doctor sinaliza
19
+ possível workflow/subagente em segundo plano — o control só reflete o lifecycle da
20
+ sessão-mãe, não a atividade de background. Capability `vault-doctor` (DIAG-1..4).
21
+
7
22
  ## [0.48.0] — 2026-07-23
8
23
 
9
24
  ### Added
@@ -1,10 +1,12 @@
1
1
  // hooks/harness-doctor.mjs — integrity checks for the a2 harness state (Wave B).
2
2
  // Pure-ish (fs reads only). `wendkeep doctor` reports errors (exit 1) + warnings.
3
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
- import { activeChange, parseTasks } from './change-core.mjs';
5
+ import { activeChange, parseTasks, backfillArtifactLinks } from './change-core.mjs';
6
+ import { relinkDerivedNotes } from './linked-notes.mjs';
6
7
  import { buildEffectiveRequirementPackage, checkSpecsState, evaluateVerdict, tasksHashOf, validateSpecImpact } from './spec-core.mjs';
7
8
  import { getLocale } from './locale.mjs';
9
+ import { readControl } from './obsidian-common.mjs';
8
10
 
9
11
  export function checkHarness(vaultBase, projectRoot) {
10
12
  const loc = getLocale(vaultBase);
@@ -63,3 +65,37 @@ export function checkHarness(vaultBase, projectRoot) {
63
65
 
64
66
  return { errors, warnings };
65
67
  }
68
+
69
+ // --- diagnóstico de links do grafo (read-only, reusa os reparos em dry-run) -----
70
+ // Surfaça os órfãos que o doctor não enxergava: notas derivadas sem sessão-fonte,
71
+ // artefatos de change sem backlink, e o estado das cores do grafo. Cada não-zero tem um
72
+ // comando de reparo (note relink / change backlink / theme sync).
73
+ export function checkVaultLinks(vaultBase) {
74
+ let derivedOrphans = 0;
75
+ try { derivedOrphans = relinkDerivedNotes(vaultBase, {}).linked.length; } catch { /* sem notas derivadas */ }
76
+ let artifactOrphans = 0;
77
+ try { artifactOrphans = backfillArtifactLinks(vaultBase, {}).changed.length; } catch { /* sem changes */ }
78
+ let graphColors = null; // true=com grupos · false=vazio/ausente de cores · null=sem graph.json
79
+ try {
80
+ const g = JSON.parse(readFileSync(join(vaultBase, '.obsidian', 'graph.json'), 'utf8'));
81
+ graphColors = Array.isArray(g.colorGroups) && g.colorGroups.length > 0;
82
+ } catch { graphColors = null; }
83
+ return { derivedOrphans, artifactOrphans, graphColors };
84
+ }
85
+
86
+ const unquoteControl = (v) => String(v ?? '').replace(/^"(.*)"$/, '$1').trim();
87
+
88
+ // O control marca `inactive` quando a sessão-mãe encerra, mesmo com um workflow/subagente
89
+ // ainda vivo em background. Se a nota da sessão foi escrita há pouco apesar do `inactive`,
90
+ // sinaliza a atividade recente — o doctor deixa de dizer "inativa" quando não está.
91
+ export function checkSessionActivity(vaultBase, { now = Date.now(), windowMs = 5 * 60000 } = {}) {
92
+ const control = readControl(vaultBase);
93
+ const active = unquoteControl(control.status) === 'active';
94
+ const sessionRel = unquoteControl(active ? control.session_file : (control.last_session_file || control.session_file));
95
+ let ageMs = null;
96
+ if (sessionRel) {
97
+ try { ageMs = now - statSync(join(vaultBase, sessionRel)).mtimeMs; } catch { ageMs = null; }
98
+ }
99
+ const backgroundSuspected = !active && sessionRel !== '' && ageMs !== null && ageMs >= 0 && ageMs < windowMs;
100
+ return { lastSession: sessionRel, active, ageMs, backgroundSuspected };
101
+ }