@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,265 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { analyzeAiEditor, maskNonProse, RULESET_VERSION, rules, serializedRules } from './ai-editor.js';
4
- import { createHash } from 'node:crypto';
5
- import { AI_SHADOW_FAIL_SET_V1 } from './ai-shadow-fixtures.js';
6
- import { generateAiShadowFailSetV1 } from './ai-shadow-generator.js';
7
- test('publishes executable rules with stable IDs and repair directions', () => {
8
- assert.equal(RULESET_VERSION, '3.5.0-local.3');
9
- assert.equal(rules.length, 182);
10
- assert.equal(createHash('sha256').update(JSON.stringify(rules.map((rule) => rule.id))).digest('hex'), '172563cd3f55b363c895bb97c37f636dd718253799c369f329bb943dad416f16');
11
- assert.equal(createHash('sha256').update(JSON.stringify(serializedRules())).digest('hex'), '542534f1223d65e7ae7d4c3da8ba22bd31b4ddae2dfe399f7672ca9e6a9cee6b');
12
- assert.equal(new Set(rules.map((rule) => rule.id)).size, rules.length);
13
- for (const rule of rules) {
14
- assert.match(rule.id, /^(ai|formula|hedge|struct|punct|bait|cringe|insider|ogilvy|format)\./);
15
- assert.ok(rule.reason.length > 0);
16
- assert.ok(rule.suggestion.length > 0);
17
- assert.equal(rule.expression.global, false, rule.id);
18
- assert.equal(rule.expression.sticky, false, rule.id);
19
- }
20
- });
21
- function profileWithPolicies(rulePolicy) {
22
- return {
23
- version: '3', id: 'founder.test', revision: 1, revisionDigest: '0'.repeat(64), sampleCount: 2,
24
- metrics: { sentenceLength: 5, sentenceVariation: 1, sentenceStructure: [], rhythm: 1, paragraphLength: 1, openingMoves: [], vocabulary: [], lexicalDensity: 0.5, pointOfView: 'mixed', punctuation: {}, caseStyle: 'mixed', questionRate: 0, transitions: [] },
25
- avoid: [], provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' }, rulePolicy,
26
- fingerprint: { contractionRate: 0, sentenceLengthDistribution: { short: 1, medium: 0, long: 0 }, bulletRate: 0, enDashRate: 0 },
27
- tolerances: { contractionRate: { absolute: 0, calibrated: false }, sentenceLengthDistribution: { absolute: 0, calibrated: false }, bulletRate: { absolute: 0, calibrated: false }, enDashRate: { absolute: 0, calibrated: false } },
28
- metricFixtures: { contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'] },
29
- };
30
- }
31
- test('applies all four v3 policy states after matching and preserves catalog order', () => {
32
- const report = analyzeAiEditor('Firstly, perhaps we leverage a holistic plan.', profileWithPolicies({
33
- 'formula.firstly': 'blocking',
34
- 'hedge.perhaps': 'advisory',
35
- 'ai.leverage': 'judgment-required',
36
- 'ai.holistic': 'disabled',
37
- }));
38
- assert.deepEqual(report.findings.map((finding) => [finding.id, finding.appliedPolicy, finding.severity]), [
39
- ['ai.leverage', 'judgment-required', 'yellow'],
40
- ['formula.firstly', 'blocking', 'red'],
41
- ['hedge.perhaps', 'advisory', 'yellow'],
42
- ]);
43
- assert.equal(report.passed, false);
44
- });
45
- test('fails closed when a v3 policy names a rule outside the catalog', () => {
46
- assert.throws(() => analyzeAiEditor('Plain text.', profileWithPolicies({ 'ai.missing': 'blocking' })), /unknown rule ID/);
47
- });
48
- test('allows a sample-derived punctuation exception only when no explicit policy exists', () => {
49
- const profile = profileWithPolicies({});
50
- profile.ruleAllowances = { 'punct.em-dash': { sampleCount: 2, evidenceDigest: 'a'.repeat(64) } };
51
- assert.equal(analyzeAiEditor('The scheduler failed — retry later.', profile).findings.some((finding) => finding.id === 'punct.em-dash'), false);
52
- profile.rulePolicy['punct.em-dash'] = 'blocking';
53
- assert.equal(analyzeAiEditor('The scheduler failed — retry later.', profile).findings.find((finding) => finding.id === 'punct.em-dash')?.appliedPolicy, 'blocking');
54
- });
55
- test('keeps non-prose regions out of AI Editor while scanning adjacent prose', () => {
56
- const tick = String.fromCharCode(96);
57
- const fence = tick.repeat(3);
58
- const draft = [
59
- '---',
60
- 'title: We leverage the launch',
61
- '---',
62
- 'We leverage the launch.',
63
- fence + 'ts',
64
- 'const message = "We leverage logs";',
65
- fence,
66
- 'Use ' + tick + 'we leverage logs' + tick + ' only as an example.',
67
- '[A link](https://example.com/we-leverage) stays useful.',
68
- ].join('\n');
69
- const report = analyzeAiEditor(draft);
70
- assert.deepEqual(report.findings.filter((finding) => finding.id === 'ai.leverage').map((finding) => finding.sentence), [1]);
71
- assert.equal(maskNonProse(draft).includes('https://example.com/we-leverage'), false);
72
- });
73
- test('preserves protected-region offsets after a supplementary Unicode character', () => {
74
- const tick = String.fromCharCode(96);
75
- const draft = '🚀 We leverage the launch. Use ' + tick + 'we leverage logs' + tick + ' as an example.';
76
- const report = analyzeAiEditor(draft);
77
- assert.deepEqual(report.findings.filter((finding) => finding.id === 'ai.leverage').map((finding) => [finding.sentence, finding.excerpt]), [
78
- [1, '🚀 We leverage the launch.'],
79
- ]);
80
- });
81
- test('uses reconciled defaults for v2 profiles and suppresses inherited duplicate emissions', () => {
82
- const report = analyzeAiEditor("It's worth noting: in other words, I think the same plan. Better results.");
83
- assert.equal(report.findings.some((finding) => finding.id === 'hedge.worth-noting'), false);
84
- assert.equal(report.findings.some((finding) => finding.id === 'struct.in-other-words'), false);
85
- assert.equal(report.findings.some((finding) => finding.id === 'hedge.i-think'), false);
86
- assert.equal(report.findings.some((finding) => finding.id === 'struct.same-better'), false);
87
- assert.ok(report.findings.every((finding) => finding.appliedPolicy !== undefined));
88
- });
89
- test('treats bare red vocabulary as pending judgment and clear sincerity or dashes as blocking', () => {
90
- const vocabulary = analyzeAiEditor('We leverage the existing scheduler.');
91
- assert.deepEqual(vocabulary.findings.find((finding) => finding.id === 'ai.leverage')?.appliedPolicy, 'judgment-required');
92
- assert.equal(vocabulary.passed, true);
93
- const blocked = analyzeAiEditor('To be honest, the scheduler failed — twice.');
94
- assert.ok(blocked.findings.some((finding) => finding.id === 'formula.performative-sincerity' && finding.appliedPolicy === 'blocking'));
95
- assert.ok(blocked.findings.some((finding) => finding.id === 'punct.em-dash' && finding.appliedPolicy === 'blocking'));
96
- assert.equal(blocked.passed, false);
97
- const advisory = analyzeAiEditor('Honestly, the scheduler failed twice.');
98
- assert.ok(advisory.findings.some((finding) => finding.id === 'hedge.performative-sincerity-adverb' && finding.appliedPolicy === 'advisory'));
99
- assert.equal(advisory.passed, true);
100
- });
101
- test('only applies the question-hook policy to document sentence one', () => {
102
- assert.ok(analyzeAiEditor('Have you checked the invoice? It is overdue.').findings.some((finding) => finding.id === 'ai.question-hook'));
103
- assert.equal(analyzeAiEditor('The invoice is overdue. Have you checked it?').findings.some((finding) => finding.id === 'ai.question-hook'), false);
104
- });
105
- test('detects representative rules from every inherited rule family', () => {
106
- const examples = [
107
- ['ai.delve', 'we will delve into it.'],
108
- ['ai.leverage', 'we leverage the existing logs.'],
109
- ['ai.tapestry', 'the tapestry explains the work.'],
110
- ['ai.holistic', 'a holistic review starts today.'],
111
- ['ai.robust', 'robust evidence supports the claim.'],
112
- ['ai.landscape', 'the market landscape changed.'],
113
- ['ai.game-changer', 'this is a game-changer.'],
114
- ['formula.firstly', 'Firstly, check the invoice.'],
115
- ['hedge.perhaps', 'Perhaps the invoice is late.'],
116
- ['struct.this-is-why', 'This is why the invoice matters.'],
117
- ['struct.not-just-but-also', 'this is not just fast but reliable.'],
118
- ['struct.rhetorical-truth', 'the hard truth is in the logs.'],
119
- ['punct.em-dash', 'the logs failed — retry later.'],
120
- ['bait.let-that-sink', 'Let that sink in.'],
121
- ['cringe.10x', 'The change delivered a 10x result.'],
122
- ['insider.nobody-tells', 'What nobody tells you is in the report.'],
123
- ['ogilvy.bandwidth', 'We lack the bandwidth this week.'],
124
- ];
125
- for (const [id, example] of examples) {
126
- const report = analyzeAiEditor(example);
127
- assert.ok(report.findings.some((finding) => finding.id === id && finding.sentence === 1), id);
128
- }
129
- });
130
- test('covers the narrow 3.5 pattern additions with an editorial example for each rule', () => {
131
- const examples = [
132
- ['ai.inflated-significance', 'This marks a pivotal moment for the project.'],
133
- ['ai.notability-name-drop', 'The renowned Ada Lovelace writer changed the field.'],
134
- ['ai.shallow-participle-analysis', 'The result matters, highlighting its importance.'],
135
- ['ai.promotional-scene-setting', 'In today’s changing technology landscape, the work starts.'],
136
- ['ai.vague-attribution', 'Experts say the change will help.'],
137
- ['ai.challenges-outlook', 'Despite these challenges, the future remains open.'],
138
- ['ai.copula-avoidance', 'The launch is serving as a testament to patience.'],
139
- ['ai.false-range', 'The course runs from the Big Bang to dark matter.'],
140
- ['ai.actorless-claim', 'It is important that we check the source.'],
141
- ['ai.chatbot-offer', 'Let me know if you would like another draft.'],
142
- ['ai.knowledge-limit-disclaimer', 'As an AI, I cannot verify that.'],
143
- ['ai.agreement-preamble', 'Absolutely, the invoice is overdue.'],
144
- ['ai.qualifier-stack', 'This is very important for the launch.'],
145
- ['ai.generic-positive-ending', 'The possibilities are endless.'],
146
- ['ai.at-its-core', 'At its core, the work is a queue.'],
147
- ['ai.section-announcement', 'Let us now explore the next step.'],
148
- ['ai.historical-implementation-aside', 'The tool was once known as Alpha and now has a new name.'],
149
- ['ai.unraised-objection', 'Some might argue that the queue is unnecessary.'],
150
- ['ai.fake-alternative', 'Whether you choose email or chat, start today.'],
151
- ['format.bold-bullet-label', '- **Decision:** Ship Tuesday.'],
152
- ['format.title-case-heading', '# Generic Title Case Heading'],
153
- ['format.emoji-heading', '# 🚀 Launch plan'],
154
- ['format.curly-quotes', 'The operator wrote “ship Tuesday” in the release note.'],
155
- ['format.hyphenated-modifier-stack', 'Use a high-trust-low-friction process.'],
156
- ];
157
- for (const [id, example] of examples)
158
- assert.ok(analyzeAiEditor(example).findings.some((finding) => finding.id === id), id);
159
- });
160
- test('keeps bounded Humanizer and Ghostwriter document cues advisory', () => {
161
- const examples = [
162
- ['ai.forced-triplet', 'The template promises speed, scale, and alignment.'],
163
- ['ai.repeated-sentence-opening', 'We checked the logs. We checked the queue. We checked the retry.'],
164
- ['format.bold-density', '**Plan** stays visible. **Owner** checks it. **Proof** ships. **Next** is Tuesday.'],
165
- ['format.repeated-heading-body', '# Release plan\n\nRelease plan explains the verified rollback.'],
166
- ['ai.clipped-fragment-run', 'No demos. No decks. No distractions. The owner checked the logs.'],
167
- ['ai.formulaic-aphorism', 'Quality over quantity is the whole lesson.'],
168
- ['ai.fake-candid-opener', "I'm going to be honest: the queue failed."],
169
- ['ai.metric-theater', 'At 3:47 AM, the slide promised a 23.6x ROI.'],
170
- ['ai.jargon-stack', 'The scalable ecosystem needs alignment, leverage, and a holistic framework.'],
171
- ['ai.sentence-length-cluster', 'The release owner carefully checks each visible rollback instruction before the scheduled production deployment window opens this morning. The release operator carefully records each visible rollback instruction before the scheduled production deployment window opens this morning. The release reviewer carefully reviews each visible rollback instruction before the scheduled production deployment window opens this morning. The release manager carefully confirms each visible rollback instruction before the scheduled production deployment window opens this morning.'],
172
- ];
173
- for (const [id, example] of examples) {
174
- const finding = analyzeAiEditor(example).findings.find((item) => item.id === id);
175
- assert.ok(finding, id);
176
- assert.equal(finding.appliedPolicy, 'advisory', id);
177
- }
178
- assert.equal(analyzeAiEditor('Three source IDs appear in the manifest.').findings.some((finding) => finding.id === 'ai.forced-triplet'), false);
179
- assert.equal(analyzeAiEditor('Owners checked the queue. Operators checked the log. Reviewers checked the proof.').findings.some((finding) => finding.id === 'ai.repeated-sentence-opening'), false);
180
- });
181
- test('keeps the frozen synthetic AI-shadow fail set generated in CI and executable without runtime generation', () => {
182
- assert.deepEqual(generateAiShadowFailSetV1(), AI_SHADOW_FAIL_SET_V1);
183
- for (const fixture of AI_SHADOW_FAIL_SET_V1)
184
- assert.ok(analyzeAiEditor(fixture.text).findings.some((finding) => finding.id === fixture.rule), fixture.id);
185
- });
186
- test('keeps counterexamples for representative inherited rules', () => {
187
- const counterexamples = [
188
- ['ai.delve', 'we inspect the logs.'],
189
- ['ai.leverage', 'we use the existing logs.'],
190
- ['ai.tapestry', 'the report explains the work.'],
191
- ['ai.holistic', 'the review covers the named files.'],
192
- ['ai.robust', 'the evidence includes three dated reports.'],
193
- ['ai.landscape', 'the market changed after the price cut.'],
194
- ['ai.game-changer', 'the release removed a manual step.'],
195
- ['formula.firstly', 'next, check the invoice.'],
196
- ['hedge.perhaps', 'the invoice is late.'],
197
- ['struct.this-is-why', 'the invoice matters because it is overdue.'],
198
- ['struct.not-just-but-also', 'the service is fast and reliable.'],
199
- ['struct.rhetorical-truth', 'the logs show the service failed.'],
200
- ['punct.em-dash', 'the logs failed; retry later.'],
201
- ['bait.let-that-sink', 'The invoice is overdue.'],
202
- ['cringe.10x', 'The result rose from 2 to 20.'],
203
- ['insider.nobody-tells', 'The report explains the missing step.'],
204
- ['ogilvy.bandwidth', 'We lack time this week.'],
205
- ];
206
- for (const [id, example] of counterexamples) {
207
- assert.equal(analyzeAiEditor(example).findings.some((finding) => finding.id === id), false, id);
208
- }
209
- });
210
- test('keeps yellow findings as review cues rather than release blockers', () => {
211
- const report = analyzeAiEditor('Firstly, the editor checked the invoice.');
212
- assert.ok(report.findings.every((finding) => finding.severity === 'yellow'));
213
- assert.equal(report.passed, true);
214
- });
215
- test('restores the benchmark signals from the 2.9.24 executable catalog', () => {
216
- const report = analyzeAiEditor("This work is meaningful. Here's the part nobody is talking about. The change delivered a 10x result.");
217
- assert.deepEqual(report.findings.map((finding) => [finding.id, finding.sentence]), [
218
- ['ai.meaningful', 1],
219
- ['struct.heres-where', 2],
220
- ['cringe.10x', 3],
221
- ]);
222
- });
223
- test('returns the same findings across repeated sentence and line analysis', () => {
224
- for (const text of ['We leverage logs. We leverage traces.', 'No demos. No decks. No distractions. Same team. Better results.']) {
225
- assert.deepEqual(analyzeAiEditor(text), analyzeAiEditor(text));
226
- }
227
- });
228
- test('reports the same rule in multiple sentences', () => {
229
- const report = analyzeAiEditor('We leverage logs. We leverage traces.');
230
- assert.deepEqual(report.findings.filter((finding) => finding.id === 'ai.leverage').map((finding) => finding.sentence), [1, 2]);
231
- });
232
- test('executes inherited cross-sentence rules and maps them to the first sentence', () => {
233
- const report = analyzeAiEditor('No demos. No decks. No distractions. Same team. Better results.');
234
- assert.deepEqual(report.findings
235
- .filter((finding) => finding.id === 'struct.negation-cascade' || finding.id === 'struct.same-better')
236
- .map((finding) => [finding.id, finding.sentence]), [['struct.negation-cascade', 1]]);
237
- });
238
- test('preserves inherited physical-line matching and line-start anchors', () => {
239
- const sameLine = analyzeAiEditor("This isn't positioning. This is proof. Forget vanity metrics. You need retention.");
240
- assert.ok(sameLine.findings.some((finding) => finding.id === 'struct.this-isnt-x-this-is-y'));
241
- assert.ok(sameLine.findings.some((finding) => finding.id === 'struct.forget-x'));
242
- assert.equal(analyzeAiEditor("This isn't positioning.\nThis is proof.").findings.some((finding) => finding.id === 'struct.this-isnt-x-this-is-y'), false);
243
- assert.equal(analyzeAiEditor('The logs failed. Of course we can retry.').findings.some((finding) => finding.id === 'cringe.of-course'), false);
244
- assert.ok(analyzeAiEditor('The logs failed.\nOf course we can retry.').findings.some((finding) => finding.id === 'cringe.of-course'));
245
- });
246
- test('retains the current question-hook and abstract-cluster detectors', () => {
247
- assert.ok(analyzeAiEditor('Have you checked the invoice?').findings.some((finding) => finding.id === 'ai.question-hook'));
248
- assert.ok(analyzeAiEditor('Clarity and strategy are missing.').findings.some((finding) => finding.id === 'ai.abstract-cluster'));
249
- });
250
- test('serializes reconstructable regular expressions and explicit scopes', () => {
251
- const catalog = serializedRules();
252
- assert.equal(catalog.length, 182);
253
- assert.ok(catalog.every((rule) => rule.scope === 'sentence' || rule.scope === 'line' || rule.scope === 'document'));
254
- const meaningful = catalog.find((rule) => rule.id === 'ai.meaningful');
255
- assert.ok(meaningful);
256
- assert.equal(new RegExp(meaningful.expression.source, meaningful.expression.flags).test('Meaningful work.'), true);
257
- });
258
- test('suppresses intentional inherited overlaps before scoring', () => {
259
- const report = analyzeAiEditor('In other words, use logs.');
260
- assert.deepEqual(report.findings.map((finding) => finding.id), ['formula.in-other-words']);
261
- assert.equal(report.score, 82);
262
- });
263
- test('returns zero AI findings for clean input', () => {
264
- assert.deepEqual(analyzeAiEditor('The launch starts Tuesday. The owner signed the release checklist.').findings, []);
265
- });
@@ -1,52 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { generateKeyPairSync, sign } from 'node:crypto';
3
- import test from 'node:test';
4
- import { canonicalJsonBytes } from './canonical-json.js';
5
- import { verifyApprovalCapability } from './approval-capability.js';
6
- const binding = {
7
- rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: '3'.repeat(64),
8
- sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64),
9
- rulesetVersion: '3.2.0', schemaVersion: '1',
10
- };
11
- const { publicKey, privateKey } = generateKeyPairSync('ed25519');
12
- const publicKeySpki = publicKey.export({ format: 'der', type: 'spki' }).toString('base64url');
13
- const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host.example', keyId: 'key-1', publicKeySpki, status: 'active' }] };
14
- function envelope(overrides = {}, signer = privateKey) {
15
- const claims = {
16
- version: '1', purpose: 'hyv.final-approval', issuer: 'host.example', audience: '@holdyourvoice/hyv',
17
- subjectArtifactFingerprint: '7'.repeat(64), sourceHash: binding.sourceHash, candidateHash: binding.candidateHash,
18
- profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'key-1',
19
- issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'nonce-1', ...overrides,
20
- };
21
- const payload = canonicalJsonBytes(claims);
22
- return { payload: payload.toString('base64url'), signature: sign(null, payload, signer).toString('base64url') };
23
- }
24
- test('verifies one canonical bound Ed25519 final-approval capability', () => {
25
- const result = verifyApprovalCapability(envelope(), trustStore, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' });
26
- assert.equal(result.ok, true);
27
- if (result.ok)
28
- assert.match(result.capabilityFingerprint, /^[a-f0-9]{64}$/);
29
- });
30
- test('fails closed for purpose, binding, trust, time, signature, and canonical encoding', () => {
31
- assert.deepEqual(verifyApprovalCapability(envelope({ purpose: 'hyv.rebuild-authorization' }), trustStore, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'wrong_purpose' });
32
- assert.deepEqual(verifyApprovalCapability(envelope({ candidateHash: '8'.repeat(64) }), trustStore, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'binding_mismatch' });
33
- assert.deepEqual(verifyApprovalCapability(envelope(), { ...trustStore, keys: [{ ...trustStore.keys[0], status: 'revoked' }] }, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'revoked_key' });
34
- assert.deepEqual(verifyApprovalCapability(envelope(), trustStore, { now: 201, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'expired' });
35
- const forged = envelope();
36
- const forgedBytes = Buffer.from(forged.signature, 'base64url');
37
- forgedBytes[0] = forgedBytes[0] ^ 1;
38
- forged.signature = forgedBytes.toString('base64url');
39
- assert.deepEqual(verifyApprovalCapability(forged, trustStore, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'invalid_signature' });
40
- const nonCanonical = envelope();
41
- nonCanonical.payload = Buffer.from(` ${Buffer.from(nonCanonical.payload, 'base64url').toString('utf8')}`).toString('base64url');
42
- assert.deepEqual(verifyApprovalCapability(nonCanonical, trustStore, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'non_canonical' });
43
- });
44
- test('rejects malformed envelopes and invalid trust stores without returning secret material', () => {
45
- const result = verifyApprovalCapability({ payload: `${envelope().payload}=`, signature: envelope().signature }, trustStore, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' });
46
- assert.deepEqual(result, { ok: false, error: 'invalid_encoding' });
47
- assert.doesNotMatch(JSON.stringify(result), /nonce-1|signature|publicKeySpki/);
48
- assert.deepEqual(verifyApprovalCapability(envelope(), { ...trustStore, keys: [...trustStore.keys, trustStore.keys[0]] }, { now: 150, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'invalid_schema' });
49
- });
50
- test('rejects an invalid host clock', () => {
51
- assert.deepEqual(verifyApprovalCapability(envelope(), trustStore, { now: Number.NaN, expectedSubjectArtifactFingerprint: '7'.repeat(64), binding, expectedPurpose: 'hyv.final-approval' }), { ok: false, error: 'invalid_schema' });
52
- });
@@ -1,38 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { chmodSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import test from 'node:test';
6
- import { approvalContextMetadataIsSafe, loadApprovalContext } from './approval-context.js';
7
- const context = { now: 0, trustStore: { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [] }, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: ['human-1'] };
8
- test('loads only a permission-checked installed approval context and replaces its clock', () => {
9
- const root = mkdtempSync(join(tmpdir(), 'hyv-approval-context-'));
10
- try {
11
- const safe = join(root, 'safe.json');
12
- writeFileSync(safe, JSON.stringify(context), { mode: 0o600 });
13
- const loaded = loadApprovalContext(safe);
14
- assert.deepEqual(loaded.authorizedSemanticEvaluatorIds.normal, ['reviewer-1']);
15
- assert.notEqual(loaded.now, 0);
16
- chmodSync(safe, 0o644);
17
- assert.throws(() => loadApprovalContext(safe), /unavailable or unsafe/);
18
- chmodSync(safe, 0o600);
19
- const link = join(root, 'link.json');
20
- symlinkSync(safe, link);
21
- assert.throws(() => loadApprovalContext(link), /unavailable or unsafe/);
22
- const malformed = join(root, 'malformed.json');
23
- writeFileSync(malformed, JSON.stringify({ ...context, authorizedSemanticEvaluatorIds: { normal: ['bad id'], highAssurance: [] } }), { mode: 0o600 });
24
- assert.throws(() => loadApprovalContext(malformed), /unavailable or unsafe/);
25
- const malformedTrust = join(root, 'malformed-trust.json');
26
- writeFileSync(malformedTrust, JSON.stringify({ ...context, trustStore: { ...context.trustStore, maxCapabilityLifetimeSeconds: 0 } }), { mode: 0o600 });
27
- assert.throws(() => loadApprovalContext(malformedTrust), /unavailable or unsafe/);
28
- }
29
- finally {
30
- rmSync(root, { recursive: true, force: true });
31
- }
32
- });
33
- test('fails closed when POSIX ownership metadata is unavailable on this host', () => {
34
- const metadata = { isFile: () => true, nlink: 1, size: 10, uid: 501, mode: 0o600 };
35
- assert.equal(approvalContextMetadataIsSafe(metadata, undefined), process.platform === 'win32');
36
- assert.equal(approvalContextMetadataIsSafe(metadata, 501), true);
37
- assert.equal(approvalContextMetadataIsSafe({ ...metadata, mode: 0o644 }, 501), false);
38
- });
@@ -1,20 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import test from 'node:test';
3
- import { evaluateIsolatedBacktest } from './backtest.js';
4
- import { buildProfile } from './voice-dna.js';
5
- test('keeps an isolated backtest generation-free and text-free in its report', () => {
6
- const samples = [
7
- 'I name the constraint first and explain the mechanism with direct words for the person doing the work. The owner checks each source before the release moves into the production queue.',
8
- 'The owner checks the source before the release moves into the production queue and records the evidence for later review. I keep the next step clear for the person handling the release.',
9
- 'I keep evidence visible and choose one next step for the person doing the work before the deployment window opens. The operator records the trade-off and names the owner for the rollback.',
10
- ];
11
- const target = 'The owner checks the source before the release and records the rollback evidence for the operator doing the production work.';
12
- const candidate = 'The owner checks the source before the release and uses leverage for the operator doing the production work while the team records each rollback decision in the release log.';
13
- const report = evaluateIsolatedBacktest('Reply to the rollout question with the owner and check.', target, candidate, buildProfile(samples), samples);
14
- assert.equal(report.version, '1');
15
- assert.ok(report.preservation.score < 100);
16
- assert.equal(report.heldout.selfSimilarity?.ceiling, 100);
17
- assert.equal(JSON.stringify(report).includes(target), false);
18
- assert.equal(JSON.stringify(report).includes(candidate), false);
19
- assert.equal(JSON.stringify(report).includes('uses leverage for the operator'), false);
20
- });