wendkeep 0.85.1 → 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.
- package/.githooks/commit-msg +16 -0
- package/.githooks/prepare-commit-msg +16 -0
- package/CHANGELOG.md +40 -0
- package/README.en.md +4 -1
- package/README.md +4 -1
- package/docs/en/commands/commit.md +159 -0
- package/docs/en/commands/evidence-embeddings.md +243 -0
- package/docs/en/commands/mcp.md +67 -7
- package/docs/pt-BR/commands/commit.md +159 -0
- package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
- package/docs/pt-BR/commands/mcp.md +66 -7
- package/hooks/evidence-context.mjs +41 -7
- package/hooks/evidence-recall.mjs +10 -0
- package/package.json +5 -2
- package/packages/cli/src/index.mjs +11 -1
- package/packages/commit/package.json +6 -0
- package/packages/commit/src/cli.mjs +89 -0
- package/packages/commit/src/commit-input.mjs +181 -0
- package/packages/commit/src/commit-message.mjs +51 -0
- package/packages/commit/src/commit-policy.mjs +144 -0
- package/packages/commit/src/git-runtime.mjs +428 -0
- package/packages/commit/src/index.mjs +28 -0
- package/packages/commit/src/proof-validation.mjs +443 -0
- package/packages/mcp/src/effects.mjs +3 -2
- package/packages/mcp/src/evidence-recall.mjs +130 -0
- package/packages/mcp/src/executor.mjs +4 -0
- package/packages/mcp/src/server.mjs +31 -1
- package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
- package/packages/vault/src/evidence-index-store.mjs +360 -0
- package/packages/vault/src/evidence-recall-page.mjs +381 -0
- package/packages/vault/src/evidence-search-index.mjs +917 -0
- package/packages/vault/src/index.mjs +12 -1
- package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
- package/packages/vault/src/memory-ledger-view.mjs +41 -0
- package/packages/vault/src/memory-rotation-store.mjs +967 -0
- package/packages/vault/src/memory-segment-store.mjs +820 -0
- package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
- package/packages/vault/src/memory-store-base.mjs +1161 -0
- package/packages/vault/src/memory-store-core.mjs +2 -0
- package/packages/vault/src/memory-store.mjs +46 -1161
- package/schema/commit-message-v1.schema.json +75 -0
- package/scripts/validate-commit-range.mjs +244 -0
- package/src/doctor.mjs +48 -5
- package/src/evidence-search-health.mjs +221 -0
- package/src/git-commit-hooks.mjs +112 -0
- package/src/init.mjs +13 -0
- package/src/memory-scale-health.mjs +210 -0
- package/src/observer-snapshot.mjs +87 -1
- package/src/skills-seed.mjs +79 -0
|
@@ -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
|
+
}
|
|
@@ -6,6 +6,7 @@ const TOOLS = [
|
|
|
6
6
|
['wendkeep_project_status', 'read', 'project:status'],
|
|
7
7
|
['wendkeep_context_status', 'read', 'context:status'],
|
|
8
8
|
['wendkeep_memory_recall', 'read', 'memory:recall'],
|
|
9
|
+
['wendkeep_evidence_recall', 'read', 'evidence:recall'],
|
|
9
10
|
['wendkeep_memory_conflicts', 'read', 'memory:conflicts'],
|
|
10
11
|
['wendkeep_change_list', 'read', 'change:list'],
|
|
11
12
|
['wendkeep_change_show', 'read', 'change:show'],
|
|
@@ -55,10 +56,10 @@ function deepFreeze(value) {
|
|
|
55
56
|
|
|
56
57
|
export const MCP_EFFECT_MANIFEST = deepFreeze({
|
|
57
58
|
schema_version: 1,
|
|
58
|
-
catalog_version: '2026-08-
|
|
59
|
+
catalog_version: '2026-08-27',
|
|
59
60
|
server_aliases: ['wendkeep', 'wendkeep-native'],
|
|
60
61
|
tools: TOOLS,
|
|
61
|
-
integrity: 'sha256:
|
|
62
|
+
integrity: 'sha256:1bb4d5dc62008ed23afa0d850a89bbbb77ea9cc31da79cde557aa2f02034fdde',
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
export function verifyMcpEffectManifest(manifest) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EVIDENCE_RECALL_DEFAULT_LIMIT,
|
|
3
|
+
EVIDENCE_RECALL_DEFAULT_MAX_BYTES,
|
|
4
|
+
EVIDENCE_RECALL_MAX_LIMIT,
|
|
5
|
+
EvidenceRecallBudgetError,
|
|
6
|
+
EvidenceRecallCursorError,
|
|
7
|
+
recallEvidencePage,
|
|
8
|
+
} from '../../vault/src/evidence-recall-page.mjs';
|
|
9
|
+
import {
|
|
10
|
+
EVIDENCE_SEARCH_DEFAULT_CANDIDATES,
|
|
11
|
+
EVIDENCE_SEARCH_DEFAULT_POSTING_BUDGET,
|
|
12
|
+
EVIDENCE_SEARCH_MAX_CANDIDATES,
|
|
13
|
+
EVIDENCE_SEARCH_MAX_POSTING_BUDGET,
|
|
14
|
+
searchEvidenceCandidates,
|
|
15
|
+
} from '../../vault/src/evidence-search-index.mjs';
|
|
16
|
+
|
|
17
|
+
export const MCP_EVIDENCE_RECALL_MAX_BYTES = 512 * 1024;
|
|
18
|
+
|
|
19
|
+
function integer(value, fallback, { min = 1, max } = {}) {
|
|
20
|
+
const number = Number(value ?? fallback);
|
|
21
|
+
if (!Number.isSafeInteger(number) || number < min || number > max) {
|
|
22
|
+
throw new RangeError(`value must be an integer between ${min} and ${max}`);
|
|
23
|
+
}
|
|
24
|
+
return number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function backend(value) {
|
|
28
|
+
const normalized = String(value ?? 'auto').trim().toLowerCase();
|
|
29
|
+
if (!['auto', 'sqlite', 'lexical'].includes(normalized)) {
|
|
30
|
+
throw new TypeError('backend must be auto, sqlite, or lexical');
|
|
31
|
+
}
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function filters(value) {
|
|
36
|
+
if (value === undefined || value === null) return {};
|
|
37
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
38
|
+
throw new TypeError('filters must be an object');
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function logicalReference(result) {
|
|
44
|
+
const { logical_path: logicalPath, ...rest } = result || {};
|
|
45
|
+
return {
|
|
46
|
+
...rest,
|
|
47
|
+
logical_ref: String(logicalPath || ''),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mappedError(error) {
|
|
52
|
+
if (String(error?.code || '').startsWith('MCP_')) return error;
|
|
53
|
+
let code = 'MCP_EVIDENCE_RECALL_FAILED';
|
|
54
|
+
if (error instanceof EvidenceRecallCursorError
|
|
55
|
+
|| error?.code === 'EVIDENCE_RECALL_CURSOR_INVALID') {
|
|
56
|
+
code = 'MCP_EVIDENCE_CURSOR_INVALID';
|
|
57
|
+
} else if (error instanceof EvidenceRecallBudgetError
|
|
58
|
+
|| error?.code === 'EVIDENCE_RECALL_BUDGET_TOO_SMALL') {
|
|
59
|
+
code = 'MCP_EVIDENCE_BUDGET_TOO_SMALL';
|
|
60
|
+
} else if (error?.code === 'EVIDENCE_SEARCH_SQLITE_UNAVAILABLE'
|
|
61
|
+
|| error?.code === 'EVIDENCE_SEARCH_FTS5_UNAVAILABLE') {
|
|
62
|
+
code = 'MCP_EVIDENCE_BACKEND_UNAVAILABLE';
|
|
63
|
+
} else if (error?.code === 'VAULT_PATH_UNSAFE') {
|
|
64
|
+
code = 'MCP_EVIDENCE_ARTIFACT_UNSAFE';
|
|
65
|
+
} else if (error instanceof TypeError || error instanceof RangeError) {
|
|
66
|
+
code = 'MCP_EVIDENCE_RECALL_INVALID';
|
|
67
|
+
}
|
|
68
|
+
return Object.assign(new Error(error?.message || 'evidence recall failed'), {
|
|
69
|
+
code,
|
|
70
|
+
cause: error,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function recallEvidenceForMcp(vaultBase, args = {}) {
|
|
75
|
+
try {
|
|
76
|
+
const query = String(args.query || '').trim();
|
|
77
|
+
if (!query) {
|
|
78
|
+
throw Object.assign(new Error('query is required'), {
|
|
79
|
+
code: 'MCP_EVIDENCE_QUERY_REQUIRED',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const limit = integer(args.limit, EVIDENCE_RECALL_DEFAULT_LIMIT, {
|
|
83
|
+
max: EVIDENCE_RECALL_MAX_LIMIT,
|
|
84
|
+
});
|
|
85
|
+
const maxBytes = integer(args.max_bytes, EVIDENCE_RECALL_DEFAULT_MAX_BYTES, {
|
|
86
|
+
min: 2,
|
|
87
|
+
max: MCP_EVIDENCE_RECALL_MAX_BYTES,
|
|
88
|
+
});
|
|
89
|
+
const candidateLimit = integer(
|
|
90
|
+
args.candidate_limit,
|
|
91
|
+
Math.max(EVIDENCE_SEARCH_DEFAULT_CANDIDATES, limit),
|
|
92
|
+
{ max: EVIDENCE_SEARCH_MAX_CANDIDATES },
|
|
93
|
+
);
|
|
94
|
+
const postingBudget = integer(
|
|
95
|
+
args.posting_budget,
|
|
96
|
+
EVIDENCE_SEARCH_DEFAULT_POSTING_BUDGET,
|
|
97
|
+
{ max: EVIDENCE_SEARCH_MAX_POSTING_BUDGET },
|
|
98
|
+
);
|
|
99
|
+
const normalizedFilters = filters(args.filters);
|
|
100
|
+
const candidates = searchEvidenceCandidates(vaultBase, query, {
|
|
101
|
+
candidateLimit,
|
|
102
|
+
postingBudget,
|
|
103
|
+
filters: normalizedFilters,
|
|
104
|
+
backend: backend(args.backend),
|
|
105
|
+
sqlite: 'auto',
|
|
106
|
+
});
|
|
107
|
+
const page = recallEvidencePage(candidates.rows, query, {
|
|
108
|
+
cursor: args.cursor || null,
|
|
109
|
+
filters: normalizedFilters,
|
|
110
|
+
limit,
|
|
111
|
+
maxBytes,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
schema_version: 1,
|
|
115
|
+
...page,
|
|
116
|
+
results: page.results.map(logicalReference),
|
|
117
|
+
candidates: {
|
|
118
|
+
backend: candidates.backend,
|
|
119
|
+
count: candidates.candidate_count,
|
|
120
|
+
posting_entries: candidates.posting_entries,
|
|
121
|
+
has_more: candidates.has_more,
|
|
122
|
+
rebuilt: candidates.rebuilt,
|
|
123
|
+
fallback_reason: candidates.fallback_reason || '',
|
|
124
|
+
},
|
|
125
|
+
complete_candidate_set: candidates.has_more !== true,
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw mappedError(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../../vault/src/memory-store.mjs';
|
|
24
24
|
import { scopeForMemoryKey } from '../../vault/src/memory-scope.mjs';
|
|
25
25
|
import { sanitizeMemoryText } from '../../vault/src/memory-schema.mjs';
|
|
26
|
+
import { recallEvidenceForMcp } from './evidence-recall.mjs';
|
|
26
27
|
|
|
27
28
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
28
29
|
const BIN = join(ROOT, 'bin', 'wendkeep.mjs');
|
|
@@ -305,6 +306,9 @@ export async function executeNativeMcpTool(tool, args, { signal } = {}) {
|
|
|
305
306
|
topK: Math.min(Number(args.limit || 10), 100),
|
|
306
307
|
}));
|
|
307
308
|
}
|
|
309
|
+
if (tool.name === 'wendkeep_evidence_recall') {
|
|
310
|
+
return sanitize(recallEvidenceForMcp(ctx.vaultBase, args));
|
|
311
|
+
}
|
|
308
312
|
if (tool.name === 'wendkeep_memory_conflicts') {
|
|
309
313
|
return sanitize(listMemoryCandidates(ctx.vaultBase, { activeOnly: true }).candidates);
|
|
310
314
|
}
|
|
@@ -3,6 +3,9 @@ import { MCP_EFFECT_MANIFEST, resolveMcpToolEffect } from './effects.mjs';
|
|
|
3
3
|
const DEFAULT_PAGE_SIZE = 50;
|
|
4
4
|
const MAX_PAGE_SIZE = 100;
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
6
|
+
const MAX_EVIDENCE_BYTES = 512 * 1024;
|
|
7
|
+
const MAX_EVIDENCE_CANDIDATES = 4096;
|
|
8
|
+
const MAX_EVIDENCE_POSTINGS = 1_048_576;
|
|
6
9
|
|
|
7
10
|
function boundedInteger(value, fallback, maximum = MAX_PAGE_SIZE) {
|
|
8
11
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -42,6 +45,7 @@ function availability(tool, nodeVersion) {
|
|
|
42
45
|
|
|
43
46
|
const TOOL_REQUIRED_ARGUMENTS = Object.freeze({
|
|
44
47
|
wendkeep_context_status: ['session_id'],
|
|
48
|
+
wendkeep_evidence_recall: ['query'],
|
|
45
49
|
wendkeep_change_show: ['change'],
|
|
46
50
|
wendkeep_change_status: ['change'],
|
|
47
51
|
wendkeep_task_show: ['session_id', 'task'],
|
|
@@ -55,6 +59,13 @@ const TOOL_REQUIRED_ARGUMENTS = Object.freeze({
|
|
|
55
59
|
wendkeep_handoff_publish: ['payload'],
|
|
56
60
|
});
|
|
57
61
|
|
|
62
|
+
const stringOrStringList = {
|
|
63
|
+
oneOf: [
|
|
64
|
+
{ type: 'string' },
|
|
65
|
+
{ type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
58
69
|
function inputSchema(tool) {
|
|
59
70
|
const required = ['project_root'];
|
|
60
71
|
required.push(...(TOOL_REQUIRED_ARGUMENTS[tool.name] || []));
|
|
@@ -87,6 +98,25 @@ function inputSchema(tool) {
|
|
|
87
98
|
query: { type: 'string' },
|
|
88
99
|
cursor: { type: 'string' },
|
|
89
100
|
limit: { type: 'integer', minimum: 1, maximum: MAX_PAGE_SIZE },
|
|
101
|
+
max_bytes: { type: 'integer', minimum: 2, maximum: MAX_EVIDENCE_BYTES },
|
|
102
|
+
candidate_limit: { type: 'integer', minimum: 1, maximum: MAX_EVIDENCE_CANDIDATES },
|
|
103
|
+
posting_budget: { type: 'integer', minimum: 1, maximum: MAX_EVIDENCE_POSTINGS },
|
|
104
|
+
backend: { type: 'string', enum: ['auto', 'sqlite', 'lexical'] },
|
|
105
|
+
filters: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
properties: {
|
|
109
|
+
authority: stringOrStringList,
|
|
110
|
+
validity: stringOrStringList,
|
|
111
|
+
entity_type: stringOrStringList,
|
|
112
|
+
project_id: stringOrStringList,
|
|
113
|
+
change_slug: stringOrStringList,
|
|
114
|
+
session_id: stringOrStringList,
|
|
115
|
+
work_session_id: stringOrStringList,
|
|
116
|
+
logical_path: stringOrStringList,
|
|
117
|
+
logical_path_prefix: stringOrStringList,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
90
120
|
payload: { type: 'object' },
|
|
91
121
|
},
|
|
92
122
|
};
|
|
@@ -339,4 +369,4 @@ export function createNativeMcpServer({
|
|
|
339
369
|
};
|
|
340
370
|
}
|
|
341
371
|
|
|
342
|
-
export { supportsObserverSql };
|
|
372
|
+
export { supportsObserverSql };
|