@holdyourvoice/hyv 3.6.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +91 -96
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +19 -13
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +15 -35
- package/dist/cli/io.js +41 -37
- package/dist/copy-spec.js +6 -6
- package/dist/editorial-packs.js +4 -3
- package/dist/fact-linter.js +45 -27
- package/dist/hidden-text.js +6 -6
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/internal.js +8 -0
- package/dist/judgment-task.js +10 -14
- package/dist/learning.js +66 -67
- package/dist/lifecycle-adapter.js +2 -5
- package/dist/local-eval.js +16 -31
- package/dist/mcp-server.js +272 -0
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp.js +2 -261
- package/dist/pipeline.js +9 -85
- package/dist/profile-compose.js +18 -11
- package/dist/profile-score.js +6 -6
- package/dist/profile.js +17 -19
- package/dist/provenance-status.js +1 -3
- package/dist/rebuild-task.js +14 -25
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-task.js +66 -65
- package/dist/rule-allowances.js +6 -10
- package/dist/semantic-review.js +71 -47
- package/dist/stage1-evaluation.js +29 -14
- package/dist/strict-quality.js +10 -18
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +34 -21
- package/dist/writing-examples.js +2 -7
- package/package.json +6 -3
- package/skills/hyv-prepare-judgment/SKILL.md +4 -0
- package/dist/agents/catalog.test.js +0 -60
- package/dist/agents/emit.test.js +0 -64
- package/dist/agents/load.test.js +0 -149
- package/dist/ai-editor.test.js +0 -265
- package/dist/approval-capability.test.js +0 -52
- package/dist/approval-context.test.js +0 -38
- package/dist/backtest.test.js +0 -20
- package/dist/benchmark.test.js +0 -328
- package/dist/canonical-json.test.js +0 -24
- package/dist/cli/context.test.js +0 -55
- package/dist/cli.test.js +0 -731
- package/dist/editorial-packs.test.js +0 -94
- package/dist/fact-linter.test.js +0 -85
- package/dist/hidden-text.test.js +0 -26
- package/dist/hygiene.test.js +0 -83
- package/dist/judgment-task.test.js +0 -162
- package/dist/learning.test.js +0 -325
- package/dist/lifecycle-adapter.test.js +0 -56
- package/dist/local-eval.test.js +0 -20
- package/dist/logic-linter-corpus.test.js +0 -22
- package/dist/logic-linter.test.js +0 -39
- package/dist/mcp-tools.test.js +0 -286
- package/dist/mcp.test.js +0 -312
- package/dist/mirror-refs.test.js +0 -63
- package/dist/pipeline.test.js +0 -247
- package/dist/preservation.test.js +0 -22
- package/dist/production-gates.test.js +0 -34
- package/dist/profile-compose.test.js +0 -32
- package/dist/profile-score.test.js +0 -22
- package/dist/profile-watch.test.js +0 -23
- package/dist/profile.test.js +0 -141
- package/dist/provenance-status.test.js +0 -22
- package/dist/rebuild-task.test.js +0 -206
- package/dist/recomposition.test.js +0 -34
- package/dist/release-audit.test.js +0 -292
- package/dist/rewrite-task.test.js +0 -166
- package/dist/rule-allowances.test.js +0 -17
- package/dist/rule-reconciliation.test.js +0 -50
- package/dist/sample-ingest.test.js +0 -52
- package/dist/semantic-review.test.js +0 -101
- package/dist/stage1-dry-run.test.js +0 -39
- package/dist/stage1-evaluation.test.js +0 -184
- package/dist/stage1-human-packet.test.js +0 -102
- package/dist/stage1-schema-contract.test.js +0 -95
- package/dist/stage2-human-packet.test.js +0 -81
- package/dist/strict-quality.test.js +0 -62
- package/dist/text-provenance.feature.test.js +0 -45
- package/dist/text.test.js +0 -16
- package/dist/voice-dna.test.js +0 -121
- package/dist/writing-examples.test.js +0 -35
package/dist/learning.test.js
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { appendFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import test from 'node:test';
|
|
6
|
-
import { addLearningInstruction, clearLearning, composeLearning, inspectLearning, migrateLearningV2ToV3, profileFingerprint, ratifyLearningEvent, recordLearningInstruction, recordVerifiedCandidate, supersedeLearningEvent } from './learning.js';
|
|
7
|
-
import { verify } from './pipeline.js';
|
|
8
|
-
import { buildProfile } from './voice-dna.js';
|
|
9
|
-
const profile = buildProfile([
|
|
10
|
-
'I write plainly. I name the work.',
|
|
11
|
-
'I keep the mechanism clear. I avoid filler.',
|
|
12
|
-
], ['leverage']);
|
|
13
|
-
function directory() {
|
|
14
|
-
return mkdtempSync(join(tmpdir(), 'holdyourvoice-learning-'));
|
|
15
|
-
}
|
|
16
|
-
function version3(id, revision) {
|
|
17
|
-
return {
|
|
18
|
-
...profile,
|
|
19
|
-
version: '3', id, revision, revisionDigest: String(revision).padStart(64, '0'),
|
|
20
|
-
provenance: { source: 'test', rights: 'test', createdAt: '2026-08-13T00:00:00.000Z' },
|
|
21
|
-
rulePolicy: {},
|
|
22
|
-
fingerprint: { contractionRate: 0, sentenceLengthDistribution: { short: 1, medium: 0, long: 0 }, bulletRate: 0, enDashRate: 0 },
|
|
23
|
-
tolerances: {
|
|
24
|
-
contractionRate: { absolute: 0.1, calibrated: false }, sentenceLengthDistribution: { absolute: 0.1, calibrated: false },
|
|
25
|
-
bulletRate: { absolute: 0.1, calibrated: false }, enDashRate: { absolute: 0.1, calibrated: false },
|
|
26
|
-
},
|
|
27
|
-
metricFixtures: { contractionRate: ['test'], sentenceLengthDistribution: ['test'], bulletRate: ['test'], enDashRate: ['test'] },
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
test('keeps compatible learning across Profile v3 revisions by stable identity', () => {
|
|
31
|
-
const root = directory();
|
|
32
|
-
try {
|
|
33
|
-
assert.equal(addLearningInstruction(version3('founder.jane', 1), 'Keep the mechanism.', { root }), true);
|
|
34
|
-
assert.deepEqual(composeLearning(version3('founder.jane', 2), { root }), [{ text: 'Keep the mechanism.', count: 1 }]);
|
|
35
|
-
assert.deepEqual(composeLearning(version3('founder.other', 2), { root }), []);
|
|
36
|
-
}
|
|
37
|
-
finally {
|
|
38
|
-
rmSync(root, { recursive: true, force: true });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
test('migrates v2 learning explicitly and idempotently without crossing identities', () => {
|
|
42
|
-
const root = directory();
|
|
43
|
-
try {
|
|
44
|
-
addLearningInstruction(profile, 'Keep it direct.', { root });
|
|
45
|
-
const first = migrateLearningV2ToV3(profile, version3('founder.jane', 1), { root, mutationId: 'migration-1' });
|
|
46
|
-
const replay = migrateLearningV2ToV3(profile, version3('founder.jane', 1), { root, mutationId: 'migration-1' });
|
|
47
|
-
assert.equal(first.status, 'recorded');
|
|
48
|
-
assert.equal(replay.status, 'already_recorded');
|
|
49
|
-
assert.deepEqual(composeLearning(version3('founder.jane', 1), { root }), [{ text: 'Keep it direct.', count: 1 }]);
|
|
50
|
-
assert.deepEqual(composeLearning(version3('founder.other', 1), { root }), []);
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
rmSync(root, { recursive: true, force: true });
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
test('imports legacy v1 JSONL and makes default migration replay idempotent', () => {
|
|
57
|
-
const root = directory();
|
|
58
|
-
try {
|
|
59
|
-
const legacyFile = join(root, 'learning', `${profileFingerprint(profile)}.jsonl`);
|
|
60
|
-
addLearningInstruction(profile, 'bootstrap', { root });
|
|
61
|
-
const legacy = { version: '1', timestamp: '2026-08-13T00:00:00.000Z', kind: 'instruction', instruction: 'Keep the legacy cadence.' };
|
|
62
|
-
appendFileSync(legacyFile, `${JSON.stringify(legacy)}\n`);
|
|
63
|
-
const target = version3('founder.jane', 1);
|
|
64
|
-
const first = migrateLearningV2ToV3(profile, target, { root });
|
|
65
|
-
const replay = migrateLearningV2ToV3(profile, target, { root });
|
|
66
|
-
assert.equal(first.status, 'recorded');
|
|
67
|
-
assert.equal(replay.status, 'already_recorded');
|
|
68
|
-
assert.ok(composeLearning(target, { root }).some((item) => item.text === 'Keep the legacy cadence.'));
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
rmSync(root, { recursive: true, force: true });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
test('inspects bounded learning metadata without leaking prose', () => {
|
|
75
|
-
const root = directory();
|
|
76
|
-
try {
|
|
77
|
-
const current = version3('founder.jane', 1);
|
|
78
|
-
recordLearningInstruction(current, 'Never expose this instruction.', { root, mutationId: 'inspect-1', authority: 'founder', provenance: 'author-interview' });
|
|
79
|
-
const inspection = inspectLearning(current, { root });
|
|
80
|
-
assert.equal(inspection.length, 1);
|
|
81
|
-
assert.equal(inspection[0]?.status, 'active');
|
|
82
|
-
assert.equal(inspection[0]?.authority, 'founder');
|
|
83
|
-
const serialized = JSON.stringify(inspection);
|
|
84
|
-
assert.doesNotMatch(serialized, /Never expose this instruction|candidate text|source text/);
|
|
85
|
-
assert.ok(inspection.every((item) => !('text' in item) && !('instruction' in item) && !('candidate' in item)));
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
rmSync(root, { recursive: true, force: true });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
test('makes mutation replay atomic and returns a text-free receipt', () => {
|
|
92
|
-
const root = directory();
|
|
93
|
-
try {
|
|
94
|
-
const first = recordLearningInstruction(version3('founder.jane', 1), 'Keep it direct.', { root, mutationId: 'instruction-1', authority: 'founder' });
|
|
95
|
-
const replay = recordLearningInstruction(version3('founder.jane', 1), 'Keep it direct.', { root, mutationId: 'instruction-1', authority: 'founder' });
|
|
96
|
-
assert.equal(first.status, 'recorded');
|
|
97
|
-
assert.equal(replay.status, 'already_recorded');
|
|
98
|
-
assert.equal(JSON.stringify(first).includes('Keep it direct.'), false);
|
|
99
|
-
assert.deepEqual(composeLearning(version3('founder.jane', 1), { root }), [{ text: 'Keep it direct.', count: 1 }]);
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
rmSync(root, { recursive: true, force: true });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
test('rejects conflicting mutation-id reuse without changing stored learning', () => {
|
|
106
|
-
const root = directory();
|
|
107
|
-
try {
|
|
108
|
-
const current = version3('founder.jane', 1);
|
|
109
|
-
assert.equal(recordLearningInstruction(current, 'First instruction.', { root, mutationId: 'same' }).status, 'recorded');
|
|
110
|
-
assert.equal(recordLearningInstruction(current, 'Conflicting instruction.', { root, mutationId: 'same' }).status, 'conflict');
|
|
111
|
-
assert.deepEqual(composeLearning(current, { root }), [{ text: 'First instruction.', count: 1 }]);
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
rmSync(root, { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
test('fails closed on corrupt learning without rewriting the original', () => {
|
|
118
|
-
const root = directory();
|
|
119
|
-
try {
|
|
120
|
-
const current = version3('founder.jane', 1);
|
|
121
|
-
recordLearningInstruction(current, 'Valid.', { root, mutationId: 'valid' });
|
|
122
|
-
const file = join(root, 'learning', 'founder.jane.jsonl');
|
|
123
|
-
appendFileSync(file, '{corrupt}\n');
|
|
124
|
-
const before = readFileSync(file, 'utf8');
|
|
125
|
-
assert.throws(() => composeLearning(current, { root }), /corrupt/i);
|
|
126
|
-
assert.equal(recordLearningInstruction(current, 'Must not write.', { root, mutationId: 'blocked' }).status, 'corrupt');
|
|
127
|
-
assert.equal(readFileSync(file, 'utf8'), before);
|
|
128
|
-
}
|
|
129
|
-
finally {
|
|
130
|
-
rmSync(root, { recursive: true, force: true });
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
test('rejects lower-authority ratification and supersession', () => {
|
|
134
|
-
const root = directory();
|
|
135
|
-
try {
|
|
136
|
-
const current = version3('founder.jane', 1);
|
|
137
|
-
const founder = recordLearningInstruction(current, 'Founder ruling.', { root, mutationId: 'founder-rule', authority: 'founder' });
|
|
138
|
-
assert.equal(ratifyLearningEvent(current, founder.eventId, { root, mutationId: 'team-ratify', authority: 'team' }).status, 'unauthorized');
|
|
139
|
-
assert.equal(supersedeLearningEvent(current, founder.eventId, { root, mutationId: 'team-supersede', authority: 'team' }).status, 'unauthorized');
|
|
140
|
-
assert.deepEqual(composeLearning(current, { root }), [{ text: 'Founder ruling.', count: 1 }]);
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
rmSync(root, { recursive: true, force: true });
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
test('inspection identifiers and digests do not depend on secret prose or provenance', () => {
|
|
147
|
-
const firstRoot = directory();
|
|
148
|
-
const secondRoot = directory();
|
|
149
|
-
try {
|
|
150
|
-
const current = version3('founder.jane', 1);
|
|
151
|
-
recordLearningInstruction(current, 'Secret alpha.', { root: firstRoot, mutationId: 'public-id', provenance: 'secret provenance alpha' });
|
|
152
|
-
recordLearningInstruction(current, 'Secret beta.', { root: secondRoot, mutationId: 'public-id', provenance: 'secret provenance beta' });
|
|
153
|
-
const publicFields = (root) => inspectLearning(current, { root }).map(({ timestamp: _timestamp, ...item }) => item);
|
|
154
|
-
assert.deepEqual(publicFields(firstRoot), publicFields(secondRoot));
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
rmSync(firstRoot, { recursive: true, force: true });
|
|
158
|
-
rmSync(secondRoot, { recursive: true, force: true });
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
test('migration fails atomically when all intended imports cannot fit', () => {
|
|
162
|
-
const root = directory();
|
|
163
|
-
try {
|
|
164
|
-
for (let index = 0; index < 40; index += 1)
|
|
165
|
-
recordLearningInstruction(profile, `Legacy ${index}.`, { root, mutationId: `legacy-${index}` });
|
|
166
|
-
const target = version3('founder.capacity', 1);
|
|
167
|
-
assert.equal(migrateLearningV2ToV3(profile, target, { root }).status, 'capacity_exceeded');
|
|
168
|
-
assert.deepEqual(composeLearning(target, { root }), []);
|
|
169
|
-
}
|
|
170
|
-
finally {
|
|
171
|
-
rmSync(root, { recursive: true, force: true });
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
test('ratifies incompatible events and supersedes them without deleting audit history', () => {
|
|
175
|
-
const root = directory();
|
|
176
|
-
try {
|
|
177
|
-
const recorded = recordLearningInstruction(version3('founder.jane', 1), 'Keep the old cadence.', { root, mutationId: 'old', compatibility: 'exact' });
|
|
178
|
-
assert.deepEqual(composeLearning(version3('founder.jane', 2), { root }), []);
|
|
179
|
-
assert.equal(ratifyLearningEvent(version3('founder.jane', 2), recorded.eventId, { root, mutationId: 'ratify-old' }).status, 'recorded');
|
|
180
|
-
assert.deepEqual(composeLearning(version3('founder.jane', 2), { root }), [{ text: 'Keep the old cadence.', count: 1 }]);
|
|
181
|
-
assert.equal(supersedeLearningEvent(version3('founder.jane', 2), recorded.eventId, { root, mutationId: 'supersede-old' }).status, 'recorded');
|
|
182
|
-
assert.deepEqual(composeLearning(version3('founder.jane', 2), { root }), []);
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
rmSync(root, { recursive: true, force: true });
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
test('compaction retains higher-authority learning before lower-authority observations', () => {
|
|
189
|
-
const root = directory();
|
|
190
|
-
try {
|
|
191
|
-
const current = version3('founder.jane', 1);
|
|
192
|
-
recordLearningInstruction(current, 'Founder ruling.', { root, mutationId: 'founder', authority: 'founder', weight: 1 });
|
|
193
|
-
for (let index = 0; index < 45; index += 1)
|
|
194
|
-
recordLearningInstruction(current, `Team observation ${index}.`, { root, mutationId: `team-${index}`, authority: 'team', weight: 100 });
|
|
195
|
-
assert.ok(composeLearning(current, { root }).some((item) => item.text === 'Founder ruling.'));
|
|
196
|
-
}
|
|
197
|
-
finally {
|
|
198
|
-
rmSync(root, { recursive: true, force: true });
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
test('compaction preserves a retained target with its ratification control', () => {
|
|
202
|
-
const root = directory();
|
|
203
|
-
try {
|
|
204
|
-
const first = version3('founder.controls', 1);
|
|
205
|
-
const second = version3('founder.controls', 2);
|
|
206
|
-
const old = recordLearningInstruction(first, 'Ratified founder ruling.', { root, mutationId: 'old-founder', authority: 'founder', compatibility: 'exact' });
|
|
207
|
-
ratifyLearningEvent(second, old.eventId, { root, mutationId: 'ratify-founder', authority: 'founder' });
|
|
208
|
-
for (let index = 0; index < 45; index += 1)
|
|
209
|
-
recordLearningInstruction(second, `Team churn ${index}.`, { root, mutationId: `churn-${index}`, authority: 'team' });
|
|
210
|
-
assert.ok(composeLearning(second, { root }).some((item) => item.text === 'Ratified founder ruling.'));
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
rmSync(root, { recursive: true, force: true });
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
test('records only resolved findings from successful verification without draft text', () => {
|
|
217
|
-
const root = directory();
|
|
218
|
-
try {
|
|
219
|
-
const original = 'I leverage the answer with useful detail and clear mechanism.';
|
|
220
|
-
const candidate = 'I use the answer with useful detail and clear mechanism.';
|
|
221
|
-
const result = verify(original, candidate, profile);
|
|
222
|
-
assert.equal(result.passed, true);
|
|
223
|
-
assert.equal(recordVerifiedCandidate(profile, result, candidate, { root }), 'recorded');
|
|
224
|
-
const eventFile = join(root, 'learning', `${profileFingerprint(profile)}.jsonl`);
|
|
225
|
-
const stored = readFileSync(eventFile, 'utf8');
|
|
226
|
-
assert.match(stored, /ai\.leverage/);
|
|
227
|
-
assert.doesNotMatch(stored, /I leverage the answer/);
|
|
228
|
-
assert.doesNotMatch(stored, /I use the answer/);
|
|
229
|
-
}
|
|
230
|
-
finally {
|
|
231
|
-
rmSync(root, { recursive: true, force: true });
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
test('keeps learning isolated by profile and bounds composed preferences', () => {
|
|
235
|
-
const root = directory();
|
|
236
|
-
try {
|
|
237
|
-
const other = buildProfile(['We explain the mechanism.', 'We keep the useful detail.']);
|
|
238
|
-
for (let index = 0; index < 12; index += 1)
|
|
239
|
-
addLearningInstruction(profile, `Keep preference ${index}.`, { root });
|
|
240
|
-
addLearningInstruction(other, 'Use a different preference.', { root });
|
|
241
|
-
const composed = composeLearning(profile, { root });
|
|
242
|
-
assert.equal(composed.length, 10);
|
|
243
|
-
assert.ok(composed.every((item) => item.text.startsWith('Keep preference')));
|
|
244
|
-
assert.ok(!composed.some((item) => item.text === 'Use a different preference.'));
|
|
245
|
-
}
|
|
246
|
-
finally {
|
|
247
|
-
rmSync(root, { recursive: true, force: true });
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
test('does not create an event for a failed candidate and clears a single profile', () => {
|
|
251
|
-
const root = directory();
|
|
252
|
-
try {
|
|
253
|
-
const failed = verify('I name the work.', 'I leverage the work.', profile);
|
|
254
|
-
assert.equal(failed.passed, false);
|
|
255
|
-
assert.equal(recordVerifiedCandidate(profile, failed, 'I leverage the work.', { root }), 'nothing_to_learn');
|
|
256
|
-
assert.equal(existsSync(join(root, 'learning', `${profileFingerprint(profile)}.jsonl`)), false);
|
|
257
|
-
addLearningInstruction(profile, 'Keep it direct.', { root });
|
|
258
|
-
assert.equal(clearLearning(profile, { root }), true);
|
|
259
|
-
assert.deepEqual(composeLearning(profile, { root }), []);
|
|
260
|
-
}
|
|
261
|
-
finally {
|
|
262
|
-
rmSync(root, { recursive: true, force: true });
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
test('bounds retained events and local instruction size', () => {
|
|
266
|
-
const root = directory();
|
|
267
|
-
try {
|
|
268
|
-
for (let index = 0; index < 45; index += 1)
|
|
269
|
-
addLearningInstruction(profile, `Keep preference ${index}.`, { root });
|
|
270
|
-
const preferences = composeLearning(profile, { root });
|
|
271
|
-
assert.ok(preferences.every((item) => Number(item.text.match(/\d+/)?.[0]) >= 5));
|
|
272
|
-
assert.throws(() => addLearningInstruction(profile, 'x'.repeat(241), { root }), /240 characters/);
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
rmSync(root, { recursive: true, force: true });
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
test('fails closed on malformed local events', () => {
|
|
279
|
-
const root = directory();
|
|
280
|
-
try {
|
|
281
|
-
addLearningInstruction(profile, 'Keep it direct.', { root });
|
|
282
|
-
appendFileSync(join(root, 'learning', `${profileFingerprint(profile)}.jsonl`), '{"version":"1","timestamp":"now","kind":"verified_candidate","resolved":{}}\n');
|
|
283
|
-
assert.throws(() => composeLearning(profile, { root }), /corrupt/);
|
|
284
|
-
}
|
|
285
|
-
finally {
|
|
286
|
-
rmSync(root, { recursive: true, force: true });
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
test('does not raise confidence when the same verified outcome repeats', () => {
|
|
290
|
-
const root = directory();
|
|
291
|
-
try {
|
|
292
|
-
const original = 'I leverage the answer with useful detail and clear mechanism.';
|
|
293
|
-
const candidate = 'I use the answer with useful detail and clear mechanism.';
|
|
294
|
-
const result = verify(original, candidate, profile);
|
|
295
|
-
assert.equal(recordVerifiedCandidate(profile, result, candidate, { root }), 'recorded');
|
|
296
|
-
assert.equal(recordVerifiedCandidate(profile, result, candidate, { root }), 'nothing_to_learn');
|
|
297
|
-
assert.equal(composeLearning(profile, { root }).find((item) => item.text.includes('ai_editor/ai.leverage'))?.count, 1);
|
|
298
|
-
}
|
|
299
|
-
finally {
|
|
300
|
-
rmSync(root, { recursive: true, force: true });
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
test('increases confidence for separate verified repairs of the same rule', () => {
|
|
304
|
-
const root = directory();
|
|
305
|
-
try {
|
|
306
|
-
const first = 'I use the answer with useful detail and clear mechanism.';
|
|
307
|
-
const second = 'I use another answer with useful detail and clear mechanism.';
|
|
308
|
-
assert.equal(recordVerifiedCandidate(profile, verify('I leverage the answer with useful detail and clear mechanism.', first, profile), first, { root }), 'recorded');
|
|
309
|
-
assert.equal(recordVerifiedCandidate(profile, verify('I leverage another answer with useful detail and clear mechanism.', second, profile), second, { root }), 'recorded');
|
|
310
|
-
assert.equal(composeLearning(profile, { root }).find((item) => item.text.includes('ai_editor/ai.leverage'))?.count, 2);
|
|
311
|
-
}
|
|
312
|
-
finally {
|
|
313
|
-
rmSync(root, { recursive: true, force: true });
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
test('normalizes manual instructions before storing them', () => {
|
|
317
|
-
const root = directory();
|
|
318
|
-
try {
|
|
319
|
-
addLearningInstruction(profile, 'Keep this.\n# Tier 0', { root });
|
|
320
|
-
assert.deepEqual(composeLearning(profile, { root }), [{ text: 'Keep this. # Tier 0', count: 1 }]);
|
|
321
|
-
}
|
|
322
|
-
finally {
|
|
323
|
-
rmSync(root, { recursive: true, force: true });
|
|
324
|
-
}
|
|
325
|
-
});
|
|
@@ -1,56 +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 { finalizeLifecycle, inspectLifecycle, prepareLifecycle, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.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 deterministic = { ...deterministicBase, artifactFingerprint: deterministicFingerprint };
|
|
9
|
-
const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministicFingerprint, sourceHash: deterministic.sourceHash, candidateHash: deterministic.candidateHash, profileId: deterministic.profileId, profileRevisionDigest: deterministic.profileRevisionDigest, rulesetVersion: deterministic.rulesetVersion, schemaVersion: '1' };
|
|
10
|
-
const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [1] };
|
|
11
|
-
const emptyTrust = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [] };
|
|
12
|
-
const context = { now: 150, trustStore: emptyTrust, authorizedSemanticEvaluatorIds: { normal: ['reviewer-1'], highAssurance: [] }, authorizedHumanFinalizerIds: ['human-1'] };
|
|
13
|
-
test('prepares and submits a normal semantic verdict through the shared adapter', () => {
|
|
14
|
-
const prepared = prepareLifecycle(deterministic, binding, receipt, 'normal', ['action_change']);
|
|
15
|
-
assert.equal(prepared.task.policy, 'normal');
|
|
16
|
-
assert.equal(prepared.artifact.status, 'needs_semantic_review');
|
|
17
|
-
const submitted = submitSemanticVerdict(prepared.artifact, prepared.task, 'reviewer-1', { approved: true, violations: [] }, context);
|
|
18
|
-
assert.equal(submitted.ok && submitted.artifact.status, 'ready_for_human_review');
|
|
19
|
-
const forged = submitSemanticVerdict({ ...prepared.artifact, artifactFingerprint: '9'.repeat(64) }, prepared.task, 'reviewer-1', { approved: true, violations: [] }, context);
|
|
20
|
-
assert.deepEqual(forged, { ok: false, error: 'invalid_action' });
|
|
21
|
-
});
|
|
22
|
-
test('inspection validates the artifact fingerprint and returns metadata only', () => {
|
|
23
|
-
const { artifact } = prepareLifecycle(deterministic, binding, receipt, 'normal', ['action_change']);
|
|
24
|
-
const inspected = inspectLifecycle(artifact);
|
|
25
|
-
assert.equal(inspected.status, 'needs_semantic_review');
|
|
26
|
-
assert.equal(inspected.artifactFingerprint, artifact.artifactFingerprint);
|
|
27
|
-
assert.doesNotMatch(JSON.stringify(inspected), /sourceHash|candidateHash|capability|signature|nonce|verdicts/);
|
|
28
|
-
assert.throws(() => inspectLifecycle({ ...artifact, artifactFingerprint: '9'.repeat(64) }), /invalid lifecycle artifact/i);
|
|
29
|
-
const { artifactFingerprint: _fingerprint, ...base } = artifact;
|
|
30
|
-
const malformedBase = { ...base, status: 'ready_for_human_review', parentArtifactFingerprint: artifact.artifactFingerprint };
|
|
31
|
-
const malformed = { ...malformedBase, artifactFingerprint: createHash('sha256').update(`hyv:lifecycle-artifact:v1\0${canonicalJson(malformedBase)}`).digest('hex') };
|
|
32
|
-
assert.throws(() => inspectLifecycle(malformed), /invalid lifecycle artifact/i);
|
|
33
|
-
for (const invalid of [{ ...base, status: 'garbage' }, { ...base, status: 'needs_escalation', parentArtifactFingerprint: artifact.artifactFingerprint, reason: 'garbage' }]) {
|
|
34
|
-
const recomputed = { ...invalid, artifactFingerprint: createHash('sha256').update(`hyv:lifecycle-artifact:v1\0${canonicalJson(invalid)}`).digest('hex') };
|
|
35
|
-
assert.throws(() => inspectLifecycle(recomputed), /invalid lifecycle artifact/i);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
test('capability validation collapses verifier details and finalization stays reducer-backed', () => {
|
|
39
|
-
const prepared = prepareLifecycle(deterministic, binding, receipt, 'normal', ['action_change']);
|
|
40
|
-
const submitted = submitSemanticVerdict(prepared.artifact, prepared.task, 'reviewer-1', { approved: true, violations: [] }, context);
|
|
41
|
-
assert.equal(submitted.ok, true);
|
|
42
|
-
if (!submitted.ok)
|
|
43
|
-
return;
|
|
44
|
-
assert.deepEqual(validateFinalApproval(submitted.artifact, { payload: 'bad', signature: 'bad' }, context), { ok: false, error: 'capability_invalid' });
|
|
45
|
-
const rejected = finalizeLifecycle(submitted.artifact, { evaluatorId: 'human-1', decision: 'reject' }, context);
|
|
46
|
-
assert.equal(rejected.ok && rejected.artifact.status, 'needs_escalation');
|
|
47
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
48
|
-
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' }] };
|
|
49
|
-
const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: submitted.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'secret-nonce' };
|
|
50
|
-
const payload = canonicalJsonBytes(claims);
|
|
51
|
-
const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
|
|
52
|
-
assert.equal(validateFinalApproval(submitted.artifact, capability, { ...context, trustStore }).ok, true);
|
|
53
|
-
const approved = finalizeLifecycle(submitted.artifact, { evaluatorId: 'human-1', decision: 'approve' }, { ...context, trustStore }, capability);
|
|
54
|
-
assert.equal(approved.ok && approved.artifact.status, 'approved');
|
|
55
|
-
assert.doesNotMatch(JSON.stringify(approved), /secret-nonce|payload|signature/);
|
|
56
|
-
});
|
package/dist/local-eval.test.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { evaluateLocalComposite } from './local-eval.js';
|
|
4
|
-
test('groups paragraph IDs before optional local composite evaluation', () => {
|
|
5
|
-
const user = [{ paragraphId: 'u1', text: 'I check the source and explain the mechanism.' }, { paragraphId: 'u2', text: 'The owner names one next step for the release.' }, { paragraphId: 'u3', text: 'I keep the evidence visible for the operator.' }];
|
|
6
|
-
const shadow = [{ paragraphId: 's1', text: 'This transformative framework unlocks synergy.' }, { paragraphId: 's2', text: 'The holistic ecosystem creates value.' }, { paragraphId: 's3', text: 'This is a game-changer for every stakeholder.' }];
|
|
7
|
-
const report = evaluateLocalComposite('We leverage a holistic framework.', 'We check the source and name the next step.', user, shadow);
|
|
8
|
-
assert.equal(report.version, '1');
|
|
9
|
-
assert.equal(report.split.trainParagraphIds.some((id) => report.split.testParagraphIds.includes(id)), false);
|
|
10
|
-
assert.ok(report.contentF1 >= 0 && report.contentF1 <= 1);
|
|
11
|
-
assert.ok(report.stylometricCosine9 >= 0 && report.stylometricCosine9 <= 1);
|
|
12
|
-
assert.equal(report.aiTellReduction.candidateFindings < report.aiTellReduction.inputFindings, true);
|
|
13
|
-
});
|
|
14
|
-
test('keeps every paragraph variant on one side of the local evaluation split', () => {
|
|
15
|
-
const user = [{ paragraphId: 'u1', text: 'I inspect the source.' }, { paragraphId: 'u1', text: 'I inspect the source before shipping.' }, { paragraphId: 'u2', text: 'I name the mechanism.' }];
|
|
16
|
-
const shadow = [{ paragraphId: 's1', text: 'This holistic framework transforms outcomes.' }, { paragraphId: 's1', text: 'This transformative framework unlocks outcomes.' }, { paragraphId: 's2', text: 'The ecosystem creates value.' }];
|
|
17
|
-
const report = evaluateLocalComposite('The source is visible.', 'I inspect the source before shipping.', user, shadow);
|
|
18
|
-
assert.equal(report.split.trainParagraphIds.includes('u1'), report.split.testParagraphIds.includes('u1') === false);
|
|
19
|
-
assert.equal(report.split.trainParagraphIds.includes('s1'), report.split.testParagraphIds.includes('s1') === false);
|
|
20
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import test from 'node:test';
|
|
4
|
-
import { lintLogic } from './logic-linter.js';
|
|
5
|
-
import { words } from './text.js';
|
|
6
|
-
const domains = ['marketing', 'engineering', 'deep-tech'];
|
|
7
|
-
function corpus(domain) {
|
|
8
|
-
return JSON.parse(readFileSync(new URL(`../test-fixtures/logic-linter/${domain}.json`, import.meta.url), 'utf8'));
|
|
9
|
-
}
|
|
10
|
-
test('keeps a 54-post long-form corpus cohesive across marketing, engineering, and deep tech', () => {
|
|
11
|
-
const posts = domains.flatMap(corpus);
|
|
12
|
-
assert.equal(posts.length, 54);
|
|
13
|
-
assert.equal(new Set(posts.map((post) => post.id)).size, 54);
|
|
14
|
-
for (const domain of domains)
|
|
15
|
-
assert.equal(corpus(domain).length, 18);
|
|
16
|
-
for (const post of posts) {
|
|
17
|
-
const wordCount = words(post.text).length;
|
|
18
|
-
assert.ok(wordCount >= 350 && wordCount <= 1_500, `${post.id} must contain 350–1,500 words; got ${wordCount}`);
|
|
19
|
-
const report = lintLogic(post.text);
|
|
20
|
-
assert.equal(report.passed, true, `${post.id}: ${JSON.stringify(report.findings)}`);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { lintLogic } from './logic-linter.js';
|
|
4
|
-
test('accepts a post that develops one connected marketing argument', () => {
|
|
5
|
-
const report = lintLogic('Most marketing teams measure clicks before they measure qualified conversations. That choice rewards cheap attention. We changed the weekly review to start with qualified conversations, then used click data to explain movement. The review now tells the team which message created demand.');
|
|
6
|
-
assert.equal(report.passed, true);
|
|
7
|
-
assert.deepEqual(report.findings, []);
|
|
8
|
-
});
|
|
9
|
-
test('blocks an abrupt, unsupported topic jump', () => {
|
|
10
|
-
const report = lintLogic('The release checklist makes rollback ownership explicit. Each service owner signs the same checklist before deployment. The best espresso machines use a dual boiler for stable temperature control. The checklist now catches missing rollback steps before they become incidents.');
|
|
11
|
-
assert.equal(report.passed, false);
|
|
12
|
-
assert.equal(report.findings[0]?.kind, 'topic_drift');
|
|
13
|
-
assert.equal(report.findings[0]?.severity, 'error');
|
|
14
|
-
});
|
|
15
|
-
test('allows an explicit bridge into a consequence', () => {
|
|
16
|
-
const report = lintLogic('The compiler now records each cache key. This means the build dashboard can explain a cache miss without replaying the job. That explanation makes incident review faster.');
|
|
17
|
-
assert.equal(report.passed, true);
|
|
18
|
-
});
|
|
19
|
-
test('does not treat an isolated rhetorical question as a hard topic-drift finding', () => {
|
|
20
|
-
const report = lintLogic('The migration writes its recovery marker before it changes customer data. What happens if execution stops between two writes? The recovery marker lets the worker resume the migration safely.');
|
|
21
|
-
assert.equal(report.passed, true);
|
|
22
|
-
});
|
|
23
|
-
test('blocks directly contradictory claims', () => {
|
|
24
|
-
const report = lintLogic('The migration is ready for production. The migration is not ready for production.');
|
|
25
|
-
assert.equal(report.passed, false);
|
|
26
|
-
assert.equal(report.findings[0]?.kind, 'internal_contradiction');
|
|
27
|
-
});
|
|
28
|
-
test('uses the brief argument map as an additional topical anchor', () => {
|
|
29
|
-
const report = lintLogic('Operators still reconcile customer changes by hand. A small approval queue records the owner and reason for each change. The queue turns a vague audit request into a traceable review.', {
|
|
30
|
-
version: '1', audience: 'operators', intent: 'explain', format: 'social',
|
|
31
|
-
argumentMap: { observation: 'manual customer change reconciliation', mechanism: 'approval queue', consequence: 'traceable audit review', readerValue: 'faster operator review' },
|
|
32
|
-
});
|
|
33
|
-
assert.equal(report.passed, true);
|
|
34
|
-
});
|
|
35
|
-
test('does not make a drift verdict for a short post', () => {
|
|
36
|
-
const report = lintLogic('The release is ready. Espresso needs fresh beans.');
|
|
37
|
-
assert.equal(report.passed, true);
|
|
38
|
-
assert.deepEqual(report.skippedChecks, ['topic_drift_short_post']);
|
|
39
|
-
});
|