@holdyourvoice/hyv 3.1.1 → 3.3.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/Readme.md +76 -17
- package/dist/ai-editor-rules.js +151 -0
- package/dist/ai-editor.js +104 -8
- package/dist/ai-editor.test.js +135 -22
- package/dist/approval-capability.js +111 -0
- package/dist/approval-capability.test.js +52 -0
- package/dist/approval-context.js +54 -0
- package/dist/approval-context.test.js +38 -0
- package/dist/benchmark.js +232 -0
- package/dist/benchmark.test.js +328 -0
- package/dist/canonical-json.js +123 -0
- package/dist/canonical-json.test.js +24 -0
- package/dist/cli.js +359 -21
- package/dist/cli.test.js +275 -7
- package/dist/copy-spec.js +35 -8
- package/dist/editorial-packs.js +25 -1
- package/dist/editorial-packs.test.js +45 -0
- package/dist/hygiene.js +91 -0
- package/dist/hygiene.test.js +73 -0
- package/dist/judgment-task.js +171 -0
- package/dist/judgment-task.test.js +162 -0
- package/dist/learning.js +240 -100
- package/dist/learning.test.js +203 -3
- package/dist/lifecycle-adapter.js +75 -0
- package/dist/lifecycle-adapter.test.js +56 -0
- package/dist/mcp-tools.js +110 -9
- package/dist/mcp-tools.test.js +188 -10
- package/dist/mcp.js +228 -9
- package/dist/mcp.test.js +248 -12
- package/dist/pipeline.js +81 -15
- package/dist/pipeline.test.js +94 -2
- package/dist/preservation.js +89 -0
- package/dist/preservation.test.js +22 -0
- package/dist/profile.js +87 -0
- package/dist/profile.test.js +114 -0
- package/dist/rebuild-task.js +226 -0
- package/dist/rebuild-task.test.js +179 -0
- package/dist/release-audit.test.js +144 -2
- package/dist/rewrite-task.js +136 -16
- package/dist/rewrite-task.test.js +72 -4
- package/dist/rule-reconciliation.test.js +50 -0
- package/dist/semantic-review.js +176 -7
- package/dist/semantic-review.test.js +98 -14
- package/dist/stage1-dry-run.test.js +39 -0
- package/dist/stage1-evaluation.js +579 -0
- package/dist/stage1-evaluation.test.js +184 -0
- package/dist/stage1-human-packet.test.js +102 -0
- package/dist/stage1-schema-contract.test.js +95 -0
- package/dist/stage2-human-packet.test.js +81 -0
- package/dist/version.js +1 -0
- package/dist/voice-dna.js +53 -1
- package/dist/voice-dna.test.js +79 -1
- package/package.json +2 -2
package/dist/mcp-tools.test.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
2
3
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import test from 'node:test';
|
|
6
|
-
import { analyzeBatchForMcp, analyzeForMcp, applyRewriteForMcp, buildProfileForMcp, patternsForMcp, prepareRewriteForMcp, rewritePromptForMcp, verifyCopySpecForMcp, verifyForMcp } from './mcp-tools.js';
|
|
7
|
+
import { analyzeBatchForMcp, analyzeForMcp, applyRebuildForMcp, applyRewriteForMcp, buildProfileForMcp, clearLearningForMcp, finalOutputCheckForMcp, finalizeLifecycleForMcp, inspectHygieneForMcp, inspectLearningForMcp, inspectLifecycleForMcp, patternsForMcp, prepareJudgmentForMcp, prepareLifecycleForMcp, prepareRebuildForMcp, prepareRewriteForMcp, ratifyLearningForMcp, recordApprovedLearningForMcp, recordLearningForMcp, reduceJudgmentForMcp, rewritePromptForMcp, submitSemanticVerdictForMcp, supersedeLearningForMcp, validateFinalApprovalForMcp, verifyCopySpecForMcp, verifyForMcp } from './mcp-tools.js';
|
|
8
|
+
import { canonicalJson } from './canonical-json.js';
|
|
7
9
|
const profile = buildProfileForMcp(['I write clearly. I keep the useful detail.', 'I make the call. Then I explain the trade-off.'], ['leverage']);
|
|
8
10
|
const profileJson = JSON.stringify(profile);
|
|
9
11
|
test('builds a portable profile for MCP without files', () => {
|
|
@@ -11,33 +13,154 @@ test('builds a portable profile for MCP without files', () => {
|
|
|
11
13
|
assert.equal(profile.sampleCount, 2);
|
|
12
14
|
});
|
|
13
15
|
test('keeps the dual-engine analysis shape through MCP tools', () => {
|
|
14
|
-
const result = analyzeForMcp('I leverage a clear plan
|
|
16
|
+
const result = analyzeForMcp('I leverage a clear plan.\u200B', profileJson);
|
|
15
17
|
assert.equal(result.voiceDna.engine, 'voice_dna');
|
|
16
18
|
assert.equal(result.aiEditor.engine, 'ai_editor');
|
|
19
|
+
assert.equal(result.hygiene.suspiciousCount, 1);
|
|
20
|
+
});
|
|
21
|
+
test('inspects Unicode hygiene through MCP without a voice profile', () => {
|
|
22
|
+
const result = inspectHygieneForMcp('one\u200Btwo\u00A0three');
|
|
23
|
+
assert.equal(result.suspiciousCount, 2);
|
|
24
|
+
assert.equal(result.fixableCount, 0);
|
|
25
|
+
});
|
|
26
|
+
test('gates exact final output through MCP without a voice profile', () => {
|
|
27
|
+
const accepted = finalOutputCheckForMcp('exact output');
|
|
28
|
+
assert.equal(accepted.accepted && accepted.output, 'exact output');
|
|
29
|
+
const rejected = finalOutputCheckForMcp('hidden\u200Boutput');
|
|
30
|
+
assert.equal(rejected.accepted, false);
|
|
31
|
+
assert.equal('output' in rejected, false);
|
|
17
32
|
});
|
|
18
33
|
test('accepts optional WritingBrief context and exposes batch findings through MCP helpers', () => {
|
|
19
|
-
const brief = JSON.stringify({
|
|
34
|
+
const brief = JSON.stringify({
|
|
35
|
+
version: '1', audience: 'founders', intent: 'start a discussion', format: 'social', evidenceStatus: 'unverified',
|
|
36
|
+
argumentMap: { observation: 'Founders repeat vague advice.', mechanism: 'The advice skips the work.', consequence: 'Readers cannot act.', readerValue: 'Avoid a vague post.' },
|
|
37
|
+
});
|
|
20
38
|
const analysis = analyzeForMcp('A pattern I keep seeing in founder posts is vague advice.', profileJson, brief);
|
|
21
|
-
assert.
|
|
39
|
+
assert.ok(analysis.editorial?.findings.some((item) => item.id === 'editorial.social.generic-opener'));
|
|
40
|
+
assert.ok(analysis.editorial?.findings.some((item) => item.id === 'editorial.evidence.unverified'));
|
|
22
41
|
const batch = analyzeBatchForMcp(['The launch needs a clear owner.', 'The launch needs a clear owner.']);
|
|
23
42
|
assert.equal(batch.findings.length, 2);
|
|
24
43
|
});
|
|
25
|
-
test('
|
|
44
|
+
test('keeps MCP verification read-only', () => {
|
|
26
45
|
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-'));
|
|
27
46
|
try {
|
|
28
|
-
const result = verifyForMcp('I leverage the answer with useful detail and clear mechanism.', 'I use the answer with useful detail and clear mechanism.', profileJson
|
|
47
|
+
const result = verifyForMcp('I leverage the answer with useful detail and clear mechanism.', 'I use the answer with useful detail and clear mechanism.', profileJson);
|
|
29
48
|
const brief = rewritePromptForMcp('I leverage a clear plan.', profileJson, { root });
|
|
30
49
|
assert.match(brief.prompt, /Tier 0/);
|
|
31
|
-
assert.
|
|
50
|
+
assert.doesNotMatch(brief.prompt, /Learned local preferences/);
|
|
32
51
|
assert.equal(result.preservationScore >= 70, true);
|
|
33
|
-
assert.equal(result
|
|
52
|
+
assert.equal('learning' in result, false);
|
|
53
|
+
assert.deepEqual(inspectLearningForMcp(profileJson, { root }), []);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
rmSync(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
test('prepares, submits, and inspects the normal lifecycle through MCP helpers', () => {
|
|
60
|
+
const deterministicBase = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
|
|
61
|
+
const deterministic = { ...deterministicBase, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex') };
|
|
62
|
+
const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministic.artifactFingerprint, sourceHash: deterministic.sourceHash, candidateHash: deterministic.candidateHash, profileId: deterministic.profileId, profileRevisionDigest: deterministic.profileRevisionDigest, rulesetVersion: deterministic.rulesetVersion, schemaVersion: '1' };
|
|
63
|
+
const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [1] };
|
|
64
|
+
const prepared = prepareLifecycleForMcp(JSON.stringify(deterministic), JSON.stringify(binding), JSON.stringify(receipt), 'normal', ['action_change']);
|
|
65
|
+
const context = { now: 0, trustStore: { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 1, keys: [] }, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: [] };
|
|
66
|
+
const submitted = submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(prepared.task), 'reviewer-1', JSON.stringify({ approved: true, violations: [] }), context);
|
|
67
|
+
assert.equal(submitted.ok && submitted.artifact.status, 'ready_for_human_review');
|
|
68
|
+
assert.equal(inspectLifecycleForMcp(JSON.stringify(submitted.ok && submitted.artifact)).status, 'ready_for_human_review');
|
|
69
|
+
assert.throws(() => prepareLifecycleForMcp(JSON.stringify(deterministic), JSON.stringify(binding), JSON.stringify(receipt), 'high_assurance', ['action_change']), /trusted embedding/);
|
|
70
|
+
const forgedTask = { ...prepared.task, allowedViolations: ['unsupported_claim'] };
|
|
71
|
+
assert.throws(() => submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(forgedTask), 'reviewer-1', JSON.stringify({ approved: false, violations: ['unsupported_claim'] }), context));
|
|
72
|
+
});
|
|
73
|
+
test('validates and finalizes signed approval with helper/core parity and no bearer output', () => {
|
|
74
|
+
const deterministicBase = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
|
|
75
|
+
const deterministic = { ...deterministicBase, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex') };
|
|
76
|
+
const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministic.artifactFingerprint, sourceHash: deterministic.sourceHash, candidateHash: deterministic.candidateHash, profileId: deterministic.profileId, profileRevisionDigest: deterministic.profileRevisionDigest, rulesetVersion: deterministic.rulesetVersion, schemaVersion: '1' };
|
|
77
|
+
const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [1] };
|
|
78
|
+
const prepared = prepareLifecycleForMcp(JSON.stringify(deterministic), JSON.stringify(binding), JSON.stringify(receipt), 'normal', ['action_change']);
|
|
79
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
80
|
+
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host', keyId: 'k1', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
|
|
81
|
+
const context = { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: ['human-1'] };
|
|
82
|
+
const submitted = submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(prepared.task), 'reviewer-1', JSON.stringify({ approved: true, violations: [] }), context);
|
|
83
|
+
assert.equal(submitted.ok, true);
|
|
84
|
+
if (!submitted.ok)
|
|
85
|
+
return;
|
|
86
|
+
const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: submitted.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'mcp-secret-nonce' };
|
|
87
|
+
const payload = Buffer.from(canonicalJson(claims));
|
|
88
|
+
const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
|
|
89
|
+
const validated = validateFinalApprovalForMcp(JSON.stringify(submitted.artifact), canonicalJson(capability), context);
|
|
90
|
+
assert.equal(validated.ok, true);
|
|
91
|
+
const finalized = finalizeLifecycleForMcp(JSON.stringify(submitted.artifact), JSON.stringify({ evaluatorId: 'human-1', decision: 'approve' }), context, canonicalJson(capability));
|
|
92
|
+
assert.equal(finalized.ok && finalized.artifact.status, 'approved');
|
|
93
|
+
assert.throws(() => finalizeLifecycleForMcp(JSON.stringify(submitted.artifact), JSON.stringify({ evaluatorId: 'human-1', decision: 'reject' }), context, canonicalJson(capability)), /does not accept/);
|
|
94
|
+
assert.doesNotMatch(JSON.stringify({ validated, finalized }), /mcp-secret-nonce|payload|signature/);
|
|
95
|
+
});
|
|
96
|
+
test('records approved learning once and treats an exact replay as a no-op', () => {
|
|
97
|
+
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-approved-'));
|
|
98
|
+
const previousHome = process.env.HYV_HOME;
|
|
99
|
+
process.env.HYV_HOME = root;
|
|
100
|
+
try {
|
|
101
|
+
const source = 'I leverage the answer with useful detail and clear mechanism.';
|
|
102
|
+
const candidate = 'I use the answer with useful detail and clear mechanism.';
|
|
103
|
+
const rewriteTask = prepareRewriteForMcp(source, profileJson);
|
|
104
|
+
const evaluation = applyRewriteForMcp(JSON.stringify(rewriteTask), JSON.stringify({
|
|
105
|
+
version: '1', taskFingerprint: rewriteTask.fingerprint, replacements: [{ sentenceId: 1, text: candidate }],
|
|
106
|
+
}), profileJson);
|
|
107
|
+
assert.equal(evaluation.status, 'needs_semantic_review');
|
|
108
|
+
const prepared = prepareLifecycleForMcp(JSON.stringify(evaluation.deterministicArtifact), JSON.stringify(evaluation.lifecycleBinding), JSON.stringify(evaluation.receipt), 'normal', []);
|
|
109
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
110
|
+
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host', keyId: 'k1', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
|
|
111
|
+
const context = { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: ['human-1'] };
|
|
112
|
+
const submitted = submitSemanticVerdictForMcp(JSON.stringify(prepared.artifact), JSON.stringify(prepared.task), 'reviewer-1', JSON.stringify({ approved: true, violations: [] }), context);
|
|
113
|
+
assert.equal(submitted.ok, true);
|
|
114
|
+
if (!submitted.ok)
|
|
115
|
+
return;
|
|
116
|
+
const binding = submitted.artifact.binding;
|
|
117
|
+
const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: submitted.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'approved-learning-secret' };
|
|
118
|
+
const payload = Buffer.from(canonicalJson(claims));
|
|
119
|
+
const capability = canonicalJson({ payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') });
|
|
120
|
+
const decision = JSON.stringify({ evaluatorId: 'human-1', decision: 'approve' });
|
|
121
|
+
const finalized = finalizeLifecycleForMcp(JSON.stringify(submitted.artifact), decision, context, capability);
|
|
122
|
+
assert.equal(finalized.ok, true);
|
|
123
|
+
if (!finalized.ok)
|
|
124
|
+
return;
|
|
125
|
+
const request = { readyJson: JSON.stringify(submitted.artifact), approvedJson: JSON.stringify(finalized.artifact), source, candidate, profileJson, decisionJson: decision, capabilityJson: capability, context };
|
|
126
|
+
assert.equal(recordApprovedLearningForMcp(request), 'recorded');
|
|
127
|
+
assert.equal(recordApprovedLearningForMcp(request), 'nothing_to_learn');
|
|
128
|
+
const stored = JSON.stringify(inspectLearningForMcp(profileJson, { root }));
|
|
129
|
+
assert.equal(JSON.parse(stored).length, 1);
|
|
130
|
+
assert.doesNotMatch(stored, /approved-learning-secret|payload|signature|I leverage|I use/);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (previousHome === undefined)
|
|
134
|
+
delete process.env.HYV_HOME;
|
|
135
|
+
else
|
|
136
|
+
process.env.HYV_HOME = previousHome;
|
|
137
|
+
rmSync(root, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
test('exposes text-free learning inspection and explicit mutations through MCP helpers', () => {
|
|
141
|
+
const root = mkdtempSync(join(tmpdir(), 'holdyourvoice-mcp-learning-'));
|
|
142
|
+
const unsigned = { ...profile, version: '3', id: 'founder.test', revision: 1, provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' }, rulePolicy: {}, fingerprint: { contractionRate: 0, sentenceLengthDistribution: { short: 1, medium: 0, long: 0 }, bulletRate: 0, enDashRate: 0 }, tolerances: { contractionRate: { absolute: 0.1, calibrated: false }, sentenceLengthDistribution: { absolute: 0.1, calibrated: false }, bulletRate: { absolute: 0.1, calibrated: false }, enDashRate: { absolute: 0.1, calibrated: false } }, metricFixtures: { contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'] } };
|
|
143
|
+
const canonical = (value) => Array.isArray(value) ? `[${value.map(canonical).join(',')}]` : value && typeof value === 'object' ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}` : JSON.stringify(value);
|
|
144
|
+
const profileV3 = { ...unsigned, revisionDigest: createHash('sha256').update(canonical(unsigned)).digest('hex') };
|
|
145
|
+
const v3Json = JSON.stringify(profileV3);
|
|
146
|
+
try {
|
|
147
|
+
const recorded = recordLearningForMcp(v3Json, 'Keep the mechanism concrete.', { root, mutationId: 'm1', authority: 'founder', provenance: 'editor-review', weight: 2, compatibility: 'exact' });
|
|
148
|
+
assert.equal(recorded.status, 'recorded');
|
|
149
|
+
assert.equal(recordLearningForMcp(v3Json, 'Different instruction.', { root, mutationId: 'm1' }).status, 'conflict');
|
|
150
|
+
const inspection = inspectLearningForMcp(v3Json, { root });
|
|
151
|
+
assert.equal(inspection[0]?.eventType, 'instruction');
|
|
152
|
+
assert.equal(JSON.stringify(inspection).includes('Keep the mechanism concrete.'), false);
|
|
153
|
+
assert.equal(ratifyLearningForMcp(v3Json, recorded.eventId, { root, mutationId: 'm2', authority: 'founder' }).status, 'recorded');
|
|
154
|
+
assert.equal(supersedeLearningForMcp(v3Json, recorded.eventId, { root, mutationId: 'm3', authority: 'founder' }).status, 'recorded');
|
|
155
|
+
assert.equal(clearLearningForMcp(v3Json, { root }).cleared, true);
|
|
34
156
|
}
|
|
35
157
|
finally {
|
|
36
158
|
rmSync(root, { recursive: true, force: true });
|
|
37
159
|
}
|
|
38
160
|
});
|
|
39
161
|
test('exposes the executable pattern IDs through MCP tools', () => {
|
|
40
|
-
|
|
162
|
+
const catalog = patternsForMcp();
|
|
163
|
+
assert.ok(catalog.rules.some((rule) => rule.id === 'ai.leverage'));
|
|
41
164
|
});
|
|
42
165
|
test('fails closed on changed CopySpec claims through MCP tools', () => {
|
|
43
166
|
const result = verifyCopySpecForMcp('The launch is on 14 August.', 'The launch is next month.', profileJson, JSON.stringify({
|
|
@@ -46,7 +169,17 @@ test('fails closed on changed CopySpec claims through MCP tools', () => {
|
|
|
46
169
|
}));
|
|
47
170
|
assert.equal(result.passed, false);
|
|
48
171
|
assert.equal(result.claims.failures[0]?.code, 'missing_immutable_claim');
|
|
49
|
-
assert.equal(result
|
|
172
|
+
assert.equal('learning' in result, false);
|
|
173
|
+
});
|
|
174
|
+
test('allows declared CopySpec atoms to survive a split MCP rewrite', () => {
|
|
175
|
+
const spec = JSON.stringify({
|
|
176
|
+
version: '1', audience: 'operators', intent: 'explain', channel: 'email',
|
|
177
|
+
claims: [{ id: 'model-size', text: 'Kimi K2.6 has 600 GB of INT4 weights.', atoms: ['Kimi K2.6 uses INT4 weights', 'payload is 600 GB'], evidence: 'Technical report.' }],
|
|
178
|
+
});
|
|
179
|
+
const preserved = verifyCopySpecForMcp('Kimi K2.6 has 600 GB of INT4 weights.', 'Kimi K2.6 uses INT4 weights. The payload is 600 GB.', profileJson, spec);
|
|
180
|
+
assert.equal(preserved.claims.passed, true);
|
|
181
|
+
const missing = verifyCopySpecForMcp('Kimi K2.6 has 600 GB of INT4 weights.', 'Kimi K2.6 uses INT4 weights.', profileJson, spec);
|
|
182
|
+
assert.deepEqual(missing.claims.failures.map((failure) => failure.code), ['missing_immutable_atom']);
|
|
50
183
|
});
|
|
51
184
|
test('prepares and applies the rewrite task through MCP helpers', () => {
|
|
52
185
|
const task = prepareRewriteForMcp('I leverage the answer with useful detail and clear mechanism.', profileJson);
|
|
@@ -56,3 +189,48 @@ test('prepares and applies the rewrite task through MCP helpers', () => {
|
|
|
56
189
|
assert.equal(result.status, 'needs_semantic_review');
|
|
57
190
|
assert.equal(result.candidate, 'I use the answer with useful detail and clear mechanism.');
|
|
58
191
|
});
|
|
192
|
+
test('prepares and reduces judgment envelopes through MCP helpers', () => {
|
|
193
|
+
const draft = 'I leverage the answer.';
|
|
194
|
+
const envelopes = ['triage', 'argument', 'form'].map((kind) => {
|
|
195
|
+
const task = prepareJudgmentForMcp('pre-edit', kind, draft, profileJson);
|
|
196
|
+
return {
|
|
197
|
+
version: '1', stage: 'pre-edit', judgmentType: kind, taskFingerprint: task.taskFingerprint,
|
|
198
|
+
bindings: { ...task.bindings, evaluatorId: 'writer.1' }, findings: [], decision: 'SHIP',
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
assert.equal(reduceJudgmentForMcp(JSON.stringify(envelopes)).decision, 'SHIP');
|
|
202
|
+
});
|
|
203
|
+
test('prepares and evaluates authorized rebuild through MCP helpers', () => {
|
|
204
|
+
const draft = 'I leverage the answer. The launch is on 14 August.';
|
|
205
|
+
const envelopes = ['triage', 'argument', 'form'].map((kind) => {
|
|
206
|
+
const task = prepareJudgmentForMcp('pre-edit', kind, draft, profileJson);
|
|
207
|
+
return {
|
|
208
|
+
version: '1', stage: 'pre-edit', judgmentType: kind, taskFingerprint: task.taskFingerprint,
|
|
209
|
+
bindings: { ...task.bindings, evaluatorId: 'writer.1' }, findings: kind === 'argument' ? [] : [], decision: kind === 'argument' ? 'REBUILD' : 'SHIP',
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
const reduction = reduceJudgmentForMcp(JSON.stringify(envelopes));
|
|
213
|
+
assert.equal(reduction.decision, 'REBUILD');
|
|
214
|
+
if (!('recommendationFingerprint' in reduction))
|
|
215
|
+
throw new Error('expected a pre-edit rebuild recommendation');
|
|
216
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
217
|
+
const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host.example', keyId: 'key-1', publicKeySpki: publicKey.export({ format: 'der', type: 'spki' }).toString('base64url'), status: 'active' }] };
|
|
218
|
+
const sourceHash = createHash('sha256').update(draft).digest('hex');
|
|
219
|
+
const identity = `legacy-v2:${createHash('sha256').update(canonicalJson(profile)).digest('hex')}`;
|
|
220
|
+
const claims = {
|
|
221
|
+
version: '1', purpose: 'hyv.rebuild-authorization', issuer: 'host.example', audience: '@holdyourvoice/hyv',
|
|
222
|
+
subjectArtifactFingerprint: reduction.recommendationFingerprint, sourceHash, candidateHash: sourceHash,
|
|
223
|
+
profileId: identity, profileRevisionDigest: identity, keyId: 'key-1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'mcp-rebuild',
|
|
224
|
+
};
|
|
225
|
+
const payload = Buffer.from(canonicalJson(claims));
|
|
226
|
+
const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
|
|
227
|
+
const copySpec = JSON.stringify({ version: '1', audience: 'operators', intent: 'explain', channel: 'email', claims: [{ id: 'launch-date', text: 'The launch is on 14 August.', evidence: 'Release calendar, 7 August.' }] });
|
|
228
|
+
const context = { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: [], highAssurance: [] }, authorizedHumanFinalizerIds: [] };
|
|
229
|
+
const task = prepareRebuildForMcp(draft, profileJson, JSON.stringify(reduction), copySpec, JSON.stringify(capability), context);
|
|
230
|
+
const result = applyRebuildForMcp(JSON.stringify(task), JSON.stringify({
|
|
231
|
+
version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint,
|
|
232
|
+
candidate: 'Ship planning now treats one calendar fact as fixed. The launch is on 14 August. Every other sentence in this note is new operational language for the release desk.',
|
|
233
|
+
}), profileJson, JSON.stringify(capability), context);
|
|
234
|
+
assert.equal(result.status, 'needs_semantic_review');
|
|
235
|
+
assert.equal(result.receipt.mode, 'REBUILD');
|
|
236
|
+
});
|
package/dist/mcp.js
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { analyzeBatchForMcp, analyzeForMcp, applyRewriteForMcp, buildProfileForMcp, patternsForMcp, prepareRewriteForMcp, rewritePromptForMcp, verifyCopySpecForMcp, verifyForMcp } from './mcp-tools.js';
|
|
4
|
+
import { analyzeBatchForMcp, analyzeForMcp, applyRebuildForMcp, applyRewriteForMcp, buildProfileForMcp, clearLearningForMcp, finalOutputCheckForMcp, finalizeLifecycleForMcp, finalizeRejectionForMcp, inspectHygieneForMcp, inspectLearningForMcp, inspectLifecycleForMcp, migrateLearningForMcp, patternsForMcp, prepareJudgmentForMcp, prepareLifecycleForMcp, prepareRebuildForMcp, prepareRewriteForMcp, ratifyLearningForMcp, recordApprovedLearningForMcp, recordLearningForMcp, reduceJudgmentForMcp, rewritePromptForMcp, submitSemanticVerdictForMcp, supersedeLearningForMcp, validateFinalApprovalForMcp, verifyCopySpecForMcp, verifyForMcp } from './mcp-tools.js';
|
|
5
|
+
import { HYV_VERSION } from './version.js';
|
|
6
|
+
import { loadApprovalContext } from './approval-context.js';
|
|
5
7
|
const writing = z.string().min(1).max(100_000);
|
|
8
|
+
const hygieneText = z.string().max(100_000);
|
|
6
9
|
const profileJson = z.string().min(1).max(50_000);
|
|
7
10
|
const copySpecJson = z.string().min(1).max(250_000);
|
|
8
11
|
const writingBriefJson = z.string().min(1).max(50_000);
|
|
9
12
|
const samples = z.array(writing).min(2).max(20);
|
|
10
13
|
const avoid = z.array(z.string().min(1).max(200)).max(50).optional();
|
|
14
|
+
const lifecycleJson = z.string().min(1).max(1_048_576);
|
|
15
|
+
const approvedLearningText = z.string().min(1).max(1_048_576);
|
|
16
|
+
const evaluatorId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
|
|
17
|
+
const semanticViolation = z.enum(['action_change', 'dropped_object', 'unsupported_claim', 'constraint_weakened', 'clarity_regression']);
|
|
18
|
+
const redactsSensitiveInputs = process.env.HYV_MCP_SENSITIVE_INPUT_REDACTION === '1';
|
|
19
|
+
const learningOptions = {
|
|
20
|
+
mutation_id: z.string().min(1).max(200).optional(),
|
|
21
|
+
authority: z.enum(['founder', 'team', 'system']).optional(),
|
|
22
|
+
provenance: z.string().min(1).max(500).optional(),
|
|
23
|
+
weight: z.number().positive().finite().optional(),
|
|
24
|
+
compatibility: z.enum(['same-or-newer', 'exact']).optional(),
|
|
25
|
+
};
|
|
26
|
+
function learningArgs(value) {
|
|
27
|
+
return { mutationId: value.mutation_id, authority: value.authority, provenance: value.provenance, weight: value.weight, compatibility: value.compatibility };
|
|
28
|
+
}
|
|
11
29
|
function json(value) {
|
|
12
30
|
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
13
31
|
}
|
|
14
32
|
function failure(error) {
|
|
15
33
|
return { content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true };
|
|
16
34
|
}
|
|
17
|
-
|
|
35
|
+
function lifecycleResult(result) {
|
|
36
|
+
return json(result.ok ? result.artifact : { error: result.error });
|
|
37
|
+
}
|
|
38
|
+
const server = new McpServer({ name: 'hold-your-voice', version: HYV_VERSION });
|
|
18
39
|
server.registerTool('hyv_build_profile', {
|
|
19
40
|
description: 'Build a portable VoiceDNA profile from at least two writing samples. The samples stay in memory and are not saved.',
|
|
20
41
|
inputSchema: { samples, avoid },
|
|
@@ -28,7 +49,7 @@ server.registerTool('hyv_build_profile', {
|
|
|
28
49
|
}
|
|
29
50
|
});
|
|
30
51
|
server.registerTool('hyv_analyze', {
|
|
31
|
-
description: 'Run
|
|
52
|
+
description: 'Run separate VoiceDNA and AI Editor checks plus a non-scoring Unicode hygiene inspection against a draft using a portable profile JSON string.',
|
|
32
53
|
inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
|
|
33
54
|
annotations: { readOnlyHint: true },
|
|
34
55
|
}, async ({ draft, profile_json, writing_brief_json }) => {
|
|
@@ -39,6 +60,16 @@ server.registerTool('hyv_analyze', {
|
|
|
39
60
|
return failure(error);
|
|
40
61
|
}
|
|
41
62
|
});
|
|
63
|
+
server.registerTool('hyv_hygiene', {
|
|
64
|
+
description: 'Inspect text for zero-width characters, bidirectional controls, Unicode tag characters, and unusual spaces without changing it or requiring a voice profile.',
|
|
65
|
+
inputSchema: { draft: hygieneText },
|
|
66
|
+
annotations: { readOnlyHint: true },
|
|
67
|
+
}, async ({ draft }) => json(inspectHygieneForMcp(draft)));
|
|
68
|
+
server.registerTool('hyv_final_check', {
|
|
69
|
+
description: 'Gate exact user-facing text from any model, tool, or interface. Returns output only when clean or after removing a leading byte-order mark; unresolved hidden characters withhold output.',
|
|
70
|
+
inputSchema: { text: hygieneText },
|
|
71
|
+
annotations: { readOnlyHint: true },
|
|
72
|
+
}, async ({ text }) => json(finalOutputCheckForMcp(text)));
|
|
42
73
|
server.registerTool('hyv_rewrite_prompt', {
|
|
43
74
|
description: 'Create a constrained editing brief. It does not rewrite the draft or call a model.',
|
|
44
75
|
inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
|
|
@@ -75,25 +106,55 @@ server.registerTool('hyv_apply_rewrite', {
|
|
|
75
106
|
return failure(error);
|
|
76
107
|
}
|
|
77
108
|
});
|
|
109
|
+
server.registerTool('hyv_prepare_judgment', {
|
|
110
|
+
description: 'Prepare a versioned pre-edit or post-candidate judgment task. It does not call a model.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
stage: z.enum(['pre-edit', 'post-candidate']),
|
|
113
|
+
kind: z.enum(['triage', 'argument', 'form', 'polarity', 'flatness', 'semantic']),
|
|
114
|
+
draft: writing,
|
|
115
|
+
profile_json: profileJson,
|
|
116
|
+
candidate: writing.optional(),
|
|
117
|
+
},
|
|
118
|
+
annotations: { readOnlyHint: true },
|
|
119
|
+
}, async ({ stage, kind, draft, profile_json, candidate }) => {
|
|
120
|
+
try {
|
|
121
|
+
return json(prepareJudgmentForMcp(stage, kind, draft, profile_json, candidate));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return failure(error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
server.registerTool('hyv_reduce_judgment', {
|
|
128
|
+
description: 'Reduce bound judgment envelopes into SHIP, EDIT, REBUILD, CLEAR, or ESCALATE. It does not call a model.',
|
|
129
|
+
inputSchema: { envelopes_json: z.string().min(1).max(250_000) },
|
|
130
|
+
annotations: { readOnlyHint: true },
|
|
131
|
+
}, async ({ envelopes_json }) => {
|
|
132
|
+
try {
|
|
133
|
+
return json(reduceJudgmentForMcp(envelopes_json));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return failure(error);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
78
139
|
server.registerTool('hyv_verify', {
|
|
79
|
-
description: 'Verify a revised candidate against an original draft and portable profile
|
|
140
|
+
description: 'Verify a revised candidate against an original draft and portable profile without changing learning state.',
|
|
80
141
|
inputSchema: { original: writing, candidate: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
|
|
81
|
-
annotations: { readOnlyHint:
|
|
142
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
82
143
|
}, async ({ original, candidate, profile_json, writing_brief_json }) => {
|
|
83
144
|
try {
|
|
84
|
-
return json(verifyForMcp(original, candidate, profile_json,
|
|
145
|
+
return json(verifyForMcp(original, candidate, profile_json, writing_brief_json));
|
|
85
146
|
}
|
|
86
147
|
catch (error) {
|
|
87
148
|
return failure(error);
|
|
88
149
|
}
|
|
89
150
|
});
|
|
90
151
|
server.registerTool('hyv_verify_copy_spec', {
|
|
91
|
-
description: 'Verify a candidate against the existing voice gates and a local CopySpec. Immutable claims
|
|
152
|
+
description: 'Verify a candidate against the existing voice gates and a local CopySpec. Immutable claims remain verbatim unless atoms are supplied; then each declared atom must remain. Prohibited claims fail closed.',
|
|
92
153
|
inputSchema: { original: writing, candidate: writing, profile_json: profileJson, copy_spec_json: copySpecJson, writing_brief_json: writingBriefJson.optional() },
|
|
93
|
-
annotations: { readOnlyHint:
|
|
154
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
94
155
|
}, async ({ original, candidate, profile_json, copy_spec_json, writing_brief_json }) => {
|
|
95
156
|
try {
|
|
96
|
-
return json(verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json,
|
|
157
|
+
return json(verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json, writing_brief_json));
|
|
97
158
|
}
|
|
98
159
|
catch (error) {
|
|
99
160
|
return failure(error);
|
|
@@ -116,4 +177,162 @@ server.registerTool('hyv_patterns', {
|
|
|
116
177
|
inputSchema: {},
|
|
117
178
|
annotations: { readOnlyHint: true },
|
|
118
179
|
}, async () => json(patternsForMcp()));
|
|
180
|
+
server.registerTool('hyv_learning_inspect', {
|
|
181
|
+
description: 'Inspect profile-scoped learning receipts without returning stored instruction or draft text.',
|
|
182
|
+
inputSchema: { profile_json: profileJson }, annotations: { readOnlyHint: true },
|
|
183
|
+
}, async ({ profile_json }) => { try {
|
|
184
|
+
return json(inspectLearningForMcp(profile_json));
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
return failure(error);
|
|
188
|
+
} });
|
|
189
|
+
server.registerTool('hyv_learning_record', {
|
|
190
|
+
description: 'Record an explicit profile-scoped learning instruction with authority and provenance metadata.',
|
|
191
|
+
inputSchema: { profile_json: profileJson, instruction: z.string().min(1).max(240), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
192
|
+
}, async (args) => { try {
|
|
193
|
+
return json(recordLearningForMcp(args.profile_json, args.instruction, learningArgs(args)));
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return failure(error);
|
|
197
|
+
} });
|
|
198
|
+
server.registerTool('hyv_learning_ratify', {
|
|
199
|
+
description: 'Ratify a learning event for a Profile v3 revision.',
|
|
200
|
+
inputSchema: { profile_json: profileJson, event_id: z.string().min(1).max(200), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
201
|
+
}, async (args) => { try {
|
|
202
|
+
return json(ratifyLearningForMcp(args.profile_json, args.event_id, learningArgs(args)));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return failure(error);
|
|
206
|
+
} });
|
|
207
|
+
server.registerTool('hyv_learning_supersede', {
|
|
208
|
+
description: 'Supersede a learning event for a Profile v3 revision.',
|
|
209
|
+
inputSchema: { profile_json: profileJson, event_id: z.string().min(1).max(200), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
210
|
+
}, async (args) => { try {
|
|
211
|
+
return json(supersedeLearningForMcp(args.profile_json, args.event_id, learningArgs(args)));
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
return failure(error);
|
|
215
|
+
} });
|
|
216
|
+
server.registerTool('hyv_learning_migrate', {
|
|
217
|
+
description: 'Migrate Profile v2 learning into a Profile v3 identity.',
|
|
218
|
+
inputSchema: { source_profile_json: profileJson, target_profile_json: profileJson, ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
219
|
+
}, async (args) => { try {
|
|
220
|
+
return json(migrateLearningForMcp(args.source_profile_json, args.target_profile_json, learningArgs(args)));
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return failure(error);
|
|
224
|
+
} });
|
|
225
|
+
server.registerTool('hyv_learning_clear', {
|
|
226
|
+
description: 'Delete all local learning state for a profile.',
|
|
227
|
+
inputSchema: { profile_json: profileJson }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
228
|
+
}, async ({ profile_json }) => { try {
|
|
229
|
+
return json(clearLearningForMcp(profile_json));
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
return failure(error);
|
|
233
|
+
} });
|
|
234
|
+
server.registerTool('hyv_lifecycle_prepare_semantic', {
|
|
235
|
+
description: 'Prepare a normal semantic-review task and its initial immutable lifecycle artifact.',
|
|
236
|
+
inputSchema: { deterministic_json: lifecycleJson, binding_json: lifecycleJson, receipt_json: lifecycleJson, policy: z.literal('normal'), allowed_violations: z.array(semanticViolation).max(5) },
|
|
237
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
238
|
+
}, async (args) => { try {
|
|
239
|
+
return json(prepareLifecycleForMcp(args.deterministic_json, args.binding_json, args.receipt_json, args.policy, args.allowed_violations));
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return failure(error);
|
|
243
|
+
} });
|
|
244
|
+
server.registerTool('hyv_lifecycle_submit_verdict', {
|
|
245
|
+
description: 'Submit one normal-policy semantic verdict using the server-installed evaluator authorization context.',
|
|
246
|
+
inputSchema: { artifact_json: lifecycleJson, task_json: lifecycleJson, evaluator_id: evaluatorId, verdict_json: lifecycleJson },
|
|
247
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
248
|
+
}, async (args) => { try {
|
|
249
|
+
return lifecycleResult(submitSemanticVerdictForMcp(args.artifact_json, args.task_json, args.evaluator_id, args.verdict_json, loadApprovalContext()));
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return failure(error);
|
|
253
|
+
} });
|
|
254
|
+
server.registerTool('hyv_lifecycle_inspect', {
|
|
255
|
+
description: 'Validate and inspect an immutable lifecycle artifact without exposing bound source or candidate hashes.',
|
|
256
|
+
inputSchema: { artifact_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
257
|
+
}, async ({ artifact_json }) => { try {
|
|
258
|
+
return json(inspectLifecycleForMcp(artifact_json));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return failure(error);
|
|
262
|
+
} });
|
|
263
|
+
if (redactsSensitiveInputs) {
|
|
264
|
+
server.registerTool('hyv_lifecycle_finalize', {
|
|
265
|
+
description: 'Finalize an authorized human approval or rejection. Capability input requires host-guaranteed sensitive-input redaction.',
|
|
266
|
+
inputSchema: { artifact_json: lifecycleJson, decision_json: lifecycleJson, capability_json: lifecycleJson.optional() },
|
|
267
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
268
|
+
}, async (args) => { try {
|
|
269
|
+
return lifecycleResult(finalizeLifecycleForMcp(args.artifact_json, args.decision_json, loadApprovalContext(), args.capability_json));
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return failure(new Error('Lifecycle finalization failed.'));
|
|
273
|
+
} });
|
|
274
|
+
server.registerTool('hyv_lifecycle_validate_final_approval', {
|
|
275
|
+
description: 'Validate a final-approval capability against the server-installed trust context.',
|
|
276
|
+
inputSchema: { artifact_json: lifecycleJson, capability_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
277
|
+
}, async (args) => { try {
|
|
278
|
+
return json(validateFinalApprovalForMcp(args.artifact_json, args.capability_json, loadApprovalContext()));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return failure(new Error('Capability validation failed.'));
|
|
282
|
+
} });
|
|
283
|
+
server.registerTool('hyv_learning_record_approved', {
|
|
284
|
+
description: 'Record one approval-revalidated, deterministic, text-free learning event.',
|
|
285
|
+
inputSchema: { ready_json: lifecycleJson, approved_json: lifecycleJson, original: approvedLearningText, candidate: approvedLearningText, profile_json: profileJson, decision_json: lifecycleJson, capability_json: lifecycleJson, copy_spec_json: copySpecJson.optional(), writing_brief_json: writingBriefJson.optional() },
|
|
286
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
|
|
287
|
+
}, async (args) => { try {
|
|
288
|
+
return json({ status: recordApprovedLearningForMcp({ readyJson: args.ready_json, approvedJson: args.approved_json, source: args.original, candidate: args.candidate, profileJson: args.profile_json, decisionJson: args.decision_json, capabilityJson: args.capability_json, context: loadApprovalContext(), copySpecJson: args.copy_spec_json, writingBriefJson: args.writing_brief_json }) });
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return failure(new Error('Approved learning was not authorized.'));
|
|
292
|
+
} });
|
|
293
|
+
server.registerTool('hyv_prepare_rebuild', {
|
|
294
|
+
description: 'Prepare a rebuild task only after an upstream REBUILD recommendation, CopySpec, and signed rebuild-authorization capability. Capability input requires host-guaranteed sensitive-input redaction.',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
draft: writing,
|
|
297
|
+
profile_json: profileJson,
|
|
298
|
+
reduction_json: lifecycleJson,
|
|
299
|
+
copy_spec_json: copySpecJson,
|
|
300
|
+
capability_json: lifecycleJson,
|
|
301
|
+
writing_brief_json: writingBriefJson.optional(),
|
|
302
|
+
},
|
|
303
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
304
|
+
}, async (args) => {
|
|
305
|
+
try {
|
|
306
|
+
return json(prepareRebuildForMcp(args.draft, args.profile_json, args.reduction_json, args.copy_spec_json, args.capability_json, loadApprovalContext(), args.writing_brief_json));
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return failure(new Error('Rebuild preparation failed.'));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
server.registerTool('hyv_apply_rebuild', {
|
|
313
|
+
description: 'Validate and evaluate a whole-document rebuild response against a prepared authorized rebuild task. Capability input requires host-guaranteed sensitive-input redaction. It never calls a provider.',
|
|
314
|
+
inputSchema: { task_json: lifecycleJson, response_json: z.string().min(1).max(100_000), profile_json: profileJson, capability_json: lifecycleJson },
|
|
315
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
316
|
+
}, async (args) => {
|
|
317
|
+
try {
|
|
318
|
+
return json(applyRebuildForMcp(args.task_json, args.response_json, args.profile_json, args.capability_json, loadApprovalContext()));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return failure(new Error('Rebuild application failed.'));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
server.registerTool('hyv_lifecycle_finalize', {
|
|
327
|
+
description: 'Record an authorized human rejection. Approval is unavailable because this host does not guarantee sensitive-input redaction.',
|
|
328
|
+
inputSchema: { artifact_json: lifecycleJson, decision_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
329
|
+
}, async (args) => {
|
|
330
|
+
try {
|
|
331
|
+
return lifecycleResult(finalizeRejectionForMcp(args.artifact_json, args.decision_json, loadApprovalContext()));
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return failure(new Error('Only rejection is available without sensitive-input redaction.'));
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
119
338
|
await server.connect(new StdioServerTransport());
|