@holdyourvoice/hyv 3.4.0 → 3.4.2

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.
@@ -67,6 +67,110 @@ export function extractFactClaims(draft) {
67
67
  }
68
68
  return output;
69
69
  }
70
+ function capabilityFinding(claim, relevant, evidenceItems) {
71
+ const claimCapabilities = capabilityObjects(claim.text);
72
+ if (!claimCapabilities.length || !relevant.length)
73
+ return undefined;
74
+ const sourceCapabilities = relevant.flatMap(({ text }) => capabilityObjects(text));
75
+ const supported = claimCapabilities.every((capability) => sourceCapabilities.some((sourceCapability) => capability.every((token) => sourceCapability.includes(token))));
76
+ if (supported && relevant.some(({ text }) => negated(text) !== negated(claim.text))) {
77
+ return finding(claim, 'capability_drift', 'error', 'The draft reverses the source capability.', evidenceItems, 'high', 'Match the source capability polarity or cite contrary evidence.');
78
+ }
79
+ if (supported)
80
+ return undefined;
81
+ const formatMismatch = claimCapabilities.some((capability) => {
82
+ const claimFormats = capability.filter((token) => CAPABILITY_FORMATS.has(token));
83
+ return claimFormats.length > 0 && sourceCapabilities.some((sourceCapability) => {
84
+ const sourceFormats = sourceCapability.filter((token) => CAPABILITY_FORMATS.has(token));
85
+ return sourceFormats.length > 0 && claimFormats.some((format) => !sourceFormats.includes(format));
86
+ });
87
+ });
88
+ if (formatMismatch) {
89
+ return finding(claim, 'capability_drift', 'error', 'The product capability differs from the supplied source.', evidenceItems, 'high', 'Match the source capability or cite contrary evidence.');
90
+ }
91
+ return finding(claim, 'missing_evidence', 'needs_human_review', 'The product capability is not established by the relevant source wording.', evidenceItems, 'medium', 'Confirm the capability with a reviewer or add evidence.');
92
+ }
93
+ function findingsForClaim(claim, input, sourceLines, semanticAdapter) {
94
+ const relevant = findRelevant(claim.text, sourceLines);
95
+ const same = sourceLines.find(({ text }) => hasOverlap(claim.text, text));
96
+ const evidenceItems = relevant.length ? relevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : fallbackEvidence(sourceLines);
97
+ const quote = claim.text.match(QUOTE)?.[1];
98
+ const sourceHasAttribution = input.sources.some((source) => /\b(said|according to|reported)\b/i.test(source.text));
99
+ if (quote && sourceHasAttribution && !input.sources.some((source) => source.text.includes(quote))) {
100
+ const quoteRelevant = sourceLines.filter(({ text }) => /\b(said|according to|reported)\b/i.test(text));
101
+ const quoteEvidence = quoteRelevant.length ? quoteRelevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : input.sources.slice(0, 1).map((source) => evidence(source, source.text));
102
+ return [finding(claim, 'quote_drift', 'error', 'The quoted wording differs from the supplied source.', quoteEvidence, 'high', 'Use the source wording or label the text as a paraphrase.')];
103
+ }
104
+ const claimDates = dates(claim.text);
105
+ const sourceDates = relevant.flatMap(({ text }) => dates(text));
106
+ const claimNumbers = numbers(claim.text);
107
+ const sourceNumbers = relevant.flatMap(({ text }) => numbers(text));
108
+ if (!claimDates.length && !claim.kinds.includes('attribution_quote') && claimNumbers.length && sourceNumbers.length && claimNumbers.some((number) => !sourceNumbers.some((sourceNumber) => normal(sourceNumber) === normal(number)))) {
109
+ return [finding(claim, 'number_drift', 'error', 'A number or unit differs from relevant source evidence.', evidenceItems, 'high', 'Correct the number or unit, or cite a newer source.')];
110
+ }
111
+ if (claimDates.length && sourceDates.length && claimDates.some((date) => !sourceDates.includes(date))) {
112
+ return [finding(claim, 'date_drift', 'error', 'The draft date differs from relevant source evidence.', evidenceItems, 'high', 'Correct the date or cite a newer source.')];
113
+ }
114
+ const claimEntities = entities(claim.text);
115
+ const sourceEntities = relevant.flatMap(({ text }) => entities(text));
116
+ if (claimEntities.length && sourceEntities.length && claimEntities.some((entity) => !sourceEntities.some((sourceEntity) => normal(sourceEntity) === normal(entity)))) {
117
+ return [finding(claim, 'entity_drift', 'error', 'A named entity differs from relevant source evidence.', evidenceItems, 'high', 'Correct the name or cite the source that supports it.')];
118
+ }
119
+ const capability = capabilityFinding(claim, relevant, evidenceItems);
120
+ if (capability)
121
+ return [capability];
122
+ if (capabilityObjects(claim.text).length && relevant.length)
123
+ return [];
124
+ const overreach = [];
125
+ if (claim.kinds.includes('causal') && relevant.length && !relevant.some(({ text }) => /\b(caused?|because|led to|resulted in)\b/i.test(text))) {
126
+ overreach.push(finding(claim, 'causal_overreach', 'warning', 'The sources describe an outcome but do not establish causation.', evidenceItems, 'medium', 'Use an association claim or add causal evidence.'));
127
+ }
128
+ if (claim.kinds.includes('comparative') && relevant.length && !relevant.some(({ text }) => /\b(better|more|less|than|best|largest|fastest)\b/i.test(text))) {
129
+ overreach.push(finding(claim, 'comparative_overreach', 'warning', 'The sources do not establish the comparison.', evidenceItems, 'medium', 'Narrow the comparison or add comparative evidence.'));
130
+ }
131
+ if (overreach.length)
132
+ return overreach;
133
+ if (claimDates.some((date) => sourceDates.includes(date)) || same)
134
+ return [];
135
+ if (!relevant.length && claim.kinds.includes('fact') && tokens(claim.text).length <= 4) {
136
+ return [finding(claim, 'missing_evidence', 'needs_human_review', 'No close source evidence was found; the wording is too sparse for a reliable deterministic verdict.', evidenceItems, 'low', 'Confirm with a reviewer or provide a source.')];
137
+ }
138
+ if (!relevant.length || (claim.kinds.includes('number') && !relevant.some(({ text }) => /\b\d+(?:\.\d+)?%?\b/.test(text)))) {
139
+ return [finding(claim, 'unsupported_claim', 'error', 'No supplied source supports this checkable claim.', evidenceItems, 'medium', 'Add a source, remove the claim, or mark it as an approved hypothesis.')];
140
+ }
141
+ const semantic = semanticAdapter?.compare({ claim: claim.text, sources: input.sources });
142
+ if (semantic === 'supported')
143
+ return [];
144
+ if (semantic === 'contradicted') {
145
+ return [finding(claim, 'semantic_contradiction', 'error', 'The configured semantic adapter found contradictory source evidence.', evidenceItems, 'medium', 'Review the cited sources and correct or qualify the claim.')];
146
+ }
147
+ return [finding(claim, 'missing_evidence', 'needs_human_review', 'Relevant source material exists, but deterministic matching could not establish support.', evidenceItems, 'low', 'Review the source context or enable an approved semantic adapter.')];
148
+ }
149
+ function draftContradictions(claims, sourceLines) {
150
+ const normalized = claims.map((claim) => ({ claim, text: normal(claim.text) }));
151
+ const findings = [];
152
+ for (let index = 0; index < normalized.length; index += 1) {
153
+ for (let other = index + 1; other < normalized.length; other += 1) {
154
+ const left = normalized[index];
155
+ const right = normalized[other];
156
+ const leftCore = normal(left.text.replace(/\bnot\b/g, ''));
157
+ const rightCore = normal(right.text.replace(/\bnot\b/g, ''));
158
+ if (leftCore === rightCore && /\bnot\b/.test(left.text) !== /\bnot\b/.test(right.text)) {
159
+ findings.push(finding(right.claim, 'draft_contradiction', 'error', 'This draft claim contradicts an earlier draft claim.', fallbackEvidence(sourceLines), 'high', 'Resolve the two claims before publishing.'));
160
+ }
161
+ }
162
+ }
163
+ return findings;
164
+ }
165
+ function summarizeFindings(claims, findings) {
166
+ const claimKey = (item) => `${item.draftLocation.start}:${item.draftLocation.end}`;
167
+ const unsupported = new Set(findings.filter((item) => item.kind === 'unsupported_claim').map(claimKey)).size;
168
+ const contradicted = new Set(findings.filter((item) => ['draft_contradiction', 'number_drift', 'date_drift', 'entity_drift', 'quote_drift', 'capability_drift', 'semantic_contradiction'].includes(item.kind)).map(claimKey)).size;
169
+ const humanReview = new Set(findings.filter((item) => item.severity === 'needs_human_review' || item.severity === 'warning').map(claimKey)).size;
170
+ const checked = claims.filter((claim) => !claim.kinds.includes('opinion')).length;
171
+ const affected = new Set(findings.map(claimKey)).size;
172
+ return { checked, supported: Math.max(0, checked - affected), unsupported, contradicted, humanReview };
173
+ }
70
174
  export function lintFacts(input) {
71
175
  if (!input.sources.length)
72
176
  throw new Error('Fact lint requires at least one source document.');
@@ -81,105 +185,10 @@ export function lintFacts(input) {
81
185
  for (const claim of claims) {
82
186
  if (claim.kinds.includes('opinion') || allowed.has(normal(claim.text)) || (claim.kinds.includes('hypothesis') && approved.has(normal(claim.text))))
83
187
  continue;
84
- const relevant = findRelevant(claim.text, sourceLines);
85
- const same = sourceLines.find(({ text }) => hasOverlap(claim.text, text));
86
- const evidenceItems = relevant.length ? relevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : fallbackEvidence(sourceLines);
87
- const quote = claim.text.match(QUOTE)?.[1];
88
- const quoteRelevant = sourceLines.filter(({ text }) => /\b(said|according to|reported)\b/i.test(text));
89
- if (quote && input.sources.some((source) => /\b(said|according to|reported)\b/i.test(source.text)) && !input.sources.some((source) => source.text.includes(quote))) {
90
- const quoteEvidence = quoteRelevant.length ? quoteRelevant.slice(0, 2).map(({ source, text }) => evidence(source, text)) : input.sources.slice(0, 1).map((source) => evidence(source, source.text));
91
- findings.push(finding(claim, 'quote_drift', 'error', 'The quoted wording differs from the supplied source.', quoteEvidence, 'high', 'Use the source wording or label the text as a paraphrase.'));
92
- continue;
93
- }
94
- const claimDates = dates(claim.text);
95
- const sourceDates = relevant.flatMap(({ text }) => dates(text));
96
- const claimNumbers = numbers(claim.text);
97
- const sourceNumbers = relevant.flatMap(({ text }) => numbers(text));
98
- if (!claimDates.length && !claim.kinds.includes('attribution_quote') && claimNumbers.length && sourceNumbers.length && claimNumbers.some((number) => !sourceNumbers.some((sourceNumber) => normal(sourceNumber) === normal(number)))) {
99
- findings.push(finding(claim, 'number_drift', 'error', 'A number or unit differs from relevant source evidence.', evidenceItems, 'high', 'Correct the number or unit, or cite a newer source.'));
100
- continue;
101
- }
102
- if (claimDates.length && sourceDates.length && claimDates.some((date) => !sourceDates.includes(date))) {
103
- findings.push(finding(claim, 'date_drift', 'error', 'The draft date differs from relevant source evidence.', evidenceItems, 'high', 'Correct the date or cite a newer source.'));
104
- continue;
105
- }
106
- const claimEntities = entities(claim.text);
107
- const sourceEntities = relevant.flatMap(({ text }) => entities(text));
108
- if (claimEntities.length && sourceEntities.length && claimEntities.some((entity) => !sourceEntities.some((sourceEntity) => normal(sourceEntity) === normal(entity)))) {
109
- findings.push(finding(claim, 'entity_drift', 'error', 'A named entity differs from relevant source evidence.', evidenceItems, 'high', 'Correct the name or cite the source that supports it.'));
110
- continue;
111
- }
112
- const claimCapabilities = capabilityObjects(claim.text);
113
- if (claimCapabilities.length && relevant.length) {
114
- const sourceCapabilities = relevant.flatMap(({ text }) => capabilityObjects(text));
115
- const supportedCapability = claimCapabilities.every((capability) => sourceCapabilities.some((sourceCapability) => capability.every((token) => sourceCapability.includes(token))));
116
- if (supportedCapability && relevant.some(({ text }) => negated(text) !== negated(claim.text))) {
117
- findings.push(finding(claim, 'capability_drift', 'error', 'The draft reverses the source capability.', evidenceItems, 'high', 'Match the source capability polarity or cite contrary evidence.'));
118
- continue;
119
- }
120
- if (supportedCapability)
121
- continue;
122
- const knownFormatMismatch = claimCapabilities.some((capability) => {
123
- const claimFormats = capability.filter((token) => CAPABILITY_FORMATS.has(token));
124
- return claimFormats.length > 0 && sourceCapabilities.some((sourceCapability) => {
125
- const sourceFormats = sourceCapability.filter((token) => CAPABILITY_FORMATS.has(token));
126
- return sourceFormats.length > 0 && claimFormats.some((format) => !sourceFormats.includes(format));
127
- });
128
- });
129
- if (knownFormatMismatch) {
130
- findings.push(finding(claim, 'capability_drift', 'error', 'The product capability differs from the supplied source.', evidenceItems, 'high', 'Match the source capability or cite contrary evidence.'));
131
- continue;
132
- }
133
- findings.push(finding(claim, 'missing_evidence', 'needs_human_review', 'The product capability is not established by the relevant source wording.', evidenceItems, 'medium', 'Confirm the capability with a reviewer or add evidence.'));
134
- continue;
135
- }
136
- const causalOverreach = claim.kinds.includes('causal') && relevant.length && !relevant.some(({ text }) => /\b(caused?|because|led to|resulted in)\b/i.test(text));
137
- const comparativeOverreach = claim.kinds.includes('comparative') && relevant.length && !relevant.some(({ text }) => /\b(better|more|less|than|best|largest|fastest)\b/i.test(text));
138
- if (causalOverreach)
139
- findings.push(finding(claim, 'causal_overreach', 'warning', 'The sources describe an outcome but do not establish causation.', evidenceItems, 'medium', 'Use an association claim or add causal evidence.'));
140
- if (comparativeOverreach)
141
- findings.push(finding(claim, 'comparative_overreach', 'warning', 'The sources do not establish the comparison.', evidenceItems, 'medium', 'Narrow the comparison or add comparative evidence.'));
142
- if (causalOverreach || comparativeOverreach)
143
- continue;
144
- if (claimDates.length && claimDates.some((date) => sourceDates.includes(date)))
145
- continue;
146
- if (same)
147
- continue;
148
- if (!relevant.length && claim.kinds.includes('fact') && tokens(claim.text).length <= 4) {
149
- findings.push(finding(claim, 'missing_evidence', 'needs_human_review', 'No close source evidence was found; the wording is too sparse for a reliable deterministic verdict.', evidenceItems, 'low', 'Confirm with a reviewer or provide a source.'));
150
- continue;
151
- }
152
- if ((!relevant.length || (claim.kinds.includes('number') && !relevant.some(({ text }) => /\b\d+(?:\.\d+)?%?\b/.test(text))))) {
153
- findings.push(finding(claim, 'unsupported_claim', 'error', 'No supplied source supports this checkable claim.', evidenceItems, 'medium', 'Add a source, remove the claim, or mark it as an approved hypothesis.'));
154
- continue;
155
- }
156
- const semantic = semanticAdapter?.compare({ claim: claim.text, sources: input.sources });
157
- if (semantic === 'supported')
158
- continue;
159
- if (semantic === 'contradicted') {
160
- findings.push(finding(claim, 'semantic_contradiction', 'error', 'The configured semantic adapter found contradictory source evidence.', evidenceItems, 'medium', 'Review the cited sources and correct or qualify the claim.'));
161
- continue;
162
- }
163
- findings.push(finding(claim, 'missing_evidence', 'needs_human_review', 'Relevant source material exists, but deterministic matching could not establish support.', evidenceItems, 'low', 'Review the source context or enable an approved semantic adapter.'));
188
+ findings.push(...findingsForClaim(claim, input, sourceLines, semanticAdapter));
164
189
  }
165
- const normalizedClaims = claims.map((claim) => ({ claim, text: normal(claim.text) }));
166
- for (let index = 0; index < normalizedClaims.length; index += 1)
167
- for (let other = index + 1; other < normalizedClaims.length; other += 1) {
168
- const left = normalizedClaims[index];
169
- const right = normalizedClaims[other];
170
- const leftCore = normal(left.text.replace(/\bnot\b/g, ''));
171
- const rightCore = normal(right.text.replace(/\bnot\b/g, ''));
172
- if (leftCore === rightCore && /\bnot\b/.test(left.text) !== /\bnot\b/.test(right.text)) {
173
- findings.push(finding(right.claim, 'draft_contradiction', 'error', 'This draft claim contradicts an earlier draft claim.', fallbackEvidence(sourceLines), 'high', 'Resolve the two claims before publishing.'));
174
- }
175
- }
176
- const claimKey = (item) => `${item.draftLocation.start}:${item.draftLocation.end}`;
177
- const unsupported = new Set(findings.filter((item) => item.kind === 'unsupported_claim').map(claimKey)).size;
178
- const contradicted = new Set(findings.filter((item) => ['draft_contradiction', 'number_drift', 'date_drift', 'entity_drift', 'quote_drift', 'capability_drift', 'semantic_contradiction'].includes(item.kind)).map(claimKey)).size;
179
- const humanReview = new Set(findings.filter((item) => item.severity === 'needs_human_review' || item.severity === 'warning').map(claimKey)).size;
180
- const checked = claims.filter((claim) => !claim.kinds.includes('opinion')).length;
181
- const affected = new Set(findings.map(claimKey)).size;
182
- return { version: '1', summary: { checked, supported: Math.max(0, checked - affected), unsupported, contradicted, humanReview }, claims, findings, skippedChecks: semanticAdapter ? [] : ['semantic_matching'] };
190
+ findings.push(...draftContradictions(claims, sourceLines));
191
+ return { version: '1', summary: summarizeFindings(claims, findings), claims, findings, skippedChecks: semanticAdapter ? [] : ['semantic_matching'] };
183
192
  }
184
193
  export function formatFactLintReport(report) {
185
194
  const lines = [`fact lint: ${report.summary.checked} checked, ${report.summary.supported} supported, ${report.findings.length} findings`];
package/dist/mcp.js CHANGED
@@ -35,31 +35,33 @@ function failure(error) {
35
35
  function lifecycleResult(result) {
36
36
  return json(result.ok ? result.artifact : { error: result.error });
37
37
  }
38
+ function guardedJson(run, publicError) {
39
+ try {
40
+ return json(run());
41
+ }
42
+ catch (error) {
43
+ return failure(publicError ? new Error(publicError) : error);
44
+ }
45
+ }
46
+ function guardedLifecycle(run, publicError) {
47
+ try {
48
+ return lifecycleResult(run());
49
+ }
50
+ catch (error) {
51
+ return failure(publicError ? new Error(publicError) : error);
52
+ }
53
+ }
38
54
  const server = new McpServer({ name: 'hold-your-voice', version: HYV_VERSION });
39
55
  server.registerTool('hyv_build_profile', {
40
56
  description: 'Build a portable VoiceDNA profile from at least two writing samples. The samples stay in memory and are not saved.',
41
57
  inputSchema: { samples, avoid },
42
58
  annotations: { readOnlyHint: true },
43
- }, async ({ samples: writingSamples, avoid: phrases }) => {
44
- try {
45
- return json(buildProfileForMcp(writingSamples, phrases));
46
- }
47
- catch (error) {
48
- return failure(error);
49
- }
50
- });
59
+ }, async ({ samples: writingSamples, avoid: phrases }) => guardedJson(() => buildProfileForMcp(writingSamples, phrases)));
51
60
  server.registerTool('hyv_analyze', {
52
61
  description: 'Run separate VoiceDNA and AI Editor checks plus a non-scoring Unicode hygiene inspection against a draft using a portable profile JSON string.',
53
62
  inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
54
63
  annotations: { readOnlyHint: true },
55
- }, async ({ draft, profile_json, writing_brief_json }) => {
56
- try {
57
- return json(analyzeForMcp(draft, profile_json, writing_brief_json));
58
- }
59
- catch (error) {
60
- return failure(error);
61
- }
62
- });
64
+ }, async ({ draft, profile_json, writing_brief_json }) => guardedJson(() => analyzeForMcp(draft, profile_json, writing_brief_json)));
63
65
  server.registerTool('hyv_hygiene', {
64
66
  description: 'Inspect text for zero-width characters, bidirectional controls, Unicode tag characters, and unusual spaces without changing it or requiring a voice profile.',
65
67
  inputSchema: { draft: hygieneText },
@@ -82,50 +84,22 @@ server.registerTool('hyv_logic_lint', {
82
84
  description: 'Run the deterministic document-coherence gate. It detects configured topic drift, unanchored inference, and direct internal contradictions; it does not verify facts or approve publication.',
83
85
  inputSchema: { draft: writing, writing_brief_json: writingBriefJson.optional() },
84
86
  annotations: { readOnlyHint: true },
85
- }, async ({ draft, writing_brief_json }) => {
86
- try {
87
- return json(logicLintForMcp(draft, writing_brief_json));
88
- }
89
- catch (error) {
90
- return failure(error);
91
- }
92
- });
87
+ }, async ({ draft, writing_brief_json }) => guardedJson(() => logicLintForMcp(draft, writing_brief_json)));
93
88
  server.registerTool('hyv_rewrite_prompt', {
94
89
  description: 'Create a constrained editing brief. It does not rewrite the draft or call a model.',
95
90
  inputSchema: { draft: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
96
91
  annotations: { readOnlyHint: true },
97
- }, async ({ draft, profile_json, writing_brief_json }) => {
98
- try {
99
- return json(rewritePromptForMcp(draft, profile_json, {}, writing_brief_json));
100
- }
101
- catch (error) {
102
- return failure(error);
103
- }
104
- });
92
+ }, async ({ draft, profile_json, writing_brief_json }) => guardedJson(() => rewritePromptForMcp(draft, profile_json, {}, writing_brief_json)));
105
93
  server.registerTool('hyv_prepare_rewrite', {
106
94
  description: 'Prepare a local, versioned rewrite task. The caller may forward it to a provider; doing so shares the draft and must be an explicit choice.',
107
95
  inputSchema: { draft: writing, profile_json: profileJson, copy_spec_json: copySpecJson.optional(), writing_brief_json: writingBriefJson.optional() },
108
96
  annotations: { readOnlyHint: true },
109
- }, async ({ draft, profile_json, copy_spec_json, writing_brief_json }) => {
110
- try {
111
- return json(prepareRewriteForMcp(draft, profile_json, copy_spec_json, writing_brief_json));
112
- }
113
- catch (error) {
114
- return failure(error);
115
- }
116
- });
97
+ }, async ({ draft, profile_json, copy_spec_json, writing_brief_json }) => guardedJson(() => prepareRewriteForMcp(draft, profile_json, copy_spec_json, writing_brief_json)));
117
98
  server.registerTool('hyv_apply_rewrite', {
118
99
  description: 'Validate and apply a model response to a prepared task, then run the local gates. It never calls a provider or stores source or candidate text.',
119
100
  inputSchema: { task_json: z.string().min(1).max(250_000), response_json: z.string().min(1).max(100_000), profile_json: profileJson },
120
101
  annotations: { readOnlyHint: true },
121
- }, async ({ task_json, response_json, profile_json }) => {
122
- try {
123
- return json(applyRewriteForMcp(task_json, response_json, profile_json));
124
- }
125
- catch (error) {
126
- return failure(error);
127
- }
128
- });
102
+ }, async ({ task_json, response_json, profile_json }) => guardedJson(() => applyRewriteForMcp(task_json, response_json, profile_json)));
129
103
  server.registerTool('hyv_prepare_judgment', {
130
104
  description: 'Prepare a versioned pre-edit or post-candidate judgment task. It does not call a model.',
131
105
  inputSchema: {
@@ -136,62 +110,27 @@ server.registerTool('hyv_prepare_judgment', {
136
110
  candidate: writing.optional(),
137
111
  },
138
112
  annotations: { readOnlyHint: true },
139
- }, async ({ stage, kind, draft, profile_json, candidate }) => {
140
- try {
141
- return json(prepareJudgmentForMcp(stage, kind, draft, profile_json, candidate));
142
- }
143
- catch (error) {
144
- return failure(error);
145
- }
146
- });
113
+ }, async ({ stage, kind, draft, profile_json, candidate }) => guardedJson(() => prepareJudgmentForMcp(stage, kind, draft, profile_json, candidate)));
147
114
  server.registerTool('hyv_reduce_judgment', {
148
115
  description: 'Reduce bound judgment envelopes into SHIP, EDIT, REBUILD, CLEAR, or ESCALATE. It does not call a model.',
149
116
  inputSchema: { envelopes_json: z.string().min(1).max(250_000) },
150
117
  annotations: { readOnlyHint: true },
151
- }, async ({ envelopes_json }) => {
152
- try {
153
- return json(reduceJudgmentForMcp(envelopes_json));
154
- }
155
- catch (error) {
156
- return failure(error);
157
- }
158
- });
118
+ }, async ({ envelopes_json }) => guardedJson(() => reduceJudgmentForMcp(envelopes_json)));
159
119
  server.registerTool('hyv_verify', {
160
120
  description: 'Verify a revised candidate against an original draft and portable profile without changing learning state.',
161
121
  inputSchema: { original: writing, candidate: writing, profile_json: profileJson, writing_brief_json: writingBriefJson.optional() },
162
122
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
163
- }, async ({ original, candidate, profile_json, writing_brief_json }) => {
164
- try {
165
- return json(verifyForMcp(original, candidate, profile_json, writing_brief_json));
166
- }
167
- catch (error) {
168
- return failure(error);
169
- }
170
- });
123
+ }, async ({ original, candidate, profile_json, writing_brief_json }) => guardedJson(() => verifyForMcp(original, candidate, profile_json, writing_brief_json)));
171
124
  server.registerTool('hyv_verify_copy_spec', {
172
125
  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.',
173
126
  inputSchema: { original: writing, candidate: writing, profile_json: profileJson, copy_spec_json: copySpecJson, writing_brief_json: writingBriefJson.optional() },
174
127
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
175
- }, async ({ original, candidate, profile_json, copy_spec_json, writing_brief_json }) => {
176
- try {
177
- return json(verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json, writing_brief_json));
178
- }
179
- catch (error) {
180
- return failure(error);
181
- }
182
- });
128
+ }, async ({ original, candidate, profile_json, copy_spec_json, writing_brief_json }) => guardedJson(() => verifyCopySpecForMcp(original, candidate, profile_json, copy_spec_json, writing_brief_json)));
183
129
  server.registerTool('hyv_batch_analyze', {
184
130
  description: 'Inspect two to one hundred drafts for repeated opening and closing sentences. It returns advisory batch findings and does not store the drafts.',
185
131
  inputSchema: { drafts: z.array(writing).min(2).max(100) },
186
132
  annotations: { readOnlyHint: true },
187
- }, async ({ drafts }) => {
188
- try {
189
- return json(analyzeBatchForMcp(drafts));
190
- }
191
- catch (error) {
192
- return failure(error);
193
- }
194
- });
133
+ }, async ({ drafts }) => guardedJson(() => analyzeBatchForMcp(drafts)));
195
134
  server.registerTool('hyv_patterns', {
196
135
  description: 'List the exact AI Editor rules that run in this extension.',
197
136
  inputSchema: {},
@@ -200,116 +139,56 @@ server.registerTool('hyv_patterns', {
200
139
  server.registerTool('hyv_learning_inspect', {
201
140
  description: 'Inspect profile-scoped learning receipts without returning stored instruction or draft text.',
202
141
  inputSchema: { profile_json: profileJson }, annotations: { readOnlyHint: true },
203
- }, async ({ profile_json }) => { try {
204
- return json(inspectLearningForMcp(profile_json));
205
- }
206
- catch (error) {
207
- return failure(error);
208
- } });
142
+ }, async ({ profile_json }) => guardedJson(() => inspectLearningForMcp(profile_json)));
209
143
  server.registerTool('hyv_learning_record', {
210
144
  description: 'Record an explicit profile-scoped learning instruction with authority and provenance metadata.',
211
145
  inputSchema: { profile_json: profileJson, instruction: z.string().min(1).max(240), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
212
- }, async (args) => { try {
213
- return json(recordLearningForMcp(args.profile_json, args.instruction, learningArgs(args)));
214
- }
215
- catch (error) {
216
- return failure(error);
217
- } });
146
+ }, async (args) => guardedJson(() => recordLearningForMcp(args.profile_json, args.instruction, learningArgs(args))));
218
147
  server.registerTool('hyv_learning_ratify', {
219
148
  description: 'Ratify a learning event for a Profile v3 revision.',
220
149
  inputSchema: { profile_json: profileJson, event_id: z.string().min(1).max(200), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
221
- }, async (args) => { try {
222
- return json(ratifyLearningForMcp(args.profile_json, args.event_id, learningArgs(args)));
223
- }
224
- catch (error) {
225
- return failure(error);
226
- } });
150
+ }, async (args) => guardedJson(() => ratifyLearningForMcp(args.profile_json, args.event_id, learningArgs(args))));
227
151
  server.registerTool('hyv_learning_supersede', {
228
152
  description: 'Supersede a learning event for a Profile v3 revision.',
229
153
  inputSchema: { profile_json: profileJson, event_id: z.string().min(1).max(200), ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
230
- }, async (args) => { try {
231
- return json(supersedeLearningForMcp(args.profile_json, args.event_id, learningArgs(args)));
232
- }
233
- catch (error) {
234
- return failure(error);
235
- } });
154
+ }, async (args) => guardedJson(() => supersedeLearningForMcp(args.profile_json, args.event_id, learningArgs(args))));
236
155
  server.registerTool('hyv_learning_migrate', {
237
156
  description: 'Migrate Profile v2 learning into a Profile v3 identity.',
238
157
  inputSchema: { source_profile_json: profileJson, target_profile_json: profileJson, ...learningOptions }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
239
- }, async (args) => { try {
240
- return json(migrateLearningForMcp(args.source_profile_json, args.target_profile_json, learningArgs(args)));
241
- }
242
- catch (error) {
243
- return failure(error);
244
- } });
158
+ }, async (args) => guardedJson(() => migrateLearningForMcp(args.source_profile_json, args.target_profile_json, learningArgs(args))));
245
159
  server.registerTool('hyv_learning_clear', {
246
160
  description: 'Delete all local learning state for a profile.',
247
161
  inputSchema: { profile_json: profileJson }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
248
- }, async ({ profile_json }) => { try {
249
- return json(clearLearningForMcp(profile_json));
250
- }
251
- catch (error) {
252
- return failure(error);
253
- } });
162
+ }, async ({ profile_json }) => guardedJson(() => clearLearningForMcp(profile_json)));
254
163
  server.registerTool('hyv_lifecycle_prepare_semantic', {
255
164
  description: 'Prepare a normal semantic-review task and its initial immutable lifecycle artifact.',
256
165
  inputSchema: { deterministic_json: lifecycleJson, binding_json: lifecycleJson, receipt_json: lifecycleJson, policy: z.literal('normal'), allowed_violations: z.array(semanticViolation).max(5) },
257
166
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
258
- }, async (args) => { try {
259
- return json(prepareLifecycleForMcp(args.deterministic_json, args.binding_json, args.receipt_json, args.policy, args.allowed_violations));
260
- }
261
- catch (error) {
262
- return failure(error);
263
- } });
167
+ }, async (args) => guardedJson(() => prepareLifecycleForMcp(args.deterministic_json, args.binding_json, args.receipt_json, args.policy, args.allowed_violations)));
264
168
  server.registerTool('hyv_lifecycle_submit_verdict', {
265
169
  description: 'Submit one normal-policy semantic verdict using the server-installed evaluator authorization context.',
266
170
  inputSchema: { artifact_json: lifecycleJson, task_json: lifecycleJson, evaluator_id: evaluatorId, verdict_json: lifecycleJson },
267
171
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
268
- }, async (args) => { try {
269
- return lifecycleResult(submitSemanticVerdictForMcp(args.artifact_json, args.task_json, args.evaluator_id, args.verdict_json, loadApprovalContext()));
270
- }
271
- catch (error) {
272
- return failure(error);
273
- } });
172
+ }, async (args) => guardedLifecycle(() => submitSemanticVerdictForMcp(args.artifact_json, args.task_json, args.evaluator_id, args.verdict_json, loadApprovalContext())));
274
173
  server.registerTool('hyv_lifecycle_inspect', {
275
174
  description: 'Validate and inspect an immutable lifecycle artifact without exposing bound source or candidate hashes.',
276
175
  inputSchema: { artifact_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
277
- }, async ({ artifact_json }) => { try {
278
- return json(inspectLifecycleForMcp(artifact_json));
279
- }
280
- catch (error) {
281
- return failure(error);
282
- } });
176
+ }, async ({ artifact_json }) => guardedJson(() => inspectLifecycleForMcp(artifact_json)));
283
177
  if (redactsSensitiveInputs) {
284
178
  server.registerTool('hyv_lifecycle_finalize', {
285
179
  description: 'Finalize an authorized human approval or rejection. Capability input requires host-guaranteed sensitive-input redaction.',
286
180
  inputSchema: { artifact_json: lifecycleJson, decision_json: lifecycleJson, capability_json: lifecycleJson.optional() },
287
181
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
288
- }, async (args) => { try {
289
- return lifecycleResult(finalizeLifecycleForMcp(args.artifact_json, args.decision_json, loadApprovalContext(), args.capability_json));
290
- }
291
- catch {
292
- return failure(new Error('Lifecycle finalization failed.'));
293
- } });
182
+ }, async (args) => guardedLifecycle(() => finalizeLifecycleForMcp(args.artifact_json, args.decision_json, loadApprovalContext(), args.capability_json), 'Lifecycle finalization failed.'));
294
183
  server.registerTool('hyv_lifecycle_validate_final_approval', {
295
184
  description: 'Validate a final-approval capability against the server-installed trust context.',
296
185
  inputSchema: { artifact_json: lifecycleJson, capability_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
297
- }, async (args) => { try {
298
- return json(validateFinalApprovalForMcp(args.artifact_json, args.capability_json, loadApprovalContext()));
299
- }
300
- catch {
301
- return failure(new Error('Capability validation failed.'));
302
- } });
186
+ }, async (args) => guardedJson(() => validateFinalApprovalForMcp(args.artifact_json, args.capability_json, loadApprovalContext()), 'Capability validation failed.'));
303
187
  server.registerTool('hyv_learning_record_approved', {
304
188
  description: 'Record one approval-revalidated, deterministic, text-free learning event.',
305
189
  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() },
306
190
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
307
- }, async (args) => { try {
308
- 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 }) });
309
- }
310
- catch {
311
- return failure(new Error('Approved learning was not authorized.'));
312
- } });
191
+ }, async (args) => guardedJson(() => ({ 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 }) }), 'Approved learning was not authorized.'));
313
192
  server.registerTool('hyv_prepare_rebuild', {
314
193
  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.',
315
194
  inputSchema: {
@@ -322,49 +201,21 @@ if (redactsSensitiveInputs) {
322
201
  recomposition_policy_json: lifecycleJson.optional(),
323
202
  },
324
203
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
325
- }, async (args) => {
326
- try {
327
- return json(prepareRebuildForMcp(args.draft, args.profile_json, args.reduction_json, args.copy_spec_json, args.capability_json, loadApprovalContext(), args.writing_brief_json, args.recomposition_policy_json));
328
- }
329
- catch {
330
- return failure(new Error('Rebuild preparation failed.'));
331
- }
332
- });
204
+ }, async (args) => guardedJson(() => prepareRebuildForMcp(args.draft, args.profile_json, args.reduction_json, args.copy_spec_json, args.capability_json, loadApprovalContext(), args.writing_brief_json, args.recomposition_policy_json), 'Rebuild preparation failed.'));
333
205
  server.registerTool('hyv_apply_rebuild', {
334
206
  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.',
335
207
  inputSchema: { task_json: lifecycleJson, response_json: z.string().min(1).max(100_000), profile_json: profileJson, capability_json: lifecycleJson },
336
208
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
337
- }, async (args) => {
338
- try {
339
- return json(applyRebuildForMcp(args.task_json, args.response_json, args.profile_json, args.capability_json, loadApprovalContext()));
340
- }
341
- catch {
342
- return failure(new Error('Rebuild application failed.'));
343
- }
344
- });
209
+ }, async (args) => guardedJson(() => applyRebuildForMcp(args.task_json, args.response_json, args.profile_json, args.capability_json, loadApprovalContext()), 'Rebuild application failed.'));
345
210
  server.registerTool('hyv_rebuild_writer_request', {
346
211
  description: 'Create the writer-only payload for a prepared rebuild. It excludes source draft, capability, profile body, and validation evidence.',
347
212
  inputSchema: { task_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
348
- }, async ({ task_json }) => {
349
- try {
350
- return json(rebuildWriterRequestForMcp(task_json));
351
- }
352
- catch {
353
- return failure(new Error('Writer request could not be prepared.'));
354
- }
355
- });
213
+ }, async ({ task_json }) => guardedJson(() => rebuildWriterRequestForMcp(task_json), 'Writer request could not be prepared.'));
356
214
  }
357
215
  else {
358
216
  server.registerTool('hyv_lifecycle_finalize', {
359
217
  description: 'Record an authorized human rejection. Approval is unavailable because this host does not guarantee sensitive-input redaction.',
360
218
  inputSchema: { artifact_json: lifecycleJson, decision_json: lifecycleJson }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
361
- }, async (args) => {
362
- try {
363
- return lifecycleResult(finalizeRejectionForMcp(args.artifact_json, args.decision_json, loadApprovalContext()));
364
- }
365
- catch {
366
- return failure(new Error('Only rejection is available without sensitive-input redaction.'));
367
- }
368
- });
219
+ }, async (args) => guardedLifecycle(() => finalizeRejectionForMcp(args.artifact_json, args.decision_json, loadApprovalContext()), 'Only rejection is available without sensitive-input redaction.'));
369
220
  }
370
221
  await server.connect(new StdioServerTransport());
@@ -174,7 +174,7 @@ test('CLI and MCP rebuild helpers share fingerprints', () => {
174
174
  version: '1', audience: 'operators', intent: 'explain', format: 'outreach',
175
175
  });
176
176
  assert.match(briefTask.prompt, /# WritingBrief/);
177
- assert.equal(HYV_VERSION, '3.4.0');
177
+ assert.equal(HYV_VERSION, '3.4.2');
178
178
  });
179
179
  test('apply rejects forged tasks, missing capability, and substituted profiles', () => {
180
180
  const reduction = rebuildRecommendation();