@holdyourvoice/hyv 3.6.1 → 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.
- 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 +45 -27
- 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 +82 -0
- package/dist/rewrite-task.js +66 -65
- 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
|
@@ -9,47 +9,32 @@ const MAX_REPLACEMENT_CHARACTERS = 10_000;
|
|
|
9
9
|
function isFailure(value) {
|
|
10
10
|
return typeof value === 'object' && value !== null && 'code' in value;
|
|
11
11
|
}
|
|
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`);
|
|
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}]`);
|
|
39
20
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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}]`);
|
|
47
32
|
}
|
|
48
33
|
}
|
|
49
|
-
return response;
|
|
50
34
|
}
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
function parseReplacements(response) {
|
|
53
38
|
if (!Array.isArray(response.replacements))
|
|
54
39
|
return failure('invalid_response_shape', 'Response replacements must be an array.', 'replacements');
|
|
55
40
|
if (response.replacements.length > MAX_REPLACEMENTS)
|
|
@@ -64,27 +49,48 @@ function parseResponse(value) {
|
|
|
64
49
|
}
|
|
65
50
|
return response;
|
|
66
51
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (typeof raw
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return
|
|
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);
|
|
70
|
+
}
|
|
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
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|
|
81
92
|
}
|
|
82
|
-
}
|
|
83
|
-
function repairFencedJson(value) {
|
|
84
|
-
if (typeof value !== 'string')
|
|
85
|
-
return { value };
|
|
86
|
-
const match = value.match(/^```json\s*\n([\s\S]*?)\n```\s*$/i);
|
|
87
|
-
return match ? { value: match[1], adapterId: 'fenced_json_v1' } : { value };
|
|
93
|
+
return { response, adapterIds: [] };
|
|
88
94
|
}
|
|
89
95
|
export function prepareRewriteTask(draft, profile, copySpec, writingBrief, authorizedSentenceIds = []) {
|
|
90
96
|
const result = analyze(draft, profile, writingBrief);
|
|
@@ -135,12 +141,7 @@ export function applyShip(task) {
|
|
|
135
141
|
};
|
|
136
142
|
}
|
|
137
143
|
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] : [];
|
|
144
|
+
const { response, adapterIds } = decodeResponse(raw);
|
|
144
145
|
if (isFailure(response))
|
|
145
146
|
return rejected(task, raw, [response], adapterIds);
|
|
146
147
|
if (response.taskFingerprint !== task.fingerprint)
|
|
@@ -205,10 +206,10 @@ function applyRangeResponse(task, response, raw, adapterIds) {
|
|
|
205
206
|
for (const operation of [...(response.hygieneOperations ?? [])].sort((left, right) => right.start - left.start)) {
|
|
206
207
|
candidate = `${candidate.slice(0, operation.start)}${operation.text}${candidate.slice(operation.end)}`;
|
|
207
208
|
}
|
|
208
|
-
const mapped = sentences(candidate);
|
|
209
|
+
const mapped = new Map(sentences(candidate).map((sentence) => [sentence.index, sentence]));
|
|
209
210
|
for (const operation of [...response.operations].reverse()) {
|
|
210
|
-
const start = mapped.
|
|
211
|
-
const end = mapped.
|
|
211
|
+
const start = mapped.get(operation.startSentenceId);
|
|
212
|
+
const end = mapped.get(operation.endSentenceId);
|
|
212
213
|
if (!start || !end)
|
|
213
214
|
return rejected(task, raw, [failure('unknown_sentence_id', 'Range sentenceId is not in this task.', 'operations')], adapterIds);
|
|
214
215
|
candidate = `${candidate.slice(0, start.start)}${operation.text}${candidate.slice(end.end)}`;
|
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.0';
|
package/dist/voice-dna.js
CHANGED
|
@@ -16,12 +16,18 @@ function top(items, limit) {
|
|
|
16
16
|
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
17
17
|
return [...counts].sort((left, right) => right[1] - left[1]).slice(0, limit).map(([item]) => item);
|
|
18
18
|
}
|
|
19
|
+
function measureWriting(text, tokens) {
|
|
20
|
+
const mapped = sentences(text);
|
|
21
|
+
return { text, mapped, tokens: tokens ?? words(text), sentenceWords: mapped.map((sentence) => words(sentence.text)) };
|
|
22
|
+
}
|
|
19
23
|
export function profileMetrics(text) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
24
|
+
return metricsFor(measureWriting(text));
|
|
25
|
+
}
|
|
26
|
+
function metricsFor(writing) {
|
|
27
|
+
const { text, mapped: draftSentences, tokens: draftWords, sentenceWords } = writing;
|
|
28
|
+
const lengths = sentenceWords.map((tokens) => tokens.length);
|
|
29
|
+
const starters = sentenceWords.map((tokens) => tokens[0]?.toLowerCase() ?? '');
|
|
30
|
+
const structures = sentenceWords.map((tokens) => tokens.slice(0, 3).map((word) => word.toLowerCase()).join(' '));
|
|
25
31
|
const lowerWords = draftWords.map((word) => word.toLowerCase());
|
|
26
32
|
const nonStop = lowerWords.filter((word) => word.length > 3 && !STOP_WORDS.has(word));
|
|
27
33
|
const first = lowerWords.filter((word) => ['i', 'we', 'my', 'our', 'us'].includes(word)).length;
|
|
@@ -52,17 +58,18 @@ export function profileMetrics(text) {
|
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
export function measureFounderFingerprint(text) {
|
|
55
|
-
|
|
61
|
+
return fingerprintFor(measureWriting(text, words(text)));
|
|
62
|
+
}
|
|
63
|
+
function fingerprintFor(writing) {
|
|
64
|
+
const { text, mapped: draftSentences, tokens: draftWords, sentenceWords } = writing;
|
|
56
65
|
const wordDenominator = Math.max(1, draftWords.length);
|
|
57
66
|
const contractionCount = draftWords.filter((word) => CONTRACTIONS.has(word.toLowerCase().replaceAll('’', "'"))).length;
|
|
58
|
-
const draftSentences = sentences(text);
|
|
59
67
|
const sentenceDenominator = Math.max(1, draftSentences.length);
|
|
60
68
|
const buckets = { short: 0, medium: 0, long: 0 };
|
|
61
|
-
for (const
|
|
62
|
-
|
|
63
|
-
if (length <= 8)
|
|
69
|
+
for (const tokens of sentenceWords) {
|
|
70
|
+
if (tokens.length <= 8)
|
|
64
71
|
buckets.short += 1;
|
|
65
|
-
else if (length <= 20)
|
|
72
|
+
else if (tokens.length <= 20)
|
|
66
73
|
buckets.medium += 1;
|
|
67
74
|
else
|
|
68
75
|
buckets.long += 1;
|
|
@@ -81,16 +88,21 @@ export function measureFounderFingerprint(text) {
|
|
|
81
88
|
enDashRate: (text.match(/–/g) ?? []).length / wordDenominator,
|
|
82
89
|
};
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
function measureSamples(samples, avoid) {
|
|
85
92
|
if (samples.length < 2)
|
|
86
93
|
throw new Error('Provide at least two local writing samples.');
|
|
87
94
|
if (samples.some((sample) => !words(sample).length))
|
|
88
95
|
throw new Error('Every local writing sample must contain writing.');
|
|
89
|
-
|
|
96
|
+
const writing = measureWriting(samples.join('\n\n'));
|
|
97
|
+
const profile = { version: '2', sampleCount: samples.length, metrics: metricsFor(writing), avoid };
|
|
98
|
+
return { writing, profile };
|
|
99
|
+
}
|
|
100
|
+
export function buildProfile(samples, avoid = []) {
|
|
101
|
+
return measureSamples(samples, avoid).profile;
|
|
90
102
|
}
|
|
91
103
|
export function buildProfileV3(samples, id, channel, avoid = [], tone) {
|
|
92
|
-
const base =
|
|
93
|
-
const
|
|
104
|
+
const { profile: base, writing } = measureSamples(samples, avoid);
|
|
105
|
+
const ruleAllowances = deriveRuleAllowances(samples);
|
|
94
106
|
const fixtureIds = samples.map((_, index) => 'sample.' + String(index + 1).padStart(3, '0'));
|
|
95
107
|
const unsigned = {
|
|
96
108
|
version: '3',
|
|
@@ -101,10 +113,10 @@ export function buildProfileV3(samples, id, channel, avoid = [], tone) {
|
|
|
101
113
|
avoid: base.avoid,
|
|
102
114
|
provenance: { source: 'local-author-owned-samples', rights: 'author-owned', createdAt: new Date().toISOString() },
|
|
103
115
|
rulePolicy: {},
|
|
104
|
-
...(Object.keys(
|
|
116
|
+
...(Object.keys(ruleAllowances).length ? { ruleAllowances } : {}),
|
|
105
117
|
channel,
|
|
106
118
|
...(tone ? { tone } : {}),
|
|
107
|
-
fingerprint:
|
|
119
|
+
fingerprint: fingerprintFor(writing),
|
|
108
120
|
tolerances: {
|
|
109
121
|
contractionRate: { absolute: 0.1, calibrated: false },
|
|
110
122
|
sentenceLengthDistribution: { absolute: 0.15, calibrated: false },
|
|
@@ -121,11 +133,12 @@ function finding(id, severity, sentence, excerpt, reason, suggestion) {
|
|
|
121
133
|
return { engine: 'voice_dna', id, severity, sentence, excerpt, reason, suggestion };
|
|
122
134
|
}
|
|
123
135
|
export function analyzeVoiceDna(text, profile) {
|
|
124
|
-
const
|
|
136
|
+
const writing = measureWriting(text);
|
|
137
|
+
const metrics = metricsFor(writing);
|
|
125
138
|
const findings = [];
|
|
126
139
|
const tolerance = Math.max(8, profile.metrics.sentenceVariation * 2.2);
|
|
127
|
-
for (const sentence of
|
|
128
|
-
const count =
|
|
140
|
+
for (const [index, sentence] of writing.mapped.entries()) {
|
|
141
|
+
const count = writing.sentenceWords[index].length;
|
|
129
142
|
if (Math.abs(count - profile.metrics.sentenceLength) > tolerance)
|
|
130
143
|
findings.push(finding('dna.sentence-length', 'yellow', sentence.index, sentence.text, `Sentence length (${count}) is outside the profile band around ${profile.metrics.sentenceLength}.`, 'Restore the writer’s usual sentence length where it improves clarity.'));
|
|
131
144
|
for (const banned of profile.avoid)
|
|
@@ -139,7 +152,7 @@ export function analyzeVoiceDna(text, profile) {
|
|
|
139
152
|
if (metrics.pointOfView !== profile.metrics.pointOfView && profile.metrics.pointOfView !== 'mixed')
|
|
140
153
|
findings.push(finding('dna.point-of-view', 'yellow', 1, text.slice(0, 160), `Draft point of view is ${metrics.pointOfView}; profile is ${profile.metrics.pointOfView}.`, 'Restore the writer’s normal narrative distance.'));
|
|
141
154
|
if (profile.version === '3') {
|
|
142
|
-
const measured =
|
|
155
|
+
const measured = fingerprintFor(writing);
|
|
143
156
|
const fingerprintChecks = [
|
|
144
157
|
['contraction-rate', measured.contractionRate, profile.fingerprint.contractionRate, profile.tolerances.contractionRate],
|
|
145
158
|
['sentence-length-distribution', Math.max(Math.abs(measured.sentenceLengthDistribution.short - profile.fingerprint.sentenceLengthDistribution.short), Math.abs(measured.sentenceLengthDistribution.medium - profile.fingerprint.sentenceLengthDistribution.medium), Math.abs(measured.sentenceLengthDistribution.long - profile.fingerprint.sentenceLengthDistribution.long)), 0, profile.tolerances.sentenceLengthDistribution],
|
package/dist/writing-examples.js
CHANGED
|
@@ -31,19 +31,14 @@ function validate(inputs) {
|
|
|
31
31
|
function indexExamples(inputs) {
|
|
32
32
|
const entries = inputs.flatMap((input) => sentences(input.text).map((sentence, position) => ({ source: input.basename, position, text: sentence.text, terms: tokens(sentence.text) })));
|
|
33
33
|
const postings = new Map();
|
|
34
|
-
const documentFrequency = new Map();
|
|
35
34
|
for (const [entryIndex, entry] of entries.entries()) {
|
|
36
|
-
const seen = new Set();
|
|
37
35
|
for (const term of entry.terms) {
|
|
38
36
|
const posting = postings.get(term) ?? new Map();
|
|
39
37
|
posting.set(entryIndex, (posting.get(entryIndex) ?? 0) + 1);
|
|
40
38
|
postings.set(term, posting);
|
|
41
|
-
seen.add(term);
|
|
42
39
|
}
|
|
43
|
-
for (const term of seen)
|
|
44
|
-
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
45
40
|
}
|
|
46
|
-
return { entries, postings
|
|
41
|
+
return { entries, postings };
|
|
47
42
|
}
|
|
48
43
|
/** Finds redacted local examples in supplied memory only; it never creates a corpus or index on disk. */
|
|
49
44
|
export function findWritingExamples(query, inputs, limit = 3) {
|
|
@@ -58,7 +53,7 @@ export function findWritingExamples(query, inputs, limit = 3) {
|
|
|
58
53
|
const posting = index.postings.get(term);
|
|
59
54
|
if (!posting)
|
|
60
55
|
continue;
|
|
61
|
-
const idf = Math.log(1 + (index.entries.length -
|
|
56
|
+
const idf = Math.log(1 + (index.entries.length - posting.size + 0.5) / (posting.size + 0.5));
|
|
62
57
|
for (const [entryIndex, frequency] of posting) {
|
|
63
58
|
const entry = index.entries[entryIndex];
|
|
64
59
|
const denominator = frequency + 1.2 * (1 - 0.75 + 0.75 * entry.terms.length / Math.max(1, averageLength));
|