@holdyourvoice/hyv 3.6.1 → 4.0.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 +91 -96
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +19 -13
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +15 -35
- package/dist/cli/io.js +41 -37
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +87 -37
- package/dist/hidden-text.js +6 -6
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/internal.js +8 -0
- package/dist/judgment-task.js +10 -14
- package/dist/learning.js +66 -67
- package/dist/lifecycle-adapter.js +2 -5
- package/dist/local-eval.js +16 -31
- package/dist/mcp-server.js +272 -0
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp.js +2 -261
- package/dist/pipeline.js +9 -85
- package/dist/profile-compose.js +18 -11
- package/dist/profile-score.js +6 -6
- package/dist/profile.js +17 -19
- package/dist/provenance-status.js +1 -3
- package/dist/rebuild-task.js +14 -25
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +125 -0
- package/dist/rewrite-task.js +117 -70
- package/dist/rule-allowances.js +6 -10
- package/dist/semantic-review.js +71 -47
- package/dist/stage1-evaluation.js +29 -14
- package/dist/strict-quality.js +10 -18
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +34 -21
- package/dist/writing-examples.js +2 -7
- package/package.json +6 -3
- package/skills/hyv-prepare-judgment/SKILL.md +4 -0
- package/dist/agents/catalog.test.js +0 -60
- package/dist/agents/emit.test.js +0 -64
- package/dist/agents/load.test.js +0 -149
- package/dist/ai-editor.test.js +0 -265
- package/dist/approval-capability.test.js +0 -52
- package/dist/approval-context.test.js +0 -38
- package/dist/backtest.test.js +0 -20
- package/dist/benchmark.test.js +0 -328
- package/dist/canonical-json.test.js +0 -24
- package/dist/cli/context.test.js +0 -55
- package/dist/cli.test.js +0 -731
- package/dist/editorial-packs.test.js +0 -94
- package/dist/fact-linter.test.js +0 -85
- package/dist/hidden-text.test.js +0 -26
- package/dist/hygiene.test.js +0 -83
- package/dist/judgment-task.test.js +0 -162
- package/dist/learning.test.js +0 -325
- package/dist/lifecycle-adapter.test.js +0 -56
- package/dist/local-eval.test.js +0 -20
- package/dist/logic-linter-corpus.test.js +0 -22
- package/dist/logic-linter.test.js +0 -39
- package/dist/mcp-tools.test.js +0 -286
- package/dist/mcp.test.js +0 -312
- package/dist/mirror-refs.test.js +0 -63
- package/dist/pipeline.test.js +0 -247
- package/dist/preservation.test.js +0 -22
- package/dist/production-gates.test.js +0 -34
- package/dist/profile-compose.test.js +0 -32
- package/dist/profile-score.test.js +0 -22
- package/dist/profile-watch.test.js +0 -23
- package/dist/profile.test.js +0 -141
- package/dist/provenance-status.test.js +0 -22
- package/dist/rebuild-task.test.js +0 -206
- package/dist/recomposition.test.js +0 -34
- package/dist/release-audit.test.js +0 -292
- package/dist/rewrite-task.test.js +0 -166
- package/dist/rule-allowances.test.js +0 -17
- package/dist/rule-reconciliation.test.js +0 -50
- package/dist/sample-ingest.test.js +0 -52
- package/dist/semantic-review.test.js +0 -101
- package/dist/stage1-dry-run.test.js +0 -39
- package/dist/stage1-evaluation.test.js +0 -184
- package/dist/stage1-human-packet.test.js +0 -102
- package/dist/stage1-schema-contract.test.js +0 -95
- package/dist/stage2-human-packet.test.js +0 -81
- package/dist/strict-quality.test.js +0 -62
- package/dist/text-provenance.feature.test.js +0 -45
- package/dist/text.test.js +0 -16
- package/dist/voice-dna.test.js +0 -121
- package/dist/writing-examples.test.js +0 -35
package/dist/rewrite-task.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deriveFactRepair, isSourceBackedRepair } from './rewrite-prompt.js';
|
|
1
2
|
import { parseWritingBrief } from './editorial-packs.js';
|
|
2
3
|
import { finalOutputCheck, hygieneSourceFindings } from './hygiene.js';
|
|
3
4
|
import { analyze, deriveEditScope, renderRewritePrompt, verifyDeterministically } from './pipeline.js';
|
|
@@ -9,47 +10,32 @@ const MAX_REPLACEMENT_CHARACTERS = 10_000;
|
|
|
9
10
|
function isFailure(value) {
|
|
10
11
|
return typeof value === 'object' && value !== null && 'code' in value;
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return failure('invalid_response_shape', 'Response must include the task fingerprint.', 'taskFingerprint');
|
|
21
|
-
if (response.mode === 'REBUILD')
|
|
22
|
-
return failure('rebuild_response_on_edit_task', 'Rebuild responses cannot satisfy edit tasks.', 'mode');
|
|
23
|
-
if (response.mode === 'SHIP' && response.version === '1') {
|
|
24
|
-
return { version: '1', mode: 'SHIP', taskFingerprint: response.taskFingerprint };
|
|
25
|
-
}
|
|
26
|
-
if (response.version === '2') {
|
|
27
|
-
if (!Array.isArray(response.operations))
|
|
28
|
-
return failure('invalid_response_shape', 'Version 2 responses require an operations array.', 'operations');
|
|
29
|
-
if (response.operations.length > MAX_REPLACEMENTS)
|
|
30
|
-
return failure('invalid_response_shape', `Response may include at most ${MAX_REPLACEMENTS} replacements.`, 'operations');
|
|
31
|
-
for (const [index, operation] of response.operations.entries()) {
|
|
32
|
-
if (!operation || typeof operation !== 'object' || !Number.isInteger(operation.startSentenceId) || !Number.isInteger(operation.endSentenceId) || typeof operation.text !== 'string') {
|
|
33
|
-
return failure('invalid_response_shape', 'Every operation requires integer startSentenceId, endSentenceId, and string text.', `operations[${index}]`);
|
|
34
|
-
}
|
|
35
|
-
if (operation.endSentenceId < operation.startSentenceId)
|
|
36
|
-
return failure('noncontiguous_range', 'A range must be inclusive and contiguous.', `operations[${index}]`);
|
|
37
|
-
if (operation.text.length > MAX_REPLACEMENT_CHARACTERS)
|
|
38
|
-
return failure('invalid_replacement_text', `Replacement text must contain at most ${MAX_REPLACEMENT_CHARACTERS} characters.`, `operations[${index}].text`);
|
|
13
|
+
function parseRangeResponse(response) {
|
|
14
|
+
if (!Array.isArray(response.operations))
|
|
15
|
+
return failure('invalid_response_shape', 'Version 2 responses require an operations array.', 'operations');
|
|
16
|
+
if (response.operations.length > MAX_REPLACEMENTS)
|
|
17
|
+
return failure('invalid_response_shape', `Response may include at most ${MAX_REPLACEMENTS} replacements.`, 'operations');
|
|
18
|
+
for (const [index, operation] of response.operations.entries()) {
|
|
19
|
+
if (!operation || typeof operation !== 'object' || !Number.isInteger(operation.startSentenceId) || !Number.isInteger(operation.endSentenceId) || typeof operation.text !== 'string') {
|
|
20
|
+
return failure('invalid_response_shape', 'Every operation requires integer startSentenceId, endSentenceId, and string text.', `operations[${index}]`);
|
|
39
21
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
22
|
+
if (operation.endSentenceId < operation.startSentenceId)
|
|
23
|
+
return failure('noncontiguous_range', 'A range must be inclusive and contiguous.', `operations[${index}]`);
|
|
24
|
+
if (operation.text.length > MAX_REPLACEMENT_CHARACTERS)
|
|
25
|
+
return failure('invalid_replacement_text', `Replacement text must contain at most ${MAX_REPLACEMENT_CHARACTERS} characters.`, `operations[${index}].text`);
|
|
26
|
+
}
|
|
27
|
+
if (response.hygieneOperations !== undefined) {
|
|
28
|
+
if (!Array.isArray(response.hygieneOperations))
|
|
29
|
+
return failure('invalid_response_shape', 'Hygiene operations must be an array.', 'hygieneOperations');
|
|
30
|
+
for (const [index, operation] of response.hygieneOperations.entries()) {
|
|
31
|
+
if (!operation || !Number.isInteger(operation.start) || !Number.isInteger(operation.end) || typeof operation.text !== 'string' || operation.end < operation.start) {
|
|
32
|
+
return failure('invalid_response_shape', 'Every hygiene operation requires integer start, end, and string text.', `hygieneOperations[${index}]`);
|
|
47
33
|
}
|
|
48
34
|
}
|
|
49
|
-
return response;
|
|
50
35
|
}
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
function parseReplacements(response) {
|
|
53
39
|
if (!Array.isArray(response.replacements))
|
|
54
40
|
return failure('invalid_response_shape', 'Response replacements must be an array.', 'replacements');
|
|
55
41
|
if (response.replacements.length > MAX_REPLACEMENTS)
|
|
@@ -64,39 +50,62 @@ function parseResponse(value) {
|
|
|
64
50
|
}
|
|
65
51
|
return response;
|
|
66
52
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (typeof raw
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
53
|
+
function parseResponse(value) {
|
|
54
|
+
const raw = typeof value === 'string' ? parseResponseJson(value) : value;
|
|
55
|
+
if (isFailure(raw))
|
|
56
|
+
return raw;
|
|
57
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
58
|
+
return failure('invalid_response_shape', 'Response must be an object.');
|
|
59
|
+
const response = raw;
|
|
60
|
+
if (typeof response.taskFingerprint !== 'string' || response.taskFingerprint.length !== 64)
|
|
61
|
+
return failure('invalid_response_shape', 'Response must include the task fingerprint.', 'taskFingerprint');
|
|
62
|
+
if (response.mode === 'REBUILD')
|
|
63
|
+
return failure('rebuild_response_on_edit_task', 'Rebuild responses cannot satisfy edit tasks.', 'mode');
|
|
64
|
+
if (response.mode === 'SHIP' && response.version === '1')
|
|
65
|
+
return { version: '1', mode: 'SHIP', taskFingerprint: response.taskFingerprint };
|
|
66
|
+
if (response.version === '2')
|
|
67
|
+
return parseRangeResponse(response);
|
|
68
|
+
if (response.version !== '1')
|
|
69
|
+
return failure('invalid_response_version', 'Response version must be "1" or "2".', 'version');
|
|
70
|
+
return parseReplacements(response);
|
|
82
71
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
function decodeResponse(raw) {
|
|
73
|
+
const source = typeof raw === 'string' ? parseResponseJson(raw) : raw;
|
|
74
|
+
const response = isFailure(source) ? source : parseResponse(source);
|
|
75
|
+
if (isFailure(response) && response.code === 'invalid_json' && typeof raw === 'string') {
|
|
76
|
+
const fenced = raw.match(/^```json\s*\n([\s\S]*?)\n```\s*$/i);
|
|
77
|
+
if (fenced)
|
|
78
|
+
return { response: parseResponse(fenced[1]), adapterIds: ['fenced_json_v1'] };
|
|
79
|
+
}
|
|
80
|
+
if (isFailure(response) && response.code === 'invalid_response_shape' && source && typeof source === 'object' && !Array.isArray(source)) {
|
|
81
|
+
const value = source;
|
|
82
|
+
if (typeof value.replacements === 'string') {
|
|
83
|
+
let replacements;
|
|
84
|
+
try {
|
|
85
|
+
replacements = JSON.parse(value.replacements);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { response, adapterIds: [] };
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(replacements))
|
|
91
|
+
return { response: parseResponse({ ...value, replacements }), adapterIds: ['stringified_replacements_v1'] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { response, adapterIds: [] };
|
|
88
95
|
}
|
|
89
96
|
export function prepareRewriteTask(draft, profile, copySpec, writingBrief, authorizedSentenceIds = []) {
|
|
90
97
|
const result = analyze(draft, profile, writingBrief);
|
|
91
|
-
const
|
|
98
|
+
const factRepair = deriveFactRepair(draft, writingBrief);
|
|
99
|
+
const prompt = renderRewritePrompt(draft, profile, result, [], writingBrief, [], factRepair, authorizedSentenceIds);
|
|
92
100
|
const mapped = sentences(draft);
|
|
93
|
-
const eligibleSentenceIds = new Set([...deriveEditScope(result, true).eligibleSentenceIds, ...authorizedSentenceIds]);
|
|
101
|
+
const eligibleSentenceIds = new Set([...deriveEditScope(result, true).eligibleSentenceIds, ...(factRepair?.eligibleSentenceIds ?? []), ...authorizedSentenceIds]);
|
|
94
102
|
const taskBase = {
|
|
95
103
|
version: '1',
|
|
96
104
|
draft,
|
|
97
105
|
sentences: mapped.map((sentence) => ({ id: sentence.index, text: sentence.text, eligible: eligibleSentenceIds.has(sentence.index) })),
|
|
98
106
|
eligibleSentenceIds: [...eligibleSentenceIds].sort((left, right) => left - right),
|
|
99
107
|
prompt,
|
|
108
|
+
...(factRepair ? { factRepair } : {}),
|
|
100
109
|
...(copySpec ? { copySpec } : {}),
|
|
101
110
|
...(writingBrief ? { writingBrief } : {}),
|
|
102
111
|
};
|
|
@@ -135,12 +144,7 @@ export function applyShip(task) {
|
|
|
135
144
|
};
|
|
136
145
|
}
|
|
137
146
|
export function applyRewriteResponse(task, raw) {
|
|
138
|
-
const
|
|
139
|
-
const parsed = isFailure(source) ? source : parseResponse(source);
|
|
140
|
-
const fenced = isFailure(parsed) && parsed.code === 'invalid_json' ? repairFencedJson(raw) : { value: source };
|
|
141
|
-
const repaired = isFailure(parsed) && parsed.code === 'invalid_response_shape' ? repairStringifiedReplacements(source) : fenced;
|
|
142
|
-
const response = repaired.adapterId ? parseResponse(repaired.value) : parsed;
|
|
143
|
-
const adapterIds = repaired.adapterId ? [repaired.adapterId] : [];
|
|
147
|
+
const { response, adapterIds } = decodeResponse(raw);
|
|
144
148
|
if (isFailure(response))
|
|
145
149
|
return rejected(task, raw, [response], adapterIds);
|
|
146
150
|
if (response.taskFingerprint !== task.fingerprint)
|
|
@@ -205,10 +209,10 @@ function applyRangeResponse(task, response, raw, adapterIds) {
|
|
|
205
209
|
for (const operation of [...(response.hygieneOperations ?? [])].sort((left, right) => right.start - left.start)) {
|
|
206
210
|
candidate = `${candidate.slice(0, operation.start)}${operation.text}${candidate.slice(operation.end)}`;
|
|
207
211
|
}
|
|
208
|
-
const mapped = sentences(candidate);
|
|
212
|
+
const mapped = new Map(sentences(candidate).map((sentence) => [sentence.index, sentence]));
|
|
209
213
|
for (const operation of [...response.operations].reverse()) {
|
|
210
|
-
const start = mapped.
|
|
211
|
-
const end = mapped.
|
|
214
|
+
const start = mapped.get(operation.startSentenceId);
|
|
215
|
+
const end = mapped.get(operation.endSentenceId);
|
|
212
216
|
if (!start || !end)
|
|
213
217
|
return rejected(task, raw, [failure('unknown_sentence_id', 'Range sentenceId is not in this task.', 'operations')], adapterIds);
|
|
214
218
|
candidate = `${candidate.slice(0, start.start)}${operation.text}${candidate.slice(end.end)}`;
|
|
@@ -238,10 +242,53 @@ export function evaluateRewriteResponse(task, raw, profile) {
|
|
|
238
242
|
const deterministicArtifact = checked.artifact;
|
|
239
243
|
if (!verification.passed) {
|
|
240
244
|
const { candidate: _candidate, ...withheld } = applied;
|
|
241
|
-
return { ...withheld, status: 'needs_escalation', verification };
|
|
245
|
+
return { ...withheld, status: 'needs_escalation', verification, feedback: rewriteFeedback(task, candidate, verification, output.changed) };
|
|
242
246
|
}
|
|
243
247
|
const lifecycleBinding = createRewriteLifecycleBinding(task, applied.receipt, deterministicArtifact);
|
|
244
|
-
|
|
248
|
+
const pendingFacts = verification.factLint?.findings.filter((finding) => finding.severity !== 'error') ?? [];
|
|
249
|
+
const feedback = pendingFacts.length ? {
|
|
250
|
+
disposition: 'review_required',
|
|
251
|
+
message: 'Deterministic checks passed, but fact findings remain uncertain. Review the cited evidence during semantic review; these findings grant no added edit permission.',
|
|
252
|
+
blockers: pendingFacts.map((finding) => ({ gate: 'facts', disposition: 'review_required', sentenceIds: [finding.draftLocation.sentence], reason: finding.reason })),
|
|
253
|
+
} : undefined;
|
|
254
|
+
return { ...applied, candidate, status: 'needs_semantic_review', verification, deterministicArtifact, lifecycleBinding, ...(feedback ? { feedback } : {}) };
|
|
255
|
+
}
|
|
256
|
+
function rewriteFeedback(task, candidate, verification, hygieneChanged) {
|
|
257
|
+
const sourceHygiene = finalOutputCheck(task.draft);
|
|
258
|
+
const stableSentenceScope = !hygieneChanged && !sourceHygiene.changed && sourceHygiene.accepted;
|
|
259
|
+
const protectedText = new Set(task.sentences.filter((sentence) => !sentence.eligible).map((sentence) => sentence.text));
|
|
260
|
+
const candidateSentences = new Map(sentences(candidate).map((sentence) => [sentence.index, sentence.text]));
|
|
261
|
+
const inScope = (ids) => stableSentenceScope && ids.length > 0 && ids.every((id) => candidateSentences.has(id) && !protectedText.has(candidateSentences.get(id)));
|
|
262
|
+
const blockers = [];
|
|
263
|
+
const add = (gate, reason, sentenceIds = [], repairable = false) => {
|
|
264
|
+
blockers.push({ gate, sentenceIds: [...new Set(sentenceIds)], reason: repairable && !stableSentenceScope ? `${reason} Hygiene normalization prevents reliable original sentence scope matching; review before retrying.` : reason, disposition: repairable && inScope(sentenceIds) ? 'repair_in_scope' : 'review_required' });
|
|
265
|
+
};
|
|
266
|
+
if (verification.strictFindings.length) {
|
|
267
|
+
for (const finding of verification.strictFindings)
|
|
268
|
+
add('analysis', finding.reason, [finding.sentence], true);
|
|
269
|
+
}
|
|
270
|
+
else if (!verification.candidate.passed)
|
|
271
|
+
add('analysis', 'The aggregate analysis gate failed without a sentence-level repair. Review the profile or task scope.');
|
|
272
|
+
for (const finding of verification.factLint?.findings ?? []) {
|
|
273
|
+
if (finding.severity === 'error')
|
|
274
|
+
add('facts', finding.reason, [finding.draftLocation.sentence], isSourceBackedRepair(finding, task.writingBrief?.factSources));
|
|
275
|
+
}
|
|
276
|
+
if (!verification.logicLint.passed)
|
|
277
|
+
add('logic', 'Resolve the reported logic conflict with a reviewer before changing claims.', verification.logicLint.findings.filter((finding) => finding.severity === 'error').map((finding) => finding.sentence));
|
|
278
|
+
if (verification.preservationScore < 70)
|
|
279
|
+
add('preservation', 'The candidate does not preserve enough source content. Review the changes before retrying.');
|
|
280
|
+
if (verification.requiredFacts && !verification.requiredFacts.passed)
|
|
281
|
+
add('required_facts', 'Required facts are missing or denied. Review their compatibility with the authorized edits.');
|
|
282
|
+
if ('claims' in verification && !verification.claims.passed)
|
|
283
|
+
add('copy_spec', 'CopySpec claim verification failed. Review the immutable claims before retrying.');
|
|
284
|
+
if (!verification.finalOutput.accepted)
|
|
285
|
+
add('hygiene', 'Final-output hygiene remains unresolved. Review the reported characters and their permitted removal.');
|
|
286
|
+
if (!blockers.length)
|
|
287
|
+
add('analysis', 'Verification failed without a localized repair. Review the verification report.');
|
|
288
|
+
const disposition = blockers.some((blocker) => blocker.disposition === 'review_required') ? 'review_required' : 'repair_in_scope';
|
|
289
|
+
return { disposition, message: disposition === 'review_required'
|
|
290
|
+
? 'Stop automatic retries. Resolve the listed blockers with a reviewer or obtain a newly authorized task; protected sentences remain locked.'
|
|
291
|
+
: 'Repair only the listed blockers within this task scope, then verify again. Sentence IDs refer to the evaluated candidate; submit replacements using the original task IDs.', blockers };
|
|
245
292
|
}
|
|
246
293
|
export function createRewriteLifecycleBinding(task, receipt, deterministic) {
|
|
247
294
|
if (!deterministic.passed || receipt.taskFingerprint !== task.fingerprint)
|
package/dist/rule-allowances.js
CHANGED
|
@@ -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
|
|
23
|
-
if (
|
|
24
|
-
|
|
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
|
}
|
package/dist/semantic-review.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
|
466
|
-
const
|
|
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 = (
|
|
472
|
-
const workflowRegressed = stage1Workflow.denominator === 0 || baselineWorkflow.denominator === 0
|
|
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: {
|
|
475
|
-
correctionVersusConfirm: { stage1
|
|
476
|
-
completion:
|
|
477
|
-
abandonment:
|
|
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 };
|
package/dist/strict-quality.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 = '
|
|
1
|
+
export const HYV_VERSION = '4.0.1';
|