wendkeep 0.61.0 → 0.63.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.
@@ -0,0 +1,102 @@
1
+ // hooks/sensors-core.mjs — native sensor runner + evidence gate (Pilar C).
2
+ // Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
3
+ // at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
4
+ import { spawnSync } from 'node:child_process';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, join, resolve } from 'node:path';
7
+
8
+ export const SENSOR_VAULT_ENV = 'WENDKEEP_SENSOR_VAULT';
9
+
10
+ export function sensorProcessEnv(vaultBase, inherited = process.env) {
11
+ return {
12
+ ...inherited,
13
+ OBSIDIAN_VAULT_PATH: vaultBase,
14
+ [SENSOR_VAULT_ENV]: vaultBase,
15
+ };
16
+ }
17
+
18
+ export function loadSensors(projectRoot, file = 'wendkeep.sensors.json') {
19
+ return loadSensorsDetailed(projectRoot, file).sensors;
20
+ }
21
+
22
+ // Missing config and broken config are different failures: absent file usually means
23
+ // wrong cwd (subdirectory), broken JSON means the config itself needs fixing. Collapsing
24
+ // both into [] made every sensor report "sensor não definido" — a misleading diagnosis.
25
+ export function loadSensorsDetailed(projectRoot, file = 'wendkeep.sensors.json') {
26
+ const path = join(projectRoot, file);
27
+ if (!existsSync(path)) return { sensors: [], missing: true, error: null, path };
28
+ try {
29
+ const data = JSON.parse(readFileSync(path, 'utf8'));
30
+ return { sensors: Array.isArray(data.sensors) ? data.sensors : [], missing: false, error: null, path };
31
+ } catch (e) {
32
+ return { sensors: [], missing: false, error: e.message, path };
33
+ }
34
+ }
35
+
36
+ // Climb the directory tree looking for a project marker (wendkeep.sensors.json or
37
+ // .wendkeep.json), like git does with .git — shells in agent harnesses keep their cwd
38
+ // across commands, so verify is often run from a subdirectory.
39
+ export function findProjectRoot(startDir) {
40
+ let dir = resolve(startDir);
41
+ for (;;) {
42
+ if (existsSync(join(dir, 'wendkeep.sensors.json')) || existsSync(join(dir, '.wendkeep.json'))) return dir;
43
+ const parent = dirname(dir);
44
+ if (parent === dir) return null;
45
+ dir = parent;
46
+ }
47
+ }
48
+
49
+ export function requiredSensors(tasks) {
50
+ return [...new Set((tasks || []).flatMap((task) => (
51
+ Array.isArray(task.sensors) && task.sensors.length ? task.sensors : [task.sensor]
52
+ )).filter(Boolean))];
53
+ }
54
+
55
+ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
56
+ const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
57
+ const ts = now || new Date().toISOString();
58
+ const evidence = [];
59
+ for (const id of ids) {
60
+ const s = byId[id];
61
+ if (!s) { evidence.push({ id, status: 'red', ts, severity: 'critical', note: 'sensor não definido' }); continue; }
62
+ const r = spawn(s.command, [], { cwd, shell: true, stdio: 'ignore', ...(env ? { env } : {}) });
63
+ const entry = { id, status: (r.status ?? 1) === 0 ? 'green' : 'red', ts, severity: s.severity || 'critical' };
64
+ if (s.type === 'mutation' && s.report) {
65
+ // Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
66
+ // attach surviving mutants so verify can turn them into fix tasks.
67
+ try { entry.survivors = parseMutationReport(JSON.parse(readFileSync(join(cwd || '.', s.report), 'utf8'))); }
68
+ catch { /* report ausente/ilegível — segue só com o exit code */ }
69
+ }
70
+ evidence.push(entry);
71
+ }
72
+ return evidence;
73
+ }
74
+
75
+ // Parse a mutation-testing-elements report (Stryker et al.): return surviving mutants
76
+ // (Survived | NoCoverage) as {file, line, mutator}.
77
+ export function parseMutationReport(json) {
78
+ const out = [];
79
+ const files = json && json.files ? json.files : {};
80
+ for (const [file, data] of Object.entries(files)) {
81
+ for (const m of (data && data.mutants) || []) {
82
+ if (m.status === 'Survived' || m.status === 'NoCoverage') {
83
+ out.push({ file, line: m.location && m.location.start ? m.location.start.line : null, mutator: m.mutatorName || 'unknown' });
84
+ }
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ // A required sensor blocks the gate when it is missing (never verified) or red at a
91
+ // non-warning severity. Warnings are advisory: a red warning does not block archive.
92
+ // Severity comes from the evidence entry (written by runSensors); absent -> critical.
93
+ export function evaluateGate(evidence, requiredIds) {
94
+ const byId = Object.fromEntries((evidence || []).map((e) => [e.id, e]));
95
+ const failing = (requiredIds || []).filter((id) => {
96
+ const e = byId[id];
97
+ if (!e) return true; // never verified
98
+ if (e.status === 'green') return false;
99
+ return (e.severity || 'critical') !== 'warning';
100
+ });
101
+ return { ok: failing.length === 0, failing };
102
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './project-vault.mjs';
2
2
  export * from './vault-path-safety.mjs';
3
+ export * from './locale.mjs';
3
4
  export * from './memory-schema.mjs';
4
5
  export * from './memory-mode.mjs';
5
6
  export * from './memory-handoff.mjs';
@@ -0,0 +1,74 @@
1
+ // packages/vault/src/locale.mjs — canonical vault locale and folder taxonomy.
2
+ // The locale is a property of the VAULT,
3
+ // stored at <vault>/.brain/config.json ({ "locale": "en" }); absent = pt-BR (full backward
4
+ // compat). Parsers stay bilingual everywhere; only RENDERING follows the locale.
5
+ import { readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ export const LOCALES = {
9
+ 'pt-BR': {
10
+ id: 'pt-BR',
11
+ folders: {
12
+ inbox: '00-Inbox',
13
+ project: '01-Projeto',
14
+ sessions: '02-Sessões',
15
+ linear: '03-Linear',
16
+ decisions: '04-Decisões',
17
+ bugs: '05-Bugs',
18
+ learnings: '06-Aprendizados',
19
+ specs: '07-Specs',
20
+ changes: '08-Mudanças',
21
+ },
22
+ months: ['01-JAN', '02-FEV', '03-MAR', '04-ABR', '05-MAI', '06-JUN', '07-JUL', '08-AGO', '09-SET', '10-OUT', '11-NOV', '12-DEZ'],
23
+ reqHeading: 'Requisito',
24
+ fixTaskVerb: 'mata mutante',
25
+ coreSections: ['Preferências do Usuário', 'Padrões Ativos', 'Pendências Abertas'],
26
+ },
27
+ en: {
28
+ id: 'en',
29
+ folders: {
30
+ inbox: '00-Inbox',
31
+ project: '01-Project',
32
+ sessions: '02-Sessions',
33
+ linear: '03-Linear',
34
+ decisions: '04-Decisions',
35
+ bugs: '05-Bugs',
36
+ learnings: '06-Learnings',
37
+ specs: '07-Specs',
38
+ changes: '08-Changes',
39
+ },
40
+ months: ['01-JAN', '02-FEB', '03-MAR', '04-APR', '05-MAY', '06-JUN', '07-JUL', '08-AUG', '09-SEP', '10-OCT', '11-NOV', '12-DEC'],
41
+ reqHeading: 'Requirement',
42
+ fixTaskVerb: 'kill mutant',
43
+ coreSections: ['User Preferences', 'Active Patterns', 'Open Items'],
44
+ },
45
+ };
46
+
47
+ export const DEFAULT_LOCALE = 'pt-BR';
48
+
49
+ // Per-process cache: one vault per process (hooks + CLI), reads are hot paths.
50
+ const cache = new Map();
51
+
52
+ export function getLocale(vaultBase) {
53
+ if (!vaultBase) return LOCALES[DEFAULT_LOCALE];
54
+ const key = String(vaultBase);
55
+ if (cache.has(key)) return cache.get(key);
56
+ let id = DEFAULT_LOCALE;
57
+ try {
58
+ const data = JSON.parse(readFileSync(join(key, '.brain', 'config.json'), 'utf8'));
59
+ if (data.locale && LOCALES[data.locale]) id = data.locale;
60
+ } catch { /* sem config = pt-BR */ }
61
+ const loc = LOCALES[id];
62
+ cache.set(key, loc);
63
+ return loc;
64
+ }
65
+
66
+ // Test hook: drop the memoized entry (tests rewrite config.json under one tmpdir).
67
+ export function clearLocaleCache() {
68
+ cache.clear();
69
+ }
70
+
71
+ // The full vault taxonomy for a locale (folders + fixed entries), in creation order.
72
+ export function vaultFolders(loc) {
73
+ return [...Object.values(loc.folders), 'Templates', '.brain'];
74
+ }