@holdyourvoice/hyv 3.6.0 → 3.6.1
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 +1 -57
- package/dist/ai-editor.js +4 -20
- package/dist/cli/agents.js +108 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/context.test.js +55 -0
- package/dist/cli/io.js +137 -0
- package/dist/cli/lifecycle.js +162 -0
- package/dist/cli/profiles.js +246 -0
- package/dist/cli/rewriting.js +111 -0
- package/dist/cli.js +5 -974
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/learning.js +16 -9
- package/dist/mcp-tools.js +8 -17
- package/dist/mcp.js +5 -11
- package/dist/mirror-refs.test.js +4 -4
- package/dist/pipeline.js +16 -27
- package/dist/rebuild-task.js +4 -27
- package/dist/rebuild-task.test.js +1 -1
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +8 -34
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
Binary file
|
package/dist/learning.js
CHANGED
|
@@ -170,6 +170,9 @@ function receipt(profile, event, status) {
|
|
|
170
170
|
const publicMetadata = { mutationId: event.mutationId, eventId: event.eventId, profileId: identity(profile), profileRevision: revision(profile), status, timestamp: event.timestamp };
|
|
171
171
|
return { version: '1', ...publicMetadata, digest: digest(publicMetadata) };
|
|
172
172
|
}
|
|
173
|
+
function requestDigest(event) {
|
|
174
|
+
return digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
175
|
+
}
|
|
173
176
|
function eventBase(profile, kind, options) {
|
|
174
177
|
const mutationId = options.mutationId ?? randomUUID();
|
|
175
178
|
const base = {
|
|
@@ -177,7 +180,7 @@ function eventBase(profile, kind, options) {
|
|
|
177
180
|
profileRevision: revision(profile), revisionDigest: revisionDigest(profile), authority: options.authority ?? 'team',
|
|
178
181
|
provenance: options.provenance ?? 'local', weight: options.weight ?? 1, compatibility: options.compatibility ?? 'same-or-newer', kind,
|
|
179
182
|
};
|
|
180
|
-
return { ...base, requestDigest:
|
|
183
|
+
return { ...base, requestDigest: requestDigest(base) };
|
|
181
184
|
}
|
|
182
185
|
function mutate(profile, event, options, validate) {
|
|
183
186
|
try {
|
|
@@ -225,7 +228,7 @@ export function recordVerifiedCandidate(profile, verification, candidate, option
|
|
|
225
228
|
if (readEvents(profile, options).some((event) => event.kind === 'verified_candidate' && event.outcome === outcome))
|
|
226
229
|
return 'nothing_to_learn';
|
|
227
230
|
const event = { ...eventBase(profile, 'verified_candidate', { ...options, mutationId: options.mutationId ?? outcome }), resolved: resolved.slice(0, MAX_RESOLVED_FINDINGS), outcome };
|
|
228
|
-
event.requestDigest =
|
|
231
|
+
event.requestDigest = requestDigest(event);
|
|
229
232
|
const result = mutate(profile, event, options);
|
|
230
233
|
return result.status === 'recorded' ? 'recorded' : result.status === 'already_recorded' ? 'nothing_to_learn' : 'write_failed';
|
|
231
234
|
}
|
|
@@ -236,19 +239,23 @@ export function recordLearningInstruction(profile, instruction, options = {}) {
|
|
|
236
239
|
if (normalized.length > MAX_INSTRUCTION_CHARACTERS)
|
|
237
240
|
throw new Error(`Learning instructions must be ${MAX_INSTRUCTION_CHARACTERS} characters or fewer.`);
|
|
238
241
|
const event = { ...eventBase(profile, 'instruction', options), instruction: normalized };
|
|
239
|
-
event.requestDigest =
|
|
242
|
+
event.requestDigest = requestDigest(event);
|
|
240
243
|
return mutate(profile, event, options);
|
|
241
244
|
}
|
|
242
245
|
export function addLearningInstruction(profile, instruction, options = {}) { return recordLearningInstruction(profile, instruction, options).status === 'recorded'; }
|
|
246
|
+
function recordControlEvent(profile, kind, targetEventId, options) {
|
|
247
|
+
const event = { ...eventBase(profile, kind, options), targetEventId };
|
|
248
|
+
event.requestDigest = requestDigest(event);
|
|
249
|
+
return mutate(profile, event, options, (events) => {
|
|
250
|
+
const target = events.find((item) => item.eventId === targetEventId);
|
|
251
|
+
return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
243
254
|
export function ratifyLearningEvent(profile, targetEventId, options = {}) {
|
|
244
|
-
|
|
245
|
-
event.requestDigest = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
246
|
-
return mutate(profile, event, options, (events) => { const target = events.find((item) => item.eventId === targetEventId); return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined; });
|
|
255
|
+
return recordControlEvent(profile, 'ratification', targetEventId, options);
|
|
247
256
|
}
|
|
248
257
|
export function supersedeLearningEvent(profile, targetEventId, options = {}) {
|
|
249
|
-
|
|
250
|
-
event.requestDigest = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
|
|
251
|
-
return mutate(profile, event, options, (events) => { const target = events.find((item) => item.eventId === targetEventId); return !target ? 'not_found' : authorityRank[event.authority] < authorityRank[target.authority] ? 'unauthorized' : undefined; });
|
|
258
|
+
return recordControlEvent(profile, 'supersession', targetEventId, options);
|
|
252
259
|
}
|
|
253
260
|
export function migrateLearningV2ToV3(source, target, options = {}) {
|
|
254
261
|
const sourceFingerprint = profileFingerprint(source);
|
package/dist/mcp-tools.js
CHANGED
|
@@ -21,14 +21,17 @@ import { scoreHeldoutProfile } from './profile-score.js';
|
|
|
21
21
|
import { findWritingExamples } from './writing-examples.js';
|
|
22
22
|
import { evaluateIsolatedBacktest } from './backtest.js';
|
|
23
23
|
import { evaluateLocalComposite } from './local-eval.js';
|
|
24
|
-
function
|
|
24
|
+
function parseDocument(text, parse, label) {
|
|
25
25
|
try {
|
|
26
|
-
return
|
|
26
|
+
return parse(JSON.parse(text));
|
|
27
27
|
}
|
|
28
28
|
catch (error) {
|
|
29
|
-
throw new Error(error instanceof Error ? error.message :
|
|
29
|
+
throw new Error(error instanceof Error ? error.message : `${label} is not valid JSON.`);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
function profileFromJson(profileJson) {
|
|
33
|
+
return parseDocument(profileJson, parseProfile, 'Profile');
|
|
34
|
+
}
|
|
32
35
|
function profileV3FromJson(profileJson) {
|
|
33
36
|
const profile = profileFromJson(profileJson);
|
|
34
37
|
if (profile.version !== '3')
|
|
@@ -36,22 +39,10 @@ function profileV3FromJson(profileJson) {
|
|
|
36
39
|
return profile;
|
|
37
40
|
}
|
|
38
41
|
function copySpecFromJson(copySpecJson) {
|
|
39
|
-
|
|
40
|
-
return parseCopySpec(JSON.parse(copySpecJson));
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
throw new Error(error instanceof Error ? error.message : 'CopySpec is not valid JSON.');
|
|
44
|
-
}
|
|
42
|
+
return parseDocument(copySpecJson, parseCopySpec, 'CopySpec');
|
|
45
43
|
}
|
|
46
44
|
function writingBriefFromJson(writingBriefJson) {
|
|
47
|
-
|
|
48
|
-
return undefined;
|
|
49
|
-
try {
|
|
50
|
-
return parseWritingBrief(JSON.parse(writingBriefJson));
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
throw new Error(error instanceof Error ? error.message : 'WritingBrief is not valid JSON.');
|
|
54
|
-
}
|
|
45
|
+
return writingBriefJson ? parseDocument(writingBriefJson, parseWritingBrief, 'WritingBrief') : undefined;
|
|
55
46
|
}
|
|
56
47
|
export function buildProfileForMcp(samples, avoid = []) {
|
|
57
48
|
return buildProfile(samples, avoid);
|
package/dist/mcp.js
CHANGED
|
@@ -12,7 +12,6 @@ const profileJson = z.string().min(1).max(50_000);
|
|
|
12
12
|
const copySpecJson = z.string().min(1).max(250_000);
|
|
13
13
|
const writingBriefJson = z.string().min(1).max(50_000);
|
|
14
14
|
const samples = z.array(writing).min(2).max(20);
|
|
15
|
-
const strictSamples = z.array(writing).min(2).max(20);
|
|
16
15
|
const heldoutSamples = z.array(writing).min(3).max(20);
|
|
17
16
|
const evalParagraphs = z.array(z.object({ paragraph_id: z.string().min(1).max(160), text: writing })).min(2).max(50);
|
|
18
17
|
const writingExamples = z.array(z.object({ basename: z.string().min(1).max(160), text: writing })).min(1).max(64);
|
|
@@ -39,9 +38,6 @@ function json(value) {
|
|
|
39
38
|
function failure(error) {
|
|
40
39
|
return { content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }], isError: true };
|
|
41
40
|
}
|
|
42
|
-
function lifecycleResult(result) {
|
|
43
|
-
return json(result.ok ? result.artifact : { error: result.error });
|
|
44
|
-
}
|
|
45
41
|
function guardedJson(run, publicError) {
|
|
46
42
|
try {
|
|
47
43
|
return json(run());
|
|
@@ -51,12 +47,10 @@ function guardedJson(run, publicError) {
|
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
49
|
function guardedLifecycle(run, publicError) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return failure(publicError ? new Error(publicError) : error);
|
|
59
|
-
}
|
|
50
|
+
return guardedJson(() => {
|
|
51
|
+
const result = run();
|
|
52
|
+
return result.ok ? result.artifact : { error: result.error };
|
|
53
|
+
}, publicError);
|
|
60
54
|
}
|
|
61
55
|
const server = new McpServer({ name: 'hold-your-voice', version: HYV_VERSION });
|
|
62
56
|
server.registerTool('hyv_build_profile', {
|
|
@@ -75,7 +69,7 @@ server.registerTool('hyv_analyze', {
|
|
|
75
69
|
}, async ({ draft, profile_json, writing_brief_json }) => guardedJson(() => analyzeForMcp(draft, profile_json, writing_brief_json)));
|
|
76
70
|
server.registerTool('hyv_strict_check', {
|
|
77
71
|
description: 'Run the calibrated local strict-quality gate. It requires a Profile v3 and local writing samples; it returns strict-ready, needs-human-review, or blocked without changing text or learning state.',
|
|
78
|
-
inputSchema: { draft: writing, profile_json: profileJson, samples
|
|
72
|
+
inputSchema: { draft: writing, profile_json: profileJson, samples, writing_brief_json: writingBriefJson.optional() },
|
|
79
73
|
annotations: { readOnlyHint: true },
|
|
80
74
|
}, async ({ draft, profile_json, samples: localSamples, writing_brief_json }) => guardedJson(() => strictCheckForMcp(draft, profile_json, localSamples, writing_brief_json)));
|
|
81
75
|
server.registerTool('hyv_score', {
|
package/dist/mirror-refs.test.js
CHANGED
|
@@ -23,7 +23,7 @@ test('reconciles branch and tag creates, rewrites, and deletions', () => {
|
|
|
23
23
|
git(root, 'clone', '--quiet', source, checkout);
|
|
24
24
|
git(checkout, 'config', 'user.name', 'Mirror Test');
|
|
25
25
|
git(checkout, 'config', 'user.email', 'mirror-test@example.invalid');
|
|
26
|
-
git(checkout, 'switch', '--quiet', '-c', 'main');
|
|
26
|
+
git(checkout, 'switch', '--quiet', '-c', 'fixture/main');
|
|
27
27
|
writeFileSync(join(checkout, 'main.txt'), 'first\n');
|
|
28
28
|
git(checkout, 'add', 'main.txt');
|
|
29
29
|
git(checkout, 'commit', '--quiet', '-m', 'first');
|
|
@@ -34,20 +34,20 @@ test('reconciles branch and tag creates, rewrites, and deletions', () => {
|
|
|
34
34
|
git(checkout, 'tag', 'v1.0.0');
|
|
35
35
|
git(checkout, 'push', '--quiet', '--all', 'origin');
|
|
36
36
|
git(checkout, 'push', '--quiet', '--tags', 'origin');
|
|
37
|
-
git(source, 'symbolic-ref', 'HEAD', 'refs/heads/main');
|
|
37
|
+
git(source, 'symbolic-ref', 'HEAD', 'refs/heads/fixture/main');
|
|
38
38
|
git(checkout, 'remote', 'set-head', 'origin', '-a');
|
|
39
39
|
git(checkout, 'remote', 'add', 'mirror', mirror);
|
|
40
40
|
execFileSync(process.execPath, [mirrorScript], { cwd: checkout, stdio: 'pipe' });
|
|
41
41
|
assert.deepEqual(refs(mirror), refs(source));
|
|
42
42
|
assert.equal(refs(mirror).some((ref) => ref.endsWith('refs/heads/HEAD')), false);
|
|
43
|
-
git(checkout, 'switch', '--quiet', 'main');
|
|
43
|
+
git(checkout, 'switch', '--quiet', 'fixture/main');
|
|
44
44
|
writeFileSync(join(checkout, 'main.txt'), 'rewritten\n');
|
|
45
45
|
git(checkout, 'add', 'main.txt');
|
|
46
46
|
git(checkout, 'commit', '--quiet', '--amend', '-m', 'rewritten main');
|
|
47
47
|
git(checkout, 'branch', '-D', 'feature/test');
|
|
48
48
|
git(checkout, 'tag', '-d', 'v1.0.0');
|
|
49
49
|
git(checkout, 'tag', 'v2.0.0');
|
|
50
|
-
git(checkout, 'push', '--quiet', '--force', 'origin', 'main');
|
|
50
|
+
git(checkout, 'push', '--quiet', '--force', 'origin', 'fixture/main');
|
|
51
51
|
git(checkout, 'push', '--quiet', 'origin', '--delete', 'feature/test');
|
|
52
52
|
git(checkout, 'push', '--quiet', 'origin', ':refs/tags/v1.0.0');
|
|
53
53
|
git(checkout, 'push', '--quiet', 'origin', 'v2.0.0');
|
package/dist/pipeline.js
CHANGED
|
@@ -32,11 +32,14 @@ export function isBlockingFinding(finding) {
|
|
|
32
32
|
export function isStrictFinding(finding) {
|
|
33
33
|
return finding.engine === 'ai_editor' || isBlockingFinding(finding);
|
|
34
34
|
}
|
|
35
|
+
function analysisFindings(result) {
|
|
36
|
+
return [...result.voiceDna.findings, ...result.aiEditor.findings, ...(result.editorial?.findings ?? [])];
|
|
37
|
+
}
|
|
35
38
|
export function strictFindings(result) {
|
|
36
|
-
return
|
|
39
|
+
return analysisFindings(result).filter(isStrictFinding);
|
|
37
40
|
}
|
|
38
41
|
export function deriveEditScope(result, strict = false) {
|
|
39
|
-
const findings =
|
|
42
|
+
const findings = analysisFindings(result);
|
|
40
43
|
const blocking = findings.filter(strict ? isStrictFinding : isBlockingFinding);
|
|
41
44
|
const pendingJudgment = strict ? [] : findings.filter((finding) => finding.appliedPolicy === 'judgment-required');
|
|
42
45
|
return {
|
|
@@ -46,7 +49,7 @@ export function deriveEditScope(result, strict = false) {
|
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
export function renderRewritePrompt(draft, profile, result, learning = [], brief, examples = []) {
|
|
49
|
-
const allFindings =
|
|
52
|
+
const allFindings = analysisFindings(result);
|
|
50
53
|
const scope = deriveEditScope(result, true);
|
|
51
54
|
const redFindings = scope.blocking;
|
|
52
55
|
const yellowFindings = allFindings.filter((finding) => !isStrictFinding(finding) && finding.appliedPolicy !== 'judgment-required');
|
|
@@ -93,8 +96,8 @@ export function rewritePrompt(draft, profile, learning = [], brief, examples = [
|
|
|
93
96
|
function compareCandidates(original, candidate, profile, brief) {
|
|
94
97
|
const baseline = analyze(original, profile, brief);
|
|
95
98
|
const checked = analyze(candidate, profile, brief);
|
|
96
|
-
const baselineFindings =
|
|
97
|
-
const checkedFindings =
|
|
99
|
+
const baselineFindings = analysisFindings(baseline);
|
|
100
|
+
const checkedFindings = analysisFindings(checked);
|
|
98
101
|
const known = new Set(baselineFindings.map((finding) => `${finding.engine}:${finding.id}:${finding.sentence}`));
|
|
99
102
|
const regressions = checkedFindings.filter((finding) => !known.has(`${finding.engine}:${finding.id}:${finding.sentence}`));
|
|
100
103
|
const preservation = legacySetPreservation(original, candidate).score;
|
|
@@ -120,8 +123,9 @@ function verifyRequiredFacts(candidate, brief) {
|
|
|
120
123
|
return result;
|
|
121
124
|
return { ...result, passed: false, failures: [...result.failures, ...reversed.map((fact) => ({ id: fact.id, code: 'missing_immutable_claim', message: `Required fact ${fact.id} is negated or denied.`, evidence: 'WritingBrief required fact.' }))] };
|
|
122
125
|
}
|
|
123
|
-
|
|
126
|
+
function verifyCandidate(original, candidate, profile, brief, rebuild) {
|
|
124
127
|
const { baseline, checked, regressions, preservation } = compareCandidates(original, candidate, profile, brief);
|
|
128
|
+
const claims = rebuild ? verifyClaims(candidate, rebuild.copySpec) : undefined;
|
|
125
129
|
const finalOutput = finalOutputCheck(candidate);
|
|
126
130
|
const logicLint = lintLogic(candidate, brief);
|
|
127
131
|
const factLint = brief?.factSources?.length ? lintFacts({ sources: brief.factSources, draft: candidate, metadata: brief.factMetadata }) : undefined;
|
|
@@ -134,38 +138,23 @@ export function verify(original, candidate, profile, brief) {
|
|
|
134
138
|
preservationScore: preservation,
|
|
135
139
|
regressions,
|
|
136
140
|
strictFindings: unresolvedStrictFindings,
|
|
141
|
+
...(claims ? { claims } : {}),
|
|
137
142
|
finalOutput,
|
|
138
143
|
logicLint,
|
|
139
144
|
...(factLint ? { factLint } : {}), ...(requiredFacts ? { requiredFacts } : {}),
|
|
140
|
-
passed: checked.passed && unresolvedStrictFindings.length === 0 && !regressions.some(isBlockingFinding) && preservation >= 70 && finalOutput.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
|
|
145
|
+
passed: checked.passed && unresolvedStrictFindings.length === 0 && !regressions.some(isBlockingFinding) && (claims ? claims.passed : preservation >= 70) && finalOutput.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
|
|
141
146
|
};
|
|
142
147
|
}
|
|
148
|
+
export function verify(original, candidate, profile, brief) {
|
|
149
|
+
return verifyCandidate(original, candidate, profile, brief);
|
|
150
|
+
}
|
|
143
151
|
export function verifyWithCopySpec(original, candidate, profile, spec, brief) {
|
|
144
152
|
const verification = verify(original, candidate, profile, brief);
|
|
145
153
|
const claims = verifyClaims(candidate, spec);
|
|
146
154
|
return { ...verification, claims, passed: verification.passed && claims.passed };
|
|
147
155
|
}
|
|
148
156
|
export function verifyRebuildWithCopySpec(original, candidate, profile, spec, brief) {
|
|
149
|
-
|
|
150
|
-
const claims = verifyClaims(candidate, spec);
|
|
151
|
-
const finalCheck = finalOutputCheck(candidate);
|
|
152
|
-
const logicLint = lintLogic(candidate, brief);
|
|
153
|
-
const factLint = brief?.factSources?.length ? lintFacts({ sources: brief.factSources, draft: candidate, metadata: brief.factMetadata }) : undefined;
|
|
154
|
-
const requiredFacts = verifyRequiredFacts(candidate, brief);
|
|
155
|
-
const unresolvedStrictFindings = strictFindings(checked);
|
|
156
|
-
return {
|
|
157
|
-
version: '2',
|
|
158
|
-
original: baseline,
|
|
159
|
-
candidate: checked,
|
|
160
|
-
preservationScore: preservation,
|
|
161
|
-
regressions,
|
|
162
|
-
strictFindings: unresolvedStrictFindings,
|
|
163
|
-
claims,
|
|
164
|
-
finalOutput: finalCheck,
|
|
165
|
-
logicLint,
|
|
166
|
-
...(factLint ? { factLint } : {}), ...(requiredFacts ? { requiredFacts } : {}),
|
|
167
|
-
passed: checked.passed && unresolvedStrictFindings.length === 0 && !regressions.some(isBlockingFinding) && claims.passed && finalCheck.accepted && logicLint.passed && !factLint?.findings.some((finding) => finding.severity === 'error') && (requiredFacts?.passed ?? true),
|
|
168
|
-
};
|
|
157
|
+
return verifyCandidate(original, candidate, profile, brief, { copySpec: spec });
|
|
169
158
|
}
|
|
170
159
|
function projectDeterministicVerificationArtifact(source, candidate, profile, verification, copySpec, writingBrief, verificationKind = 'claims' in verification ? 'copy_spec' : 'standard') {
|
|
171
160
|
const identity = profileIdentity(profile);
|
package/dist/rebuild-task.js
CHANGED
|
@@ -10,26 +10,13 @@ import { HYV_VERSION } from './version.js';
|
|
|
10
10
|
import { buildRecompositionBrief, measureLexicalResidual, parseRecompositionPolicy } from './recomposition.js';
|
|
11
11
|
import { provenanceStatusForRebuild, writerRequestForRebuild } from './provenance-status.js';
|
|
12
12
|
import { fingerprint, profileIdentity, sha256 as digest } from './internal.js';
|
|
13
|
-
|
|
13
|
+
import { failure, parseResponseJson, projectLifecycleBinding } from './rewrite-response.js';
|
|
14
14
|
const MAX_CANDIDATE_CHARACTERS = 100_000;
|
|
15
|
-
function failure(code, message, path) {
|
|
16
|
-
return { code, message, ...(path ? { path } : {}) };
|
|
17
|
-
}
|
|
18
15
|
function isFailure(value) {
|
|
19
16
|
return typeof value === 'object' && value !== null && 'code' in value && 'message' in value;
|
|
20
17
|
}
|
|
21
|
-
function parseJson(value) {
|
|
22
|
-
if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
|
|
23
|
-
return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(value);
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return failure('invalid_json', 'Response must be valid JSON.');
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
18
|
function parseRebuildResponse(value) {
|
|
32
|
-
const raw = typeof value === 'string' ?
|
|
19
|
+
const raw = typeof value === 'string' ? parseResponseJson(value) : value;
|
|
33
20
|
if (isFailure(raw))
|
|
34
21
|
return raw;
|
|
35
22
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
@@ -159,7 +146,7 @@ function rejected(task, raw, failures) {
|
|
|
159
146
|
};
|
|
160
147
|
}
|
|
161
148
|
export function applyRebuildResponse(task, raw) {
|
|
162
|
-
const response = parseRebuildResponse(typeof raw === 'string' ?
|
|
149
|
+
const response = parseRebuildResponse(typeof raw === 'string' ? parseResponseJson(raw) : raw);
|
|
163
150
|
if (isFailure(response))
|
|
164
151
|
return rejected(task, raw, [response]);
|
|
165
152
|
if (response.taskFingerprint !== task.fingerprint) {
|
|
@@ -227,15 +214,5 @@ export function createRebuildLifecycleBinding(task, receipt, deterministic) {
|
|
|
227
214
|
if (!deterministic.passed || receipt.taskFingerprint !== task.fingerprint || receipt.mode !== 'REBUILD' || deterministic.verificationKind !== 'rebuild') {
|
|
228
215
|
throw new Error('Lifecycle binding requires a passed rebuild artifact for this rebuild task.');
|
|
229
216
|
}
|
|
230
|
-
return
|
|
231
|
-
rewriteTaskFingerprint: task.fingerprint,
|
|
232
|
-
rewriteResponseFingerprint: receipt.responseFingerprint,
|
|
233
|
-
deterministicArtifactFingerprint: deterministic.artifactFingerprint,
|
|
234
|
-
sourceHash: deterministic.sourceHash,
|
|
235
|
-
candidateHash: deterministic.candidateHash,
|
|
236
|
-
profileId: deterministic.profileId,
|
|
237
|
-
profileRevisionDigest: deterministic.profileRevisionDigest,
|
|
238
|
-
rulesetVersion: deterministic.rulesetVersion,
|
|
239
|
-
schemaVersion: '1',
|
|
240
|
-
};
|
|
217
|
+
return projectLifecycleBinding(task.fingerprint, receipt, deterministic);
|
|
241
218
|
}
|
|
@@ -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.6.
|
|
177
|
+
assert.equal(HYV_VERSION, '3.6.1');
|
|
178
178
|
});
|
|
179
179
|
test('apply rejects forged tasks, missing capability, and substituted profiles', () => {
|
|
180
180
|
const reduction = rebuildRecommendation();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const MAX_RESPONSE_BYTES = 100_000;
|
|
2
|
+
export function failure(code, message, path) {
|
|
3
|
+
return { code, message, ...(path ? { path } : {}) };
|
|
4
|
+
}
|
|
5
|
+
export function parseResponseJson(value) {
|
|
6
|
+
if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
|
|
7
|
+
return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return failure('invalid_json', 'Response must be valid JSON.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function projectLifecycleBinding(taskFingerprint, receipt, deterministic) {
|
|
16
|
+
return {
|
|
17
|
+
rewriteTaskFingerprint: taskFingerprint,
|
|
18
|
+
rewriteResponseFingerprint: receipt.responseFingerprint,
|
|
19
|
+
deterministicArtifactFingerprint: deterministic.artifactFingerprint,
|
|
20
|
+
sourceHash: deterministic.sourceHash,
|
|
21
|
+
candidateHash: deterministic.candidateHash,
|
|
22
|
+
profileId: deterministic.profileId,
|
|
23
|
+
profileRevisionDigest: deterministic.profileRevisionDigest,
|
|
24
|
+
rulesetVersion: deterministic.rulesetVersion,
|
|
25
|
+
schemaVersion: '1',
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/rewrite-task.js
CHANGED
|
@@ -3,30 +3,14 @@ import { finalOutputCheck, hygieneSourceFindings } from './hygiene.js';
|
|
|
3
3
|
import { analyze, deriveEditScope, renderRewritePrompt, verifyDeterministically } from './pipeline.js';
|
|
4
4
|
import { sentences } from './text.js';
|
|
5
5
|
import { fingerprint } from './internal.js';
|
|
6
|
-
|
|
6
|
+
import { failure, parseResponseJson, projectLifecycleBinding } from './rewrite-response.js';
|
|
7
7
|
const MAX_REPLACEMENTS = 100;
|
|
8
8
|
const MAX_REPLACEMENT_CHARACTERS = 10_000;
|
|
9
|
-
function failure(code, message, path) {
|
|
10
|
-
return { code, message, ...(path ? { path } : {}) };
|
|
11
|
-
}
|
|
12
|
-
function responseFingerprint(response) {
|
|
13
|
-
return fingerprint(response);
|
|
14
|
-
}
|
|
15
|
-
function parseJson(value) {
|
|
16
|
-
if (Buffer.byteLength(value) > MAX_RESPONSE_BYTES)
|
|
17
|
-
return failure('response_too_large', `Response exceeds ${MAX_RESPONSE_BYTES} bytes.`);
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(value);
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return failure('invalid_json', 'Response must be valid JSON.');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
9
|
function isFailure(value) {
|
|
26
10
|
return typeof value === 'object' && value !== null && 'code' in value;
|
|
27
11
|
}
|
|
28
12
|
function parseResponse(value) {
|
|
29
|
-
const raw = typeof value === 'string' ?
|
|
13
|
+
const raw = typeof value === 'string' ? parseResponseJson(value) : value;
|
|
30
14
|
if (isFailure(raw))
|
|
31
15
|
return raw;
|
|
32
16
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
@@ -133,7 +117,7 @@ export function parseRewriteTask(value) {
|
|
|
133
117
|
return task;
|
|
134
118
|
}
|
|
135
119
|
function rejected(task, raw, failures, adapterIds = []) {
|
|
136
|
-
return { status: 'repairable', failures, receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint:
|
|
120
|
+
return { status: 'repairable', failures, receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint: fingerprint(raw), adapterIds, replacementSentenceIds: [] } };
|
|
137
121
|
}
|
|
138
122
|
export function applyShip(task) {
|
|
139
123
|
return {
|
|
@@ -151,7 +135,7 @@ export function applyShip(task) {
|
|
|
151
135
|
};
|
|
152
136
|
}
|
|
153
137
|
export function applyRewriteResponse(task, raw) {
|
|
154
|
-
const source = typeof raw === 'string' ?
|
|
138
|
+
const source = typeof raw === 'string' ? parseResponseJson(raw) : raw;
|
|
155
139
|
const parsed = isFailure(source) ? source : parseResponse(source);
|
|
156
140
|
const fenced = isFailure(parsed) && parsed.code === 'invalid_json' ? repairFencedJson(raw) : { value: source };
|
|
157
141
|
const repaired = isFailure(parsed) && parsed.code === 'invalid_response_shape' ? repairStringifiedReplacements(source) : fenced;
|
|
@@ -163,7 +147,7 @@ export function applyRewriteResponse(task, raw) {
|
|
|
163
147
|
return rejected(task, raw, [failure('task_fingerprint_mismatch', 'Response task fingerprint does not match this task.', 'taskFingerprint')], adapterIds);
|
|
164
148
|
if ('mode' in response && response.mode === 'SHIP') {
|
|
165
149
|
const shipped = applyShip(task);
|
|
166
|
-
return { ...shipped, receipt: { ...shipped.receipt, adapterIds, responseFingerprint:
|
|
150
|
+
return { ...shipped, receipt: { ...shipped.receipt, adapterIds, responseFingerprint: fingerprint(raw) } };
|
|
167
151
|
}
|
|
168
152
|
if (response.version === '2')
|
|
169
153
|
return applyRangeResponse(task, response, raw, adapterIds);
|
|
@@ -189,7 +173,7 @@ export function applyRewriteResponse(task, raw) {
|
|
|
189
173
|
if (replacement !== undefined)
|
|
190
174
|
candidate = `${candidate.slice(0, sentence.start)}${replacement}${candidate.slice(sentence.end)}`;
|
|
191
175
|
}
|
|
192
|
-
return { status: 'accepted', candidate, failures: [], receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint:
|
|
176
|
+
return { status: 'accepted', candidate, failures: [], receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint: fingerprint(raw), adapterIds, replacementSentenceIds: [...seen].sort((left, right) => left - right), mode: 'EDIT' } };
|
|
193
177
|
}
|
|
194
178
|
function applyRangeResponse(task, response, raw, adapterIds) {
|
|
195
179
|
const sentenceMap = new Map(task.sentences.map((sentence) => [sentence.id, sentence]));
|
|
@@ -236,7 +220,7 @@ function applyRangeResponse(task, response, raw, adapterIds) {
|
|
|
236
220
|
receipt: {
|
|
237
221
|
version: '1',
|
|
238
222
|
taskFingerprint: task.fingerprint,
|
|
239
|
-
responseFingerprint:
|
|
223
|
+
responseFingerprint: fingerprint(raw),
|
|
240
224
|
adapterIds,
|
|
241
225
|
operationRanges: response.operations.map((operation) => ({ startSentenceId: operation.startSentenceId, endSentenceId: operation.endSentenceId })),
|
|
242
226
|
mode: 'EDIT',
|
|
@@ -262,15 +246,5 @@ export function evaluateRewriteResponse(task, raw, profile) {
|
|
|
262
246
|
export function createRewriteLifecycleBinding(task, receipt, deterministic) {
|
|
263
247
|
if (!deterministic.passed || receipt.taskFingerprint !== task.fingerprint)
|
|
264
248
|
throw new Error('Lifecycle binding requires a passed deterministic artifact for this rewrite task.');
|
|
265
|
-
return
|
|
266
|
-
rewriteTaskFingerprint: task.fingerprint,
|
|
267
|
-
rewriteResponseFingerprint: receipt.responseFingerprint,
|
|
268
|
-
deterministicArtifactFingerprint: deterministic.artifactFingerprint,
|
|
269
|
-
sourceHash: deterministic.sourceHash,
|
|
270
|
-
candidateHash: deterministic.candidateHash,
|
|
271
|
-
profileId: deterministic.profileId,
|
|
272
|
-
profileRevisionDigest: deterministic.profileRevisionDigest,
|
|
273
|
-
rulesetVersion: deterministic.rulesetVersion,
|
|
274
|
-
schemaVersion: '1',
|
|
275
|
-
};
|
|
249
|
+
return projectLifecycleBinding(task.fingerprint, receipt, deterministic);
|
|
276
250
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const HYV_VERSION = '3.6.
|
|
1
|
+
export const HYV_VERSION = '3.6.1';
|