@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/voice-dna.test.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { analyzeVoiceDna, buildProfile, buildProfileV3, measureFounderFingerprint } from './voice-dna.js';
|
|
4
|
-
import { parseProfile } from './profile.js';
|
|
5
|
-
function profileV3(overrides = {}) {
|
|
6
|
-
const base = buildProfile(['i write plainly. i name the work.', 'i keep the mechanism clear. i avoid filler.']);
|
|
7
|
-
return {
|
|
8
|
-
...base,
|
|
9
|
-
version: '3',
|
|
10
|
-
id: 'founder.test',
|
|
11
|
-
revision: 1,
|
|
12
|
-
revisionDigest: '0'.repeat(64),
|
|
13
|
-
provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' },
|
|
14
|
-
rulePolicy: {},
|
|
15
|
-
fingerprint: {
|
|
16
|
-
contractionRate: 0,
|
|
17
|
-
sentenceLengthDistribution: { short: 1, medium: 0, long: 0 },
|
|
18
|
-
bulletRate: 0,
|
|
19
|
-
enDashRate: 0,
|
|
20
|
-
...overrides,
|
|
21
|
-
},
|
|
22
|
-
tolerances: {
|
|
23
|
-
contractionRate: { absolute: 0.05, calibrated: true },
|
|
24
|
-
sentenceLengthDistribution: { absolute: 0.05, calibrated: false },
|
|
25
|
-
bulletRate: { absolute: 0.05, calibrated: false },
|
|
26
|
-
enDashRate: { absolute: 0.05, calibrated: false },
|
|
27
|
-
},
|
|
28
|
-
metricFixtures: {
|
|
29
|
-
contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'],
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
test('requires two local samples before creating a profile', () => {
|
|
34
|
-
assert.throws(() => buildProfile(['One sample is not enough.']), /at least two local writing samples/);
|
|
35
|
-
});
|
|
36
|
-
test('requires every local sample to contain writing', () => {
|
|
37
|
-
assert.throws(() => buildProfile(['', ' ']), /must contain writing/);
|
|
38
|
-
});
|
|
39
|
-
test('reports local avoid-list matches as red sentence findings', () => {
|
|
40
|
-
const profile = buildProfile(['i write plainly. i name the work.', 'i keep the mechanism clear. i avoid filler.'], ['unlock']);
|
|
41
|
-
const report = analyzeVoiceDna('i unlock the answer. i name the work.', profile);
|
|
42
|
-
assert.deepEqual(report.findings.filter((finding) => finding.id === 'dna.avoid-list').map((finding) => [finding.severity, finding.sentence]), [['red', 1]]);
|
|
43
|
-
assert.equal(report.passed, false);
|
|
44
|
-
});
|
|
45
|
-
test('keeps a case-style mismatch as a yellow review cue', () => {
|
|
46
|
-
const profile = buildProfile(['i write plainly. i name the work.', 'i keep the mechanism clear. i avoid filler.']);
|
|
47
|
-
const report = analyzeVoiceDna('This sentence starts in standard case.', profile);
|
|
48
|
-
assert.ok(report.findings.some((finding) => finding.id === 'dna.case-style' && finding.severity === 'yellow'));
|
|
49
|
-
assert.equal(report.passed, true);
|
|
50
|
-
});
|
|
51
|
-
test('keeps the documented VoiceDNA review checks as yellow findings', () => {
|
|
52
|
-
const profile = buildProfile([
|
|
53
|
-
'i write short sentences. i ask no questions. i name the mechanism.',
|
|
54
|
-
'i keep the language plain. i explain the work. i use first person.',
|
|
55
|
-
]);
|
|
56
|
-
const report = analyzeVoiceDna('You explain a much longer sentence with several extra words so the reader can follow the full mechanism in detail. Do you agree?', profile);
|
|
57
|
-
for (const id of ['dna.sentence-length', 'dna.question-rate', 'dna.point-of-view']) {
|
|
58
|
-
assert.ok(report.findings.some((finding) => finding.id === id && finding.severity === 'yellow'), id);
|
|
59
|
-
}
|
|
60
|
-
assert.equal(report.passed, true);
|
|
61
|
-
});
|
|
62
|
-
test('keeps buildProfile and v2 analysis output exactly on version 2', () => {
|
|
63
|
-
const profile = buildProfile(['i write plainly.', 'i name the work.']);
|
|
64
|
-
assert.equal(profile.version, '2');
|
|
65
|
-
assert.deepEqual(Object.keys(profile), ['version', 'sampleCount', 'metrics', 'avoid']);
|
|
66
|
-
assert.equal(analyzeVoiceDna('i write plainly.', profile).version, '2');
|
|
67
|
-
});
|
|
68
|
-
test('builds a signed channel-specific Profile v3 without retaining sample prose', () => {
|
|
69
|
-
const profile = buildProfileV3([
|
|
70
|
-
'I write directly about the release — and name the owner.',
|
|
71
|
-
'I keep the mechanism visible — then choose the next step.',
|
|
72
|
-
], 'founder.email', 'email', [], { formality: 0.4, confidence: 0.7, warmth: 0.6, energy: 0.3, complexity: 0.5 });
|
|
73
|
-
assert.equal(profile.channel, 'email');
|
|
74
|
-
assert.equal(profile.ruleAllowances?.['punct.em-dash']?.sampleCount, 2);
|
|
75
|
-
assert.strictEqual(parseProfile(profile), profile);
|
|
76
|
-
assert.equal(JSON.stringify(profile).includes('keep the mechanism visible'), false);
|
|
77
|
-
});
|
|
78
|
-
test('measures contractions in both directions and accepts curly apostrophes', () => {
|
|
79
|
-
assert.equal(measureFounderFingerprint("I can't stop because I won't stop.").contractionRate, 2 / 7);
|
|
80
|
-
assert.equal(measureFounderFingerprint('I cannot stop because I will not stop.').contractionRate, 0);
|
|
81
|
-
assert.equal(measureFounderFingerprint("I can’t stop.").contractionRate, 1 / 3);
|
|
82
|
-
});
|
|
83
|
-
test('measures sentence buckets, bullets, and en dashes with stable denominators', () => {
|
|
84
|
-
const text = '- One short line.\n- This medium sentence contains exactly nine ordinary words right now.\nThis final sentence has more than twenty words so it lands in the long distribution bucket without relying on punctuation tricks or hidden tokens – done.';
|
|
85
|
-
const metrics = measureFounderFingerprint(text);
|
|
86
|
-
assert.deepEqual(metrics.sentenceLengthDistribution, { short: 1 / 3, medium: 1 / 3, long: 1 / 3 });
|
|
87
|
-
assert.equal(metrics.bulletRate, 2 / 3);
|
|
88
|
-
assert.equal(metrics.enDashRate, 1 / 38);
|
|
89
|
-
});
|
|
90
|
-
test('returns zeroed deterministic metrics for empty and tiny inputs', () => {
|
|
91
|
-
assert.deepEqual(measureFounderFingerprint(''), {
|
|
92
|
-
contractionRate: 0,
|
|
93
|
-
sentenceLengthDistribution: { short: 0, medium: 0, long: 0 },
|
|
94
|
-
bulletRate: 0,
|
|
95
|
-
enDashRate: 0,
|
|
96
|
-
});
|
|
97
|
-
assert.equal(measureFounderFingerprint('–').enDashRate, 1);
|
|
98
|
-
});
|
|
99
|
-
test('reports v3 contraction drift in both directions without failing', () => {
|
|
100
|
-
const highTarget = analyzeVoiceDna('I cannot stop because I will not stop.', profileV3({ contractionRate: 0.5 }));
|
|
101
|
-
const lowTarget = analyzeVoiceDna("I can't stop because I won't stop.", profileV3());
|
|
102
|
-
for (const report of [highTarget, lowTarget]) {
|
|
103
|
-
assert.ok(report.findings.some((item) => item.id === 'dna.fingerprint.contraction-rate' && item.severity === 'yellow'));
|
|
104
|
-
assert.equal(report.passed, true);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
test('reports all v3 fingerprint drift in deterministic report-only order', () => {
|
|
108
|
-
const profile = profileV3({
|
|
109
|
-
sentenceLengthDistribution: { short: 0, medium: 1, long: 0 },
|
|
110
|
-
bulletRate: 1,
|
|
111
|
-
enDashRate: 0.5,
|
|
112
|
-
});
|
|
113
|
-
const report = analyzeVoiceDna('One short sentence.', profile);
|
|
114
|
-
assert.deepEqual(report.findings.filter((item) => item.id.startsWith('dna.fingerprint.')).map((item) => item.id), [
|
|
115
|
-
'dna.fingerprint.sentence-length-distribution',
|
|
116
|
-
'dna.fingerprint.bullet-rate',
|
|
117
|
-
'dna.fingerprint.en-dash-rate',
|
|
118
|
-
]);
|
|
119
|
-
assert.ok(report.findings.filter((item) => item.id.startsWith('dna.fingerprint.')).every((item) => item.severity === 'yellow'));
|
|
120
|
-
assert.equal(report.passed, true);
|
|
121
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { findWritingExamples } from './writing-examples.js';
|
|
4
|
-
test('retrieves at most three redacted excerpts with basenames only', () => {
|
|
5
|
-
const result = findWritingExamples('The rollout preserves the retry queue.', [
|
|
6
|
-
{ basename: 'email.md', text: 'The retry queue preserves work after a rollout; contact owner@example.com.' },
|
|
7
|
-
{ basename: 'notes.md', text: 'A rollback keeps the queue visible for operators.' },
|
|
8
|
-
{ basename: 'third.md', text: 'This unrelated sentence does not rank.' },
|
|
9
|
-
]);
|
|
10
|
-
assert.equal(result.length, 2);
|
|
11
|
-
assert.deepEqual(result.map((item) => item.source), ['email.md', 'notes.md']);
|
|
12
|
-
assert.match(result[0].text, /REDACTED:EMAIL/);
|
|
13
|
-
assert.equal(result[0].text.includes('owner@example.com'), false);
|
|
14
|
-
assert.equal(JSON.stringify(result).includes('/'), false);
|
|
15
|
-
});
|
|
16
|
-
test('requires explicit bounded in-memory samples and a real basename', () => {
|
|
17
|
-
assert.throws(() => findWritingExamples('queue', []), /one to 64/);
|
|
18
|
-
assert.throws(() => findWritingExamples('queue', [{ basename: '/private/email.md', text: 'The queue is visible.' }]), /basenames only/);
|
|
19
|
-
});
|
|
20
|
-
test('lookup cannot open a socket', async () => {
|
|
21
|
-
const net = (await import('node:' + 'net')).default;
|
|
22
|
-
const writable = net;
|
|
23
|
-
const originalConnect = writable.connect;
|
|
24
|
-
const originalCreateConnection = writable.createConnection;
|
|
25
|
-
const denied = () => { throw new Error('network must remain unavailable'); };
|
|
26
|
-
writable.connect = denied;
|
|
27
|
-
writable.createConnection = denied;
|
|
28
|
-
try {
|
|
29
|
-
assert.equal(findWritingExamples('queue', [{ basename: 'one.md', text: 'The queue stays local.' }]).length, 1);
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
writable.connect = originalConnect;
|
|
33
|
-
writable.createConnection = originalCreateConnection;
|
|
34
|
-
}
|
|
35
|
-
});
|