@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.
- package/Readme.md +70 -127
- package/dist/cli.js +365 -327
- package/dist/fact-linter.js +107 -98
- package/dist/mcp.js +43 -192
- package/dist/rebuild-task.test.js +1 -1
- package/dist/stage1-evaluation.js +74 -48
- package/dist/text.js +8 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/fact-linter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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 }) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 }) =>
|
|
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) =>
|
|
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) =>
|
|
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 }) =>
|
|
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) =>
|
|
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) =>
|
|
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) => {
|
|
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.
|
|
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();
|