@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.
- package/Readme.md +93 -154
- package/dist/ai-editor-rules.js +1 -0
- package/dist/ai-editor.js +23 -33
- package/dist/analysis.js +33 -0
- package/dist/cli/agents.js +88 -0
- package/dist/cli/checks.js +226 -0
- package/dist/cli/io.js +141 -0
- package/dist/cli/lifecycle.js +162 -0
- package/dist/cli/profiles.js +246 -0
- package/dist/cli/rewriting.js +111 -0
- package/dist/cli.js +5 -974
- 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 +77 -71
- 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 +10 -19
- package/dist/mcp.js +2 -267
- package/dist/pipeline.js +18 -105
- 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 +18 -52
- package/dist/recomposition.js +18 -13
- package/dist/rewrite-prompt.js +82 -0
- package/dist/rewrite-response.js +27 -0
- package/dist/rewrite-task.js +73 -98
- 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.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
|
@@ -1,206 +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 { applyRewriteResponse, prepareRewriteTask } from './rewrite-task.js';
|
|
6
|
-
import { applyRebuildResponse, evaluateRebuildResponse, parseRebuildTask, prepareRebuildTask } from './rebuild-task.js';
|
|
7
|
-
import { bindJudgmentEnvelope, preparePreEditJudgment, reducePreEdit } from './judgment-task.js';
|
|
8
|
-
import { verifyDeterministically, verifyRebuildWithCopySpec } from './pipeline.js';
|
|
9
|
-
import { prepareLifecycle, recordApprovedLearning } from './lifecycle-adapter.js';
|
|
10
|
-
import { buildProfile } from './voice-dna.js';
|
|
11
|
-
import { HYV_VERSION } from './version.js';
|
|
12
|
-
const profile = buildProfile([
|
|
13
|
-
'I write clear notes. I keep the mechanism visible.',
|
|
14
|
-
'I name the trade-off. Then I make the next step plain.',
|
|
15
|
-
], ['leverage']);
|
|
16
|
-
const draft = 'I leverage the answer. The launch is on 14 August.';
|
|
17
|
-
const rebuilt = 'Ship planning now treats one calendar fact as fixed. The launch is on 14 August. Every other sentence in this note is new operational language for the release desk.';
|
|
18
|
-
const copySpec = {
|
|
19
|
-
version: '1',
|
|
20
|
-
audience: 'operators',
|
|
21
|
-
intent: 'explain',
|
|
22
|
-
channel: 'email',
|
|
23
|
-
claims: [{ id: 'launch-date', text: 'The launch is on 14 August.', evidence: 'Release calendar, 7 August.' }],
|
|
24
|
-
};
|
|
25
|
-
const recompositionPolicy = {
|
|
26
|
-
version: '1', mode: 'meaning-first',
|
|
27
|
-
lexicalResidual: { ngramSize: 5, maxSharedNgramFraction: 0, maxLongestSharedRunTokens: 4 },
|
|
28
|
-
acknowledgement: 'Measures shared wording only; does not detect or prove removal of a watermark.',
|
|
29
|
-
};
|
|
30
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
31
|
-
const trustStore = {
|
|
32
|
-
version: '1',
|
|
33
|
-
audience: '@holdyourvoice/hyv',
|
|
34
|
-
maxCapabilityLifetimeSeconds: 300,
|
|
35
|
-
keys: [{ issuer: 'host.example', keyId: 'key-1', publicKeySpki: publicKey.export({ format: 'der', type: 'spki' }).toString('base64url'), status: 'active' }],
|
|
36
|
-
};
|
|
37
|
-
function envelope(task, decision, extra = {}) {
|
|
38
|
-
return {
|
|
39
|
-
version: '1',
|
|
40
|
-
stage: task.stage,
|
|
41
|
-
judgmentType: task.judgmentType,
|
|
42
|
-
taskFingerprint: task.taskFingerprint,
|
|
43
|
-
bindings: { ...task.bindings, evaluatorId: 'writer.1' },
|
|
44
|
-
findings: [],
|
|
45
|
-
decision,
|
|
46
|
-
...extra,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function rebuildRecommendation(text = draft) {
|
|
50
|
-
const triage = preparePreEditJudgment(text, profile, 'triage');
|
|
51
|
-
const argument = preparePreEditJudgment(text, profile, 'argument');
|
|
52
|
-
const form = preparePreEditJudgment(text, profile, 'form');
|
|
53
|
-
return reducePreEdit([
|
|
54
|
-
bindJudgmentEnvelope(triage, envelope(triage, 'SHIP')),
|
|
55
|
-
bindJudgmentEnvelope(argument, envelope(argument, 'REBUILD')),
|
|
56
|
-
bindJudgmentEnvelope(form, envelope(form, 'SHIP')),
|
|
57
|
-
]);
|
|
58
|
-
}
|
|
59
|
-
function capability(reduction, overrides = {}, source = draft) {
|
|
60
|
-
const sourceHash = createHash('sha256').update(source).digest('hex');
|
|
61
|
-
const identity = `legacy-v2:${createHash('sha256').update(canonicalJson(profile)).digest('hex')}`;
|
|
62
|
-
const claims = {
|
|
63
|
-
version: '1',
|
|
64
|
-
purpose: 'hyv.rebuild-authorization',
|
|
65
|
-
issuer: 'host.example',
|
|
66
|
-
audience: '@holdyourvoice/hyv',
|
|
67
|
-
subjectArtifactFingerprint: reduction.recommendationFingerprint,
|
|
68
|
-
sourceHash,
|
|
69
|
-
candidateHash: sourceHash,
|
|
70
|
-
profileId: identity,
|
|
71
|
-
profileRevisionDigest: identity,
|
|
72
|
-
keyId: 'key-1',
|
|
73
|
-
issuedAt: 100,
|
|
74
|
-
notBefore: 100,
|
|
75
|
-
expiresAt: 200,
|
|
76
|
-
nonce: 'nonce-rebuild',
|
|
77
|
-
...overrides,
|
|
78
|
-
};
|
|
79
|
-
const payload = canonicalJsonBytes(claims);
|
|
80
|
-
return { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
|
|
81
|
-
}
|
|
82
|
-
function evaluate(task, raw, reduction, boundProfile = profile) {
|
|
83
|
-
return evaluateRebuildResponse(task, raw, boundProfile, capability(reduction), trustStore, 150);
|
|
84
|
-
}
|
|
85
|
-
test('pre-edit reductions carry a stable recommendation fingerprint', () => {
|
|
86
|
-
const left = rebuildRecommendation();
|
|
87
|
-
const right = rebuildRecommendation();
|
|
88
|
-
assert.equal(left.decision, 'REBUILD');
|
|
89
|
-
assert.match(left.recommendationFingerprint, /^[a-f0-9]{64}$/);
|
|
90
|
-
assert.equal(left.recommendationFingerprint, right.recommendationFingerprint);
|
|
91
|
-
});
|
|
92
|
-
test('a caller cannot self-select rebuild or submit a forged authorization', () => {
|
|
93
|
-
const reduction = rebuildRecommendation();
|
|
94
|
-
const edit = { ...reduction, decision: 'EDIT' };
|
|
95
|
-
assert.throws(() => prepareRebuildTask(draft, profile, edit, copySpec, capability(reduction), trustStore, 150), /upstream REBUILD recommendation/);
|
|
96
|
-
assert.throws(() => prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction, { purpose: 'hyv.final-approval' }), trustStore, 150), /Rebuild authorization is invalid/);
|
|
97
|
-
assert.throws(() => prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction, { nonce: 'stale' }), { ...trustStore, keys: [{ ...trustStore.keys[0], status: 'revoked' }] }, 150), /Rebuild authorization is invalid/);
|
|
98
|
-
assert.throws(() => prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction, { sourceHash: '8'.repeat(64) }), trustStore, 150), /Rebuild authorization is invalid/);
|
|
99
|
-
assert.throws(() => prepareRebuildTask(draft, profile, { ...reduction, recommendationFingerprint: 'a'.repeat(64) }, copySpec, capability(reduction), trustStore, 150), /upstream REBUILD recommendation/);
|
|
100
|
-
});
|
|
101
|
-
test('missing CopySpec blocks rebuild before a candidate is evaluated', () => {
|
|
102
|
-
const reduction = rebuildRecommendation();
|
|
103
|
-
assert.throws(() => prepareRebuildTask(draft, profile, reduction, { ...copySpec, claims: [] }, capability(reduction), trustStore, 150), /CopySpec/);
|
|
104
|
-
});
|
|
105
|
-
test('edit and rebuild responses are mutually incompatible', () => {
|
|
106
|
-
const reduction = rebuildRecommendation();
|
|
107
|
-
const rebuildTask = prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction), trustStore, 150);
|
|
108
|
-
const editTask = prepareRewriteTask(draft, profile);
|
|
109
|
-
const rebuildOnEdit = applyRewriteResponse(editTask, { version: '1', mode: 'REBUILD', taskFingerprint: editTask.fingerprint, candidate: rebuilt });
|
|
110
|
-
assert.equal(rebuildOnEdit.status, 'repairable');
|
|
111
|
-
assert.equal(rebuildOnEdit.failures[0]?.code, 'rebuild_response_on_edit_task');
|
|
112
|
-
const replacementsOnRebuild = applyRebuildResponse(rebuildTask, { version: '1', taskFingerprint: rebuildTask.fingerprint, replacements: [{ sentenceId: 1, text: 'I use the answer.' }] });
|
|
113
|
-
assert.equal(replacementsOnRebuild.status, 'repairable');
|
|
114
|
-
assert.equal(replacementsOnRebuild.failures[0]?.code, 'edit_response_on_rebuild_task');
|
|
115
|
-
const unsigned = applyRebuildResponse(rebuildTask, { version: '1', mode: 'REBUILD', taskFingerprint: rebuildTask.fingerprint, candidate: rebuilt });
|
|
116
|
-
assert.equal(unsigned.status, 'accepted');
|
|
117
|
-
assert.equal(unsigned.receipt.authorizationFingerprint, undefined);
|
|
118
|
-
assert.equal(unsigned.receipt.preservationBypass, undefined);
|
|
119
|
-
});
|
|
120
|
-
test('authorized rebuild allows low lexical survival while claims and hygiene still block', () => {
|
|
121
|
-
const reduction = rebuildRecommendation();
|
|
122
|
-
const task = prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction), trustStore, 150);
|
|
123
|
-
const passed = evaluate(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: rebuilt }, reduction);
|
|
124
|
-
assert.equal(passed.status, 'needs_semantic_review');
|
|
125
|
-
assert.ok((passed.verification?.preservationScore ?? 100) < 70);
|
|
126
|
-
assert.equal(passed.receipt.mode, 'REBUILD');
|
|
127
|
-
assert.equal(passed.receipt.preservationBypass, true);
|
|
128
|
-
assert.ok(Array.isArray(passed.receipt.replacementSentenceIds));
|
|
129
|
-
assert.ok((passed.receipt.replacementSentenceIds?.length ?? 0) > 0);
|
|
130
|
-
assert.equal(passed.deterministicArtifact?.verificationKind, 'rebuild');
|
|
131
|
-
assert.equal(prepareLifecycle(passed.deterministicArtifact, passed.lifecycleBinding, passed.receipt, 'normal', ['action_change']).artifact.status, 'needs_semantic_review');
|
|
132
|
-
assert.equal(verifyRebuildWithCopySpec(draft, rebuilt, profile, copySpec).passed, true);
|
|
133
|
-
const missingClaim = evaluate(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: 'Operators should delay the launch indefinitely.' }, reduction);
|
|
134
|
-
assert.equal(missingClaim.status, 'needs_escalation');
|
|
135
|
-
const hygiene = evaluate(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: `${rebuilt}\u200B` }, reduction);
|
|
136
|
-
assert.equal(hygiene.status, 'needs_escalation');
|
|
137
|
-
assert.equal(hygiene.candidate, undefined);
|
|
138
|
-
assert.equal(hygiene.verification?.finalOutput.accepted, false);
|
|
139
|
-
const cleaned = evaluate(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: `${rebuilt}\u0007` }, reduction);
|
|
140
|
-
assert.equal(cleaned.status, 'needs_semantic_review');
|
|
141
|
-
assert.equal(cleaned.candidate, rebuilt);
|
|
142
|
-
assert.equal(cleaned.verification?.finalOutput.changed, true);
|
|
143
|
-
assert.equal(cleaned.deterministicArtifact?.candidateHash, createHash('sha256').update(rebuilt).digest('hex'));
|
|
144
|
-
});
|
|
145
|
-
test('rebuild disagreement cannot record accepted learning', () => {
|
|
146
|
-
const reduction = rebuildRecommendation();
|
|
147
|
-
const task = prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction), trustStore, 150);
|
|
148
|
-
const failed = evaluate(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: 'Operators should delay the launch indefinitely.' }, reduction);
|
|
149
|
-
assert.equal(failed.status, 'needs_escalation');
|
|
150
|
-
const evaluated = evaluate(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: rebuilt }, reduction);
|
|
151
|
-
assert.equal(evaluated.status, 'needs_semantic_review');
|
|
152
|
-
const standard = verifyDeterministically(draft, rebuilt, profile, copySpec);
|
|
153
|
-
assert.notEqual(standard.artifact.artifactFingerprint, evaluated.deterministicArtifact?.artifactFingerprint);
|
|
154
|
-
assert.throws(() => recordApprovedLearning({
|
|
155
|
-
ready: { version: '1', status: 'ready_for_human_review', artifactFingerprint: '1'.repeat(64), transitionFingerprint: '2'.repeat(64), binding: evaluated.lifecycleBinding, semanticPolicy: 'normal', semanticTaskFingerprint: '3'.repeat(64), semanticEvidenceScopeFingerprint: '4'.repeat(64), verdictFingerprints: [] },
|
|
156
|
-
approved: { version: '1', status: 'approved', artifactFingerprint: '5'.repeat(64), transitionFingerprint: '6'.repeat(64), binding: evaluated.lifecycleBinding, semanticPolicy: 'normal', semanticTaskFingerprint: '3'.repeat(64), semanticEvidenceScopeFingerprint: '4'.repeat(64), verdictFingerprints: [] },
|
|
157
|
-
decision: { evaluatorId: 'human.1', decision: 'approve' },
|
|
158
|
-
capability: capability(reduction),
|
|
159
|
-
source: draft,
|
|
160
|
-
candidate: rebuilt,
|
|
161
|
-
profile,
|
|
162
|
-
context: { now: 150, trustStore, authorizedSemanticEvaluatorIds: { normal: [], highAssurance: [] }, authorizedHumanFinalizerIds: ['human.1'] },
|
|
163
|
-
copySpec,
|
|
164
|
-
}), /Invalid lifecycle artifact|Approved learning is not authorized|does not match deterministic verification/);
|
|
165
|
-
});
|
|
166
|
-
test('CLI and MCP rebuild helpers share fingerprints', () => {
|
|
167
|
-
const reduction = rebuildRecommendation();
|
|
168
|
-
const task = prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction), trustStore, 150);
|
|
169
|
-
const parsed = parseRebuildTask(JSON.parse(JSON.stringify(task)));
|
|
170
|
-
assert.equal(parsed.fingerprint, task.fingerprint);
|
|
171
|
-
assert.equal(parsed.authorizationFingerprint, task.authorizationFingerprint);
|
|
172
|
-
assert.match(task.prompt, /whole-document candidate/);
|
|
173
|
-
const briefTask = prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction, { nonce: 'nonce-brief' }), trustStore, 150, {
|
|
174
|
-
version: '1', audience: 'operators', intent: 'explain', format: 'outreach',
|
|
175
|
-
});
|
|
176
|
-
assert.match(briefTask.prompt, /# WritingBrief/);
|
|
177
|
-
assert.equal(HYV_VERSION, '3.6.0');
|
|
178
|
-
});
|
|
179
|
-
test('apply rejects forged tasks, missing capability, and substituted profiles', () => {
|
|
180
|
-
const reduction = rebuildRecommendation();
|
|
181
|
-
const task = prepareRebuildTask(draft, profile, reduction, copySpec, capability(reduction), trustStore, 150);
|
|
182
|
-
const response = { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: rebuilt };
|
|
183
|
-
const { fingerprint: _ignored, ...base } = task;
|
|
184
|
-
const forgedBase = { ...base, authorizationFingerprint: 'a'.repeat(64) };
|
|
185
|
-
const forged = { ...forgedBase, fingerprint: createHash('sha256').update(canonicalJson(forgedBase)).digest('hex') };
|
|
186
|
-
assert.equal(parseRebuildTask(forged).fingerprint, forged.fingerprint);
|
|
187
|
-
assert.throws(() => evaluateRebuildResponse(forged, { ...response, taskFingerprint: forged.fingerprint }, profile, capability(reduction), trustStore, 150), /Rebuild authorization is invalid/);
|
|
188
|
-
assert.throws(() => evaluateRebuildResponse(task, response, profile, {}, trustStore, 150), /Rebuild authorization is invalid/);
|
|
189
|
-
const other = buildProfile(['I speak in a different register altogether.', 'I keep every sentence longer than the first profile would.'], ['mechanism']);
|
|
190
|
-
assert.throws(() => evaluate(task, response, reduction, other), /Rebuild profile binding does not match this task/);
|
|
191
|
-
});
|
|
192
|
-
test('meaning-first rebuild binds the residual policy and blocks carried-over wording', () => {
|
|
193
|
-
const source = 'The operating review keeps the launch checklist small and the handoff calm. The launch is on 14 August.';
|
|
194
|
-
const reduction = rebuildRecommendation(source);
|
|
195
|
-
const task = prepareRebuildTask(source, profile, reduction, copySpec, capability(reduction, {}, source), trustStore, 150, undefined, recompositionPolicy);
|
|
196
|
-
assert.doesNotMatch(task.prompt, /The operating review keeps the launch checklist/);
|
|
197
|
-
assert.match(task.prompt, /Meaning-first recomposition contract/);
|
|
198
|
-
const repeated = evaluateRebuildResponse(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: source }, profile, capability(reduction, {}, source), trustStore, 150);
|
|
199
|
-
assert.equal(repeated.status, 'needs_escalation');
|
|
200
|
-
assert.equal(repeated.failures[0]?.code, 'lexical_residual_exceeds_policy');
|
|
201
|
-
assert.equal(repeated.receipt.lexicalResidual?.passed, false);
|
|
202
|
-
assert.equal(repeated.candidate, undefined);
|
|
203
|
-
const fresh = evaluateRebuildResponse(task, { version: '1', mode: 'REBUILD', taskFingerprint: task.fingerprint, candidate: 'Release owners now work from a compact calendar note. The launch is on 14 August. Nothing else in this message repeats the original framing.' }, profile, capability(reduction, {}, source), trustStore, 150);
|
|
204
|
-
assert.equal(fresh.status, 'needs_semantic_review');
|
|
205
|
-
assert.equal(fresh.receipt.lexicalResidual?.passed, true);
|
|
206
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
import { buildRecompositionBrief, measureLexicalResidual, parseRecompositionPolicy } from './recomposition.js';
|
|
4
|
-
const policy = {
|
|
5
|
-
version: '1', mode: 'meaning-first',
|
|
6
|
-
lexicalResidual: { ngramSize: 5, maxSharedNgramFraction: 0, maxLongestSharedRunTokens: 4 },
|
|
7
|
-
acknowledgement: 'Measures shared wording only; does not detect or prove removal of a watermark.',
|
|
8
|
-
};
|
|
9
|
-
const copySpec = {
|
|
10
|
-
version: '1', audience: 'operators', intent: 'explain', channel: 'email',
|
|
11
|
-
claims: [{ id: 'launch-date', text: 'The launch is on 14 August.', evidence: 'Release calendar.', atoms: ['The launch is on 14 August.'] }],
|
|
12
|
-
};
|
|
13
|
-
test('parses only explicit meaning-first recomposition policies', () => {
|
|
14
|
-
assert.deepEqual(parseRecompositionPolicy(policy), policy);
|
|
15
|
-
assert.throws(() => parseRecompositionPolicy({ ...policy, lexicalResidual: { ...policy.lexicalResidual, ngramSize: 4 } }), /not valid/);
|
|
16
|
-
assert.throws(() => parseRecompositionPolicy({ ...policy, acknowledgement: 'watermark removed' }), /not valid/);
|
|
17
|
-
});
|
|
18
|
-
test('builds a recomposition brief without carrying the source draft', () => {
|
|
19
|
-
const brief = buildRecompositionBrief(copySpec, { version: '1', audience: 'operators', intent: 'explain', format: 'outreach' });
|
|
20
|
-
assert.match(brief, /Meaning-first recomposition contract/);
|
|
21
|
-
assert.match(brief, /The launch is on 14 August/);
|
|
22
|
-
assert.doesNotMatch(brief, /watermark-free|dewatermarked/i);
|
|
23
|
-
});
|
|
24
|
-
test('measures lexical carry-over without calling it watermark detection', () => {
|
|
25
|
-
const source = 'The operating review keeps the launch checklist small and the handoff calm. The launch is on 14 August.';
|
|
26
|
-
const identical = measureLexicalResidual(source, source, copySpec, policy);
|
|
27
|
-
assert.equal(identical.passed, false);
|
|
28
|
-
assert.ok(identical.sharedNgramFraction > 0);
|
|
29
|
-
assert.match(identical.statement, /not a watermark detector/);
|
|
30
|
-
const fresh = measureLexicalResidual(source, 'Release owners now work from a compact calendar note. The launch is on 14 August. Nothing else in this message repeats the original framing.', copySpec, policy);
|
|
31
|
-
assert.equal(fresh.passed, true);
|
|
32
|
-
assert.equal(fresh.sharedNgramFraction, 0);
|
|
33
|
-
assert.ok(fresh.allowedResiduals.some((item) => item.reason === 'copy-spec-atom'));
|
|
34
|
-
});
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
5
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
6
|
-
import test from 'node:test';
|
|
7
|
-
const audit = new URL('../scripts/release-audit.mjs', import.meta.url).pathname;
|
|
8
|
-
const mirrorWorkflow = `on:
|
|
9
|
-
push:
|
|
10
|
-
delete:
|
|
11
|
-
workflow_dispatch:
|
|
12
|
-
schedule:
|
|
13
|
-
- cron: '17 3 * * *'
|
|
14
|
-
|
|
15
|
-
concurrency:
|
|
16
|
-
group: mirror-to-stitchflow
|
|
17
|
-
cancel-in-progress: false
|
|
18
|
-
|
|
19
|
-
jobs:
|
|
20
|
-
mirror:
|
|
21
|
-
if: github.repository == 'shashank-sn/holdyourvoice'
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
timeout-minutes: 10
|
|
24
|
-
steps:
|
|
25
|
-
- uses: actions/checkout@v7
|
|
26
|
-
- run: |
|
|
27
|
-
if [ -z "\${MIRROR_DEPLOY_KEY:-}" ]; then
|
|
28
|
-
exit 1
|
|
29
|
-
fi
|
|
30
|
-
node scripts/mirror-refs.mjs
|
|
31
|
-
`;
|
|
32
|
-
const mirrorReconciler = `
|
|
33
|
-
const headsRefspec = '+refs/remotes/origin/*:refs/heads/*';
|
|
34
|
-
const tagsRefspec = '+refs/tags/*:refs/tags/*';
|
|
35
|
-
execFileSync('git', ['update-ref', '--no-deref', '-d', 'refs/remotes/origin/HEAD']);
|
|
36
|
-
spawnSync('git', ['push', '--prune', 'mirror', headsRefspec, tagsRefspec]);
|
|
37
|
-
execFileSync('git', ['ls-remote', '--refs', 'mirror', 'refs/heads/*', 'refs/tags/*']);
|
|
38
|
-
`;
|
|
39
|
-
const stage1Files = [
|
|
40
|
-
'scripts/evaluate-rewrite-benchmark.mjs',
|
|
41
|
-
'scripts/run-stage1-dry-run.mjs',
|
|
42
|
-
'scripts/run-stage1-human-packet.mjs',
|
|
43
|
-
'scripts/run-stage2-human-packet.mjs',
|
|
44
|
-
'benchmarks/schema/protocol-manifest.v1.schema.json',
|
|
45
|
-
'benchmarks/schema/run-event.v1.schema.json',
|
|
46
|
-
'benchmarks/schema/blind-packet.v1.schema.json',
|
|
47
|
-
'benchmarks/schema/blind-mapping.v1.schema.json',
|
|
48
|
-
'benchmarks/schema/reviewer-record.v1.schema.json',
|
|
49
|
-
'benchmarks/schema/ratings-seal.v1.schema.json',
|
|
50
|
-
'benchmarks/schema/aggregate-report.v1.schema.json',
|
|
51
|
-
'benchmarks/schema/checkpoint-disposition.v1.schema.json',
|
|
52
|
-
];
|
|
53
|
-
function fixture(files) {
|
|
54
|
-
const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-audit-'));
|
|
55
|
-
execFileSync('git', ['init', '--quiet'], { cwd: directory });
|
|
56
|
-
const defaults = {
|
|
57
|
-
'package.json': JSON.stringify({ name: 'audit-fixture', license: 'MIT', files: ['dist', 'skills', 'Readme.md', 'LICENSE'], scripts: { 'stage1:evaluate': 'node scripts/evaluate-rewrite-benchmark.mjs', 'stage1:dry-run': 'npm run build && node scripts/run-stage1-dry-run.mjs', 'stage1:human-packet': 'npm run build && node scripts/run-stage1-human-packet.mjs', 'stage2:human-packet': 'npm run build && node scripts/run-stage2-human-packet.mjs' }, version: '1.0.0', type: 'module', bin: { hyv: 'dist/cli.js' }, engines: { node: '>=20' } }),
|
|
58
|
-
'mcpb/manifest.json': JSON.stringify({ version: '1.0.0' }),
|
|
59
|
-
'claude-plugin/.claude-plugin/plugin.json': JSON.stringify({ version: '1.0.0' }),
|
|
60
|
-
'.claude-plugin/marketplace.json': JSON.stringify({ plugins: [{ name: 'hold-your-voice', version: '1.0.0' }] }),
|
|
61
|
-
'claude-plugin/.mcp.json': JSON.stringify({ mcpServers: { 'hold-your-voice': { args: ['--package=@holdyourvoice/hyv@1.0.0'] } } }),
|
|
62
|
-
'Readme.md': '# public',
|
|
63
|
-
LICENSE: [
|
|
64
|
-
'Permission is hereby granted, free of charge, to any person obtaining a copy',
|
|
65
|
-
'The above copyright notice and this permission notice shall be included in all',
|
|
66
|
-
'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND',
|
|
67
|
-
].join('\n'),
|
|
68
|
-
'src/version.ts': "export const HYV_VERSION = '1.0.0';",
|
|
69
|
-
'src/stage1-evaluation.ts': "const baseline = '4e6269121d551c008a34db73077e1e4fea41b3f9'; const stage1 = '550ea24f652291dca13757fdbd2f0fa0b5e3f621';",
|
|
70
|
-
'.github/workflows/mirror-to-stitchflow.yml': mirrorWorkflow,
|
|
71
|
-
'scripts/mirror-refs.mjs': mirrorReconciler,
|
|
72
|
-
'skills/hyv-test/agent.json': '{}',
|
|
73
|
-
'skills/hyv-test/SKILL.md': '# test',
|
|
74
|
-
'skills/hyv-test/agents/openai.yaml': 'name: test',
|
|
75
|
-
...Object.fromEntries(stage1Files.map((file) => [file, '{}'])),
|
|
76
|
-
};
|
|
77
|
-
for (const [file, text] of Object.entries({ ...defaults, ...files })) {
|
|
78
|
-
const path = join(directory, file);
|
|
79
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
-
writeFileSync(path, text);
|
|
81
|
-
}
|
|
82
|
-
execFileSync('git', ['add', 'README.md'], { cwd: directory });
|
|
83
|
-
return directory;
|
|
84
|
-
}
|
|
85
|
-
test('accepts the complete public package contract', () => {
|
|
86
|
-
const directory = fixture({ 'README.md': '# public' });
|
|
87
|
-
try {
|
|
88
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
89
|
-
assert.equal(result.status, 0, result.stderr);
|
|
90
|
-
assert.match(result.stdout, /release audit passed/);
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
rmSync(directory, { recursive: true, force: true });
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
test('requires the mirror workflow to run only in the public source repository', () => {
|
|
97
|
-
const directory = fixture({
|
|
98
|
-
'README.md': '# public',
|
|
99
|
-
'.github/workflows/mirror-to-stitchflow.yml': mirrorWorkflow.replace(" if: github.repository == 'shashank-sn/holdyourvoice'\n", ''),
|
|
100
|
-
});
|
|
101
|
-
try {
|
|
102
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
103
|
-
assert.notEqual(result.status, 0);
|
|
104
|
-
assert.match(result.stderr, /mirror workflow must run only in the public source repository with a bounded timeout/);
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
rmSync(directory, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
for (const requirement of [
|
|
111
|
-
{ name: 'deleted refs', fragment: ' delete:\n', error: /mirror workflow must reconcile deleted refs/ },
|
|
112
|
-
{ name: 'manual recovery', fragment: ' workflow_dispatch:\n', error: /mirror workflow must support manual recovery/ },
|
|
113
|
-
{ name: 'scheduled reconciliation', fragment: " schedule:\n - cron: '17 3 \* \* \*'\n", error: /mirror workflow must reconcile refs on a schedule/ },
|
|
114
|
-
{ name: 'serialized updates', fragment: ' group: mirror-to-stitchflow\n', error: /mirror workflow must serialize full-ref updates and finish the active update/ },
|
|
115
|
-
{ name: 'non-cancelled active updates', fragment: ' cancel-in-progress: false\n', error: /mirror workflow must serialize full-ref updates and finish the active update/ },
|
|
116
|
-
{ name: 'bounded execution', fragment: ' timeout-minutes: 10\n', error: /mirror workflow must run only in the public source repository with a bounded timeout/ },
|
|
117
|
-
{ name: 'current checkout action', fragment: ' - uses: actions/checkout@v7\n', error: /mirror workflow must use the current checkout action/ },
|
|
118
|
-
{ name: 'deploy-key validation', fragment: ' if [ -z "${MIRROR_DEPLOY_KEY:-}" ]; then\n', error: /mirror workflow must validate the source deploy key/ },
|
|
119
|
-
{ name: 'tested reconciliation', fragment: ' node scripts/mirror-refs.mjs\n', error: /mirror workflow must run the tested ref reconciler/ },
|
|
120
|
-
]) {
|
|
121
|
-
test(`requires mirror ${requirement.name}`, () => {
|
|
122
|
-
const directory = fixture({
|
|
123
|
-
'README.md': '# public',
|
|
124
|
-
'.github/workflows/mirror-to-stitchflow.yml': mirrorWorkflow.replace(requirement.fragment, ''),
|
|
125
|
-
});
|
|
126
|
-
try {
|
|
127
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
128
|
-
assert.notEqual(result.status, 0);
|
|
129
|
-
assert.match(result.stderr, requirement.error);
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
rmSync(directory, { recursive: true, force: true });
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
for (const requirement of [
|
|
137
|
-
{ name: 'safe remote HEAD exclusion', fragment: "execFileSync('git', ['update-ref', '--no-deref', '-d', 'refs/remotes/origin/HEAD']);\n", error: /mirror reconciler must exclude the remote HEAD pseudo-ref without deleting the default branch/ },
|
|
138
|
-
{ name: 'destination pruning', fragment: "spawnSync('git', ['push', '--prune', 'mirror', headsRefspec, tagsRefspec]);\n", error: /mirror reconciler must prune destination refs/ },
|
|
139
|
-
{ name: 'post-push ref verification', fragment: "execFileSync('git', ['ls-remote', '--refs', 'mirror', 'refs/heads/*', 'refs/tags/*']);\n", error: /mirror reconciler must verify source and mirror ref parity/ },
|
|
140
|
-
]) {
|
|
141
|
-
test(`requires mirror reconciler ${requirement.name}`, () => {
|
|
142
|
-
const directory = fixture({
|
|
143
|
-
'README.md': '# public',
|
|
144
|
-
'scripts/mirror-refs.mjs': mirrorReconciler.replace(requirement.fragment, ''),
|
|
145
|
-
});
|
|
146
|
-
try {
|
|
147
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
148
|
-
assert.notEqual(result.status, 0);
|
|
149
|
-
assert.match(result.stderr, requirement.error);
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
rmSync(directory, { recursive: true, force: true });
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
test('requires exact Stage 1 scripts even when the checkpoint files remain', () => {
|
|
157
|
-
const directory = fixture({
|
|
158
|
-
'README.md': '# public',
|
|
159
|
-
'package.json': JSON.stringify({ name: 'audit-fixture', license: 'MIT', files: ['dist', 'Readme.md', 'LICENSE'], scripts: { 'stage1:evaluate': 'node changed.mjs' }, version: '1.0.0', type: 'module', bin: { hyv: 'dist/cli.js' }, engines: { node: '>=20' } }),
|
|
160
|
-
});
|
|
161
|
-
try {
|
|
162
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
163
|
-
assert.notEqual(result.status, 0);
|
|
164
|
-
assert.match(result.stderr, /Stage 1 script contract has drifted: stage1:evaluate/);
|
|
165
|
-
assert.match(result.stderr, /Stage 1 script contract has drifted: stage1:dry-run/);
|
|
166
|
-
assert.match(result.stderr, /Stage 1 script contract has drifted: stage1:human-packet/);
|
|
167
|
-
assert.match(result.stderr, /Stage 1 script contract has drifted: stage2:human-packet/);
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
rmSync(directory, { recursive: true, force: true });
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
test('rejects unquoted credentials in an untracked source file', () => {
|
|
174
|
-
const directory = fixture({ 'README.md': '# public', 'src/unsafe.ts': ['const ', 'API_KEY', '=topsecret;'].join('') });
|
|
175
|
-
try {
|
|
176
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
177
|
-
assert.notEqual(result.status, 0);
|
|
178
|
-
assert.match(result.stderr, /credential marker: src\/unsafe\.ts/);
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
rmSync(directory, { recursive: true, force: true });
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
test('requires the Claude extension version to match npm', () => {
|
|
185
|
-
const directory = fixture({
|
|
186
|
-
'README.md': '# public',
|
|
187
|
-
'package.json': JSON.stringify({ name: 'audit-fixture', license: 'MIT', files: ['dist', 'Readme.md', 'LICENSE'], version: '1.0.0', type: 'module', bin: { hyv: 'dist/cli.js' }, engines: { node: '>=20' } }),
|
|
188
|
-
'mcpb/manifest.json': JSON.stringify({ version: '1.0.1' }),
|
|
189
|
-
});
|
|
190
|
-
try {
|
|
191
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
192
|
-
assert.notEqual(result.status, 0);
|
|
193
|
-
assert.match(result.stderr, /MCPB manifest version must match package\.json/);
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
rmSync(directory, { recursive: true, force: true });
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
test('requires every Claude package surface to match npm', () => {
|
|
200
|
-
const directory = fixture({
|
|
201
|
-
'README.md': '# public',
|
|
202
|
-
'claude-plugin/.claude-plugin/plugin.json': JSON.stringify({ version: '0.9.0' }),
|
|
203
|
-
'.claude-plugin/marketplace.json': JSON.stringify({ plugins: [{ name: 'other', version: '1.0.0' }, { name: 'hold-your-voice', version: '0.9.0' }] }),
|
|
204
|
-
'claude-plugin/.mcp.json': JSON.stringify({ mcpServers: { 'hold-your-voice': { args: ['--package=@holdyourvoice/hyv@0.9.0'] } } }),
|
|
205
|
-
});
|
|
206
|
-
try {
|
|
207
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
208
|
-
assert.notEqual(result.status, 0);
|
|
209
|
-
assert.match(result.stderr, /Claude plugin version must match package\.json/);
|
|
210
|
-
assert.match(result.stderr, /Claude marketplace version must match package\.json/);
|
|
211
|
-
assert.match(result.stderr, /Claude plugin package pin must match package\.json/);
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
rmSync(directory, { recursive: true, force: true });
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
test('requires the MCP runtime version to match npm', () => {
|
|
218
|
-
const directory = fixture({ 'README.md': '# public', 'src/version.ts': "export const HYV_VERSION = '0.9.0';" });
|
|
219
|
-
try {
|
|
220
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
221
|
-
assert.notEqual(result.status, 0);
|
|
222
|
-
assert.match(result.stderr, /MCP runtime version must match package\.json/);
|
|
223
|
-
}
|
|
224
|
-
finally {
|
|
225
|
-
rmSync(directory, { recursive: true, force: true });
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
test('requires the npm package runtime contract', () => {
|
|
229
|
-
const directory = fixture({
|
|
230
|
-
'README.md': '# public',
|
|
231
|
-
'package.json': JSON.stringify({
|
|
232
|
-
name: 'audit-fixture',
|
|
233
|
-
license: 'MIT',
|
|
234
|
-
files: ['LICENSE'],
|
|
235
|
-
version: '1.0.0',
|
|
236
|
-
type: 'commonjs',
|
|
237
|
-
bin: { hyv: 'src/cli.ts' },
|
|
238
|
-
engines: { node: '>=18' },
|
|
239
|
-
}),
|
|
240
|
-
});
|
|
241
|
-
try {
|
|
242
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
243
|
-
assert.notEqual(result.status, 0);
|
|
244
|
-
assert.match(result.stderr, /npm package must include dist/);
|
|
245
|
-
assert.match(result.stderr, /npm package must include skills/);
|
|
246
|
-
assert.match(result.stderr, /npm package must include Readme\.md/);
|
|
247
|
-
assert.match(result.stderr, /npm package must use the ESM runtime contract/);
|
|
248
|
-
assert.match(result.stderr, /npm hyv binary must point to dist\/cli\.js/);
|
|
249
|
-
assert.match(result.stderr, /npm package must require Node 20 or newer/);
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
rmSync(directory, { recursive: true, force: true });
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
test('rejects every package manifest path outside the public allowlist', () => {
|
|
256
|
-
const directory = fixture({
|
|
257
|
-
'README.md': '# public',
|
|
258
|
-
'package.json': JSON.stringify({
|
|
259
|
-
name: 'audit-fixture',
|
|
260
|
-
license: 'MIT',
|
|
261
|
-
files: ['dist', 'Readme.md', 'LICENSE', 'docs', 'benchmarks/private', 'profiles'],
|
|
262
|
-
version: '1.0.0',
|
|
263
|
-
type: 'module',
|
|
264
|
-
bin: { hyv: 'dist/cli.js' },
|
|
265
|
-
engines: { node: '>=20' },
|
|
266
|
-
}),
|
|
267
|
-
});
|
|
268
|
-
try {
|
|
269
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
270
|
-
assert.notEqual(result.status, 0);
|
|
271
|
-
assert.match(result.stderr, /npm package exposes an unexpected path: docs/);
|
|
272
|
-
assert.match(result.stderr, /npm package exposes an unexpected path: benchmarks\/private/);
|
|
273
|
-
assert.match(result.stderr, /npm package exposes an unexpected path: profiles/);
|
|
274
|
-
}
|
|
275
|
-
finally {
|
|
276
|
-
rmSync(directory, { recursive: true, force: true });
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
test('rejects a forbidden nested file from the actual npm package list', () => {
|
|
280
|
-
const directory = fixture({
|
|
281
|
-
'README.md': '# public',
|
|
282
|
-
'dist/profiles/private.json': '{"secret":true}',
|
|
283
|
-
});
|
|
284
|
-
try {
|
|
285
|
-
const result = spawnSync(process.execPath, [audit], { cwd: directory, encoding: 'utf8' });
|
|
286
|
-
assert.notEqual(result.status, 0);
|
|
287
|
-
assert.match(result.stderr, /npm package contains an unexpected file: dist\/profiles\/private\.json/);
|
|
288
|
-
}
|
|
289
|
-
finally {
|
|
290
|
-
rmSync(directory, { recursive: true, force: true });
|
|
291
|
-
}
|
|
292
|
-
});
|