@holdyourvoice/hyv 3.6.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/Readme.md +93 -154
  2. package/dist/ai-editor-rules.js +1 -0
  3. package/dist/ai-editor.js +23 -33
  4. package/dist/analysis.js +33 -0
  5. package/dist/cli/agents.js +88 -0
  6. package/dist/cli/checks.js +226 -0
  7. package/dist/cli/io.js +141 -0
  8. package/dist/cli/lifecycle.js +162 -0
  9. package/dist/cli/profiles.js +246 -0
  10. package/dist/cli/rewriting.js +111 -0
  11. package/dist/cli.js +5 -974
  12. package/dist/copy-spec.js +6 -6
  13. package/dist/editorial-packs.js +4 -3
  14. package/dist/fact-linter.js +45 -27
  15. package/dist/hidden-text.js +6 -6
  16. package/dist/hold-your-voice.mcpb +0 -0
  17. package/dist/internal.js +8 -0
  18. package/dist/judgment-task.js +10 -14
  19. package/dist/learning.js +77 -71
  20. package/dist/lifecycle-adapter.js +2 -5
  21. package/dist/local-eval.js +16 -31
  22. package/dist/mcp-server.js +272 -0
  23. package/dist/mcp-tools.js +10 -19
  24. package/dist/mcp.js +2 -267
  25. package/dist/pipeline.js +18 -105
  26. package/dist/profile-compose.js +18 -11
  27. package/dist/profile-score.js +6 -6
  28. package/dist/profile.js +17 -19
  29. package/dist/provenance-status.js +1 -3
  30. package/dist/rebuild-task.js +18 -52
  31. package/dist/recomposition.js +18 -13
  32. package/dist/rewrite-prompt.js +82 -0
  33. package/dist/rewrite-response.js +27 -0
  34. package/dist/rewrite-task.js +73 -98
  35. package/dist/rule-allowances.js +6 -10
  36. package/dist/semantic-review.js +71 -47
  37. package/dist/stage1-evaluation.js +29 -14
  38. package/dist/strict-quality.js +10 -18
  39. package/dist/version.js +1 -1
  40. package/dist/voice-dna.js +34 -21
  41. package/dist/writing-examples.js +2 -7
  42. package/package.json +6 -3
  43. package/skills/hyv-prepare-judgment/SKILL.md +4 -0
  44. package/dist/agents/catalog.test.js +0 -60
  45. package/dist/agents/emit.test.js +0 -64
  46. package/dist/agents/load.test.js +0 -149
  47. package/dist/ai-editor.test.js +0 -265
  48. package/dist/approval-capability.test.js +0 -52
  49. package/dist/approval-context.test.js +0 -38
  50. package/dist/backtest.test.js +0 -20
  51. package/dist/benchmark.test.js +0 -328
  52. package/dist/canonical-json.test.js +0 -24
  53. package/dist/cli.test.js +0 -731
  54. package/dist/editorial-packs.test.js +0 -94
  55. package/dist/fact-linter.test.js +0 -85
  56. package/dist/hidden-text.test.js +0 -26
  57. package/dist/hygiene.test.js +0 -83
  58. package/dist/judgment-task.test.js +0 -162
  59. package/dist/learning.test.js +0 -325
  60. package/dist/lifecycle-adapter.test.js +0 -56
  61. package/dist/local-eval.test.js +0 -20
  62. package/dist/logic-linter-corpus.test.js +0 -22
  63. package/dist/logic-linter.test.js +0 -39
  64. package/dist/mcp-tools.test.js +0 -286
  65. package/dist/mcp.test.js +0 -312
  66. package/dist/mirror-refs.test.js +0 -63
  67. package/dist/pipeline.test.js +0 -247
  68. package/dist/preservation.test.js +0 -22
  69. package/dist/production-gates.test.js +0 -34
  70. package/dist/profile-compose.test.js +0 -32
  71. package/dist/profile-score.test.js +0 -22
  72. package/dist/profile-watch.test.js +0 -23
  73. package/dist/profile.test.js +0 -141
  74. package/dist/provenance-status.test.js +0 -22
  75. package/dist/rebuild-task.test.js +0 -206
  76. package/dist/recomposition.test.js +0 -34
  77. package/dist/release-audit.test.js +0 -292
  78. package/dist/rewrite-task.test.js +0 -166
  79. package/dist/rule-allowances.test.js +0 -17
  80. package/dist/rule-reconciliation.test.js +0 -50
  81. package/dist/sample-ingest.test.js +0 -52
  82. package/dist/semantic-review.test.js +0 -101
  83. package/dist/stage1-dry-run.test.js +0 -39
  84. package/dist/stage1-evaluation.test.js +0 -184
  85. package/dist/stage1-human-packet.test.js +0 -102
  86. package/dist/stage1-schema-contract.test.js +0 -95
  87. package/dist/stage2-human-packet.test.js +0 -81
  88. package/dist/strict-quality.test.js +0 -62
  89. package/dist/text-provenance.feature.test.js +0 -45
  90. package/dist/text.test.js +0 -16
  91. package/dist/voice-dna.test.js +0 -121
  92. package/dist/writing-examples.test.js +0 -35
@@ -1,166 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { createHash } from 'node:crypto';
3
- import test from 'node:test';
4
- import { applyRewriteResponse, createRewriteLifecycleBinding, evaluateRewriteResponse, parseRewriteTask, prepareRewriteTask } from './rewrite-task.js';
5
- import { parseWritingBrief } from './editorial-packs.js';
6
- import { canonicalJson } from './canonical-json.js';
7
- import { verifyDeterministically } from './pipeline.js';
8
- import { buildProfile } from './voice-dna.js';
9
- const profile = buildProfile([
10
- 'I write clear notes. I keep the mechanism visible.',
11
- 'I name the trade-off. Then I make the next step plain.',
12
- ], ['leverage']);
13
- test('applies only eligible numbered replacements and preserves all clean bytes', () => {
14
- const task = prepareRewriteTask('I leverage the answer. The launch is on 14 August.', profile);
15
- const result = applyRewriteResponse(task, {
16
- version: '1',
17
- taskFingerprint: task.fingerprint,
18
- replacements: [{ sentenceId: 1, text: 'I use the answer.' }],
19
- });
20
- assert.equal(result.status, 'accepted');
21
- assert.equal(result.candidate, 'I use the answer. The launch is on 14 August.');
22
- assert.deepEqual(result.receipt.adapterIds, []);
23
- });
24
- test('makes every active AI Editor finding eligible under the default strict rewrite policy', () => {
25
- const task = prepareRewriteTask('I write clear notes. This work is meaningful. I keep the mechanism visible.', profile);
26
- assert.deepEqual(task.eligibleSentenceIds, [2]);
27
- assert.deepEqual(task.sentences.map((sentence) => sentence.eligible), [false, true, false]);
28
- assert.match(task.prompt, /Every active AI Editor finding is a required repair/);
29
- });
30
- test('makes advisory and judgment-required findings required repairs', () => {
31
- const advisory = prepareRewriteTask('Firstly, check the invoice.', profile);
32
- assert.deepEqual(advisory.eligibleSentenceIds, [1]);
33
- const neutralProfile = buildProfile(['I write plainly.', 'I name the mechanism.']);
34
- const pending = prepareRewriteTask('We leverage the scheduler.', neutralProfile);
35
- assert.deepEqual(pending.eligibleSentenceIds, [1]);
36
- assert.match(pending.prompt, /Every active AI Editor finding is a required repair/);
37
- const blocking = prepareRewriteTask('The scheduler failed — twice. The owner checked it.', profile);
38
- assert.deepEqual(blocking.eligibleSentenceIds, [1]);
39
- });
40
- test('keeps blocking VoiceDNA avoid findings eligible', () => {
41
- const task = prepareRewriteTask('The plan uses leverage.', profile);
42
- assert.deepEqual(task.eligibleSentenceIds, [1]);
43
- });
44
- test('makes reconciled active founder reframes eligible under the default strict policy', () => {
45
- const task = prepareRewriteTask("This isn't positioning. This is proof.", profile);
46
- assert.deepEqual(task.eligibleSentenceIds, [1]);
47
- });
48
- test('preserves a clean draft byte-for-byte when the rewrite response is empty', () => {
49
- const draft = 'I write clear notes.\n\nI keep the mechanism visible.\n';
50
- const task = prepareRewriteTask(draft, profile);
51
- assert.deepEqual(task.eligibleSentenceIds, []);
52
- const result = applyRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [] });
53
- assert.equal(result.status, 'accepted');
54
- assert.equal(result.candidate, draft);
55
- });
56
- test('rejects an attempted edit to a clean sentence without changing any source bytes', () => {
57
- const draft = 'I leverage the answer.\n\nThe launch starts Tuesday.\n';
58
- const task = prepareRewriteTask(draft, profile);
59
- const result = applyRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 2, text: 'The launch starts Wednesday.' }] });
60
- assert.equal(result.status, 'repairable');
61
- assert.equal(result.candidate, undefined);
62
- assert.equal(result.failures[0]?.code, 'ineligible_sentence_id');
63
- assert.equal(task.draft, draft);
64
- });
65
- test('carries WritingBrief context into a fingerprinted rewrite task', () => {
66
- const brief = parseWritingBrief({ version: '1', audience: 'founders', intent: 'start a discussion', format: 'social' });
67
- const task = prepareRewriteTask('A pattern I keep seeing in founder posts is vague advice.', profile, undefined, brief);
68
- assert.equal(task.writingBrief?.format, 'social');
69
- assert.deepEqual(task.eligibleSentenceIds, []);
70
- });
71
- test('rejects a fingerprint-valid rewrite task with malformed WritingBrief data', () => {
72
- const task = prepareRewriteTask('I leverage the answer.', profile);
73
- const { fingerprint: _fingerprint, ...taskBase } = task;
74
- const base = { ...taskBase, writingBrief: { version: '1', audience: 'operators', intent: 'write', format: 'general', prohibitedTerms: [42] } };
75
- const malformed = { ...base, fingerprint: createHash('sha256').update(canonicalJson(base)).digest('hex') };
76
- assert.throws(() => parseRewriteTask(malformed), /WritingBrief/);
77
- });
78
- test('rejects a fingerprint-valid rewrite task with a null WritingBrief', () => {
79
- const task = prepareRewriteTask('I leverage the answer.', profile);
80
- const { fingerprint: _fingerprint, ...taskBase } = task;
81
- const base = { ...taskBase, writingBrief: null };
82
- const malformed = { ...base, fingerprint: createHash('sha256').update(canonicalJson(base)).digest('hex') };
83
- assert.throws(() => parseRewriteTask(malformed), /WritingBrief/);
84
- });
85
- test('rejects a duplicate, unknown, or clean sentence replacement before it creates a candidate', () => {
86
- const task = prepareRewriteTask('I leverage the answer. The launch is on 14 August.', profile);
87
- const duplicate = applyRewriteResponse(task, JSON.stringify({
88
- version: '1', taskFingerprint: task.fingerprint,
89
- replacements: [{ sentenceId: 1, text: 'I use the answer.' }, { sentenceId: 1, text: 'I choose the answer.' }],
90
- }));
91
- assert.equal(duplicate.status, 'repairable');
92
- assert.equal(duplicate.candidate, undefined);
93
- assert.equal(duplicate.failures[0]?.code, 'duplicate_sentence_id');
94
- const unknown = applyRewriteResponse(task, {
95
- version: '1', taskFingerprint: task.fingerprint,
96
- replacements: [{ sentenceId: 99, text: 'I use the answer.' }],
97
- });
98
- assert.equal(unknown.status, 'repairable');
99
- assert.equal(unknown.failures[0]?.code, 'unknown_sentence_id');
100
- });
101
- test('repairs only a stringified replacement list after initial schema rejection', () => {
102
- const task = prepareRewriteTask('I leverage the answer.', profile);
103
- const result = applyRewriteResponse(task, JSON.stringify({
104
- version: '1', taskFingerprint: task.fingerprint,
105
- replacements: JSON.stringify([{ sentenceId: 1, text: 'I use the answer.' }]),
106
- }));
107
- assert.equal(result.status, 'accepted');
108
- assert.equal(result.candidate, 'I use the answer.');
109
- assert.deepEqual(result.receipt.adapterIds, ['stringified_replacements_v1']);
110
- });
111
- test('keeps a valid response byte-for-byte unchanged by repair adapters', () => {
112
- const task = prepareRewriteTask('I leverage the answer.', profile);
113
- const response = JSON.stringify({ version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the answer.' }] });
114
- const result = applyRewriteResponse(task, response);
115
- assert.equal(result.status, 'accepted');
116
- assert.equal(result.receipt.responseFingerprint.length, 64);
117
- assert.deepEqual(result.receipt.adapterIds, []);
118
- assert.deepEqual(result.receipt.replacementSentenceIds, [1]);
119
- });
120
- test('withholds an unresolved final-output candidate from evaluation results', () => {
121
- const task = prepareRewriteTask('I leverage the answer.', profile);
122
- const result = evaluateRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the answer.\u200B' }] }, profile);
123
- assert.equal(result.status, 'needs_escalation');
124
- assert.equal(result.candidate, undefined);
125
- assert.equal(result.verification?.finalOutput.accepted, false);
126
- });
127
- test('returns the final-gate-cleaned candidate and hashes that value', () => {
128
- const task = prepareRewriteTask('I leverage the answer with useful detail and clear mechanism.', profile);
129
- const result = evaluateRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the\u0007 answer with useful detail and clear mechanism.' }] }, profile);
130
- assert.equal(result.status, 'needs_semantic_review');
131
- assert.equal(result.candidate, 'I use the answer with useful detail and clear mechanism.');
132
- assert.equal(result.verification?.finalOutput.changed, true);
133
- assert.equal(result.deterministicArtifact?.candidateHash, createHash('sha256').update('I use the answer with useful detail and clear mechanism.').digest('hex'));
134
- });
135
- test('binds the task, response, deterministic artifact, text hashes, and profile revision', () => {
136
- const draft = 'I write clear notes.';
137
- const task = prepareRewriteTask(draft, profile);
138
- const applied = applyRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [] });
139
- assert.equal(applied.status, 'accepted');
140
- const artifact = verifyDeterministically(draft, applied.candidate, profile).artifact;
141
- const binding = createRewriteLifecycleBinding(task, applied.receipt, artifact);
142
- assert.equal(binding.rewriteTaskFingerprint, task.fingerprint);
143
- assert.equal(binding.rewriteResponseFingerprint, applied.receipt.responseFingerprint);
144
- assert.equal(binding.deterministicArtifactFingerprint, artifact.artifactFingerprint);
145
- assert.match(binding.sourceHash, /^[a-f0-9]{64}$/);
146
- });
147
- test('returns the deterministic artifact and lifecycle binding at the semantic gate', () => {
148
- const task = prepareRewriteTask('I write clear notes.', profile);
149
- const result = evaluateRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [] }, profile);
150
- assert.equal(result.status, 'needs_semantic_review');
151
- assert.equal(result.deterministicArtifact?.passed, true);
152
- assert.equal(result.lifecycleBinding?.deterministicArtifactFingerprint, result.deterministicArtifact?.artifactFingerprint);
153
- });
154
- test('keeps object response fingerprints stable across property order', () => {
155
- const task = prepareRewriteTask('I leverage the answer.', profile);
156
- const first = applyRewriteResponse(task, { version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the answer.' }] });
157
- const second = applyRewriteResponse(task, { replacements: [{ text: 'I use the answer.', sentenceId: 1 }], taskFingerprint: task.fingerprint, version: '1' });
158
- assert.equal(first.receipt.responseFingerprint, second.receipt.responseFingerprint);
159
- });
160
- test('repairs only an exact outer JSON code fence after JSON parsing fails', () => {
161
- const task = prepareRewriteTask('I leverage the answer with useful detail and clear mechanism.', profile);
162
- const response = `\`\`\`json\n${JSON.stringify({ version: '1', taskFingerprint: task.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the answer with useful detail and clear mechanism.' }] })}\n\`\`\``;
163
- const result = applyRewriteResponse(task, response);
164
- assert.equal(result.status, 'accepted');
165
- assert.deepEqual(result.receipt.adapterIds, ['fenced_json_v1']);
166
- });
@@ -1,17 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { deriveRuleAllowances } from './rule-allowances.js';
4
- test('derives only non-verbatim style allowances from repeated sample evidence', () => {
5
- const allowances = deriveRuleAllowances([
6
- 'I write this way — with a deliberate turn.',
7
- 'The point is direct — then I explain it.',
8
- 'The note says “ship Tuesday” after the check.',
9
- 'The runbook repeats “ship Tuesday” as a decision.',
10
- 'A plain sample has no allowance.',
11
- ]);
12
- assert.deepEqual(Object.keys(allowances), ['punct.em-dash', 'format.curly-quotes']);
13
- assert.equal(allowances['punct.em-dash'].sampleCount, 2);
14
- assert.match(allowances['punct.em-dash'].evidenceDigest, /^[a-f0-9]{64}$/);
15
- assert.equal(allowances['format.curly-quotes'].sampleCount, 2);
16
- assert.equal(JSON.stringify(allowances).includes('deliberate turn'), false);
17
- });
@@ -1,50 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { rules } from './ai-editor-rules.js';
4
- function rule(id) {
5
- const match = rules.find((candidate) => candidate.id === id);
6
- assert.ok(match, `missing rule ${id}`);
7
- return match;
8
- }
9
- test('struct.not-x-y only detects an explicit two-beat contrast', () => {
10
- const matcher = rule('struct.not-x-y').expression;
11
- assert.equal(matcher.test('Not speed. Reliability.'), true);
12
- assert.equal(matcher.test('This does not include the standard onboarding steps every team runs.'), false);
13
- });
14
- test('keeps the explicit this-isnt reframe matcher available to policy', () => {
15
- const matcher = rule('struct.this-isnt-x-this-is-y').expression;
16
- assert.equal(matcher.test("This isn't positioning. This is proof."), true);
17
- assert.equal(matcher.test('This positioning claim has proof.'), false);
18
- });
19
- test('treats both long dash characters as red findings', () => {
20
- const emDash = rule('punct.em-dash');
21
- const enDash = rule('punct.en-dash');
22
- assert.equal(emDash.severity, 'red');
23
- assert.equal(enDash.severity, 'red');
24
- assert.equal(emDash.expression.test('The job failed — retry it.'), true);
25
- assert.equal(enDash.expression.test('Use pages 6–11.'), true);
26
- assert.equal(emDash.expression.test('The job failed; retry it.'), false);
27
- assert.equal(enDash.expression.test('Use pages 6-11.'), false);
28
- });
29
- test('detects clear performative-sincerity phrases as red', () => {
30
- const matcher = rule('formula.performative-sincerity');
31
- assert.equal(matcher.severity, 'red');
32
- for (const text of ['To be honest, the launch failed.', 'In all honesty, the launch failed.']) {
33
- assert.equal(matcher.expression.test(text), true, text);
34
- }
35
- assert.equal(matcher.expression.test('The honest report says the launch failed.'), false);
36
- });
37
- test('detects performative-sincerity adverbs as yellow', () => {
38
- const matcher = rule('hedge.performative-sincerity-adverb');
39
- assert.equal(matcher.severity, 'yellow');
40
- for (const text of [
41
- 'Honestly, the launch failed.',
42
- 'Genuinely, the launch failed.',
43
- 'Truly, the launch failed.',
44
- 'Frankly, the launch failed.',
45
- 'Actually, the launch failed.',
46
- ]) {
47
- assert.equal(matcher.expression.test(text), true, text);
48
- }
49
- assert.equal(matcher.expression.test('The genuine invoice arrived.'), false);
50
- });
@@ -1,52 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { ingestGmailSentMbox, ingestTelegramDesktopJson } from './sample-ingest.js';
4
- test('ingests only owner Gmail sent messages and redacts sensitive data before output', () => {
5
- const mbox = [
6
- 'From sender@example.com Mon Jan 1 00:00:00 2026',
7
- 'From: Owner <owner@example.com>',
8
- '',
9
- 'Call +1 415 555 0100 or owner@example.com. Secret client detail stays out.',
10
- 'From sender@example.com Mon Jan 2 00:00:00 2026',
11
- 'From: Other <other@example.com>',
12
- '',
13
- 'This message must not become a sample.',
14
- ].join('\n');
15
- const result = ingestGmailSentMbox(mbox, 'Owner <owner@example.com>', ['secret client']);
16
- assert.equal(result.samples.length, 1);
17
- assert.match(result.samples[0], /REDACTED:PHONE/);
18
- assert.match(result.samples[0], /REDACTED:EMAIL/);
19
- assert.equal(result.samples[0].includes('Secret client'), false);
20
- assert.equal(result.receipt.droppedSentences, 1);
21
- assert.equal(JSON.stringify(result.receipt).includes('Secret client'), false);
22
- assert.equal(JSON.stringify(result.receipt).includes('owner@example.com'), false);
23
- });
24
- test('ingests Telegram Desktop JSON by explicit owner and never places raw text in the receipt', () => {
25
- const source = JSON.stringify({ messages: [
26
- { type: 'message', from: 'Shashank', text: ['A link is ', { type: 'link', text: 'https://example.com/private' }, '.'] },
27
- { type: 'message', from: 'Other', text: 'Ignore me.' },
28
- ] });
29
- const result = ingestTelegramDesktopJson(source, 'Shashank');
30
- assert.equal(result.samples.length, 1);
31
- assert.match(result.samples[0], /REDACTED:URL/);
32
- assert.equal(JSON.stringify(result.receipt).includes('Ignore me'), false);
33
- assert.equal(JSON.stringify(result.receipt).includes('example.com'), false);
34
- assert.equal(result.receipt.presidio, 'NOT_CONFIGURED');
35
- });
36
- test('parsing local exports cannot open a socket', async () => {
37
- const net = (await import('node:' + 'net')).default;
38
- const writable = net;
39
- const originalConnect = writable.connect;
40
- const originalCreateConnection = writable.createConnection;
41
- const denied = () => { throw new Error('network must remain unavailable'); };
42
- writable.connect = denied;
43
- writable.createConnection = denied;
44
- try {
45
- const result = ingestTelegramDesktopJson(JSON.stringify({ messages: [{ type: 'message', from: 'Owner', text: 'A local export stays local.' }] }), 'Owner');
46
- assert.equal(result.samples.length, 1);
47
- }
48
- finally {
49
- writable.connect = originalConnect;
50
- writable.createConnection = originalCreateConnection;
51
- }
52
- });
@@ -1,101 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { createHash, generateKeyPairSync, sign } from 'node:crypto';
3
- import test from 'node:test';
4
- import { canonicalJson, canonicalJsonBytes } from './canonical-json.js';
5
- import { createInitialLifecycleArtifact, parseSemanticVerdict, prepareSemanticReviewTask, reduceRewriteLifecycle, reviewSemanticVerdicts } from './semantic-review.js';
6
- const deterministicBase = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
7
- const deterministicFingerprint = createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex');
8
- const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministicFingerprint, sourceHash: deterministicBase.sourceHash, candidateHash: deterministicBase.candidateHash, profileId: deterministicBase.profileId, profileRevisionDigest: deterministicBase.profileRevisionDigest, rulesetVersion: deterministicBase.rulesetVersion, schemaVersion: '1' };
9
- const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [3, 1] };
10
- function task(policy = 'normal') { return prepareSemanticReviewTask(binding, policy, receipt, ['action_change']); }
11
- function deterministic(passed = true) { const base = { ...deterministicBase, passed }; return { ...base, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(base)}`).digest('hex') }; }
12
- const emptyTrust = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [] };
13
- const context = { now: 150, trustStore: emptyTrust, authorizedSemanticEvaluatorIds: { normal: ['one'], highAssurance: ['one', 'two', 'three'] }, authorizedHumanFinalizerIds: ['human-1'] };
14
- test('preserves the version 3.2 semantic aggregation API', () => {
15
- const verdicts = ['one', 'two', 'three'].map((id) => parseSemanticVerdict(id, { approved: true, violations: [] }));
16
- assert.equal(reviewSemanticVerdicts(verdicts).status, 'accepted');
17
- });
18
- test('normal review clears with one clean authorized verdict and rejection escalates', () => {
19
- const reviewTask = task();
20
- const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
21
- const clean = parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] });
22
- const cleared = reduceRewriteLifecycle(initial, { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [clean] }, context);
23
- assert.equal(cleared.ok && cleared.artifact.status, 'ready_for_human_review');
24
- const rejected = parseSemanticVerdict(reviewTask, 'one', { approved: false, violations: ['action_change'] });
25
- const escalated = reduceRewriteLifecycle(initial, { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [rejected] }, context);
26
- assert.equal(escalated.ok && escalated.artifact.status, 'needs_escalation');
27
- });
28
- test('high assurance is permutation-stable and requires three configured evaluators', () => {
29
- const reviewTask = task('high_assurance');
30
- const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
31
- const verdicts = ['three', 'one', 'two'].map((id) => parseSemanticVerdict(reviewTask, id, { approved: true, violations: [] }));
32
- const action = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts };
33
- const left = reduceRewriteLifecycle(initial, action, context);
34
- const right = reduceRewriteLifecycle(initial, { ...action, verdicts: [...verdicts].reverse() }, context);
35
- assert.deepEqual(left, right);
36
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...action, verdicts: verdicts.slice(0, 2) }, context), { ok: false, error: 'invalid_verdict_count' });
37
- });
38
- test('enforces replay, parent order, terminal states, bindings, and evaluator authority', () => {
39
- const reviewTask = task();
40
- const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
41
- const action = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] })] };
42
- const next = reduceRewriteLifecycle(initial, action, context);
43
- assert.equal(next.ok, true);
44
- if (!next.ok)
45
- return;
46
- assert.strictEqual(reduceRewriteLifecycle(next.artifact, action, context).ok, true);
47
- assert.deepEqual(reduceRewriteLifecycle(next.artifact, { ...action, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: false, violations: ['action_change'] })] }, context), { ok: false, error: 'conflicting_replay' });
48
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...action, parentArtifactFingerprint: '9'.repeat(64) }, context), { ok: false, error: 'stale_parent' });
49
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...action, verdicts: [parseSemanticVerdict(reviewTask, 'unknown', { approved: true, violations: [] })] }, context), { ok: false, error: 'evaluator_not_authorized' });
50
- const terminal = reduceRewriteLifecycle(createInitialLifecycleArtifact(reviewTask, deterministic()), { ...action, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: false, violations: ['action_change'] })] }, context);
51
- if (terminal.ok)
52
- assert.deepEqual(reduceRewriteLifecycle(terminal.artifact, { ...action, parentArtifactFingerprint: terminal.artifact.artifactFingerprint }, context), { ok: false, error: 'terminal_state' });
53
- });
54
- test('authorized human rejection escalates; approval requires a valid external capability', () => {
55
- const reviewTask = task();
56
- const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
57
- const semantic = reduceRewriteLifecycle(initial, { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] })] }, context);
58
- assert.equal(semantic.ok, true);
59
- if (!semantic.ok)
60
- return;
61
- const approvalBase = { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: { kind: 'candidate' }, decision: 'approve' };
62
- assert.deepEqual(reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: approvalBase }, context), { ok: false, error: 'capability_required' });
63
- assert.deepEqual(reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: { ...approvalBase, capability: { payload: 'bad', signature: 'bad' } } }, context), { ok: false, error: 'capability_invalid' });
64
- const rejection = reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: { kind: 'candidate' }, decision: 'reject' } }, context);
65
- assert.equal(rejection.ok && rejection.artifact.status, 'needs_escalation');
66
- const { publicKey, privateKey } = generateKeyPairSync('ed25519');
67
- const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host', keyId: 'k1', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
68
- const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: semantic.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'nonce' };
69
- const payload = canonicalJsonBytes(claims);
70
- const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
71
- const approved = reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: { kind: 'candidate' }, decision: 'approve', capability } }, { ...context, trustStore });
72
- assert.equal(approved.ok && approved.artifact.status, 'approved');
73
- assert.doesNotMatch(JSON.stringify(approved), /nonce|payload|signature/);
74
- });
75
- test('failed deterministic verification cannot create an initial lifecycle artifact', () => {
76
- assert.throws(() => createInitialLifecycleArtifact(task(), deterministic(false)), /passed deterministic verification/);
77
- const reviewTask = task();
78
- assert.throws(() => createInitialLifecycleArtifact({ ...reviewTask, evidenceScope: { sentenceIds: [99] } }, deterministic()), /passed deterministic verification/);
79
- });
80
- test('rejects oversized or deeply nested actions as invalid without throwing', () => {
81
- const reviewTask = task();
82
- const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
83
- const verdict = parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] });
84
- const oversized = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: Array.from({ length: 1000 }, () => verdict) };
85
- assert.deepEqual(reduceRewriteLifecycle(initial, oversized, context), { ok: false, error: 'invalid_action' });
86
- let nested = 'x';
87
- for (let index = 0; index < 70; index += 1)
88
- nested = [nested];
89
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...oversized, verdicts: [{ ...verdict, binding: nested }] }, context), { ok: false, error: 'invalid_action' });
90
- const malformedFinalization = { version: '1', type: 'human_finalization', parentArtifactFingerprint: initial.artifactFingerprint, finalization: { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: initial.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: null, decision: 'reject' } };
91
- assert.deepEqual(reduceRewriteLifecycle(initial, malformedFinalization, context), { ok: false, error: 'invalid_action' });
92
- });
93
- test('semantic submissions reject changed task, binding, and evidence scope', () => {
94
- const reviewTask = task();
95
- const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
96
- const verdict = parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] });
97
- const base = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [verdict] };
98
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...base, taskFingerprint: '9'.repeat(64) }, context), { ok: false, error: 'task_fingerprint_mismatch' });
99
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...base, verdicts: [{ ...verdict, binding: { ...binding, candidateHash: '9'.repeat(64) } }] }, context), { ok: false, error: 'invalid_binding' });
100
- assert.deepEqual(reduceRewriteLifecycle(initial, { ...base, verdicts: [{ ...verdict, evidenceScope: { sentenceIds: [99] } }] }, context), { ok: false, error: 'evidence_scope_mismatch' });
101
- });
@@ -1,39 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { execFileSync } from 'node:child_process';
3
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import test from 'node:test';
7
- test('synthetic Stage 1 packet executes outside the repository and remains blocked', () => {
8
- const parent = mkdtempSync(join(tmpdir(), 'hyv-stage1-dry-run-test-'));
9
- const outputRoot = join(parent, 'packet');
10
- try {
11
- const output = JSON.parse(execFileSync(process.execPath, ['scripts/run-stage1-dry-run.mjs', '--out', outputRoot], { encoding: 'utf8' }));
12
- assert.equal(output.decision, 'BLOCKED');
13
- assert.equal(output.promotable, false);
14
- assert.deepEqual(output.blockers, ['human_writer_evidence_deferred', 'synthetic_fixture_evidence', 'locked_human_evidence_required']);
15
- const report = JSON.parse(readFileSync(join(outputRoot, 'report.json'), 'utf8'));
16
- assert.equal(report.decision, 'BLOCKED');
17
- assert.equal(report.promotable, false);
18
- assert.match(report.reportDigest, /^[a-f0-9]{64}$/);
19
- }
20
- finally {
21
- rmSync(parent, { recursive: true, force: true });
22
- }
23
- });
24
- test('synthetic Stage 1 packet refuses repository output', () => {
25
- assert.throws(() => execFileSync(process.execPath, ['scripts/run-stage1-dry-run.mjs', '--out', join(process.cwd(), 'stage1-output')], { stdio: 'pipe' }), /Dry-run output must stay outside the repository/);
26
- });
27
- test('synthetic Stage 1 packet refuses a symlinked ancestor into the repository', () => {
28
- const parent = mkdtempSync(join(tmpdir(), 'hyv-stage1-symlink-test-'));
29
- const repositoryTarget = join(process.cwd(), 'stage1-symlink-target');
30
- mkdirSync(repositoryTarget);
31
- symlinkSync(repositoryTarget, join(parent, 'redirect'));
32
- try {
33
- assert.throws(() => execFileSync(process.execPath, ['scripts/run-stage1-dry-run.mjs', '--out', join(parent, 'redirect', 'packet')], { stdio: 'pipe' }), /Dry-run output must stay outside the repository/);
34
- }
35
- finally {
36
- rmSync(parent, { recursive: true, force: true });
37
- rmSync(repositoryTarget, { recursive: true, force: true });
38
- }
39
- });