@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.
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: digest({ ...base, eventId: undefined, timestamp: undefined, requestDigest: undefined }) };
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 = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
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 = digest({ ...event, eventId: undefined, timestamp: undefined, requestDigest: undefined });
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
- const event = { ...eventBase(profile, 'ratification', options), targetEventId };
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
- const event = { ...eventBase(profile, 'supersession', options), targetEventId };
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 profileFromJson(profileJson) {
24
+ function parseDocument(text, parse, label) {
25
25
  try {
26
- return parseProfile(JSON.parse(profileJson));
26
+ return parse(JSON.parse(text));
27
27
  }
28
28
  catch (error) {
29
- throw new Error(error instanceof Error ? error.message : 'Profile is not valid JSON.');
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
- try {
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
- if (!writingBriefJson)
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
- try {
55
- return lifecycleResult(run());
56
- }
57
- catch (error) {
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: strictSamples, writing_brief_json: writingBriefJson.optional() },
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', {
@@ -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 [...result.voiceDna.findings, ...result.aiEditor.findings, ...(result.editorial?.findings ?? [])].filter(isStrictFinding);
39
+ return analysisFindings(result).filter(isStrictFinding);
37
40
  }
38
41
  export function deriveEditScope(result, strict = false) {
39
- const findings = [...result.voiceDna.findings, ...result.aiEditor.findings, ...(result.editorial?.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 = [...result.voiceDna.findings, ...result.aiEditor.findings, ...(result.editorial?.findings ?? [])];
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 = [...baseline.voiceDna.findings, ...baseline.aiEditor.findings, ...(baseline.editorial?.findings ?? [])];
97
- const checkedFindings = [...checked.voiceDna.findings, ...checked.aiEditor.findings, ...(checked.editorial?.findings ?? [])];
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
- export function verify(original, candidate, profile, brief) {
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
- const { baseline, checked, regressions, preservation } = compareCandidates(original, candidate, profile, brief);
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);
@@ -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
- const MAX_RESPONSE_BYTES = 100_000;
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' ? parseJson(value) : value;
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' ? parseJson(raw) : raw);
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.0');
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
+ }
@@ -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
- const MAX_RESPONSE_BYTES = 100_000;
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' ? parseJson(value) : value;
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: responseFingerprint(raw), adapterIds, replacementSentenceIds: [] } };
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' ? parseJson(raw) : raw;
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: responseFingerprint(raw) } };
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: responseFingerprint(raw), adapterIds, replacementSentenceIds: [...seen].sort((left, right) => left - right), mode: 'EDIT' } };
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: responseFingerprint(raw),
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.0';
1
+ export const HYV_VERSION = '3.6.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "3.6.0",
3
+ "version": "3.6.1",
4
4
  "description": "A local-first dual-engine writing gate that protects voice and catches generic AI patterns.",
5
5
  "type": "module",
6
6
  "bin": {