wendkeep 0.77.0 → 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/CHANGELOG.md +23 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/commands/changes-and-verification.md +8 -1
- package/docs/en/commands/verify.md +22 -5
- package/docs/pt-BR/commands/changes-and-verification.md +8 -1
- package/docs/pt-BR/commands/verify.md +22 -6
- package/hooks/change-core.mjs +2 -1
- package/hooks/harness-doctor.mjs +51 -1
- package/hooks/spec-core.mjs +26 -8
- package/package.json +2 -2
- package/packages/harness/src/sensors-core.mjs +57 -3
- package/packages/vault/src/evidence-envelope.mjs +73 -0
- package/packages/vault/src/index.mjs +1 -0
- package/packages/vault/src/memory-handoff.mjs +46 -5
- package/packages/vault/src/vault-path-safety.mjs +11 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +92 -0
- package/src/change.mjs +93 -10
- package/src/evidence-envelope.mjs +288 -0
- package/src/skills-seed.mjs +11 -5
- package/src/verify.mjs +85 -22
|
@@ -5,6 +5,12 @@ import { basename, join, relative } from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
7
7
|
import { scopeForMemoryKey } from './memory-scope.mjs';
|
|
8
|
+
import {
|
|
9
|
+
evaluateEvidenceBinding,
|
|
10
|
+
evidenceCheckoutBinding,
|
|
11
|
+
evidenceCheckoutBindingMatches,
|
|
12
|
+
evidenceSensors,
|
|
13
|
+
} from './evidence-envelope.mjs';
|
|
8
14
|
|
|
9
15
|
const SHARED_HANDOFF_FIELDS = Object.freeze([
|
|
10
16
|
['objective', 'objective.current'],
|
|
@@ -177,9 +183,33 @@ export function collectLifecycleEvidence(vaultBase, { changeSlug = '', summary =
|
|
|
177
183
|
};
|
|
178
184
|
}
|
|
179
185
|
const sensorPath = join(archivedDir, 'evidencia.json');
|
|
180
|
-
const
|
|
181
|
-
|
|
186
|
+
const sensorEnvelope = readJson(sensorPath);
|
|
187
|
+
const sensors = evidenceSensors(sensorEnvelope);
|
|
188
|
+
if (sensors.length && sensors.every((item) => item?.status === 'green')) {
|
|
182
189
|
evidence.sensors = [...new Set(sensors.map((item) => String(item.id || '')).filter(Boolean))].sort();
|
|
190
|
+
evidence.sensors_path = vaultRel(vaultBase, sensorPath);
|
|
191
|
+
const assessed = evaluateEvidenceBinding(sensorEnvelope, { change_slug: slug });
|
|
192
|
+
evidence.sensors_binding = assessed.state;
|
|
193
|
+
if (sensorEnvelope?.schema_version === 2 && assessed.state === 'bound') {
|
|
194
|
+
const checkoutBinding = evidenceCheckoutBinding(sensorEnvelope);
|
|
195
|
+
const verification = readJson(join(archivedDir, 'verificacao.json'));
|
|
196
|
+
const crossBound = verification?.evidenceEnvelopeId === sensorEnvelope.envelope_id
|
|
197
|
+
&& verdict?.evidenceEnvelopeId === sensorEnvelope.envelope_id
|
|
198
|
+
&& evidenceCheckoutBindingMatches(verification?.evidenceBinding, checkoutBinding)
|
|
199
|
+
&& evidenceCheckoutBindingMatches(verdict?.evidenceBinding, checkoutBinding);
|
|
200
|
+
if (!crossBound) {
|
|
201
|
+
evidence.sensors_binding = 'stale';
|
|
202
|
+
evidence.sensors_binding_reasons = ['archived verification/verdict binding mismatch'];
|
|
203
|
+
}
|
|
204
|
+
} else if (assessed.reasons.length) {
|
|
205
|
+
evidence.sensors_binding_reasons = assessed.reasons;
|
|
206
|
+
}
|
|
207
|
+
if (sensorEnvelope?.schema_version === 2) {
|
|
208
|
+
evidence.sensors_envelope_id = sensorEnvelope.envelope_id;
|
|
209
|
+
evidence.sensors_tasks_hash = sensorEnvelope.tasks_sha256;
|
|
210
|
+
evidence.sensors_repository_id = sensorEnvelope.repository_id;
|
|
211
|
+
evidence.sensors_worktree_id = sensorEnvelope.worktree_id;
|
|
212
|
+
}
|
|
183
213
|
}
|
|
184
214
|
}
|
|
185
215
|
}
|
|
@@ -279,12 +309,23 @@ export function buildSessionMemoryEvents({
|
|
|
279
309
|
if (Array.isArray(evidence.sensors) && evidence.sensors.length) {
|
|
280
310
|
events.push(makeEvent(context, {
|
|
281
311
|
memoryKey: 'quality.latest-sensors',
|
|
282
|
-
value:
|
|
283
|
-
|
|
284
|
-
|
|
312
|
+
value: {
|
|
313
|
+
ids: [...new Set(evidence.sensors.map(String))].sort(),
|
|
314
|
+
evidence_state: evidence.sensors_binding || 'legacy-unbound',
|
|
315
|
+
...(evidence.sensors_envelope_id ? { envelope_id: evidence.sensors_envelope_id } : {}),
|
|
316
|
+
...(evidence.sensors_binding && !['bound', 'legacy-unbound'].includes(evidence.sensors_binding)
|
|
317
|
+
? { recovery: 'reabra a change e rode wendkeep verify --deep + wk-verify' }
|
|
318
|
+
: {}),
|
|
319
|
+
},
|
|
320
|
+
authority: evidence.sensors_binding === 'bound' ? 'verified' : 'reported',
|
|
321
|
+
evidence: [evidence.sensors_path].filter(Boolean),
|
|
285
322
|
scopeContext: {
|
|
286
323
|
changeSlug: evidence.change?.slug || normalizedShared?.change_slug,
|
|
287
324
|
tasksHash: evidence.sensors_tasks_hash || normalizedShared?.tasks_hash,
|
|
325
|
+
repositoryId: evidence.sensors_repository_id,
|
|
326
|
+
worktreeId: evidence.sensors_worktree_id,
|
|
327
|
+
evidenceEnvelopeId: evidence.sensors_envelope_id,
|
|
328
|
+
evidenceState: evidence.sensors_binding,
|
|
288
329
|
},
|
|
289
330
|
}));
|
|
290
331
|
}
|
|
@@ -225,10 +225,20 @@ export function writeVaultFileSync(vaultBase, targetPath, content, encoding = 'u
|
|
|
225
225
|
export function writeVaultFileAtomic(vaultBase, targetPath, content, encoding = 'utf8', {
|
|
226
226
|
label = 'arquivo atômico do Vault',
|
|
227
227
|
code = 'VAULT_PATH_UNSAFE',
|
|
228
|
+
scopeRoot = '',
|
|
229
|
+
beforeRename,
|
|
228
230
|
} = {}) {
|
|
229
231
|
const checked = assertVaultPathSafe(vaultBase, targetPath, {
|
|
230
232
|
expectedType: 'file', label, code,
|
|
231
233
|
});
|
|
234
|
+
if (scopeRoot) {
|
|
235
|
+
const scope = assertVaultPathSafe(vaultBase, scopeRoot, {
|
|
236
|
+
allowMissing: false, expectedType: 'directory', label: `escopo de ${label}`, code,
|
|
237
|
+
});
|
|
238
|
+
if (!containedBy(scope.target, checked.target)) {
|
|
239
|
+
throw unsafe(`${label} escapa do escopo autorizado: ${checked.target}`, code);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
232
242
|
assertVaultPathSafe(vaultBase, dirname(checked.target), {
|
|
233
243
|
allowMissing: false, expectedType: 'directory', label: `ancestral de ${label}`, code,
|
|
234
244
|
});
|
|
@@ -245,6 +255,7 @@ export function writeVaultFileAtomic(vaultBase, targetPath, content, encoding =
|
|
|
245
255
|
allowMissing: false, expectedType: 'file', label: `temporário de ${label}`, code,
|
|
246
256
|
});
|
|
247
257
|
assertVaultPathSafe(vaultBase, checked.target, { expectedType: 'file', label, code });
|
|
258
|
+
if (typeof beforeRename === 'function') beforeRename({ temporary: tmp, target: checked.target });
|
|
248
259
|
renameSync(tmp, checked.target);
|
|
249
260
|
tmpCreated = false;
|
|
250
261
|
assertVaultPathSafe(vaultBase, checked.target, {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/rogersialves/wendkeep/schema/wendkeep.evidence-envelope-v2.schema.json",
|
|
4
|
+
"title": "WendKeep Evidence Envelope v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"project_id",
|
|
10
|
+
"repository_id",
|
|
11
|
+
"worktree_id",
|
|
12
|
+
"work_session_id",
|
|
13
|
+
"change_slug",
|
|
14
|
+
"branch",
|
|
15
|
+
"base_sha",
|
|
16
|
+
"head_sha",
|
|
17
|
+
"index_tree_sha",
|
|
18
|
+
"worktree_digest",
|
|
19
|
+
"dirty",
|
|
20
|
+
"tasks_sha256",
|
|
21
|
+
"effective_spec_sha256",
|
|
22
|
+
"sensor_config_sha256",
|
|
23
|
+
"wendkeep_version",
|
|
24
|
+
"platform",
|
|
25
|
+
"started_at",
|
|
26
|
+
"finished_at",
|
|
27
|
+
"sensors",
|
|
28
|
+
"envelope_id"
|
|
29
|
+
],
|
|
30
|
+
"properties": {
|
|
31
|
+
"schema_version": { "const": 2 },
|
|
32
|
+
"project_id": { "type": "string", "minLength": 1 },
|
|
33
|
+
"repository_id": { "type": "string", "minLength": 1 },
|
|
34
|
+
"worktree_id": { "type": "string", "minLength": 1 },
|
|
35
|
+
"work_session_id": { "type": "string", "minLength": 1 },
|
|
36
|
+
"change_slug": { "type": "string", "minLength": 1 },
|
|
37
|
+
"branch": { "type": "string", "minLength": 1 },
|
|
38
|
+
"base_sha": { "$ref": "#/$defs/gitObject" },
|
|
39
|
+
"head_sha": { "$ref": "#/$defs/gitObject" },
|
|
40
|
+
"index_tree_sha": { "$ref": "#/$defs/gitObject" },
|
|
41
|
+
"worktree_digest": { "$ref": "#/$defs/sha256" },
|
|
42
|
+
"dirty": { "type": "boolean" },
|
|
43
|
+
"tasks_sha256": { "$ref": "#/$defs/sha256" },
|
|
44
|
+
"effective_spec_sha256": { "$ref": "#/$defs/sha256" },
|
|
45
|
+
"sensor_config_sha256": { "$ref": "#/$defs/sha256" },
|
|
46
|
+
"wendkeep_version": { "type": "string", "minLength": 1 },
|
|
47
|
+
"platform": { "type": "string", "minLength": 1 },
|
|
48
|
+
"started_at": { "type": "string", "format": "date-time" },
|
|
49
|
+
"finished_at": { "type": "string", "format": "date-time" },
|
|
50
|
+
"envelope_id": { "$ref": "#/$defs/sha256" },
|
|
51
|
+
"sensors": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": { "$ref": "#/$defs/sensor" }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"$defs": {
|
|
57
|
+
"gitObject": { "type": "string", "pattern": "^[a-f0-9]{40,64}$" },
|
|
58
|
+
"sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
|
|
59
|
+
"sensor": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": [
|
|
62
|
+
"id",
|
|
63
|
+
"status",
|
|
64
|
+
"severity",
|
|
65
|
+
"command",
|
|
66
|
+
"command_sha256",
|
|
67
|
+
"started_at",
|
|
68
|
+
"finished_at",
|
|
69
|
+
"duration_ms",
|
|
70
|
+
"exit_code",
|
|
71
|
+
"output_sha256",
|
|
72
|
+
"output_tail"
|
|
73
|
+
],
|
|
74
|
+
"properties": {
|
|
75
|
+
"id": { "type": "string", "minLength": 1 },
|
|
76
|
+
"status": { "enum": ["green", "red"] },
|
|
77
|
+
"severity": { "enum": ["critical", "warning"] },
|
|
78
|
+
"command": { "type": "string" },
|
|
79
|
+
"command_sha256": { "$ref": "#/$defs/sha256" },
|
|
80
|
+
"started_at": { "type": "string", "format": "date-time" },
|
|
81
|
+
"finished_at": { "type": "string", "format": "date-time" },
|
|
82
|
+
"duration_ms": { "type": "number", "minimum": 0 },
|
|
83
|
+
"exit_code": { "type": ["integer", "null"] },
|
|
84
|
+
"output_sha256": { "$ref": "#/$defs/sha256" },
|
|
85
|
+
"output_tail": { "type": "string", "maxLength": 2000 },
|
|
86
|
+
"note": { "type": "string", "maxLength": 2000 },
|
|
87
|
+
"survivors": { "type": "array" },
|
|
88
|
+
"ts": { "type": "string", "format": "date-time" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/change.mjs
CHANGED
|
@@ -19,13 +19,24 @@ import {
|
|
|
19
19
|
isGuideCompactChange,
|
|
20
20
|
setActiveChange,
|
|
21
21
|
} from '../hooks/change-core.mjs';
|
|
22
|
-
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
22
|
+
import { evaluateGate, loadSensorsDetailed, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
23
23
|
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
24
24
|
import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
25
25
|
import { getLocale } from '../hooks/locale.mjs';
|
|
26
26
|
import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
|
|
27
27
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
28
28
|
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
29
|
+
import {
|
|
30
|
+
captureGitSnapshot,
|
|
31
|
+
resolveEvidenceIdentity,
|
|
32
|
+
sensorConfigSha256,
|
|
33
|
+
} from './evidence-envelope.mjs';
|
|
34
|
+
import {
|
|
35
|
+
evaluateEvidenceBinding,
|
|
36
|
+
evidenceCheckoutBinding,
|
|
37
|
+
evidenceCheckoutBindingMatches,
|
|
38
|
+
evidenceSensors,
|
|
39
|
+
} from '../packages/vault/src/evidence-envelope.mjs';
|
|
29
40
|
|
|
30
41
|
function observerMarkdownUnder(vaultBase, relativeRoot) {
|
|
31
42
|
const output = [];
|
|
@@ -74,15 +85,15 @@ export function runChange(argv) {
|
|
|
74
85
|
const VALUE_FLAGS = new Set(['--vault', '--change', '--project', '--session']);
|
|
75
86
|
const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
|
|
76
87
|
const projectRoot = resolve(opt(rest, '--project') || process.cwd());
|
|
88
|
+
const sessionId = opt(rest, '--session')
|
|
89
|
+
|| process.env.CODEX_THREAD_ID
|
|
90
|
+
|| process.env.CLAUDE_SESSION_ID
|
|
91
|
+
|| '';
|
|
77
92
|
let contextResolved = false;
|
|
78
93
|
let resolvedContext = null;
|
|
79
94
|
const context = () => {
|
|
80
95
|
if (contextResolved) return resolvedContext;
|
|
81
96
|
contextResolved = true;
|
|
82
|
-
const sessionId = opt(rest, '--session')
|
|
83
|
-
|| process.env.CODEX_THREAD_ID
|
|
84
|
-
|| process.env.CLAUDE_SESSION_ID
|
|
85
|
-
|| '';
|
|
86
97
|
try {
|
|
87
98
|
resolvedContext = resolveCommandActiveContext({ vaultBase, projectRoot, sessionId });
|
|
88
99
|
return resolvedContext;
|
|
@@ -196,19 +207,48 @@ export function runChange(argv) {
|
|
|
196
207
|
}
|
|
197
208
|
let evidence = null;
|
|
198
209
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
199
|
-
if (evidence) for (const e of evidence) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
210
|
+
if (evidence) for (const e of evidenceSensors(evidence)) process.stdout.write(` ${e.status === 'green' ? '✓' : '✗'} ${e.id} (${e.severity || 'critical'})\n`);
|
|
200
211
|
else process.stdout.write('evidencia: ausente\n');
|
|
201
212
|
const reqIds = [...new Set(tasks.flatMap((t) => t.reqs ?? []))];
|
|
202
213
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
203
214
|
if (effective.errors.length || effective.missing.length) {
|
|
204
215
|
process.stdout.write(`spec efetiva: inválida (${[...effective.errors, ...effective.missing.map((id) => `req órfão ${id}`)].join('; ')})\n`);
|
|
205
216
|
}
|
|
217
|
+
if (evidence) {
|
|
218
|
+
let expected = {
|
|
219
|
+
change_slug: slug,
|
|
220
|
+
tasks_sha256: tasksHashOf(tarefasMd),
|
|
221
|
+
effective_spec_sha256: `sha256:${effective.hash}`,
|
|
222
|
+
};
|
|
223
|
+
let unavailable = '';
|
|
224
|
+
try {
|
|
225
|
+
const ids = requiredSensors(tasks);
|
|
226
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
227
|
+
expected = {
|
|
228
|
+
...expected,
|
|
229
|
+
identity: resolveEvidenceIdentity({
|
|
230
|
+
vaultBase, projectRoot, changeSlug: slug, sessionId, context: context(),
|
|
231
|
+
}),
|
|
232
|
+
snapshot: captureGitSnapshot(projectRoot),
|
|
233
|
+
sensor_config_sha256: sensorConfigSha256(loaded.sensors, ids),
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
unavailable = error.code || error.message;
|
|
237
|
+
}
|
|
238
|
+
const binding = evaluateEvidenceBinding(evidence, expected);
|
|
239
|
+
process.stdout.write(`evidence-binding: ${binding.state}${binding.reasons.length ? ` (${binding.reasons.join('; ')})` : ''}${unavailable ? ` [current snapshot unavailable: ${unavailable}]` : ''}\n`);
|
|
240
|
+
}
|
|
206
241
|
let verdict = null;
|
|
207
242
|
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
208
243
|
if (!verdict) process.stdout.write(`verdict: ausente — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ' (verdict trivial automático)'}\n`);
|
|
209
244
|
else if (!reqIds.length) process.stdout.write(`verdict: ${verdict.ok === true ? 'ok (trivial)' : 'não-ok — re-verifique'}\n`);
|
|
210
245
|
else {
|
|
211
|
-
const v = evaluateVerdict(verdict, reqIds, {
|
|
246
|
+
const v = evaluateVerdict(verdict, reqIds, {
|
|
247
|
+
tasksHash: tasksHashOf(tarefasMd),
|
|
248
|
+
effectiveSpecHash: effective.hash,
|
|
249
|
+
evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
|
|
250
|
+
evidenceBinding: evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : undefined,
|
|
251
|
+
});
|
|
212
252
|
process.stdout.write(`verdict: ${v.ok ? 'ok' : v.stale ? 'stale — re-verifique' : `incompleto: falta ${v.missing.join(', ')}`}\n`);
|
|
213
253
|
}
|
|
214
254
|
try { process.stdout.write(`mutation-round: ${readFileSync(join(dir, '.mutation-round'), 'utf8').trim()}/3\n`); } catch { /* sem rodadas */ }
|
|
@@ -289,9 +329,34 @@ export function runChange(argv) {
|
|
|
289
329
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
290
330
|
if (effective.errors.length) return { ok: false, failing: [`spec efetiva inválida: ${effective.errors.join('; ')}`] };
|
|
291
331
|
if (effective.missing.length) return { ok: false, failing: [formatOrphanReqs(effective.missing)] };
|
|
292
|
-
let evidence =
|
|
332
|
+
let evidence = null;
|
|
293
333
|
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* no evidence */ }
|
|
294
|
-
const
|
|
334
|
+
const sensorEvidence = evidenceSensors(evidence);
|
|
335
|
+
if (required.length && (!evidence || evidence.schema_version !== 2)) {
|
|
336
|
+
return { ok: false, failing: ['evidência legacy-unbound não satisfaz autoridade v2 — rode `wendkeep verify` novamente'] };
|
|
337
|
+
}
|
|
338
|
+
if (evidence?.schema_version === 2) {
|
|
339
|
+
let currentBinding;
|
|
340
|
+
try {
|
|
341
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
342
|
+
currentBinding = evaluateEvidenceBinding(evidence, {
|
|
343
|
+
change_slug: slug,
|
|
344
|
+
identity: resolveEvidenceIdentity({
|
|
345
|
+
vaultBase, projectRoot, changeSlug: slug, sessionId, context: selectedContext,
|
|
346
|
+
}),
|
|
347
|
+
snapshot: captureGitSnapshot(projectRoot),
|
|
348
|
+
tasks_sha256: tasksHashOf(tarefasMd),
|
|
349
|
+
effective_spec_sha256: `sha256:${effective.hash}`,
|
|
350
|
+
sensor_config_sha256: sensorConfigSha256(loaded.sensors, required),
|
|
351
|
+
});
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return { ok: false, failing: [`binding atual indisponível (${error.code || error.message}) — recupere o contexto e rode \`wendkeep verify\` novamente`] };
|
|
354
|
+
}
|
|
355
|
+
if (currentBinding.state !== 'bound') {
|
|
356
|
+
return { ok: false, failing: [`evidência ${currentBinding.state} (${currentBinding.reasons.join('; ')}) — rode \`wendkeep verify\` novamente`] };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const s = evaluateGate(sensorEvidence, required);
|
|
295
360
|
if (!s.ok) return s;
|
|
296
361
|
// Verdict SEMPRE exigido (0.31.0) — a exigência universal vive AQUI no gate; a semântica
|
|
297
362
|
// reqless→ok de evaluateVerdict (spec-core) não muda porque `verify --deep` e `change
|
|
@@ -310,6 +375,19 @@ export function runChange(argv) {
|
|
|
310
375
|
}
|
|
311
376
|
let verification = null;
|
|
312
377
|
try { verification = JSON.parse(readFileSync(join(dir, 'verificacao.json'), 'utf8')); } catch { /* none */ }
|
|
378
|
+
const checkoutBinding = evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : null;
|
|
379
|
+
if (evidence?.schema_version === 2 && verification?.evidenceEnvelopeId !== evidence.envelope_id) {
|
|
380
|
+
return { ok: false, failing: ['pacote de verificação não está ligado ao envelope atual — rode `wendkeep verify --deep` novamente'] };
|
|
381
|
+
}
|
|
382
|
+
if (checkoutBinding && !evidenceCheckoutBindingMatches(verification?.evidenceBinding, checkoutBinding)) {
|
|
383
|
+
return { ok: false, failing: ['binding do pacote de verificação diverge do checkout provado — rode `wendkeep verify --deep` novamente'] };
|
|
384
|
+
}
|
|
385
|
+
if (evidence?.schema_version === 2 && verdict.evidenceEnvelopeId !== evidence.envelope_id) {
|
|
386
|
+
return { ok: false, failing: [`verdict não está ligado ao envelope atual — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
|
|
387
|
+
}
|
|
388
|
+
if (checkoutBinding && !evidenceCheckoutBindingMatches(verdict.evidenceBinding, checkoutBinding)) {
|
|
389
|
+
return { ok: false, failing: [`binding do verdict diverge do checkout provado — rode \`wendkeep verify --deep\`${reqIds.length ? ' + wk-verify' : ''}`] };
|
|
390
|
+
}
|
|
313
391
|
if (verification?.effectiveSpecHash && verification.effectiveSpecHash !== effective.hash) {
|
|
314
392
|
return { ok: false, failing: ['pacote de verificação stale (spec efetiva mudou) — rode `wendkeep verify --deep` novamente'] };
|
|
315
393
|
}
|
|
@@ -317,7 +395,12 @@ export function runChange(argv) {
|
|
|
317
395
|
return { ok: false, failing: ['verdict sem effectiveSpecHash — rode a skill wk-verify novamente'] };
|
|
318
396
|
}
|
|
319
397
|
if (reqIds.length) {
|
|
320
|
-
const v = evaluateVerdict(verdict, reqIds, {
|
|
398
|
+
const v = evaluateVerdict(verdict, reqIds, {
|
|
399
|
+
tasksHash: hash,
|
|
400
|
+
effectiveSpecHash: effective.hash,
|
|
401
|
+
evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
|
|
402
|
+
evidenceBinding: checkoutBinding || undefined,
|
|
403
|
+
});
|
|
321
404
|
if (!v.ok) {
|
|
322
405
|
if (v.stale) return { ok: false, failing: ['verdict stale (tarefas.md mudou depois da verificação) — re-verifique: `wendkeep verify --deep` + wk-verify'] };
|
|
323
406
|
return { ok: false, failing: [`verdict incompleto: falta ${v.missing.join(', ')}`] };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { isUtf8 } from 'node:buffer';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
7
|
+
import {
|
|
8
|
+
discoverWorktreeRepository,
|
|
9
|
+
readWorktreeRegistry,
|
|
10
|
+
worktreeIdentity,
|
|
11
|
+
} from '../packages/vault/src/worktree-metadata.mjs';
|
|
12
|
+
import {
|
|
13
|
+
canonicalSha256,
|
|
14
|
+
evaluateEvidenceBinding,
|
|
15
|
+
evidenceSensors,
|
|
16
|
+
} from '../packages/vault/src/evidence-envelope.mjs';
|
|
17
|
+
|
|
18
|
+
export { canonicalSha256, evaluateEvidenceBinding, evidenceSensors };
|
|
19
|
+
|
|
20
|
+
const PACKAGE_VERSION = JSON.parse(readFileSync(
|
|
21
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'),
|
|
22
|
+
'utf8',
|
|
23
|
+
)).version;
|
|
24
|
+
|
|
25
|
+
export function sensorConfigSha256(sensors, ids) {
|
|
26
|
+
const selected = new Set(ids || []);
|
|
27
|
+
const canonical = (sensors || [])
|
|
28
|
+
.filter((sensor) => selected.has(sensor.id))
|
|
29
|
+
.map((sensor) => sensor)
|
|
30
|
+
.sort((left, right) => String(left.id || '').localeCompare(String(right.id || '')));
|
|
31
|
+
return canonicalSha256(canonical);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedPath(value) {
|
|
35
|
+
return String(value || '').replaceAll('\\', '/');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BINARY_EXTENSIONS = new Set([
|
|
39
|
+
'.7z', '.a', '.avi', '.bin', '.bmp', '.class', '.dll', '.doc', '.docx', '.dylib', '.eot',
|
|
40
|
+
'.exe', '.gif', '.gz', '.ico', '.jar', '.jpeg', '.jpg', '.mov', '.mp3', '.mp4', '.o',
|
|
41
|
+
'.ogg', '.otf', '.pdf', '.png', '.so', '.tar', '.tif', '.tiff', '.ttf', '.wav', '.webm',
|
|
42
|
+
'.webp', '.woff', '.woff2', '.xls', '.xlsx', '.zip',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function normalizedContent(content, binary = false) {
|
|
46
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content || '');
|
|
47
|
+
if (binary || bytes.includes(0) || !isUtf8(bytes)) return bytes;
|
|
48
|
+
return Buffer.from(bytes.toString('utf8').replace(/\r\n?/g, '\n'), 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function digestWorktreeEntries(entries) {
|
|
52
|
+
const canonical = (entries || []).map((entry) => ({
|
|
53
|
+
layer: String(entry.layer || ''),
|
|
54
|
+
status: String(entry.status || ''),
|
|
55
|
+
path: normalizedPath(entry.path),
|
|
56
|
+
...(entry.oldPath ? { old_path: normalizedPath(entry.oldPath) } : {}),
|
|
57
|
+
content_mode: entry.binary ? 'binary' : 'text',
|
|
58
|
+
content_sha256: entry.content == null ? null : canonicalSha256(normalizedContent(entry.content, entry.binary)),
|
|
59
|
+
})).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
60
|
+
return canonicalSha256(canonical);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function gitResult(projectRoot, args, { spawn = spawnSync, allowFailure = false } = {}) {
|
|
64
|
+
const result = spawn('git', args, {
|
|
65
|
+
cwd: projectRoot,
|
|
66
|
+
encoding: null,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
});
|
|
70
|
+
if (!allowFailure && (result.error || result.status !== 0)) {
|
|
71
|
+
const detail = Buffer.from(result.stderr || '').toString('utf8').trim();
|
|
72
|
+
const error = new Error(`git ${args.join(' ')} falhou${detail ? `: ${detail}` : ''}`);
|
|
73
|
+
error.code = 'WENDKEEP_EVIDENCE_GIT_FAILED';
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function gitText(projectRoot, args, options) {
|
|
80
|
+
const result = gitResult(projectRoot, args, options);
|
|
81
|
+
if (result.status !== 0) return '';
|
|
82
|
+
return Buffer.from(result.stdout || '').toString('utf8').trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function gitBuffer(projectRoot, args, options) {
|
|
86
|
+
const result = gitResult(projectRoot, args, options);
|
|
87
|
+
return result.status === 0 ? Buffer.from(result.stdout || '') : Buffer.alloc(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseNameStatus(buffer, layer) {
|
|
91
|
+
const tokens = buffer.toString('utf8').split('\0');
|
|
92
|
+
if (tokens.at(-1) === '') tokens.pop();
|
|
93
|
+
const entries = [];
|
|
94
|
+
for (let index = 0; index < tokens.length;) {
|
|
95
|
+
const status = tokens[index++];
|
|
96
|
+
const renamed = /^[RC]/.test(status);
|
|
97
|
+
const oldPath = renamed ? tokens[index++] : '';
|
|
98
|
+
const path = tokens[index++];
|
|
99
|
+
if (!path) continue;
|
|
100
|
+
entries.push({ layer, status, path, ...(oldPath ? { oldPath } : {}) });
|
|
101
|
+
}
|
|
102
|
+
return entries;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function binaryAttributes(projectRoot, paths, options) {
|
|
106
|
+
const unique = [...new Set(paths.filter(Boolean))];
|
|
107
|
+
if (!unique.length) return new Map();
|
|
108
|
+
const tokens = gitBuffer(projectRoot, ['check-attr', '-z', 'binary', 'text', '--', ...unique], {
|
|
109
|
+
...options, allowFailure: true,
|
|
110
|
+
}).toString('utf8').split('\0');
|
|
111
|
+
if (tokens.at(-1) === '') tokens.pop();
|
|
112
|
+
const attributes = new Map();
|
|
113
|
+
for (let index = 0; index + 2 < tokens.length; index += 3) {
|
|
114
|
+
const [path, attribute, value] = tokens.slice(index, index + 3);
|
|
115
|
+
const entry = attributes.get(path) || {};
|
|
116
|
+
entry[attribute] = value;
|
|
117
|
+
attributes.set(path, entry);
|
|
118
|
+
}
|
|
119
|
+
return attributes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pathIsBinary(path, attributes) {
|
|
123
|
+
const values = attributes.get(path) || {};
|
|
124
|
+
if (values.binary === 'set' || values.text === 'unset') return true;
|
|
125
|
+
if (values.binary === 'unset' || values.text === 'set' || values.text === 'auto') return false;
|
|
126
|
+
const normalized = normalizedPath(path).toLowerCase();
|
|
127
|
+
const dot = normalized.lastIndexOf('.');
|
|
128
|
+
return dot >= 0 && BINARY_EXTENSIONS.has(normalized.slice(dot));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readWorkingPath(projectRoot, path) {
|
|
132
|
+
const root = resolve(projectRoot);
|
|
133
|
+
const absolute = resolve(root, ...normalizedPath(path).split('/'));
|
|
134
|
+
const scoped = relative(root, absolute);
|
|
135
|
+
if (scoped === '..' || scoped.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(scoped)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
if (!existsSync(absolute) || !statSync(absolute).isFile()) return null;
|
|
140
|
+
return readFileSync(absolute);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function changedEntries(projectRoot, options) {
|
|
147
|
+
const staged = parseNameStatus(gitBuffer(projectRoot, [
|
|
148
|
+
'diff', '--cached', '--name-status', '-z', '--find-renames', '--no-ext-diff',
|
|
149
|
+
], options), 'index');
|
|
150
|
+
const unstaged = parseNameStatus(gitBuffer(projectRoot, [
|
|
151
|
+
'diff', '--name-status', '-z', '--find-renames', '--no-ext-diff',
|
|
152
|
+
], options), 'worktree');
|
|
153
|
+
const untracked = gitBuffer(projectRoot, [
|
|
154
|
+
'ls-files', '--others', '--exclude-standard', '-z',
|
|
155
|
+
], options).toString('utf8').split('\0').filter(Boolean).map((path) => ({
|
|
156
|
+
layer: 'untracked', status: '?', path,
|
|
157
|
+
}));
|
|
158
|
+
const attributes = binaryAttributes(projectRoot, [
|
|
159
|
+
...staged.map((entry) => entry.path),
|
|
160
|
+
...unstaged.map((entry) => entry.path),
|
|
161
|
+
...untracked.map((entry) => entry.path),
|
|
162
|
+
], options);
|
|
163
|
+
|
|
164
|
+
for (const entry of staged) {
|
|
165
|
+
entry.binary = pathIsBinary(entry.path, attributes);
|
|
166
|
+
entry.content = /^D/.test(entry.status)
|
|
167
|
+
? null
|
|
168
|
+
: gitBuffer(projectRoot, ['show', `:${entry.path}`], { ...options, allowFailure: true });
|
|
169
|
+
}
|
|
170
|
+
for (const entry of [...unstaged, ...untracked]) {
|
|
171
|
+
entry.binary = pathIsBinary(entry.path, attributes);
|
|
172
|
+
entry.content = /^D/.test(entry.status) ? null : readWorkingPath(projectRoot, entry.path);
|
|
173
|
+
}
|
|
174
|
+
return [...staged, ...unstaged, ...untracked];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveBaseSha(projectRoot, headSha, options) {
|
|
178
|
+
const upstream = gitText(projectRoot, ['rev-parse', '--verify', '@{upstream}'], {
|
|
179
|
+
...options, allowFailure: true,
|
|
180
|
+
});
|
|
181
|
+
let candidate = upstream;
|
|
182
|
+
if (!candidate) {
|
|
183
|
+
candidate = gitText(projectRoot, ['rev-parse', '--verify', 'refs/heads/main'], {
|
|
184
|
+
...options, allowFailure: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (!candidate) return headSha;
|
|
188
|
+
return gitText(projectRoot, ['merge-base', headSha, candidate], {
|
|
189
|
+
...options, allowFailure: true,
|
|
190
|
+
}) || headSha;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function captureGitSnapshot(projectRoot, { spawn = spawnSync } = {}) {
|
|
194
|
+
const options = { spawn };
|
|
195
|
+
const headSha = gitText(projectRoot, ['rev-parse', 'HEAD'], options);
|
|
196
|
+
const branch = gitText(projectRoot, ['symbolic-ref', '--short', '-q', 'HEAD'], {
|
|
197
|
+
...options, allowFailure: true,
|
|
198
|
+
}) || 'HEAD';
|
|
199
|
+
const indexTreeSha = gitText(projectRoot, ['write-tree'], options);
|
|
200
|
+
const entries = changedEntries(projectRoot, options);
|
|
201
|
+
return {
|
|
202
|
+
branch,
|
|
203
|
+
base_sha: resolveBaseSha(projectRoot, headSha, options),
|
|
204
|
+
head_sha: headSha,
|
|
205
|
+
index_tree_sha: indexTreeSha,
|
|
206
|
+
worktree_digest: digestWorktreeEntries(entries),
|
|
207
|
+
dirty: entries.length > 0,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveEvidenceIdentity({
|
|
212
|
+
vaultBase,
|
|
213
|
+
projectRoot,
|
|
214
|
+
changeSlug,
|
|
215
|
+
sessionId = '',
|
|
216
|
+
context = null,
|
|
217
|
+
spawn = spawnSync,
|
|
218
|
+
} = {}) {
|
|
219
|
+
const project = readProjectForValidation(vaultBase);
|
|
220
|
+
const repository = discoverWorktreeRepository({ startDir: projectRoot, spawn });
|
|
221
|
+
const { registry } = readWorktreeRegistry(repository);
|
|
222
|
+
if (registry && project.ok && registry.projectId !== project.projectId) {
|
|
223
|
+
const error = new Error('PROJECT.json e registry de worktrees pertencem a projetos diferentes');
|
|
224
|
+
error.code = 'WENDKEEP_EVIDENCE_IDENTITY_MISMATCH';
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
const repositoryId = context?.repositoryId
|
|
228
|
+
|| registry?.repositoryId
|
|
229
|
+
|| canonicalSha256({ git_common_dir: normalizedPath(repository.commonDir).toLowerCase() });
|
|
230
|
+
const projectId = context?.projectId
|
|
231
|
+
|| (project.ok ? project.projectId : canonicalSha256({ repository_id: repositoryId }));
|
|
232
|
+
const worktreeId = context?.worktreeId || worktreeIdentity(repositoryId, repository.gitDir);
|
|
233
|
+
const requestedSession = String(sessionId || '').trim();
|
|
234
|
+
const workSessionId = context?.workSessionId
|
|
235
|
+
|| requestedSession
|
|
236
|
+
|| canonicalSha256({ project_id: projectId, worktree_id: worktreeId, change_slug: changeSlug });
|
|
237
|
+
return {
|
|
238
|
+
project_id: projectId,
|
|
239
|
+
repository_id: repositoryId,
|
|
240
|
+
worktree_id: worktreeId,
|
|
241
|
+
work_session_id: workSessionId,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function assertStableHead(startSnapshot, finishSnapshot) {
|
|
246
|
+
if (startSnapshot?.head_sha !== finishSnapshot?.head_sha) {
|
|
247
|
+
const error = new Error(
|
|
248
|
+
`HEAD mudou durante verify (${startSnapshot?.head_sha || 'ausente'} -> ${finishSnapshot?.head_sha || 'ausente'}); rode novamente no commit estável`,
|
|
249
|
+
);
|
|
250
|
+
error.code = 'WENDKEEP_EVIDENCE_HEAD_CHANGED';
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function buildEvidenceEnvelope({
|
|
256
|
+
identity,
|
|
257
|
+
changeSlug,
|
|
258
|
+
snapshot,
|
|
259
|
+
tasksSha256,
|
|
260
|
+
effectiveSpecSha256,
|
|
261
|
+
sensorConfigSha256: configSha256,
|
|
262
|
+
sensors,
|
|
263
|
+
startedAt,
|
|
264
|
+
finishedAt,
|
|
265
|
+
version = PACKAGE_VERSION,
|
|
266
|
+
runtimePlatform = `${process.platform}-${process.arch}`,
|
|
267
|
+
} = {}) {
|
|
268
|
+
const envelope = {
|
|
269
|
+
schema_version: 2,
|
|
270
|
+
...identity,
|
|
271
|
+
change_slug: changeSlug,
|
|
272
|
+
branch: snapshot.branch,
|
|
273
|
+
base_sha: snapshot.base_sha,
|
|
274
|
+
head_sha: snapshot.head_sha,
|
|
275
|
+
index_tree_sha: snapshot.index_tree_sha,
|
|
276
|
+
worktree_digest: snapshot.worktree_digest,
|
|
277
|
+
dirty: snapshot.dirty,
|
|
278
|
+
tasks_sha256: tasksSha256,
|
|
279
|
+
effective_spec_sha256: effectiveSpecSha256,
|
|
280
|
+
sensor_config_sha256: configSha256,
|
|
281
|
+
wendkeep_version: version,
|
|
282
|
+
platform: runtimePlatform,
|
|
283
|
+
started_at: startedAt,
|
|
284
|
+
finished_at: finishedAt,
|
|
285
|
+
sensors,
|
|
286
|
+
};
|
|
287
|
+
return { ...envelope, envelope_id: canonicalSha256(envelope) };
|
|
288
|
+
}
|