@holdyourvoice/hyv 3.2.0 → 3.3.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.
Files changed (50) hide show
  1. package/Readme.md +51 -11
  2. package/dist/ai-editor-rules.js +5 -2
  3. package/dist/ai-editor.js +52 -9
  4. package/dist/ai-editor.test.js +62 -10
  5. package/dist/approval-capability.js +111 -0
  6. package/dist/approval-capability.test.js +52 -0
  7. package/dist/approval-context.js +54 -0
  8. package/dist/approval-context.test.js +38 -0
  9. package/dist/benchmark.js +232 -0
  10. package/dist/benchmark.test.js +328 -0
  11. package/dist/canonical-json.js +123 -0
  12. package/dist/canonical-json.test.js +24 -0
  13. package/dist/cli.js +272 -19
  14. package/dist/cli.test.js +205 -8
  15. package/dist/hygiene.js +6 -0
  16. package/dist/hygiene.test.js +7 -1
  17. package/dist/judgment-task.js +171 -0
  18. package/dist/judgment-task.test.js +162 -0
  19. package/dist/learning.js +240 -100
  20. package/dist/learning.test.js +203 -3
  21. package/dist/lifecycle-adapter.js +75 -0
  22. package/dist/lifecycle-adapter.test.js +56 -0
  23. package/dist/mcp-tools.js +101 -7
  24. package/dist/mcp-tools.test.js +156 -6
  25. package/dist/mcp.js +213 -6
  26. package/dist/mcp.test.js +210 -11
  27. package/dist/pipeline.js +78 -14
  28. package/dist/pipeline.test.js +36 -2
  29. package/dist/preservation.js +89 -0
  30. package/dist/preservation.test.js +22 -0
  31. package/dist/profile.js +87 -0
  32. package/dist/profile.test.js +114 -0
  33. package/dist/rebuild-task.js +226 -0
  34. package/dist/rebuild-task.test.js +179 -0
  35. package/dist/release-audit.test.js +111 -2
  36. package/dist/rewrite-task.js +136 -16
  37. package/dist/rewrite-task.test.js +62 -7
  38. package/dist/rule-reconciliation.test.js +50 -0
  39. package/dist/semantic-review.js +176 -7
  40. package/dist/semantic-review.test.js +98 -14
  41. package/dist/stage1-dry-run.test.js +39 -0
  42. package/dist/stage1-evaluation.js +579 -0
  43. package/dist/stage1-evaluation.test.js +184 -0
  44. package/dist/stage1-human-packet.test.js +102 -0
  45. package/dist/stage1-schema-contract.test.js +95 -0
  46. package/dist/stage2-human-packet.test.js +81 -0
  47. package/dist/version.js +1 -1
  48. package/dist/voice-dna.js +53 -1
  49. package/dist/voice-dna.test.js +79 -1
  50. package/package.json +2 -2
@@ -1,6 +1,34 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import { analyzeVoiceDna, buildProfile } from './voice-dna.js';
3
+ import { analyzeVoiceDna, buildProfile, measureFounderFingerprint } from './voice-dna.js';
4
+ function profileV3(overrides = {}) {
5
+ const base = buildProfile(['i write plainly. i name the work.', 'i keep the mechanism clear. i avoid filler.']);
6
+ return {
7
+ ...base,
8
+ version: '3',
9
+ id: 'founder.test',
10
+ revision: 1,
11
+ revisionDigest: '0'.repeat(64),
12
+ provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' },
13
+ rulePolicy: {},
14
+ fingerprint: {
15
+ contractionRate: 0,
16
+ sentenceLengthDistribution: { short: 1, medium: 0, long: 0 },
17
+ bulletRate: 0,
18
+ enDashRate: 0,
19
+ ...overrides,
20
+ },
21
+ tolerances: {
22
+ contractionRate: { absolute: 0.05, calibrated: true },
23
+ sentenceLengthDistribution: { absolute: 0.05, calibrated: false },
24
+ bulletRate: { absolute: 0.05, calibrated: false },
25
+ enDashRate: { absolute: 0.05, calibrated: false },
26
+ },
27
+ metricFixtures: {
28
+ contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'],
29
+ },
30
+ };
31
+ }
4
32
  test('requires two local samples before creating a profile', () => {
5
33
  assert.throws(() => buildProfile(['One sample is not enough.']), /at least two local writing samples/);
6
34
  });
@@ -30,3 +58,53 @@ test('keeps the documented VoiceDNA review checks as yellow findings', () => {
30
58
  }
31
59
  assert.equal(report.passed, true);
32
60
  });
61
+ test('keeps buildProfile and v2 analysis output exactly on version 2', () => {
62
+ const profile = buildProfile(['i write plainly.', 'i name the work.']);
63
+ assert.equal(profile.version, '2');
64
+ assert.deepEqual(Object.keys(profile), ['version', 'sampleCount', 'metrics', 'avoid']);
65
+ assert.equal(analyzeVoiceDna('i write plainly.', profile).version, '2');
66
+ });
67
+ test('measures contractions in both directions and accepts curly apostrophes', () => {
68
+ assert.equal(measureFounderFingerprint("I can't stop because I won't stop.").contractionRate, 2 / 7);
69
+ assert.equal(measureFounderFingerprint('I cannot stop because I will not stop.').contractionRate, 0);
70
+ assert.equal(measureFounderFingerprint("I can’t stop.").contractionRate, 1 / 3);
71
+ });
72
+ test('measures sentence buckets, bullets, and en dashes with stable denominators', () => {
73
+ 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.';
74
+ const metrics = measureFounderFingerprint(text);
75
+ assert.deepEqual(metrics.sentenceLengthDistribution, { short: 1 / 3, medium: 1 / 3, long: 1 / 3 });
76
+ assert.equal(metrics.bulletRate, 2 / 3);
77
+ assert.equal(metrics.enDashRate, 1 / 38);
78
+ });
79
+ test('returns zeroed deterministic metrics for empty and tiny inputs', () => {
80
+ assert.deepEqual(measureFounderFingerprint(''), {
81
+ contractionRate: 0,
82
+ sentenceLengthDistribution: { short: 0, medium: 0, long: 0 },
83
+ bulletRate: 0,
84
+ enDashRate: 0,
85
+ });
86
+ assert.equal(measureFounderFingerprint('–').enDashRate, 1);
87
+ });
88
+ test('reports v3 contraction drift in both directions without failing', () => {
89
+ const highTarget = analyzeVoiceDna('I cannot stop because I will not stop.', profileV3({ contractionRate: 0.5 }));
90
+ const lowTarget = analyzeVoiceDna("I can't stop because I won't stop.", profileV3());
91
+ for (const report of [highTarget, lowTarget]) {
92
+ assert.ok(report.findings.some((item) => item.id === 'dna.fingerprint.contraction-rate' && item.severity === 'yellow'));
93
+ assert.equal(report.passed, true);
94
+ }
95
+ });
96
+ test('reports all v3 fingerprint drift in deterministic report-only order', () => {
97
+ const profile = profileV3({
98
+ sentenceLengthDistribution: { short: 0, medium: 1, long: 0 },
99
+ bulletRate: 1,
100
+ enDashRate: 0.5,
101
+ });
102
+ const report = analyzeVoiceDna('One short sentence.', profile);
103
+ assert.deepEqual(report.findings.filter((item) => item.id.startsWith('dna.fingerprint.')).map((item) => item.id), [
104
+ 'dna.fingerprint.sentence-length-distribution',
105
+ 'dna.fingerprint.bullet-rate',
106
+ 'dna.fingerprint.en-dash-rate',
107
+ ]);
108
+ assert.ok(report.findings.filter((item) => item.id.startsWith('dna.fingerprint.')).every((item) => item.severity === 'yellow'));
109
+ assert.equal(report.passed, true);
110
+ });
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@holdyourvoice/hyv",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "A local-first dual-engine writing gate that protects voice and catches generic AI patterns.",
5
5
  "type": "module",
6
6
  "bin": { "hyv": "dist/cli.js" },
7
7
  "files": ["dist", "Readme.md", "LICENSE"],
8
- "scripts": { "build": "tsc -p tsconfig.json", "bundle:claude": "esbuild src/mcp.ts --bundle --platform=node --format=esm --target=node20 --outfile=mcpb/server/index.js", "build:claude": "npm run build && npm run bundle:claude", "pack:claude": "npm run build:claude && node scripts/pack-claude.mjs", "test": "npm run build && node --test dist/**/*.test.js", "check:release": "node scripts/release-audit.mjs", "prepack": "npm run check:release && npm test" },
8
+ "scripts": { "build": "tsc -p tsconfig.json", "bundle:claude": "esbuild src/mcp.ts --bundle --platform=node --format=esm --target=node20 --outfile=mcpb/server/index.js", "build:claude": "npm run build && npm run bundle:claude", "pack:claude": "npm run build:claude && node scripts/pack-claude.mjs", "stage1:evaluate": "node scripts/evaluate-rewrite-benchmark.mjs", "stage1:dry-run": "npm run build && node scripts/run-stage1-dry-run.mjs", "stage1:human-packet": "npm run build && node scripts/run-stage1-human-packet.mjs", "stage2:human-packet": "npm run build && node scripts/run-stage2-human-packet.mjs", "test": "npm run build && node --test dist/**/*.test.js", "check:release": "node scripts/release-audit.mjs", "prepack": "npm run check:release && npm test" },
9
9
  "engines": { "node": ">=20" },
10
10
  "license": "MIT",
11
11
  "keywords": ["ai-writing", "cli", "editing", "voice", "writing"],