wendkeep 0.76.9 → 0.78.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/src/verify.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // `wendkeep verify [--change <slug>]` — run a change's task sensors, record evidence.
2
2
  // Sensors run at the PROJECT root (--project or cwd); the change + evidence live in
3
3
  // the VAULT. Writes 08-Mudanças/<slug>/evidencia.json; exit 1 if a critical sensor is red.
4
- import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { readFileSync, unlinkSync } from 'node:fs';
5
5
  import { isAbsolute, join, resolve } from 'node:path';
6
6
  import { parseTasks, activeChange, appendFixTasks, healSpecBacklinks } from '../hooks/change-core.mjs';
7
7
  import {
@@ -21,6 +21,15 @@ import {
21
21
  import { addLesson } from '../hooks/lessons-core.mjs';
22
22
  import { getLocale } from '../hooks/locale.mjs';
23
23
  import { resolveCommandActiveContext } from './active-context-runtime.mjs';
24
+ import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
25
+ import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
26
+ import {
27
+ assertStableHead,
28
+ buildEvidenceEnvelope,
29
+ captureGitSnapshot,
30
+ resolveEvidenceIdentity,
31
+ sensorConfigSha256,
32
+ } from './evidence-envelope.mjs';
24
33
 
25
34
  function today() {
26
35
  const d = new Date();
@@ -34,6 +43,23 @@ function opt(argv, name) {
34
43
  return eq ? eq.slice(name.length + 1) : undefined;
35
44
  }
36
45
 
46
+ export function createChangeAuthorityWriter(vaultBase, changeDir, {
47
+ writeAtomic = writeVaultFileAtomic,
48
+ beforeRename,
49
+ } = {}) {
50
+ return (file, content) => writeAtomic(
51
+ vaultBase,
52
+ join(changeDir, file),
53
+ content,
54
+ 'utf8',
55
+ {
56
+ scopeRoot: changeDir,
57
+ label: `artefato de evidência ${file}`,
58
+ ...(beforeRename ? { beforeRename: (details) => beforeRename({ file, ...details }) } : {}),
59
+ },
60
+ );
61
+ }
62
+
37
63
  export function runVerify(argv) {
38
64
  const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
39
65
  if (!vaultRaw) { process.stderr.write('wendkeep verify: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
@@ -41,23 +67,23 @@ export function runVerify(argv) {
41
67
  // --project wins; otherwise climb from cwd to the nearest project marker (agent shells
42
68
  // keep their cwd across commands, so verify from a subdirectory is a recurring miss).
43
69
  const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
70
+ const requestedSession = opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '';
44
71
  let commandContext = null;
45
- if (!opt(argv, '--change')) {
46
- try {
47
- commandContext = resolveCommandActiveContext({
48
- vaultBase,
49
- projectRoot,
50
- sessionId: opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '',
51
- });
52
- } catch (error) {
53
- process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
54
- process.exit(2);
55
- }
72
+ try {
73
+ commandContext = resolveCommandActiveContext({
74
+ vaultBase,
75
+ projectRoot,
76
+ sessionId: requestedSession,
77
+ });
78
+ } catch (error) {
79
+ process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
80
+ process.exit(2);
56
81
  }
57
82
  const slug = opt(argv, '--change') || activeChange(vaultBase, { context: commandContext });
58
83
  if (!slug) { process.stderr.write('wendkeep verify: no change (--change or active).\n'); process.exit(2); }
59
84
 
60
85
  const changeDir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
86
+ const writeAuthority = createChangeAuthorityWriter(vaultBase, changeDir);
61
87
  let tarefas = '';
62
88
  try { tarefas = readFileSync(join(changeDir, 'tarefas.md'), 'utf8'); }
63
89
  catch { process.stderr.write(`wendkeep verify: change not found: ${slug}\n`); process.exit(2); }
@@ -65,7 +91,8 @@ export function runVerify(argv) {
65
91
  // Backlink dos specs escritos à mão pro hub proposta (grafo conectado). Idempotente, fail-quiet.
66
92
  try { healSpecBacklinks(changeDir, vaultBase); } catch { /* heal é bônus */ }
67
93
 
68
- const ids = requiredSensors(parseTasks(tarefas));
94
+ const tasks = parseTasks(tarefas);
95
+ const ids = requiredSensors(tasks);
69
96
  const loaded = loadSensorsDetailed(projectRoot);
70
97
  if (loaded.error) {
71
98
  process.stderr.write(`wendkeep verify: wendkeep.sensors.json inválido em ${loaded.path}: ${loaded.error}\n`);
@@ -75,14 +102,52 @@ export function runVerify(argv) {
75
102
  process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
76
103
  }
77
104
  const sensors = loaded.sensors;
105
+ const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
106
+ const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
107
+ const tasksHash = tasksHashOf(tarefas);
108
+ const startedAt = new Date().toISOString();
109
+ let startSnapshot;
110
+ let identity;
111
+ try {
112
+ startSnapshot = captureGitSnapshot(projectRoot);
113
+ identity = resolveEvidenceIdentity({
114
+ vaultBase,
115
+ projectRoot,
116
+ changeSlug: slug,
117
+ sessionId: requestedSession,
118
+ context: commandContext,
119
+ });
120
+ } catch (error) {
121
+ process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_EVIDENCE_BINDING_FAILED'}: ${error.message}\n`);
122
+ process.exit(2);
123
+ }
78
124
  const evidence = runSensors(sensors, ids, {
79
125
  cwd: projectRoot,
80
126
  env: sensorProcessEnv(vaultBase),
81
127
  });
82
- writeFileSync(join(changeDir, 'evidencia.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8');
128
+ let finishSnapshot;
129
+ try {
130
+ finishSnapshot = captureGitSnapshot(projectRoot);
131
+ assertStableHead(startSnapshot, finishSnapshot);
132
+ } catch (error) {
133
+ process.stderr.write(`wendkeep verify: ${error.code || 'WENDKEEP_EVIDENCE_BINDING_FAILED'}: ${error.message}\n`);
134
+ process.exit(2);
135
+ }
136
+ const envelope = buildEvidenceEnvelope({
137
+ identity,
138
+ changeSlug: slug,
139
+ snapshot: startSnapshot,
140
+ tasksSha256: tasksHash,
141
+ effectiveSpecSha256: `sha256:${effective.hash}`,
142
+ sensorConfigSha256: sensorConfigSha256(sensors, ids),
143
+ sensors: evidence,
144
+ startedAt,
145
+ finishedAt: new Date().toISOString(),
146
+ });
147
+ writeAuthority('evidencia.json', `${JSON.stringify(envelope, null, 2)}\n`);
83
148
  // Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
84
149
  // gate can reject evidence gone stale (a sensor task added after this verify run).
85
- writeFileSync(join(changeDir, '.evidence-hash'), tasksHashOf(tarefas), 'utf8');
150
+ writeAuthority('.evidence-hash', tasksHash);
86
151
 
87
152
  // Mutation survivors -> fix tasks (Wave B), bounded at 3 rounds then escalate. A surviving
88
153
  // mutant always fails verify (exit 1): the suite does not discriminate yet. A clean report
@@ -108,7 +173,7 @@ export function runVerify(argv) {
108
173
  } else {
109
174
  let added = 0;
110
175
  for (const e of withSurvivors) added += appendFixTasks(changeDir, e.survivors, e.id);
111
- writeFileSync(roundFile, String(round + 1), 'utf8');
176
+ writeAuthority('.mutation-round', String(round + 1));
112
177
  process.stdout.write(`verify: ${added} fix-task(s) de mutação (rodada ${round + 1}/3)\n`);
113
178
  }
114
179
  process.stderr.write('verify: mutantes sobreviventes — a suíte não discrimina ainda.\n');
@@ -127,11 +192,7 @@ export function runVerify(argv) {
127
192
  // --deep (Q2=B): assemble the verification package the wk-verify skill judges. A trivial
128
193
  // change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
129
194
  if (argv.includes('--deep')) {
130
- const tasks = parseTasks(tarefas);
131
- const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
132
- const tasksHash = tasksHashOf(tarefas);
133
195
  captureSpecBaseline(vaultBase, changeDir);
134
- const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
135
196
  if (effective.errors.length) {
136
197
  process.stderr.write(`verify --deep: spec efetiva inválida: ${effective.errors.join('; ')}\n`);
137
198
  process.exit(1);
@@ -144,6 +205,8 @@ export function runVerify(argv) {
144
205
  slug,
145
206
  tasksHash,
146
207
  effectiveSpecHash: effective.hash,
208
+ evidenceEnvelopeId: envelope.envelope_id,
209
+ evidenceBinding: evidenceCheckoutBinding(envelope),
147
210
  requirements: effective.requirements.map((req) => {
148
211
  return {
149
212
  id: req.id,
@@ -157,9 +220,9 @@ export function runVerify(argv) {
157
220
  tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
158
221
  sensors: evidence,
159
222
  };
160
- writeFileSync(join(changeDir, 'verificacao.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
223
+ writeAuthority('verificacao.json', `${JSON.stringify(pkg, null, 2)}\n`);
161
224
  if (reqIds.length === 0) {
162
- writeFileSync(join(changeDir, 'verdict.json'), `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, notes: ['trivial: sem requisito'] }, null, 2)}\n`, 'utf8');
225
+ writeAuthority('verdict.json', `${JSON.stringify({ slug, ok: true, coverage: [], tasksHash, effectiveSpecHash: effective.hash, evidenceEnvelopeId: envelope.envelope_id, evidenceBinding: pkg.evidenceBinding, notes: ['trivial: sem requisito'] }, null, 2)}\n`);
163
226
  process.stdout.write('verify --deep: pacote + verdict trivial escritos\n');
164
227
  } else {
165
228
  process.stdout.write('verify --deep: pacote escrito — rode a skill wk-verify pra gravar verdict.json\n');