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/CHANGELOG.md +39 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/changes-and-verification.md +8 -1
- package/docs/en/commands/verify.md +22 -5
- package/docs/en/commands/worktrees.md +80 -32
- package/docs/pt-BR/commands/changes-and-verification.md +8 -1
- package/docs/pt-BR/commands/verify.md +22 -6
- package/docs/pt-BR/commands/worktrees.md +80 -31
- 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/cli/src/index.mjs +1 -1
- 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
- package/src/worktree-cleanup.mjs +712 -0
- package/src/worktree.mjs +130 -19
package/hooks/harness-doctor.mjs
CHANGED
|
@@ -17,6 +17,14 @@ import {
|
|
|
17
17
|
import { parseObservabilityCheckpoint } from './session-observability-state.mjs';
|
|
18
18
|
import { readObservabilityStore } from './session-observability-store.mjs';
|
|
19
19
|
import { assessObservabilityFreshness } from './session-observability-lifecycle.mjs';
|
|
20
|
+
import { evaluateEvidenceBinding } from '../packages/vault/src/evidence-envelope.mjs';
|
|
21
|
+
import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
|
|
22
|
+
import { loadSensorsDetailed, requiredSensors } from './sensors-core.mjs';
|
|
23
|
+
import {
|
|
24
|
+
captureGitSnapshot,
|
|
25
|
+
resolveEvidenceIdentity,
|
|
26
|
+
sensorConfigSha256,
|
|
27
|
+
} from '../src/evidence-envelope.mjs';
|
|
20
28
|
|
|
21
29
|
export function checkSessionObservability(vaultBase, deps = {}) {
|
|
22
30
|
const readRegistry = deps.readRegistry || readSessionRegistry;
|
|
@@ -144,10 +152,52 @@ export function checkHarness(vaultBase, projectRoot) {
|
|
|
144
152
|
const effective = buildEffectiveRequirementPackage(vaultBase, dir, reqIds);
|
|
145
153
|
errors.push(...effective.errors.map((e) => `${name}: spec efetiva inválida: ${e}`));
|
|
146
154
|
if (effective.missing.length) errors.push(`req órfão em ${name}: ${effective.missing.map((id) => `[req:${id}]`).join(', ')} não existe na spec efetiva`);
|
|
155
|
+
let evidence = null;
|
|
156
|
+
try { evidence = JSON.parse(readFileSync(join(dir, 'evidencia.json'), 'utf8')); } catch { /* sem evidência */ }
|
|
157
|
+
if (evidence) {
|
|
158
|
+
const expected = {
|
|
159
|
+
change_slug: name,
|
|
160
|
+
tasks_sha256: tasksHashOf(tarefasMd),
|
|
161
|
+
effective_spec_sha256: `sha256:${effective.hash}`,
|
|
162
|
+
};
|
|
163
|
+
let bindingUnavailable = '';
|
|
164
|
+
if (evidence.schema_version === 2 && projectRoot) {
|
|
165
|
+
try {
|
|
166
|
+
const loaded = loadSensorsDetailed(projectRoot);
|
|
167
|
+
if (loaded.error) throw new Error(`wendkeep.sensors.json inválido: ${loaded.error}`);
|
|
168
|
+
expected.identity = resolveEvidenceIdentity({
|
|
169
|
+
vaultBase,
|
|
170
|
+
projectRoot,
|
|
171
|
+
changeSlug: name,
|
|
172
|
+
sessionId: evidence.work_session_id,
|
|
173
|
+
});
|
|
174
|
+
expected.snapshot = captureGitSnapshot(projectRoot);
|
|
175
|
+
expected.sensor_config_sha256 = sensorConfigSha256(
|
|
176
|
+
loaded.sensors,
|
|
177
|
+
requiredSensors(tasks),
|
|
178
|
+
);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
bindingUnavailable = error.code || error.message;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const binding = evaluateEvidenceBinding(evidence, expected);
|
|
184
|
+
if (binding.state === 'legacy-unbound') {
|
|
185
|
+
attention.push(`${name}: evidence legacy-unbound — rode wendkeep verify novamente`);
|
|
186
|
+
} else if (binding.state !== 'bound') {
|
|
187
|
+
attention.push(`${name}: evidence ${binding.state} (${binding.reasons.join('; ')}) — rode wendkeep verify novamente`);
|
|
188
|
+
} else if (bindingUnavailable) {
|
|
189
|
+
attention.push(`${name}: evidence binding atual indisponível (${bindingUnavailable}) — rode doctor da raiz Git e depois wendkeep verify`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
147
192
|
let verdict = null;
|
|
148
193
|
try { verdict = JSON.parse(readFileSync(join(dir, 'verdict.json'), 'utf8')); } catch { /* sem verdict */ }
|
|
149
194
|
if (verdict && reqIds.length) {
|
|
150
|
-
const v = evaluateVerdict(verdict, reqIds, {
|
|
195
|
+
const v = evaluateVerdict(verdict, reqIds, {
|
|
196
|
+
tasksHash: tasksHashOf(tarefasMd),
|
|
197
|
+
effectiveSpecHash: effective.hash,
|
|
198
|
+
evidenceEnvelopeId: evidence?.schema_version === 2 ? evidence.envelope_id : undefined,
|
|
199
|
+
evidenceBinding: evidence?.schema_version === 2 ? evidenceCheckoutBinding(evidence) : undefined,
|
|
200
|
+
});
|
|
151
201
|
if (!v.ok) attention.push(`verdict stale/incompleto em ${name}${v.missing.length ? `: falta cobrir ${v.missing.join(', ')}` : ''}`);
|
|
152
202
|
}
|
|
153
203
|
}
|
package/hooks/spec-core.mjs
CHANGED
|
@@ -5,12 +5,12 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { getLocale } from './locale.mjs';
|
|
7
7
|
import {
|
|
8
|
-
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileSync,
|
|
8
|
+
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, writeVaultFileAtomic, writeVaultFileSync,
|
|
9
9
|
} from './vault-path-safety.mjs';
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Canonical SHA-256 fingerprint of tarefas.md — freshness binding between package/verdict and gate.
|
|
12
12
|
export function tasksHashOf(md) {
|
|
13
|
-
return createHash('
|
|
13
|
+
return `sha256:${createHash('sha256').update(String(md).replace(/\r\n?/g, '\n')).digest('hex')}`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function contentHashOf(value) {
|
|
@@ -168,7 +168,11 @@ function recordPromotedSpecs(vaultBase, capabilities) {
|
|
|
168
168
|
return state;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
export function captureSpecBaseline(vaultBase, changeDir, {
|
|
171
|
+
export function captureSpecBaseline(vaultBase, changeDir, {
|
|
172
|
+
refresh = false,
|
|
173
|
+
writeAtomic = writeVaultFileAtomic,
|
|
174
|
+
beforeRename,
|
|
175
|
+
} = {}) {
|
|
172
176
|
const path = join(changeDir, SPEC_BASELINE_FILE);
|
|
173
177
|
const checked = assertVaultPathSafe(vaultBase, path, {
|
|
174
178
|
expectedType: 'file', label: 'baseline de specs da change',
|
|
@@ -177,12 +181,12 @@ export function captureSpecBaseline(vaultBase, changeDir, { refresh = false } =
|
|
|
177
181
|
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* rebuild malformed baseline */ }
|
|
178
182
|
}
|
|
179
183
|
const baseline = { version: 1, capturedAt: new Date().toISOString(), specs: readLivingSpecs(vaultBase) };
|
|
180
|
-
|
|
184
|
+
writeAtomic(
|
|
181
185
|
vaultBase,
|
|
182
186
|
path,
|
|
183
187
|
`${JSON.stringify(baseline, null, 2)}\n`,
|
|
184
188
|
'utf8',
|
|
185
|
-
{ label: 'baseline de specs da change' },
|
|
189
|
+
{ scopeRoot: changeDir, label: 'baseline de specs da change', beforeRename },
|
|
186
190
|
);
|
|
187
191
|
return baseline;
|
|
188
192
|
}
|
|
@@ -474,10 +478,24 @@ export function promoteSpecs(vaultBase, changeDir, specs, { changeWikilink, date
|
|
|
474
478
|
// Gate check for the independent verdict (Wave A). A requirement-bearing change must have
|
|
475
479
|
// a verdict that is ok and covers every declared req id. A requirement-less change passes:
|
|
476
480
|
// nothing for an independent verifier to check — the sensor gate is already the proof.
|
|
477
|
-
export function evaluateVerdict(verdict, reqIds, {
|
|
481
|
+
export function evaluateVerdict(verdict, reqIds, {
|
|
482
|
+
tasksHash,
|
|
483
|
+
effectiveSpecHash,
|
|
484
|
+
evidenceEnvelopeId,
|
|
485
|
+
evidenceBinding,
|
|
486
|
+
} = {}) {
|
|
478
487
|
const ids = reqIds || [];
|
|
479
|
-
if (ids.length === 0) return { ok: true, missing: [] };
|
|
488
|
+
if (ids.length === 0 && !evidenceEnvelopeId) return { ok: true, missing: [] };
|
|
480
489
|
if (!verdict || verdict.ok !== true) return { ok: false, missing: [] };
|
|
490
|
+
if (evidenceEnvelopeId && verdict.evidenceEnvelopeId !== evidenceEnvelopeId) {
|
|
491
|
+
return { ok: false, missing: [], stale: true };
|
|
492
|
+
}
|
|
493
|
+
if (evidenceBinding && (!verdict.evidenceBinding || Object.entries(evidenceBinding).some(
|
|
494
|
+
([key, value]) => verdict.evidenceBinding[key] !== value,
|
|
495
|
+
))) {
|
|
496
|
+
return { ok: false, missing: [], stale: true };
|
|
497
|
+
}
|
|
498
|
+
if (ids.length === 0) return { ok: true, missing: [] };
|
|
481
499
|
// Freshness (G3/#6): a verdict minted against a different tarefas.md is stale. Verdicts
|
|
482
500
|
// without a hash (pre-0.6.1) are accepted for backward compat.
|
|
483
501
|
if (tasksHash && verdict.tasksHash && verdict.tasksHash !== tasksHash) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
|
|
44
|
+
"precheck": "node --check src/worktree.mjs && node --check src/worktree-cleanup.mjs && node --check src/evidence-envelope.mjs && node --check src/context.mjs && node --check src/active-context-health.mjs && node --check src/active-context-runtime.mjs && node --check hooks/active-context-store.mjs && node --check hooks/change-core.mjs && node --check hooks/brain-inject.mjs && node --check hooks/change-context.mjs && node --check packages/vault/src/worktree-metadata.mjs && node --check packages/vault/src/evidence-envelope.mjs",
|
|
45
45
|
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/active-context-health.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/active-context-handoff-evidence.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
46
46
|
"test": "node --test --test-concurrency=2",
|
|
47
47
|
"test:core": "node scripts/run-scope.mjs core",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Pure-ish: `spawn` is injectable so runs are testable without a shell. Config lives
|
|
3
3
|
// at the PROJECT ROOT (wendkeep.sensors.json); evidence lives per-change in the vault.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
7
|
import { dirname, join, resolve } from 'node:path';
|
|
7
8
|
|
|
@@ -22,6 +23,22 @@ function sanitizeSensorDiagnostic(value) {
|
|
|
22
23
|
.trim();
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function sha256(value) {
|
|
27
|
+
return `sha256:${createHash('sha256').update(String(value || '')).digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sensorNow(now) {
|
|
31
|
+
const value = typeof now === 'function' ? now() : (now || new Date().toISOString());
|
|
32
|
+
if (value instanceof Date) return value.toISOString();
|
|
33
|
+
if (typeof value === 'string') return value;
|
|
34
|
+
return new Date(value).toISOString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function elapsedMilliseconds(startedAt, finishedAt) {
|
|
38
|
+
const elapsed = Date.parse(finishedAt) - Date.parse(startedAt);
|
|
39
|
+
return Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
function sensorFailureNote(result = {}) {
|
|
26
43
|
const status = result.status ?? 'null';
|
|
27
44
|
const header = [
|
|
@@ -86,11 +103,29 @@ export function requiredSensors(tasks) {
|
|
|
86
103
|
|
|
87
104
|
export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } = {}) {
|
|
88
105
|
const byId = Object.fromEntries((sensors || []).map((s) => [s.id, s]));
|
|
89
|
-
const ts = now || new Date().toISOString();
|
|
90
106
|
const evidence = [];
|
|
91
107
|
for (const id of ids) {
|
|
108
|
+
const startedAt = sensorNow(now);
|
|
92
109
|
const s = byId[id];
|
|
93
|
-
if (!s) {
|
|
110
|
+
if (!s) {
|
|
111
|
+
const finishedAt = sensorNow(now);
|
|
112
|
+
evidence.push({
|
|
113
|
+
id,
|
|
114
|
+
status: 'red',
|
|
115
|
+
ts: startedAt,
|
|
116
|
+
severity: 'critical',
|
|
117
|
+
started_at: startedAt,
|
|
118
|
+
finished_at: finishedAt,
|
|
119
|
+
duration_ms: elapsedMilliseconds(startedAt, finishedAt),
|
|
120
|
+
exit_code: null,
|
|
121
|
+
command: '',
|
|
122
|
+
command_sha256: sha256(''),
|
|
123
|
+
output_sha256: sha256(''),
|
|
124
|
+
output_tail: '',
|
|
125
|
+
note: 'sensor não definido',
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
94
129
|
const r = spawn(s.command, [], {
|
|
95
130
|
cwd,
|
|
96
131
|
shell: true,
|
|
@@ -99,7 +134,26 @@ export function runSensors(sensors, ids, { spawn = spawnSync, cwd, env, now } =
|
|
|
99
134
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
135
|
...(env ? { env } : {}),
|
|
101
136
|
});
|
|
102
|
-
const
|
|
137
|
+
const finishedAt = sensorNow(now);
|
|
138
|
+
const rawOutput = [r.stdout, r.stderr].filter(Boolean).join('\n');
|
|
139
|
+
const outputTail = sanitizeSensorDiagnostic(rawOutput);
|
|
140
|
+
const boundedOutputTail = outputTail.length > SENSOR_DIAGNOSTIC_MAX_LENGTH
|
|
141
|
+
? `…${outputTail.slice(-(SENSOR_DIAGNOSTIC_MAX_LENGTH - 1))}`
|
|
142
|
+
: outputTail;
|
|
143
|
+
const entry = {
|
|
144
|
+
id,
|
|
145
|
+
status: (r.status ?? 1) === 0 ? 'green' : 'red',
|
|
146
|
+
ts: startedAt,
|
|
147
|
+
severity: s.severity || 'critical',
|
|
148
|
+
command: sanitizeSensorDiagnostic(s.command),
|
|
149
|
+
command_sha256: sha256(s.command),
|
|
150
|
+
started_at: startedAt,
|
|
151
|
+
finished_at: finishedAt,
|
|
152
|
+
duration_ms: elapsedMilliseconds(startedAt, finishedAt),
|
|
153
|
+
exit_code: Number.isInteger(r.status) ? r.status : null,
|
|
154
|
+
output_sha256: sha256(rawOutput),
|
|
155
|
+
output_tail: boundedOutputTail,
|
|
156
|
+
};
|
|
103
157
|
if (entry.status === 'red') entry.note = sensorFailureNote(r);
|
|
104
158
|
if (s.type === 'mutation' && s.report) {
|
|
105
159
|
// Delegated mutation (Wave B): read the tool's mutation-testing-elements report and
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function stableValue(value) {
|
|
4
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
5
|
+
if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {
|
|
6
|
+
return Object.fromEntries(
|
|
7
|
+
Object.keys(value).sort().map((key) => [key, stableValue(value[key])]),
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function canonicalSha256(value) {
|
|
14
|
+
const bytes = Buffer.isBuffer(value) || value instanceof Uint8Array
|
|
15
|
+
? value
|
|
16
|
+
: JSON.stringify(stableValue(value));
|
|
17
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function evidenceSensors(evidence) {
|
|
21
|
+
if (Array.isArray(evidence)) return evidence;
|
|
22
|
+
return Array.isArray(evidence?.sensors) ? evidence.sensors : [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CHECKOUT_BINDING_KEYS = [
|
|
26
|
+
'project_id', 'repository_id', 'worktree_id', 'head_sha', 'index_tree_sha', 'worktree_digest',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function evidenceCheckoutBinding(evidence = {}) {
|
|
30
|
+
return Object.fromEntries(CHECKOUT_BINDING_KEYS.map((key) => [key, evidence[key]]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function evidenceCheckoutBindingMatches(actual, expected) {
|
|
34
|
+
return Boolean(actual && expected) && CHECKOUT_BINDING_KEYS.every(
|
|
35
|
+
(key) => actual[key] === expected[key] && expected[key] != null,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function evaluateEvidenceBinding(evidence, expected = {}) {
|
|
40
|
+
if (!evidence) return { state: 'unproven', reasons: ['evidence missing'] };
|
|
41
|
+
if (Array.isArray(evidence) || evidence.schema_version !== 2) {
|
|
42
|
+
return { state: 'legacy-unbound', reasons: ['evidence schema v1 has no checkout binding'] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const contextReasons = [];
|
|
46
|
+
if (expected.change_slug != null && evidence.change_slug !== expected.change_slug) {
|
|
47
|
+
contextReasons.push('change_slug mismatch');
|
|
48
|
+
}
|
|
49
|
+
for (const key of ['project_id', 'repository_id', 'worktree_id', 'work_session_id']) {
|
|
50
|
+
if (expected.identity?.[key] != null && evidence[key] !== expected.identity[key]) {
|
|
51
|
+
contextReasons.push(`${key} mismatch`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (contextReasons.length) return { state: 'context-mismatch', reasons: contextReasons };
|
|
55
|
+
|
|
56
|
+
const staleReasons = [];
|
|
57
|
+
const unsigned = { ...evidence };
|
|
58
|
+
delete unsigned.envelope_id;
|
|
59
|
+
if (!evidence.envelope_id || canonicalSha256(unsigned) !== evidence.envelope_id) {
|
|
60
|
+
staleReasons.push('envelope_id invalid');
|
|
61
|
+
}
|
|
62
|
+
for (const key of ['branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest', 'dirty']) {
|
|
63
|
+
if (expected.snapshot?.[key] != null && evidence[key] !== expected.snapshot[key]) {
|
|
64
|
+
staleReasons.push(`${key} changed`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const key of ['tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256']) {
|
|
68
|
+
if (expected[key] != null && evidence[key] !== expected[key]) staleReasons.push(`${key} changed`);
|
|
69
|
+
}
|
|
70
|
+
return staleReasons.length
|
|
71
|
+
? { state: 'stale', reasons: staleReasons }
|
|
72
|
+
: { state: 'bound', reasons: [] };
|
|
73
|
+
}
|
|
@@ -8,5 +8,6 @@ export * from './memory-handoff.mjs';
|
|
|
8
8
|
export * from './memory-store.mjs';
|
|
9
9
|
export * from './memory-scope.mjs';
|
|
10
10
|
export * from './evidence-recall.mjs';
|
|
11
|
+
export * from './evidence-envelope.mjs';
|
|
11
12
|
export * from './validate-core.mjs';
|
|
12
13
|
export * from './validate-memory.mjs';
|
|
@@ -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
|
+
}
|