wendkeep 0.86.0 → 0.87.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,443 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { basename, isAbsolute } from 'node:path';
3
+
4
+ import { parseTasks } from '../../../hooks/change-core.mjs';
5
+ import { evaluateVerdict, tasksHashOf } from '../../../hooks/spec-core.mjs';
6
+ import { deriveTaskContracts, evaluateTaskContracts } from '../../../src/task-contracts.mjs';
7
+ import { evaluateTddAttestation } from '../../../src/tdd-attestation.mjs';
8
+ import { sensorConfigSha256 } from '../../../src/evidence-envelope.mjs';
9
+ import { classifyReceipt } from '../../../src/provenance-gate.mjs';
10
+ import { verifyReceiptChain } from '../../../src/receipt-ledger.mjs';
11
+ import { requiredSensors, runSensors } from '../../harness/src/sensors-core.mjs';
12
+ import {
13
+ canonicalSha256,
14
+ evaluateEvidenceBinding,
15
+ evidenceSensors,
16
+ } from '../../vault/src/evidence-envelope.mjs';
17
+
18
+ const SHA256 = /^[a-f0-9]{64}$/;
19
+ const SIGNED_REF = /^(.*)@sha256:([a-f0-9]{64})$/;
20
+ const SENSOR_HASH = /^sha256:[a-f0-9]{64}$/;
21
+ const GIT_OBJECT = /^[a-f0-9]{40,64}$/;
22
+ const ENVELOPE_REQUIRED = [
23
+ 'schema_version', 'project_id', 'repository_id', 'worktree_id', 'work_session_id',
24
+ 'change_slug', 'branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest',
25
+ 'dirty', 'tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256',
26
+ 'wendkeep_version', 'platform', 'started_at', 'finished_at', 'sensors', 'envelope_id',
27
+ ];
28
+ const ENVELOPE_ALLOWED = new Set([...ENVELOPE_REQUIRED, 'host_coverage', 'tdd_attestations']);
29
+ const PROOF_BINDING_KEYS = [
30
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
31
+ 'branch', 'base_sha', 'head_sha', 'index_tree_sha', 'worktree_digest', 'dirty',
32
+ 'tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256',
33
+ ];
34
+ const EXPECTED_CONTEXT_FIELDS = [
35
+ 'projectId', 'repositoryId', 'worktreeId', 'workSessionId', 'changeSlug',
36
+ 'branch', 'baseSha', 'headSha', 'indexTreeSha', 'worktreeDigest', 'dirty',
37
+ 'tasksSha256', 'effectiveSpecSha256', 'sensorConfigSha256',
38
+ ];
39
+ const SENSOR_PROOF = Symbol('wendkeep.commit.sensor-proof');
40
+ const REMOTE_EVIDENCE_KINDS = new Set(['adr', 'design', 'spec', 'task']);
41
+ const SENSOR_RESULT_BINDING_FIELDS = [
42
+ 'id', 'status', 'severity', 'command', 'command_sha256', 'exit_code',
43
+ 'output_sha256', 'output_tail',
44
+ ];
45
+
46
+ function fail(code, message) {
47
+ throw Object.assign(new Error(message), { code });
48
+ }
49
+
50
+ export function contentSha256(content) {
51
+ return createHash('sha256').update(String(content ?? ''), 'utf8').digest('hex');
52
+ }
53
+
54
+ export function signedEvidenceRef(path, content) {
55
+ return `${path}@sha256:${contentSha256(content)}`;
56
+ }
57
+
58
+ export function parseSignedEvidenceRef(value) {
59
+ const match = String(value || '').match(SIGNED_REF);
60
+ if (!match) fail('WENDKEEP_COMMIT_EVIDENCE_DIGEST_MISSING', 'evidence reference must carry a SHA-256 content digest');
61
+ const path = match[1].replaceAll('\\', '/');
62
+ if (!path || isAbsolute(path) || path.split('/').some((segment) => !segment || segment === '.' || segment === '..')) {
63
+ fail('WENDKEEP_COMMIT_REFERENCE_INVALID', 'signed evidence must use a canonical repository-relative path');
64
+ }
65
+ return { path, sha256: match[2] };
66
+ }
67
+
68
+ export function commitTaskSensorIds(entries = []) {
69
+ const taskEntries = entries.filter((entry) => entry.kind === 'task');
70
+ if (taskEntries.length !== 1) return [];
71
+ return requiredSensors(parseTasks(taskEntries[0].content));
72
+ }
73
+
74
+ export function commitTaskRequirementIds(entries = []) {
75
+ const taskEntries = entries.filter((entry) => entry.kind === 'task');
76
+ if (taskEntries.length !== 1) return [];
77
+ return [...new Set(parseTasks(taskEntries[0].content).flatMap((task) => task.reqs || []))];
78
+ }
79
+
80
+ export function collectCommitSensorProof({ sensors = [], ids = [], cwd, env } = {}) {
81
+ const selected = [...new Set(ids.map((id) => String(id || '').trim()).filter(Boolean))];
82
+ const definitions = new Map(sensors.map((sensor) => [String(sensor?.id || ''), sensor]));
83
+ const missing = selected.filter((id) => !definitions.has(id));
84
+ if (missing.length) {
85
+ fail('WENDKEEP_COMMIT_SENSOR_CONFIG_MISSING', `required commit sensor is not configured: ${missing.join(', ')}`);
86
+ }
87
+ const results = runSensors(sensors, selected, { cwd, env });
88
+ return Object.freeze({
89
+ [SENSOR_PROOF]: true,
90
+ ids: selected,
91
+ definitions: selected.map((id) => definitions.get(id)),
92
+ results,
93
+ configSha256: sensorConfigSha256(sensors, selected),
94
+ });
95
+ }
96
+
97
+ function json(content, path) {
98
+ try { return JSON.parse(content); }
99
+ catch { fail('WENDKEEP_COMMIT_EVIDENCE_INVALID', `structured evidence is invalid JSON: ${path}`); }
100
+ }
101
+
102
+ function validateSensor(sensor, path) {
103
+ const required = [
104
+ 'id', 'status', 'severity', 'command', 'command_sha256', 'started_at', 'finished_at',
105
+ 'duration_ms', 'exit_code', 'output_sha256', 'output_tail',
106
+ ];
107
+ if (!sensor || required.some((field) => sensor[field] === undefined)) {
108
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `sensor provenance is incomplete: ${path}`);
109
+ }
110
+ if (!sensor.id || sensor.status !== 'green' || !['critical', 'warning'].includes(sensor.severity) || sensor.exit_code !== 0
111
+ || !sensor.command || sensor.command_sha256 !== `sha256:${contentSha256(sensor.command)}`
112
+ || !SENSOR_HASH.test(sensor.output_sha256)
113
+ || !Number.isFinite(sensor.duration_ms) || sensor.duration_ms < 0
114
+ || typeof sensor.output_tail !== 'string' || sensor.output_tail.length > 2_000
115
+ || Number.isNaN(Date.parse(sensor.started_at)) || Number.isNaN(Date.parse(sensor.finished_at))) {
116
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `sensor result is not a complete green observation: ${path}`);
117
+ }
118
+ }
119
+
120
+ function executionSensors(task, proof) {
121
+ const ids = requiredSensors(task.tasks);
122
+ if (!ids.length) return [];
123
+ if (!proof?.[SENSOR_PROOF]) {
124
+ fail('WENDKEEP_COMMIT_TESTS_UNPROVEN', 'required sensors were not executed by the canonical collector');
125
+ }
126
+ if (JSON.stringify([...proof.ids].sort()) !== JSON.stringify([...ids].sort())) {
127
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', 'executed sensors do not match canonical task requirements');
128
+ }
129
+ const definitions = new Map(proof.definitions.map((sensor) => [String(sensor?.id || ''), sensor]));
130
+ for (const result of proof.results) {
131
+ validateSensor(result, `sensor:${result.id}`);
132
+ const definition = definitions.get(result.id);
133
+ if (!definition || result.command !== definition.command
134
+ || result.severity !== (definition.severity || 'critical')) {
135
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', `sensor result does not match configured command: ${result.id}`);
136
+ }
137
+ }
138
+ if (proof.results.length !== ids.length) {
139
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', 'canonical sensor result set is incomplete');
140
+ }
141
+ return proof.results;
142
+ }
143
+
144
+ function assertEnvelopeSensorsMatchExecution(envelopeSensors, collectedSensors) {
145
+ if (!envelopeSensors.length) return;
146
+ if (!collectedSensors.length) {
147
+ fail('WENDKEEP_COMMIT_SENSOR_PROOF_UNAUTHENTICATED', 'Envelope sensors require canonical reexecution');
148
+ }
149
+ const envelopeIds = envelopeSensors.map((sensor) => sensor.id);
150
+ const executionIds = collectedSensors.map((sensor) => sensor.id);
151
+ if (new Set(envelopeIds).size !== envelopeIds.length
152
+ || JSON.stringify([...envelopeIds].sort()) !== JSON.stringify([...executionIds].sort())) {
153
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', 'Envelope sensors do not exactly match reexecuted task sensors');
154
+ }
155
+ const observed = new Map(collectedSensors.map((sensor) => [sensor.id, sensor]));
156
+ for (const sensor of envelopeSensors) {
157
+ const actual = observed.get(sensor.id);
158
+ if (!actual || SENSOR_RESULT_BINDING_FIELDS.some((field) => sensor[field] !== actual[field])) {
159
+ fail('WENDKEEP_COMMIT_SENSOR_BINDING_MISMATCH', `Envelope sensor does not match canonical reexecution: ${sensor.id}`);
160
+ }
161
+ }
162
+ }
163
+
164
+ function validateAttestation(attestation, path) {
165
+ const identity = Object.fromEntries([
166
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
167
+ ].map((field) => [field, String(attestation?.[field] || '').trim()]));
168
+ const causalSeal = {
169
+ ...identity,
170
+ task_id: String(attestation?.task_id || '').trim(),
171
+ requirement_id: String(attestation?.requirement_id || '').trim(),
172
+ };
173
+ if (attestation?.schema_version !== 1 || !SHA256.test(String(attestation.attestation_id || ''))
174
+ || Object.values(causalSeal).some((value) => !value)
175
+ || attestation.attestation_id !== canonicalSha256(causalSeal).replace(/^sha256:/, '')
176
+ || !['red-observed', 'green-observed', 'invalid', 'waived'].includes(attestation.state)
177
+ || !Array.isArray(attestation.test_paths)
178
+ || attestation.test_paths.some((testPath) => typeof testPath !== 'string' || !testPath.trim())) {
179
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `TDD attestation is invalid: ${path}`);
180
+ }
181
+ }
182
+
183
+ function validateEnvelopeShape(payload, path) {
184
+ const missing = ENVELOPE_REQUIRED.filter((field) => payload?.[field] === undefined);
185
+ const unknown = Object.keys(payload || {}).filter((field) => !ENVELOPE_ALLOWED.has(field));
186
+ const invalidText = [
187
+ 'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
188
+ 'branch', 'wendkeep_version', 'platform',
189
+ ].some((field) => typeof payload?.[field] !== 'string' || !payload[field]);
190
+ const invalidGit = ['base_sha', 'head_sha', 'index_tree_sha']
191
+ .some((field) => !GIT_OBJECT.test(String(payload?.[field] || '')));
192
+ const invalidHash = ['worktree_digest', 'tasks_sha256', 'effective_spec_sha256', 'sensor_config_sha256', 'envelope_id']
193
+ .some((field) => !SENSOR_HASH.test(String(payload?.[field] || '')));
194
+ const invalidDates = ['started_at', 'finished_at']
195
+ .some((field) => typeof payload?.[field] !== 'string' || Number.isNaN(Date.parse(payload[field])));
196
+ if (missing.length || unknown.length || invalidText || invalidGit || invalidHash || invalidDates
197
+ || typeof payload?.dirty !== 'boolean' || !Array.isArray(payload?.sensors)
198
+ || (payload.tdd_attestations !== undefined && !Array.isArray(payload.tdd_attestations))) {
199
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `Evidence Envelope schema/binding is incomplete: ${path}`);
200
+ }
201
+ }
202
+
203
+ function validateEnvelope(payload, entry, context) {
204
+ if (payload?.schema_version !== 2) {
205
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `Evidence Envelope v2 is required: ${entry.path}`);
206
+ }
207
+ const sensors = evidenceSensors(payload);
208
+ if (!sensors.length) fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `Evidence Envelope has no sensors: ${entry.path}`);
209
+ validateEnvelopeShape(payload, entry.path);
210
+ const missingExpected = EXPECTED_CONTEXT_FIELDS.filter((field) => (
211
+ context[field] === undefined || context[field] === null || context[field] === ''
212
+ ));
213
+ if (missingExpected.length) {
214
+ fail(
215
+ 'WENDKEEP_COMMIT_BINDING_INCOMPLETE',
216
+ `canonical expected binding is incomplete (${missingExpected.join(', ')}): ${entry.path}`,
217
+ );
218
+ }
219
+ const expected = {
220
+ change_slug: context.changeSlug,
221
+ identity: {
222
+ project_id: context.projectId,
223
+ repository_id: context.repositoryId,
224
+ worktree_id: context.worktreeId,
225
+ work_session_id: context.workSessionId,
226
+ },
227
+ snapshot: {
228
+ branch: context.branch,
229
+ base_sha: context.baseSha,
230
+ head_sha: context.headSha,
231
+ index_tree_sha: context.indexTreeSha,
232
+ worktree_digest: context.worktreeDigest,
233
+ dirty: context.dirty,
234
+ },
235
+ tasks_sha256: context.tasksSha256,
236
+ effective_spec_sha256: context.effectiveSpecSha256,
237
+ sensor_config_sha256: context.sensorConfigSha256,
238
+ };
239
+ const assessment = evaluateEvidenceBinding(payload, expected);
240
+ if (assessment.state !== 'bound') {
241
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', `Evidence Envelope is not internally bound: ${entry.path}`);
242
+ }
243
+ for (const sensor of sensors) validateSensor(sensor, entry.path);
244
+ const attestations = (payload.tdd_attestations || []).map((attestation) => {
245
+ validateAttestation(attestation, entry.path);
246
+ return evaluateTddAttestation(attestation, {
247
+ branch: payload.branch,
248
+ head_sha: payload.head_sha,
249
+ index_tree_sha: payload.index_tree_sha,
250
+ worktree_digest: payload.worktree_digest,
251
+ }, { mutationSurvivors: sensors.flatMap((sensor) => sensor.survivors || []) });
252
+ });
253
+ if (attestations.some((attestation) => !['green-observed', 'waived'].includes(attestation.state))) {
254
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `TDD attestation is stale or invalid: ${entry.path}`);
255
+ }
256
+ return { payload, sensors, attestations };
257
+ }
258
+
259
+ function validateAuthority(entry, authority) {
260
+ const normalized = entry.content.replace(/\r\n?/g, '\n');
261
+ if (entry.kind === 'adr') {
262
+ const id = authority?.adr || '';
263
+ if (authority?.kind !== 'adr' || entry.path !== authority.ref
264
+ || !basename(entry.path).toUpperCase().includes(id)
265
+ || !new RegExp(`^(?:#{1,6}\\s+|id:\\s*["']?)${id}\\b`, 'mi').test(normalized)
266
+ || (authority.issue && !new RegExp(`(^|\\s)${authority.issue.replace('#', '#\\s*')}\\b`, 'm').test(normalized))) {
267
+ fail('WENDKEEP_COMMIT_AUTHORITY_MISMATCH', 'ADR artifact does not match its causal ID, path and issue');
268
+ }
269
+ }
270
+ if (entry.kind === 'design') {
271
+ const issue = authority?.issue || '';
272
+ const issueNumber = issue.replace('#', '');
273
+ if (authority?.kind !== 'native' || entry.path !== authority.design
274
+ || !new RegExp(`^#{1,6}\\s+(?:.*\\s)?#${issueNumber}(?:\\s|\\b|—|-)`, 'mi').test(normalized)) {
275
+ fail('WENDKEEP_COMMIT_AUTHORITY_MISMATCH', 'design artifact does not own the declared issue at the canonical path');
276
+ }
277
+ }
278
+ }
279
+
280
+ function requirePublicAuthority(entries, authority) {
281
+ const kind = authority?.kind === 'adr' ? 'adr' : 'design';
282
+ const matches = entries.filter((entry) => entry.kind === kind);
283
+ if (matches.length !== 1) {
284
+ fail('WENDKEEP_COMMIT_REMOTE_AUTHORITY_MISSING', `exactly one versioned ${kind} authority artifact is required`);
285
+ }
286
+ }
287
+
288
+ function taskFacts(entries, context, envelope, collectedSensors = []) {
289
+ const taskEntries = entries.filter((entry) => entry.kind === 'task');
290
+ if (taskEntries.length !== 1) fail('WENDKEEP_COMMIT_EVIDENCE_INCOMPLETE', 'exactly one canonical task artifact is required');
291
+ const source = taskEntries[0].content;
292
+ const tasks = parseTasks(source);
293
+ if (!tasks.length) fail('WENDKEEP_COMMIT_TASKS_INVALID', 'canonical task artifact has no typed checklist tasks');
294
+ if (new Set(tasks.map((task) => task.id)).size !== tasks.length) {
295
+ fail('WENDKEEP_COMMIT_TASKS_INVALID', 'canonical task IDs must be unique');
296
+ }
297
+ const contracts = deriveTaskContracts({
298
+ projectId: envelope?.payload.project_id || context.projectId || 'commit-project',
299
+ changeSlug: envelope?.payload.change_slug || context.changeSlug || 'commit-authority',
300
+ tasks,
301
+ profile: context.profile || 'OFF',
302
+ activeContextId: context.activeContextId || envelope?.payload.work_session_id || context.stagedHash,
303
+ headSha: envelope?.payload.head_sha || context.headSha || context.stagedHash,
304
+ tasksSha256: tasksHashOf(source),
305
+ effectiveSpecSha256: envelope?.payload.effective_spec_sha256 || context.effectiveSpecSha256 || canonicalSha256([]),
306
+ artifactManifestSha256: canonicalSha256([]),
307
+ evidenceEnvelopeId: envelope?.payload.envelope_id || null,
308
+ tddAttestations: envelope?.attestations || [],
309
+ });
310
+ const evaluations = evaluateTaskContracts({
311
+ contracts,
312
+ binding: contracts[0]?.binding || {},
313
+ requirement_ids: [...new Set(tasks.flatMap((task) => task.reqs || []))],
314
+ sensor_results: [...(envelope?.sensors || []), ...collectedSensors],
315
+ artifact_results: [],
316
+ });
317
+ if (!evaluations.length || evaluations.some((result) => !result.can_complete)) {
318
+ fail('WENDKEEP_COMMIT_TASKS_INCOMPLETE', 'canonical Task Contracts are not completed');
319
+ }
320
+ const renderedTasks = tasks.map((task) => `${task.id}: ${task.text}`);
321
+ return { source, tasks, renderedTasks, tasksHash: tasksHashOf(source) };
322
+ }
323
+
324
+ function publicSpecFacts(entries, tasks) {
325
+ const specs = entries.filter((entry) => entry.kind === 'spec');
326
+ const requirements = [...new Set(tasks.flatMap((task) => task.reqs || []))];
327
+ if (requirements.length && !specs.length) {
328
+ fail('WENDKEEP_COMMIT_REMOTE_SPEC_MISSING', 'requirements require a versioned sanitized public spec');
329
+ }
330
+ for (const requirement of requirements) {
331
+ const escaped = requirement.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
332
+ const heading = new RegExp(`^#{1,6}\\s+(?:Requisito|Requirement):\\s*${escaped}(?:\\s|—|-|$)`, 'mi');
333
+ const jsonId = new RegExp(`"(?:id|requirement_id)"\\s*:\\s*"${escaped}"`);
334
+ if (!specs.some((entry) => heading.test(entry.content) || jsonId.test(entry.content))) {
335
+ fail('WENDKEEP_COMMIT_REMOTE_SPEC_INCOMPLETE', `public spec does not define requirement ${requirement}`);
336
+ }
337
+ }
338
+ return specs;
339
+ }
340
+
341
+ function validateVerdict(payload, entry, task, envelope) {
342
+ if (!envelope) fail('WENDKEEP_COMMIT_EVIDENCE_INCOMPLETE', 'verdict requires its Evidence Envelope v2');
343
+ if (!payload?.author_session_id || !payload?.verifier_session_id
344
+ || payload.author_session_id === payload.verifier_session_id) {
345
+ fail('WENDKEEP_COMMIT_VERDICT_NOT_INDEPENDENT', `verdict lacks independent author/verifier identities: ${entry.path}`);
346
+ }
347
+ const reqIds = [...new Set(task.tasks.flatMap((item) => item.reqs || []))];
348
+ const expectedBinding = Object.fromEntries(PROOF_BINDING_KEYS.map((key) => [key, envelope.payload[key]]));
349
+ if (payload.tasksHash !== task.tasksHash
350
+ || payload.effectiveSpecHash !== envelope.payload.effective_spec_sha256.replace(/^sha256:/, '')
351
+ || payload.evidenceEnvelopeId !== envelope.payload.envelope_id
352
+ || !payload.evidenceBinding
353
+ || PROOF_BINDING_KEYS.some((key) => payload.evidenceBinding[key] !== expectedBinding[key])
354
+ || !Array.isArray(payload.coverage)
355
+ || payload.coverage.some((item) => !item || typeof item.req !== 'string'
356
+ || item.covered !== true || typeof item.evidence !== 'string' || !item.evidence.trim())) {
357
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `verdict lacks complete canonical seals or coverage: ${entry.path}`);
358
+ }
359
+ const verdict = evaluateVerdict(payload, reqIds, {
360
+ tasksHash: task.tasksHash,
361
+ effectiveSpecHash: envelope.payload.effective_spec_sha256?.replace(/^sha256:/, ''),
362
+ evidenceEnvelopeId: envelope.payload.envelope_id,
363
+ evidenceBinding: expectedBinding,
364
+ });
365
+ if (!verdict.ok || verdict.stale || verdict.missing?.length) {
366
+ fail('WENDKEEP_COMMIT_EVIDENCE_UNVERIFIED', `verdict is stale or has incomplete coverage: ${entry.path}`);
367
+ }
368
+ }
369
+
370
+ function validateReceipt(payload, entry, stagedHash) {
371
+ if (!Array.isArray(payload?.records) || !payload.records.length) {
372
+ fail('WENDKEEP_COMMIT_RECEIPT_INVALID', `receipt bundle has no signed chain: ${entry.path}`);
373
+ }
374
+ verifyReceiptChain({ records: payload.records, checkpoint: payload.checkpoint ?? null });
375
+ const receipt = payload.records.at(-1);
376
+ const subject = payload.subject || receipt.subject || {};
377
+ if (subject.staged_diff_sha256 !== stagedHash) {
378
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', `receipt is not bound to the staged diff: ${entry.path}`);
379
+ }
380
+ const result = classifyReceipt({ receipt: { ...receipt, ...receipt.subject }, observation: payload.observation, subject });
381
+ if (result.state !== 'verified') {
382
+ fail('WENDKEEP_COMMIT_RECEIPT_INVALID', `receipt schema, chain, observation or status is not verified: ${entry.path}`);
383
+ }
384
+ }
385
+
386
+ export function validateCommitProofSet({ entries, authority, stagedHash, context = {} } = {}) {
387
+ if (!SHA256.test(String(stagedHash || ''))) fail('WENDKEEP_COMMIT_STALE_INPUT', 'staged diff hash is invalid');
388
+ requirePublicAuthority(entries || [], authority);
389
+ for (const entry of entries || []) {
390
+ if (contentSha256(entry.content) !== entry.sha256) {
391
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', `evidence digest mismatch: ${entry.path}`);
392
+ }
393
+ if (!entry.content.trim()) fail('WENDKEEP_COMMIT_EVIDENCE_EMPTY', `evidence is empty: ${entry.path}`);
394
+ if (['adr', 'design'].includes(entry.kind)) validateAuthority(entry, authority);
395
+ }
396
+ const structured = (entries || []).map((entry) => (
397
+ ['evidence', 'receipt', 'verdict'].includes(entry.kind) ? { entry, payload: json(entry.content, entry.path) } : null
398
+ )).filter(Boolean);
399
+ const taskEntry = (entries || []).find((entry) => entry.kind === 'task');
400
+ const preliminaryTasks = taskEntry ? parseTasks(taskEntry.content) : [];
401
+ const expectedContext = taskEntry
402
+ ? { ...context, tasksSha256: tasksHashOf(taskEntry.content) }
403
+ : context;
404
+ const hasVerdict = structured.some(({ entry }) => entry.kind === 'verdict');
405
+ const envelopes = structured.filter(({ entry }) => entry.kind === 'evidence')
406
+ .map(({ entry, payload }) => validateEnvelope(payload, entry, expectedContext));
407
+ if (envelopes.length > 1) fail('WENDKEEP_COMMIT_EVIDENCE_AMBIGUOUS', 'only one Evidence Envelope is allowed');
408
+ const sensors = envelopes[0]?.sensors || [];
409
+ const authenticatedAttestation = envelopes[0]?.attestations
410
+ ?.some((attestation) => attestation.state === 'green-observed') || false;
411
+ if (envelopes[0] && !hasVerdict && !authenticatedAttestation) {
412
+ fail(
413
+ 'WENDKEEP_COMMIT_SENSOR_PROOF_UNAUTHENTICATED',
414
+ 'Evidence Envelope sensor record requires an independently bound verdict or attestation',
415
+ );
416
+ }
417
+ const collectedSensors = executionSensors({ tasks: preliminaryTasks }, context.executionProof);
418
+ assertEnvelopeSensorsMatchExecution(sensors, collectedSensors);
419
+ const task = taskFacts(entries || [], { ...expectedContext, stagedHash }, envelopes[0], collectedSensors);
420
+ publicSpecFacts(entries || [], task.tasks);
421
+ if (envelopes[0] && envelopes[0].payload.tasks_sha256 !== task.tasksHash) {
422
+ fail('WENDKEEP_COMMIT_EVIDENCE_STALE', 'Evidence Envelope tasks_sha256 does not match the canonical task artifact');
423
+ }
424
+ for (const { entry, payload } of structured) {
425
+ if (entry.kind === 'verdict') validateVerdict(payload, entry, task, envelopes[0]);
426
+ if (entry.kind === 'receipt') validateReceipt(payload, entry, stagedHash);
427
+ }
428
+ const tests = [
429
+ ...collectedSensors.map((sensor) => `sensor:${sensor.id} (${sensor.command})`),
430
+ ];
431
+ if (!tests.length) {
432
+ fail('WENDKEEP_COMMIT_TESTS_UNPROVEN', 'no authenticated execution proof supplies tests');
433
+ }
434
+ return {
435
+ evidence: (entries || []).filter((entry) => REMOTE_EVIDENCE_KINDS.has(entry.kind)).map((entry) => ({
436
+ kind: entry.kind,
437
+ ref: signedEvidenceRef(entry.path, entry.content),
438
+ status: 'verified',
439
+ })),
440
+ tasks: task.renderedTasks,
441
+ tests: [...new Set(tests)].sort((left, right) => left.localeCompare(right, 'en')),
442
+ };
443
+ }
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://wendkeep.dev/schema/commit-message-v1.schema.json",
4
+ "title": "WendKeep evidence-based commit input",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "subject", "capability", "authority", "evidence"],
8
+ "properties": {
9
+ "schema_version": { "const": 1 },
10
+ "subject": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["type", "summary"],
14
+ "properties": {
15
+ "type": { "enum": ["feat", "fix", "refactor", "perf"] },
16
+ "scope": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]*$" },
17
+ "summary": { "type": "string", "minLength": 1, "maxLength": 120 }
18
+ }
19
+ },
20
+ "capability": { "type": "string", "minLength": 1, "maxLength": 500 },
21
+ "authority": {
22
+ "oneOf": [
23
+ {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "required": ["kind", "adr", "ref"],
27
+ "properties": {
28
+ "kind": { "const": "adr" },
29
+ "adr": { "type": "string", "pattern": "^ADR-[0-9]{4,}$" },
30
+ "ref": { "type": "string", "minLength": 1 },
31
+ "issue": { "type": "string", "pattern": "^#[0-9]+$" }
32
+ }
33
+ },
34
+ {
35
+ "type": "object",
36
+ "additionalProperties": false,
37
+ "required": ["kind", "issue", "design"],
38
+ "properties": {
39
+ "kind": { "const": "native" },
40
+ "issue": { "type": "string", "pattern": "^#[0-9]+$" },
41
+ "design": { "type": "string", "pattern": "^(docs/superpowers/specs|plans)/(?!\\.{1,2}(?:/|$))(?!.*//)(?!.*\\/\\.{1,2}(?:\\/|$))[a-zA-Z0-9._/-]+\\.md$" }
42
+ }
43
+ }
44
+ ]
45
+ },
46
+ "staged_diff": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["sha256", "files"],
50
+ "properties": {
51
+ "sha256": { "type": "string", "pattern": "^[a-fA-F0-9]{64}$" },
52
+ "files": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }
53
+ }
54
+ },
55
+ "evidence": {
56
+ "type": "array",
57
+ "minItems": 1,
58
+ "items": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "required": ["kind", "ref"],
62
+ "properties": {
63
+ "kind": { "enum": ["adr", "design", "evidence", "receipt", "spec", "task", "verdict"] },
64
+ "ref": { "type": "string", "minLength": 1 }
65
+ }
66
+ }
67
+ },
68
+ "limits": { "type": "array", "items": { "type": "string", "minLength": 1 } },
69
+ "identity": {
70
+ "type": "object",
71
+ "additionalProperties": false,
72
+ "properties": { "agent": { "type": "string" } }
73
+ }
74
+ }
75
+ }