@holdyourvoice/hyv 3.6.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/Readme.md +93 -154
  2. package/dist/ai-editor-rules.js +1 -0
  3. package/dist/ai-editor.js +23 -33
  4. package/dist/analysis.js +33 -0
  5. package/dist/cli/agents.js +88 -0
  6. package/dist/cli/checks.js +226 -0
  7. package/dist/cli/io.js +141 -0
  8. package/dist/cli/lifecycle.js +162 -0
  9. package/dist/cli/profiles.js +246 -0
  10. package/dist/cli/rewriting.js +111 -0
  11. package/dist/cli.js +5 -974
  12. package/dist/copy-spec.js +6 -6
  13. package/dist/editorial-packs.js +4 -3
  14. package/dist/fact-linter.js +45 -27
  15. package/dist/hidden-text.js +6 -6
  16. package/dist/hold-your-voice.mcpb +0 -0
  17. package/dist/internal.js +8 -0
  18. package/dist/judgment-task.js +10 -14
  19. package/dist/learning.js +77 -71
  20. package/dist/lifecycle-adapter.js +2 -5
  21. package/dist/local-eval.js +16 -31
  22. package/dist/mcp-server.js +272 -0
  23. package/dist/mcp-tools.js +10 -19
  24. package/dist/mcp.js +2 -267
  25. package/dist/pipeline.js +18 -105
  26. package/dist/profile-compose.js +18 -11
  27. package/dist/profile-score.js +6 -6
  28. package/dist/profile.js +17 -19
  29. package/dist/provenance-status.js +1 -3
  30. package/dist/rebuild-task.js +18 -52
  31. package/dist/recomposition.js +18 -13
  32. package/dist/rewrite-prompt.js +82 -0
  33. package/dist/rewrite-response.js +27 -0
  34. package/dist/rewrite-task.js +73 -98
  35. package/dist/rule-allowances.js +6 -10
  36. package/dist/semantic-review.js +71 -47
  37. package/dist/stage1-evaluation.js +29 -14
  38. package/dist/strict-quality.js +10 -18
  39. package/dist/version.js +1 -1
  40. package/dist/voice-dna.js +34 -21
  41. package/dist/writing-examples.js +2 -7
  42. package/package.json +6 -3
  43. package/skills/hyv-prepare-judgment/SKILL.md +4 -0
  44. package/dist/agents/catalog.test.js +0 -60
  45. package/dist/agents/emit.test.js +0 -64
  46. package/dist/agents/load.test.js +0 -149
  47. package/dist/ai-editor.test.js +0 -265
  48. package/dist/approval-capability.test.js +0 -52
  49. package/dist/approval-context.test.js +0 -38
  50. package/dist/backtest.test.js +0 -20
  51. package/dist/benchmark.test.js +0 -328
  52. package/dist/canonical-json.test.js +0 -24
  53. package/dist/cli.test.js +0 -731
  54. package/dist/editorial-packs.test.js +0 -94
  55. package/dist/fact-linter.test.js +0 -85
  56. package/dist/hidden-text.test.js +0 -26
  57. package/dist/hygiene.test.js +0 -83
  58. package/dist/judgment-task.test.js +0 -162
  59. package/dist/learning.test.js +0 -325
  60. package/dist/lifecycle-adapter.test.js +0 -56
  61. package/dist/local-eval.test.js +0 -20
  62. package/dist/logic-linter-corpus.test.js +0 -22
  63. package/dist/logic-linter.test.js +0 -39
  64. package/dist/mcp-tools.test.js +0 -286
  65. package/dist/mcp.test.js +0 -312
  66. package/dist/mirror-refs.test.js +0 -63
  67. package/dist/pipeline.test.js +0 -247
  68. package/dist/preservation.test.js +0 -22
  69. package/dist/production-gates.test.js +0 -34
  70. package/dist/profile-compose.test.js +0 -32
  71. package/dist/profile-score.test.js +0 -22
  72. package/dist/profile-watch.test.js +0 -23
  73. package/dist/profile.test.js +0 -141
  74. package/dist/provenance-status.test.js +0 -22
  75. package/dist/rebuild-task.test.js +0 -206
  76. package/dist/recomposition.test.js +0 -34
  77. package/dist/release-audit.test.js +0 -292
  78. package/dist/rewrite-task.test.js +0 -166
  79. package/dist/rule-allowances.test.js +0 -17
  80. package/dist/rule-reconciliation.test.js +0 -50
  81. package/dist/sample-ingest.test.js +0 -52
  82. package/dist/semantic-review.test.js +0 -101
  83. package/dist/stage1-dry-run.test.js +0 -39
  84. package/dist/stage1-evaluation.test.js +0 -184
  85. package/dist/stage1-human-packet.test.js +0 -102
  86. package/dist/stage1-schema-contract.test.js +0 -95
  87. package/dist/stage2-human-packet.test.js +0 -81
  88. package/dist/strict-quality.test.js +0 -62
  89. package/dist/text-provenance.feature.test.js +0 -45
  90. package/dist/text.test.js +0 -16
  91. package/dist/voice-dna.test.js +0 -121
  92. package/dist/writing-examples.test.js +0 -35
@@ -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,69 +3,38 @@ 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
- function parseResponse(value) {
29
- const raw = typeof value === 'string' ? parseJson(value) : value;
30
- if (isFailure(raw))
31
- return raw;
32
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
33
- return failure('invalid_response_shape', 'Response must be an object.');
34
- const response = raw;
35
- if (typeof response.taskFingerprint !== 'string' || response.taskFingerprint.length !== 64)
36
- return failure('invalid_response_shape', 'Response must include the task fingerprint.', 'taskFingerprint');
37
- if (response.mode === 'REBUILD')
38
- return failure('rebuild_response_on_edit_task', 'Rebuild responses cannot satisfy edit tasks.', 'mode');
39
- if (response.mode === 'SHIP' && response.version === '1') {
40
- return { version: '1', mode: 'SHIP', taskFingerprint: response.taskFingerprint };
41
- }
42
- if (response.version === '2') {
43
- if (!Array.isArray(response.operations))
44
- return failure('invalid_response_shape', 'Version 2 responses require an operations array.', 'operations');
45
- if (response.operations.length > MAX_REPLACEMENTS)
46
- return failure('invalid_response_shape', `Response may include at most ${MAX_REPLACEMENTS} replacements.`, 'operations');
47
- for (const [index, operation] of response.operations.entries()) {
48
- if (!operation || typeof operation !== 'object' || !Number.isInteger(operation.startSentenceId) || !Number.isInteger(operation.endSentenceId) || typeof operation.text !== 'string') {
49
- return failure('invalid_response_shape', 'Every operation requires integer startSentenceId, endSentenceId, and string text.', `operations[${index}]`);
50
- }
51
- if (operation.endSentenceId < operation.startSentenceId)
52
- return failure('noncontiguous_range', 'A range must be inclusive and contiguous.', `operations[${index}]`);
53
- if (operation.text.length > MAX_REPLACEMENT_CHARACTERS)
54
- return failure('invalid_replacement_text', `Replacement text must contain at most ${MAX_REPLACEMENT_CHARACTERS} characters.`, `operations[${index}].text`);
12
+ function parseRangeResponse(response) {
13
+ if (!Array.isArray(response.operations))
14
+ return failure('invalid_response_shape', 'Version 2 responses require an operations array.', 'operations');
15
+ if (response.operations.length > MAX_REPLACEMENTS)
16
+ return failure('invalid_response_shape', `Response may include at most ${MAX_REPLACEMENTS} replacements.`, 'operations');
17
+ for (const [index, operation] of response.operations.entries()) {
18
+ if (!operation || typeof operation !== 'object' || !Number.isInteger(operation.startSentenceId) || !Number.isInteger(operation.endSentenceId) || typeof operation.text !== 'string') {
19
+ return failure('invalid_response_shape', 'Every operation requires integer startSentenceId, endSentenceId, and string text.', `operations[${index}]`);
55
20
  }
56
- if (response.hygieneOperations !== undefined) {
57
- if (!Array.isArray(response.hygieneOperations))
58
- return failure('invalid_response_shape', 'Hygiene operations must be an array.', 'hygieneOperations');
59
- for (const [index, operation] of response.hygieneOperations.entries()) {
60
- if (!operation || !Number.isInteger(operation.start) || !Number.isInteger(operation.end) || typeof operation.text !== 'string' || operation.end < operation.start) {
61
- return failure('invalid_response_shape', 'Every hygiene operation requires integer start, end, and string text.', `hygieneOperations[${index}]`);
62
- }
21
+ if (operation.endSentenceId < operation.startSentenceId)
22
+ return failure('noncontiguous_range', 'A range must be inclusive and contiguous.', `operations[${index}]`);
23
+ if (operation.text.length > MAX_REPLACEMENT_CHARACTERS)
24
+ return failure('invalid_replacement_text', `Replacement text must contain at most ${MAX_REPLACEMENT_CHARACTERS} characters.`, `operations[${index}].text`);
25
+ }
26
+ if (response.hygieneOperations !== undefined) {
27
+ if (!Array.isArray(response.hygieneOperations))
28
+ return failure('invalid_response_shape', 'Hygiene operations must be an array.', 'hygieneOperations');
29
+ for (const [index, operation] of response.hygieneOperations.entries()) {
30
+ if (!operation || !Number.isInteger(operation.start) || !Number.isInteger(operation.end) || typeof operation.text !== 'string' || operation.end < operation.start) {
31
+ return failure('invalid_response_shape', 'Every hygiene operation requires integer start, end, and string text.', `hygieneOperations[${index}]`);
63
32
  }
64
33
  }
65
- return response;
66
34
  }
67
- if (response.version !== '1')
68
- return failure('invalid_response_version', 'Response version must be "1" or "2".', 'version');
35
+ return response;
36
+ }
37
+ function parseReplacements(response) {
69
38
  if (!Array.isArray(response.replacements))
70
39
  return failure('invalid_response_shape', 'Response replacements must be an array.', 'replacements');
71
40
  if (response.replacements.length > MAX_REPLACEMENTS)
@@ -80,27 +49,48 @@ function parseResponse(value) {
80
49
  }
81
50
  return response;
82
51
  }
83
- function repairStringifiedReplacements(value) {
84
- if (!value || typeof value !== 'object' || Array.isArray(value))
85
- return { value };
86
- const raw = value;
87
- if (typeof raw.replacements !== 'string')
88
- return { value };
89
- try {
90
- const replacements = JSON.parse(raw.replacements);
91
- if (!Array.isArray(replacements))
92
- return { value };
93
- return { value: { ...raw, replacements }, adapterId: 'stringified_replacements_v1' };
94
- }
95
- catch {
96
- return { value };
97
- }
52
+ function parseResponse(value) {
53
+ const raw = typeof value === 'string' ? parseResponseJson(value) : value;
54
+ if (isFailure(raw))
55
+ return raw;
56
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
57
+ return failure('invalid_response_shape', 'Response must be an object.');
58
+ const response = raw;
59
+ if (typeof response.taskFingerprint !== 'string' || response.taskFingerprint.length !== 64)
60
+ return failure('invalid_response_shape', 'Response must include the task fingerprint.', 'taskFingerprint');
61
+ if (response.mode === 'REBUILD')
62
+ return failure('rebuild_response_on_edit_task', 'Rebuild responses cannot satisfy edit tasks.', 'mode');
63
+ if (response.mode === 'SHIP' && response.version === '1')
64
+ return { version: '1', mode: 'SHIP', taskFingerprint: response.taskFingerprint };
65
+ if (response.version === '2')
66
+ return parseRangeResponse(response);
67
+ if (response.version !== '1')
68
+ return failure('invalid_response_version', 'Response version must be "1" or "2".', 'version');
69
+ return parseReplacements(response);
98
70
  }
99
- function repairFencedJson(value) {
100
- if (typeof value !== 'string')
101
- return { value };
102
- const match = value.match(/^```json\s*\n([\s\S]*?)\n```\s*$/i);
103
- return match ? { value: match[1], adapterId: 'fenced_json_v1' } : { value };
71
+ function decodeResponse(raw) {
72
+ const source = typeof raw === 'string' ? parseResponseJson(raw) : raw;
73
+ const response = isFailure(source) ? source : parseResponse(source);
74
+ if (isFailure(response) && response.code === 'invalid_json' && typeof raw === 'string') {
75
+ const fenced = raw.match(/^```json\s*\n([\s\S]*?)\n```\s*$/i);
76
+ if (fenced)
77
+ return { response: parseResponse(fenced[1]), adapterIds: ['fenced_json_v1'] };
78
+ }
79
+ if (isFailure(response) && response.code === 'invalid_response_shape' && source && typeof source === 'object' && !Array.isArray(source)) {
80
+ const value = source;
81
+ if (typeof value.replacements === 'string') {
82
+ let replacements;
83
+ try {
84
+ replacements = JSON.parse(value.replacements);
85
+ }
86
+ catch {
87
+ return { response, adapterIds: [] };
88
+ }
89
+ if (Array.isArray(replacements))
90
+ return { response: parseResponse({ ...value, replacements }), adapterIds: ['stringified_replacements_v1'] };
91
+ }
92
+ }
93
+ return { response, adapterIds: [] };
104
94
  }
105
95
  export function prepareRewriteTask(draft, profile, copySpec, writingBrief, authorizedSentenceIds = []) {
106
96
  const result = analyze(draft, profile, writingBrief);
@@ -133,7 +123,7 @@ export function parseRewriteTask(value) {
133
123
  return task;
134
124
  }
135
125
  function rejected(task, raw, failures, adapterIds = []) {
136
- return { status: 'repairable', failures, receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint: responseFingerprint(raw), adapterIds, replacementSentenceIds: [] } };
126
+ return { status: 'repairable', failures, receipt: { version: '1', taskFingerprint: task.fingerprint, responseFingerprint: fingerprint(raw), adapterIds, replacementSentenceIds: [] } };
137
127
  }
138
128
  export function applyShip(task) {
139
129
  return {
@@ -151,19 +141,14 @@ export function applyShip(task) {
151
141
  };
152
142
  }
153
143
  export function applyRewriteResponse(task, raw) {
154
- const source = typeof raw === 'string' ? parseJson(raw) : raw;
155
- const parsed = isFailure(source) ? source : parseResponse(source);
156
- const fenced = isFailure(parsed) && parsed.code === 'invalid_json' ? repairFencedJson(raw) : { value: source };
157
- const repaired = isFailure(parsed) && parsed.code === 'invalid_response_shape' ? repairStringifiedReplacements(source) : fenced;
158
- const response = repaired.adapterId ? parseResponse(repaired.value) : parsed;
159
- const adapterIds = repaired.adapterId ? [repaired.adapterId] : [];
144
+ const { response, adapterIds } = decodeResponse(raw);
160
145
  if (isFailure(response))
161
146
  return rejected(task, raw, [response], adapterIds);
162
147
  if (response.taskFingerprint !== task.fingerprint)
163
148
  return rejected(task, raw, [failure('task_fingerprint_mismatch', 'Response task fingerprint does not match this task.', 'taskFingerprint')], adapterIds);
164
149
  if ('mode' in response && response.mode === 'SHIP') {
165
150
  const shipped = applyShip(task);
166
- return { ...shipped, receipt: { ...shipped.receipt, adapterIds, responseFingerprint: responseFingerprint(raw) } };
151
+ return { ...shipped, receipt: { ...shipped.receipt, adapterIds, responseFingerprint: fingerprint(raw) } };
167
152
  }
168
153
  if (response.version === '2')
169
154
  return applyRangeResponse(task, response, raw, adapterIds);
@@ -189,7 +174,7 @@ export function applyRewriteResponse(task, raw) {
189
174
  if (replacement !== undefined)
190
175
  candidate = `${candidate.slice(0, sentence.start)}${replacement}${candidate.slice(sentence.end)}`;
191
176
  }
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' } };
177
+ 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
178
  }
194
179
  function applyRangeResponse(task, response, raw, adapterIds) {
195
180
  const sentenceMap = new Map(task.sentences.map((sentence) => [sentence.id, sentence]));
@@ -221,10 +206,10 @@ function applyRangeResponse(task, response, raw, adapterIds) {
221
206
  for (const operation of [...(response.hygieneOperations ?? [])].sort((left, right) => right.start - left.start)) {
222
207
  candidate = `${candidate.slice(0, operation.start)}${operation.text}${candidate.slice(operation.end)}`;
223
208
  }
224
- const mapped = sentences(candidate);
209
+ const mapped = new Map(sentences(candidate).map((sentence) => [sentence.index, sentence]));
225
210
  for (const operation of [...response.operations].reverse()) {
226
- const start = mapped.find((sentence) => sentence.index === operation.startSentenceId);
227
- const end = mapped.find((sentence) => sentence.index === operation.endSentenceId);
211
+ const start = mapped.get(operation.startSentenceId);
212
+ const end = mapped.get(operation.endSentenceId);
228
213
  if (!start || !end)
229
214
  return rejected(task, raw, [failure('unknown_sentence_id', 'Range sentenceId is not in this task.', 'operations')], adapterIds);
230
215
  candidate = `${candidate.slice(0, start.start)}${operation.text}${candidate.slice(end.end)}`;
@@ -236,7 +221,7 @@ function applyRangeResponse(task, response, raw, adapterIds) {
236
221
  receipt: {
237
222
  version: '1',
238
223
  taskFingerprint: task.fingerprint,
239
- responseFingerprint: responseFingerprint(raw),
224
+ responseFingerprint: fingerprint(raw),
240
225
  adapterIds,
241
226
  operationRanges: response.operations.map((operation) => ({ startSentenceId: operation.startSentenceId, endSentenceId: operation.endSentenceId })),
242
227
  mode: 'EDIT',
@@ -262,15 +247,5 @@ export function evaluateRewriteResponse(task, raw, profile) {
262
247
  export function createRewriteLifecycleBinding(task, receipt, deterministic) {
263
248
  if (!deterministic.passed || receipt.taskFingerprint !== task.fingerprint)
264
249
  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
- };
250
+ return projectLifecycleBinding(task.fingerprint, receipt, deterministic);
276
251
  }
@@ -5,13 +5,6 @@ const RULE_MATCHERS = {
5
5
  'punct.en-dash': /–/u,
6
6
  'format.curly-quotes': /[“”]/u,
7
7
  };
8
- function evidenceDigest(ruleId, samples) {
9
- const evidence = samples
10
- .filter((sample) => RULE_MATCHERS[ruleId].test(sample))
11
- .map((sample) => sample.replace(/\s+/gu, ' ').trim())
12
- .sort();
13
- return createHash('sha256').update(JSON.stringify({ ruleId, evidence })).digest('hex');
14
- }
15
8
  /**
16
9
  * Returns only non-verbatim evidence for stylistic exceptions shown in two or
17
10
  * more author-owned samples. Callers add the result to a signed Profile v3.
@@ -19,9 +12,12 @@ function evidenceDigest(ruleId, samples) {
19
12
  export function deriveRuleAllowances(samples) {
20
13
  const allowances = {};
21
14
  for (const ruleId of SAMPLE_ALLOWANCE_RULE_IDS) {
22
- const count = samples.filter((sample) => RULE_MATCHERS[ruleId].test(sample)).length;
23
- if (count >= 2)
24
- allowances[ruleId] = { sampleCount: count, evidenceDigest: evidenceDigest(ruleId, samples) };
15
+ const matched = samples.filter((sample) => RULE_MATCHERS[ruleId].test(sample));
16
+ if (matched.length < 2)
17
+ continue;
18
+ const evidence = matched.map((sample) => sample.replace(/\s+/gu, ' ').trim()).sort();
19
+ const evidenceDigest = createHash('sha256').update(JSON.stringify({ ruleId, evidence })).digest('hex');
20
+ allowances[ruleId] = { sampleCount: matched.length, evidenceDigest };
25
21
  }
26
22
  return allowances;
27
23
  }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.js';
3
3
  import { verifyApprovalCapability } from './approval-capability.js';
4
- import { exactKeys as exact, isPlainObject as plain } from './internal.js';
4
+ import { exactKeys as exact, isPlainObject as plain, verificationMatchesBinding } from './internal.js';
5
5
  const violations = new Set(['action_change', 'dropped_object', 'unsupported_claim', 'constraint_weakened', 'clarity_regression']);
6
6
  const HEX = /^[a-f0-9]{64}$/;
7
7
  const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
@@ -94,7 +94,7 @@ export function createInitialLifecycleArtifact(task, deterministic) {
94
94
  const { taskFingerprint, ...taskBase } = task;
95
95
  const { artifactFingerprint, ...deterministicBase } = deterministic;
96
96
  const expectedDeterministicFingerprint = createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex');
97
- if (taskFingerprint !== hash('hyv:semantic-task:v1', taskBase) || !deterministic.passed || artifactFingerprint !== expectedDeterministicFingerprint || deterministic.artifactFingerprint !== task.binding.deterministicArtifactFingerprint || deterministic.sourceHash !== task.binding.sourceHash || deterministic.candidateHash !== task.binding.candidateHash || deterministic.profileId !== task.binding.profileId || deterministic.profileRevisionDigest !== task.binding.profileRevisionDigest || deterministic.rulesetVersion !== task.binding.rulesetVersion)
97
+ if (taskFingerprint !== hash('hyv:semantic-task:v1', taskBase) || !deterministic.passed || artifactFingerprint !== expectedDeterministicFingerprint || !verificationMatchesBinding(deterministic, task.binding))
98
98
  throw new Error('Lifecycle creation requires its bound passed deterministic verification artifact.');
99
99
  return artifact({ version: '1', status: 'needs_semantic_review', transitionFingerprint: hash('hyv:lifecycle-initial:v1', { binding: task.binding, semanticPolicy: task.policy }), binding: task.binding, semanticPolicy: task.policy, semanticTaskFingerprint: task.taskFingerprint, semanticEvidenceScopeFingerprint: hash('hyv:semantic-evidence-scope:v1', task.evidenceScope), verdictFingerprints: [] });
100
100
  }
@@ -120,6 +120,72 @@ function normalizeAction(value) {
120
120
  }
121
121
  return undefined;
122
122
  }
123
+ function nextArtifact(current, transitionFingerprint, status, verdictFingerprints, extra = {}) {
124
+ return { ok: true, artifact: artifact({
125
+ version: '1', status, parentArtifactFingerprint: current.artifactFingerprint,
126
+ transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy,
127
+ semanticTaskFingerprint: current.semanticTaskFingerprint,
128
+ semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint,
129
+ verdictFingerprints, ...extra,
130
+ }) };
131
+ }
132
+ function submitReview(current, action, context, transitionFingerprint) {
133
+ if (current.status !== 'needs_semantic_review')
134
+ return fail('out_of_order_transition');
135
+ const required = current.semanticPolicy === 'normal' ? 1 : 3;
136
+ if (action.verdicts.length !== required)
137
+ return fail('invalid_verdict_count');
138
+ const ids = action.verdicts.map((verdict) => verdict.evaluatorId);
139
+ if (new Set(ids).size !== ids.length)
140
+ return fail('duplicate_evaluator');
141
+ const authorized = current.semanticPolicy === 'normal'
142
+ ? context.authorizedSemanticEvaluatorIds.normal
143
+ : context.authorizedSemanticEvaluatorIds.highAssurance;
144
+ if (current.semanticPolicy === 'high_assurance' && new Set(authorized).size < 3)
145
+ return fail('invalid_verdict_count');
146
+ if (ids.some((id) => !authorized.includes(id)))
147
+ return fail('evaluator_not_authorized');
148
+ for (const verdict of action.verdicts) {
149
+ if (action.taskFingerprint !== current.semanticTaskFingerprint || verdict.taskFingerprint !== action.taskFingerprint)
150
+ return fail('task_fingerprint_mismatch');
151
+ if (!same(verdict.binding, current.binding))
152
+ return fail('invalid_binding');
153
+ if (verdict.judgmentType !== 'semantic' || hash('hyv:semantic-evidence-scope:v1', verdict.evidenceScope) !== current.semanticEvidenceScopeFingerprint)
154
+ return fail('evidence_scope_mismatch');
155
+ if ((verdict.approved && verdict.violations.length) || (!verdict.approved && !verdict.violations.length))
156
+ return fail('contradictory_verdict');
157
+ }
158
+ const fingerprints = action.verdicts.map((verdict) => hash('hyv:semantic-verdict:v1', verdict));
159
+ const approvals = action.verdicts.filter((verdict) => verdict.approved).length;
160
+ if (approvals === required)
161
+ return nextArtifact(current, transitionFingerprint, 'ready_for_human_review', fingerprints);
162
+ return nextArtifact(current, transitionFingerprint, 'needs_escalation', fingerprints, {
163
+ reason: approvals === 0 ? 'semantic_rejection' : 'semantic_disagreement',
164
+ });
165
+ }
166
+ function finalizeReview(current, action, context, transitionFingerprint) {
167
+ if (current.status !== 'ready_for_human_review')
168
+ return fail('out_of_order_transition');
169
+ const finalization = action.finalization;
170
+ if (finalization.parentArtifactFingerprint !== current.artifactFingerprint || !same(finalization.binding, current.binding) || finalization.judgmentType !== 'human_finalization' || finalization.evidenceScope.kind !== 'candidate')
171
+ return fail('invalid_binding');
172
+ if (!context.authorizedHumanFinalizerIds.includes(finalization.evaluatorId))
173
+ return fail('human_finalizer_not_authorized');
174
+ if (finalization.decision === 'reject') {
175
+ return nextArtifact(current, transitionFingerprint, 'needs_escalation', current.verdictFingerprints, { reason: 'human_rejection' });
176
+ }
177
+ if (!finalization.capability)
178
+ return fail('capability_required');
179
+ const capability = verifyApprovalCapability(finalization.capability, context.trustStore, {
180
+ now: context.now, expectedSubjectArtifactFingerprint: current.artifactFingerprint,
181
+ binding: current.binding, expectedPurpose: 'hyv.final-approval',
182
+ });
183
+ if (!capability.ok)
184
+ return fail('capability_invalid');
185
+ return nextArtifact(current, transitionFingerprint, 'approved', current.verdictFingerprints, {
186
+ capabilityFingerprint: capability.capabilityFingerprint,
187
+ });
188
+ }
123
189
  export function reduceRewriteLifecycle(current, rawAction, context) {
124
190
  let action;
125
191
  let transitionFingerprint;
@@ -140,49 +206,7 @@ export function reduceRewriteLifecycle(current, rawAction, context) {
140
206
  return fail('stale_parent');
141
207
  if (current.status === 'approved' || current.status === 'needs_escalation')
142
208
  return fail('terminal_state');
143
- if (action.type === 'semantic_submission') {
144
- if (current.status !== 'needs_semantic_review')
145
- return fail('out_of_order_transition');
146
- const required = current.semanticPolicy === 'normal' ? 1 : 3;
147
- if (action.verdicts.length !== required)
148
- return fail('invalid_verdict_count');
149
- const ids = action.verdicts.map((verdict) => verdict.evaluatorId);
150
- if (new Set(ids).size !== ids.length)
151
- return fail('duplicate_evaluator');
152
- const authorized = current.semanticPolicy === 'normal' ? context.authorizedSemanticEvaluatorIds.normal : context.authorizedSemanticEvaluatorIds.highAssurance;
153
- if (current.semanticPolicy === 'high_assurance' && new Set(authorized).size < 3)
154
- return fail('invalid_verdict_count');
155
- if (ids.some((id) => !authorized.includes(id)))
156
- return fail('evaluator_not_authorized');
157
- for (const verdict of action.verdicts) {
158
- if (action.taskFingerprint !== current.semanticTaskFingerprint || verdict.taskFingerprint !== action.taskFingerprint)
159
- return fail('task_fingerprint_mismatch');
160
- if (!same(verdict.binding, current.binding))
161
- return fail('invalid_binding');
162
- if (verdict.judgmentType !== 'semantic' || hash('hyv:semantic-evidence-scope:v1', verdict.evidenceScope) !== current.semanticEvidenceScopeFingerprint)
163
- return fail('evidence_scope_mismatch');
164
- if ((verdict.approved && verdict.violations.length) || (!verdict.approved && !verdict.violations.length))
165
- return fail('contradictory_verdict');
166
- }
167
- const verdictFingerprints = action.verdicts.map((verdict) => hash('hyv:semantic-verdict:v1', verdict));
168
- const approvals = action.verdicts.filter((verdict) => verdict.approved).length;
169
- const status = approvals === required ? 'ready_for_human_review' : 'needs_escalation';
170
- const reason = status === 'needs_escalation' ? (approvals === 0 ? 'semantic_rejection' : 'semantic_disagreement') : undefined;
171
- return { ok: true, artifact: artifact({ version: '1', status, parentArtifactFingerprint: current.artifactFingerprint, transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy, semanticTaskFingerprint: current.semanticTaskFingerprint, semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint, verdictFingerprints, ...(reason ? { reason } : {}) }) };
172
- }
173
- if (current.status !== 'ready_for_human_review')
174
- return fail('out_of_order_transition');
175
- const finalization = action.finalization;
176
- if (finalization.parentArtifactFingerprint !== current.artifactFingerprint || !same(finalization.binding, current.binding) || finalization.judgmentType !== 'human_finalization' || finalization.evidenceScope.kind !== 'candidate')
177
- return fail('invalid_binding');
178
- if (!context.authorizedHumanFinalizerIds.includes(finalization.evaluatorId))
179
- return fail('human_finalizer_not_authorized');
180
- if (finalization.decision === 'reject')
181
- return { ok: true, artifact: artifact({ version: '1', status: 'needs_escalation', parentArtifactFingerprint: current.artifactFingerprint, transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy, semanticTaskFingerprint: current.semanticTaskFingerprint, semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint, verdictFingerprints: current.verdictFingerprints, reason: 'human_rejection' }) };
182
- if (!finalization.capability)
183
- return fail('capability_required');
184
- const capability = verifyApprovalCapability(finalization.capability, context.trustStore, { now: context.now, expectedSubjectArtifactFingerprint: current.artifactFingerprint, binding: current.binding, expectedPurpose: 'hyv.final-approval' });
185
- if (!capability.ok)
186
- return fail('capability_invalid');
187
- return { ok: true, artifact: artifact({ version: '1', status: 'approved', parentArtifactFingerprint: current.artifactFingerprint, transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy, semanticTaskFingerprint: current.semanticTaskFingerprint, semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint, verdictFingerprints: current.verdictFingerprints, capabilityFingerprint: capability.capabilityFingerprint }) };
209
+ return action.type === 'semantic_submission'
210
+ ? submitReview(current, action, context, transitionFingerprint)
211
+ : finalizeReview(current, action, context, transitionFingerprint);
188
212
  }
@@ -449,9 +449,19 @@ function parseReleaseAudit(protocol, input) {
449
449
  fail('release_audit_binding_mismatch');
450
450
  return audit;
451
451
  }
452
- function correctionCount(ratings, mapping, arm, result) {
453
- const label = mapping.labels.A === arm ? 'A' : 'B';
454
- return ratings.filter((rating) => rating.correctionVersusConfirm[label] === result).length;
452
+ function rate(numerator, denominator) {
453
+ return { numerator, denominator, rate: numerator / denominator, uncertainty95: wilson95(numerator, denominator) };
454
+ }
455
+ function corrections(ratings, label, denominator) {
456
+ let corrections = 0;
457
+ let confirms = 0;
458
+ for (const rating of ratings) {
459
+ if (rating.correctionVersusConfirm[label] === 'correction')
460
+ corrections += 1;
461
+ if (rating.correctionVersusConfirm[label] === 'confirm')
462
+ confirms += 1;
463
+ }
464
+ return { corrections, confirms, denominator, correctionRate: corrections / denominator, uncertainty95: wilson95(corrections, denominator) };
455
465
  }
456
466
  function workflowForArm(runs, arm) {
457
467
  const armRuns = runs.filter((run) => run.arm === arm);
@@ -460,21 +470,26 @@ function workflowForArm(runs, arm) {
460
470
  function calculateEvaluation(protocol, runs, ratings, mapping, summary) {
461
471
  const completedRatings = ratings.filter((rating) => rating.workflow === 'completed');
462
472
  const preferenceDenominator = protocol.intentToTreat.expectedReviewers * protocol.cases.length;
463
- const preferred = completedRatings.reduce((total, rating) => total + (rating.preferredLabel === 'tie' ? 0.5 : mapping.labels[rating.preferredLabel] === 'stage1' ? 1 : 0), 0);
473
+ let preferred = 0;
474
+ for (const rating of completedRatings) {
475
+ if (rating.preferredLabel === 'tie')
476
+ preferred += 0.5;
477
+ else if (mapping.labels[rating.preferredLabel] === 'stage1')
478
+ preferred += 1;
479
+ }
464
480
  const preferenceRate = preferenceDenominator ? preferred / preferenceDenominator : 0;
465
- const stage1Confirm = correctionCount(completedRatings, mapping, 'stage1', 'confirm');
466
- const baselineConfirm = correctionCount(completedRatings, mapping, 'baseline', 'confirm');
467
- const stage1Correction = correctionCount(completedRatings, mapping, 'stage1', 'correction');
468
- const baselineCorrection = correctionCount(completedRatings, mapping, 'baseline', 'correction');
481
+ const stage1 = corrections(completedRatings, mapping.labels.A === 'stage1' ? 'A' : 'B', preferenceDenominator);
482
+ const baseline = corrections(completedRatings, mapping.labels.A === 'baseline' ? 'A' : 'B', preferenceDenominator);
469
483
  const baselineWorkflow = workflowForArm(runs, 'baseline');
470
484
  const stage1Workflow = workflowForArm(runs, 'stage1');
471
- const correctionMarginMet = (stage1Confirm - baselineConfirm) / preferenceDenominator >= protocol.analysis.margin;
472
- const workflowRegressed = stage1Workflow.denominator === 0 || baselineWorkflow.denominator === 0 || stage1Workflow.completed / stage1Workflow.denominator < baselineWorkflow.completed / baselineWorkflow.denominator;
485
+ const correctionMarginMet = (stage1.confirms - baseline.confirms) / preferenceDenominator >= protocol.analysis.margin;
486
+ const workflowRegressed = stage1Workflow.denominator === 0 || baselineWorkflow.denominator === 0
487
+ || stage1Workflow.completed / stage1Workflow.denominator < baselineWorkflow.completed / baselineWorkflow.denominator;
473
488
  const metrics = {
474
- preference: { numerator: preferred, denominator: preferenceDenominator, rate: preferenceRate, uncertainty95: wilson95(preferred, preferenceDenominator) },
475
- correctionVersusConfirm: { stage1: { corrections: stage1Correction, confirms: stage1Confirm, denominator: preferenceDenominator, correctionRate: stage1Correction / preferenceDenominator, uncertainty95: wilson95(stage1Correction, preferenceDenominator) }, baseline: { corrections: baselineCorrection, confirms: baselineConfirm, denominator: preferenceDenominator, correctionRate: baselineCorrection / preferenceDenominator, uncertainty95: wilson95(baselineCorrection, preferenceDenominator) } },
476
- completion: { numerator: completedRatings.length, denominator: preferenceDenominator, rate: completedRatings.length / preferenceDenominator, uncertainty95: wilson95(completedRatings.length, preferenceDenominator) },
477
- abandonment: { numerator: ratings.length - completedRatings.length, denominator: preferenceDenominator, rate: (ratings.length - completedRatings.length) / preferenceDenominator, uncertainty95: wilson95(ratings.length - completedRatings.length, preferenceDenominator) },
489
+ preference: { ...rate(preferred, preferenceDenominator), rate: preferenceRate },
490
+ correctionVersusConfirm: { stage1, baseline },
491
+ completion: rate(completedRatings.length, preferenceDenominator),
492
+ abandonment: rate(ratings.length - completedRatings.length, preferenceDenominator),
478
493
  providerRuns: { completed: summary.completed, abandoned: summary.abandonments, hardFailures: summary.hardFailures, timeouts: summary.timeouts, denominator: summary.denominator },
479
494
  };
480
495
  return { correctionMarginMet, metrics, preferenceDenominator, preferenceRate, workflowRegressed };
@@ -6,25 +6,17 @@ const MINIMUM_WORDS = 1_500;
6
6
  function finding(id, disposition, reason, suggestion, sentence) {
7
7
  return { id, disposition, reason, suggestion, ...(sentence === undefined ? {} : { sentence }) };
8
8
  }
9
+ const FINGERPRINT_KEYS = new Map([
10
+ ['dna.fingerprint.contraction-rate', 'contractionRate'],
11
+ ['dna.fingerprint.sentence-length-distribution', 'sentenceLengthDistribution'],
12
+ ['dna.fingerprint.bullet-rate', 'bulletRate'],
13
+ ['dna.fingerprint.en-dash-rate', 'enDashRate'],
14
+ ]);
9
15
  function strictFinding(source, profile) {
10
- if (source.engine === 'ai_editor') {
11
- return finding(`strict.${source.engine}.${source.id}`, 'block', source.reason, source.suggestion, source.sentence);
12
- }
13
- if (source.engine === 'voice_dna') {
14
- if (source.severity === 'red')
15
- return finding(`strict.${source.engine}.${source.id}`, 'block', source.reason, source.suggestion, source.sentence);
16
- if (source.id.startsWith('dna.fingerprint.')) {
17
- const metric = source.id.slice('dna.fingerprint.'.length);
18
- const key = metric === 'contraction-rate' ? 'contractionRate'
19
- : metric === 'sentence-length-distribution' ? 'sentenceLengthDistribution'
20
- : metric === 'bullet-rate' ? 'bulletRate'
21
- : metric === 'en-dash-rate' ? 'enDashRate' : undefined;
22
- if (key && profile.tolerances[key].calibrated)
23
- return finding(`strict.${source.engine}.${source.id}`, 'block', source.reason, source.suggestion, source.sentence);
24
- }
25
- return finding(`strict.${source.engine}.${source.id}`, 'review', source.reason, source.suggestion, source.sentence);
26
- }
27
- return finding(`strict.${source.engine}.${source.id}`, source.severity === 'red' ? 'block' : 'review', source.reason, source.suggestion, source.sentence);
16
+ const metric = FINGERPRINT_KEYS.get(source.id);
17
+ const calibratedDrift = source.engine === 'voice_dna' && metric && profile.tolerances[metric].calibrated;
18
+ const blocks = source.engine === 'ai_editor' || source.severity === 'red' || calibratedDrift;
19
+ return finding(`strict.${source.engine}.${source.id}`, blocks ? 'block' : 'review', source.reason, source.suggestion, source.sentence);
28
20
  }
29
21
  export function evaluateStrictQuality(draft, profile, samples, brief) {
30
22
  const readiness = assessProfileReadiness(samples);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const HYV_VERSION = '3.6.0';
1
+ export const HYV_VERSION = '4.0.0';