@holdyourvoice/hyv 3.2.0 → 3.3.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 (50) hide show
  1. package/Readme.md +23 -10
  2. package/dist/ai-editor-rules.js +5 -2
  3. package/dist/ai-editor.js +52 -9
  4. package/dist/ai-editor.test.js +62 -10
  5. package/dist/approval-capability.js +111 -0
  6. package/dist/approval-capability.test.js +52 -0
  7. package/dist/approval-context.js +54 -0
  8. package/dist/approval-context.test.js +38 -0
  9. package/dist/benchmark.js +232 -0
  10. package/dist/benchmark.test.js +328 -0
  11. package/dist/canonical-json.js +123 -0
  12. package/dist/canonical-json.test.js +24 -0
  13. package/dist/cli.js +272 -19
  14. package/dist/cli.test.js +205 -8
  15. package/dist/hygiene.js +6 -0
  16. package/dist/hygiene.test.js +7 -1
  17. package/dist/judgment-task.js +171 -0
  18. package/dist/judgment-task.test.js +162 -0
  19. package/dist/learning.js +240 -100
  20. package/dist/learning.test.js +203 -3
  21. package/dist/lifecycle-adapter.js +75 -0
  22. package/dist/lifecycle-adapter.test.js +56 -0
  23. package/dist/mcp-tools.js +101 -7
  24. package/dist/mcp-tools.test.js +156 -6
  25. package/dist/mcp.js +213 -6
  26. package/dist/mcp.test.js +210 -11
  27. package/dist/pipeline.js +78 -14
  28. package/dist/pipeline.test.js +36 -2
  29. package/dist/preservation.js +89 -0
  30. package/dist/preservation.test.js +22 -0
  31. package/dist/profile.js +87 -0
  32. package/dist/profile.test.js +114 -0
  33. package/dist/rebuild-task.js +226 -0
  34. package/dist/rebuild-task.test.js +179 -0
  35. package/dist/release-audit.test.js +111 -2
  36. package/dist/rewrite-task.js +136 -16
  37. package/dist/rewrite-task.test.js +62 -7
  38. package/dist/rule-reconciliation.test.js +50 -0
  39. package/dist/semantic-review.js +176 -7
  40. package/dist/semantic-review.test.js +98 -14
  41. package/dist/stage1-dry-run.test.js +39 -0
  42. package/dist/stage1-evaluation.js +579 -0
  43. package/dist/stage1-evaluation.test.js +184 -0
  44. package/dist/stage1-human-packet.test.js +102 -0
  45. package/dist/stage1-schema-contract.test.js +95 -0
  46. package/dist/stage2-human-packet.test.js +81 -0
  47. package/dist/version.js +1 -1
  48. package/dist/voice-dna.js +53 -1
  49. package/dist/voice-dna.test.js +79 -1
  50. package/package.json +2 -2
@@ -1,12 +1,61 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.js';
3
+ import { verifyApprovalCapability } from './approval-capability.js';
1
4
  const violations = new Set(['action_change', 'dropped_object', 'unsupported_claim', 'constraint_weakened', 'clarity_regression']);
2
- export function parseSemanticVerdict(evaluatorId, value) {
3
- if (!value || typeof value !== 'object' || Array.isArray(value))
4
- throw new Error('Semantic evaluator response must be an object.');
5
- const verdict = value;
6
- if (typeof verdict.approved !== 'boolean' || !Array.isArray(verdict.violations) || !verdict.violations.every((item) => typeof item === 'string' && violations.has(item))) {
7
- throw new Error('Semantic evaluator response must include approved and known violations.');
5
+ const HEX = /^[a-f0-9]{64}$/;
6
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
7
+ function hash(domain, value) { return createHash('sha256').update(`${domain}\0`).update(canonicalJson(value)).digest('hex'); }
8
+ function same(left, right) { return canonicalJson(left) === canonicalJson(right); }
9
+ function exact(value, required, optional = []) { return required.every((key) => key in value) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key)); }
10
+ function plain(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; }
11
+ function bindingValid(binding) {
12
+ return binding.schemaVersion === '1' && [binding.rewriteTaskFingerprint, binding.rewriteResponseFingerprint, binding.deterministicArtifactFingerprint, binding.sourceHash, binding.candidateHash].every((item) => HEX.test(item))
13
+ && ID.test(binding.profileId) && ID.test(binding.profileRevisionDigest) && ID.test(binding.rulesetVersion);
14
+ }
15
+ export function prepareSemanticReviewTask(binding, policy, receipt, allowedViolations) {
16
+ if (!bindingValid(binding) || !['normal', 'high_assurance'].includes(policy))
17
+ throw new Error('Semantic review task has an invalid binding or policy.');
18
+ if (receipt.taskFingerprint !== binding.rewriteTaskFingerprint || receipt.responseFingerprint !== binding.rewriteResponseFingerprint || !Array.isArray(receipt.replacementSentenceIds))
19
+ throw new Error('Semantic review task requires its bound rewrite receipt.');
20
+ const evidenceScope = { sentenceIds: [...new Set(receipt.replacementSentenceIds)].sort((left, right) => left - right) };
21
+ if (evidenceScope.sentenceIds.some((id) => !Number.isSafeInteger(id) || id < 1) || new Set(allowedViolations).size !== allowedViolations.length || allowedViolations.some((item) => !violations.has(item)))
22
+ throw new Error('Semantic review task has invalid evidence scope or violations.');
23
+ const base = { version: '1', judgmentType: 'semantic', binding, policy, evidenceScope, allowedViolations: [...allowedViolations].sort() };
24
+ return { ...base, taskFingerprint: hash('hyv:semantic-task:v1', base) };
25
+ }
26
+ export function isValidSemanticReviewTask(task) {
27
+ try {
28
+ if (!plain(task) || !exact(task, ['version', 'judgmentType', 'binding', 'policy', 'evidenceScope', 'allowedViolations', 'taskFingerprint'])
29
+ || task.version !== '1' || task.judgmentType !== 'semantic' || !bindingValid(task.binding) || !['normal', 'high_assurance'].includes(task.policy)
30
+ || !plain(task.evidenceScope) || !exact(task.evidenceScope, ['sentenceIds']) || !Array.isArray(task.evidenceScope.sentenceIds)
31
+ || task.evidenceScope.sentenceIds.some((id) => !Number.isSafeInteger(id) || id < 1) || new Set(task.evidenceScope.sentenceIds).size !== task.evidenceScope.sentenceIds.length
32
+ || !Array.isArray(task.allowedViolations) || task.allowedViolations.some((item) => !violations.has(item)) || new Set(task.allowedViolations).size !== task.allowedViolations.length)
33
+ return false;
34
+ const { taskFingerprint, ...base } = task;
35
+ return taskFingerprint === hash('hyv:semantic-task:v1', base);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ export function parseSemanticVerdict(taskOrEvaluatorId, evaluatorIdOrValue, maybeValue) {
42
+ if (typeof taskOrEvaluatorId === 'string') {
43
+ const raw = evaluatorIdOrValue;
44
+ if (!plain(raw) || typeof raw.approved !== 'boolean' || !Array.isArray(raw.violations) || raw.violations.some((item) => typeof item !== 'string' || !violations.has(item)))
45
+ throw new Error('Semantic evaluator response must include approved and known violations.');
46
+ return { evaluatorId: taskOrEvaluatorId, approved: raw.approved, violations: raw.violations };
8
47
  }
9
- return { evaluatorId, approved: verdict.approved, violations: verdict.violations };
48
+ const task = taskOrEvaluatorId;
49
+ const evaluatorId = evaluatorIdOrValue;
50
+ const value = maybeValue;
51
+ if (!isValidSemanticReviewTask(task) || !ID.test(evaluatorId) || !value || typeof value !== 'object' || Array.isArray(value))
52
+ throw new Error('Semantic evaluator response must be a bounded object.');
53
+ const raw = value;
54
+ if (Object.keys(raw).some((key) => !['approved', 'violations'].includes(key)) || typeof raw.approved !== 'boolean' || !Array.isArray(raw.violations)
55
+ || raw.violations.some((item) => typeof item !== 'string' || !violations.has(item) || !task.allowedViolations.includes(item)) || new Set(raw.violations).size !== raw.violations.length
56
+ || (raw.approved && raw.violations.length !== 0) || (!raw.approved && raw.violations.length === 0))
57
+ throw new Error('Semantic evaluator response contradicts its known violations.');
58
+ return { version: '1', judgmentType: 'semantic', taskFingerprint: task.taskFingerprint, binding: task.binding, evidenceScope: task.evidenceScope, evaluatorId, approved: raw.approved, violations: raw.violations };
10
59
  }
11
60
  export function reviewSemanticVerdicts(verdicts) {
12
61
  const ids = new Set(verdicts.map((verdict) => verdict.evaluatorId));
@@ -18,3 +67,123 @@ export function reviewSemanticVerdicts(verdicts) {
18
67
  return { status: 'needs_escalation', verdicts, reason: 'evaluator_disagreement' };
19
68
  return { status: 'needs_escalation', verdicts, reason: 'semantic_violation' };
20
69
  }
70
+ function artifact(base) {
71
+ return { ...base, artifactFingerprint: hash('hyv:lifecycle-artifact:v1', base) };
72
+ }
73
+ export function isValidLifecycleArtifact(value) {
74
+ if (!plain(value) || value.version !== '1' || !['needs_semantic_review', 'ready_for_human_review', 'approved', 'needs_escalation'].includes(value.status) || !bindingValid(value.binding) || !['normal', 'high_assurance'].includes(value.semanticPolicy)
75
+ || !HEX.test(value.transitionFingerprint) || !HEX.test(value.semanticTaskFingerprint) || !HEX.test(value.semanticEvidenceScopeFingerprint)
76
+ || !Array.isArray(value.verdictFingerprints) || value.verdictFingerprints.some((item) => !HEX.test(item)) || new Set(value.verdictFingerprints).size !== value.verdictFingerprints.length)
77
+ return false;
78
+ const requiredKeys = ['version', 'status', 'artifactFingerprint', 'transitionFingerprint', 'binding', 'semanticPolicy', 'semanticTaskFingerprint', 'semanticEvidenceScopeFingerprint', 'verdictFingerprints'];
79
+ const optionalKeys = ['parentArtifactFingerprint', 'capabilityFingerprint', 'reason'];
80
+ if (!exact(value, requiredKeys, optionalKeys))
81
+ return false;
82
+ const requiredVerdicts = value.semanticPolicy === 'normal' ? 1 : 3;
83
+ if (value.status === 'needs_semantic_review' && (value.parentArtifactFingerprint !== undefined || value.verdictFingerprints.length !== 0 || value.capabilityFingerprint !== undefined || value.reason !== undefined))
84
+ return false;
85
+ if (value.status === 'ready_for_human_review' && (!value.parentArtifactFingerprint || !HEX.test(value.parentArtifactFingerprint) || value.verdictFingerprints.length !== requiredVerdicts || value.capabilityFingerprint !== undefined || value.reason !== undefined))
86
+ return false;
87
+ if (value.status === 'approved' && (!value.parentArtifactFingerprint || !HEX.test(value.parentArtifactFingerprint) || value.verdictFingerprints.length !== requiredVerdicts || !value.capabilityFingerprint || !HEX.test(value.capabilityFingerprint) || value.reason !== undefined))
88
+ return false;
89
+ if (value.status === 'needs_escalation' && (!value.parentArtifactFingerprint || !HEX.test(value.parentArtifactFingerprint) || !value.reason || !['semantic_rejection', 'semantic_disagreement', 'human_rejection'].includes(value.reason) || value.verdictFingerprints.length !== requiredVerdicts || value.capabilityFingerprint !== undefined))
90
+ return false;
91
+ const { artifactFingerprint, ...base } = value;
92
+ return HEX.test(artifactFingerprint) && artifactFingerprint === hash('hyv:lifecycle-artifact:v1', base);
93
+ }
94
+ export function createInitialLifecycleArtifact(task, deterministic) {
95
+ const { taskFingerprint, ...taskBase } = task;
96
+ const { artifactFingerprint, ...deterministicBase } = deterministic;
97
+ const expectedDeterministicFingerprint = createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex');
98
+ if (taskFingerprint !== hash('hyv:semantic-task:v1', taskBase) || !deterministic.passed || artifactFingerprint !== expectedDeterministicFingerprint || deterministic.artifactFingerprint !== task.binding.deterministicArtifactFingerprint || deterministic.sourceHash !== task.binding.sourceHash || deterministic.candidateHash !== task.binding.candidateHash || deterministic.profileId !== task.binding.profileId || deterministic.profileRevisionDigest !== task.binding.profileRevisionDigest || deterministic.rulesetVersion !== task.binding.rulesetVersion)
99
+ throw new Error('Lifecycle creation requires its bound passed deterministic verification artifact.');
100
+ return artifact({ version: '1', status: 'needs_semantic_review', transitionFingerprint: hash('hyv:lifecycle-initial:v1', { binding: task.binding, semanticPolicy: task.policy }), binding: task.binding, semanticPolicy: task.policy, semanticTaskFingerprint: task.taskFingerprint, semanticEvidenceScopeFingerprint: hash('hyv:semantic-evidence-scope:v1', task.evidenceScope), verdictFingerprints: [] });
101
+ }
102
+ function fail(error) { return { ok: false, error }; }
103
+ function normalizeAction(value) {
104
+ if (!plain(value) || value.version !== '1' || typeof value.parentArtifactFingerprint !== 'string' || !HEX.test(value.parentArtifactFingerprint))
105
+ return undefined;
106
+ const action = value;
107
+ if (action.type === 'semantic_submission') {
108
+ if (!exact(value, ['version', 'type', 'parentArtifactFingerprint', 'taskFingerprint', 'verdicts']) || !HEX.test(action.taskFingerprint) || !Array.isArray(action.verdicts) || action.verdicts.length < 1 || action.verdicts.length > 3)
109
+ return undefined;
110
+ for (const verdict of action.verdicts) {
111
+ if (!verdict || typeof verdict !== 'object' || !exact(verdict, ['version', 'judgmentType', 'taskFingerprint', 'binding', 'evidenceScope', 'evaluatorId', 'approved', 'violations']) || verdict.version !== '1' || verdict.judgmentType !== 'semantic' || !ID.test(verdict.evaluatorId) || typeof verdict.approved !== 'boolean' || !Array.isArray(verdict.violations) || verdict.violations.some((item) => !violations.has(item)) || new Set(verdict.violations).size !== verdict.violations.length)
112
+ return undefined;
113
+ }
114
+ return { ...action, verdicts: [...action.verdicts].map((verdict) => ({ ...verdict, violations: [...verdict.violations].sort() })).sort((left, right) => left.evaluatorId < right.evaluatorId ? -1 : left.evaluatorId > right.evaluatorId ? 1 : 0) };
115
+ }
116
+ if (action.type === 'human_finalization' && exact(action, ['version', 'type', 'parentArtifactFingerprint', 'finalization']) && action.finalization?.version === '1') {
117
+ const finalization = action.finalization;
118
+ if (!plain(finalization) || !exact(finalization, ['version', 'judgmentType', 'parentArtifactFingerprint', 'binding', 'evaluatorId', 'evidenceScope', 'decision'], ['capability']) || action.finalization.judgmentType !== 'human_finalization' || !ID.test(action.finalization.evaluatorId) || !HEX.test(action.finalization.parentArtifactFingerprint) || !bindingValid(action.finalization.binding) || !plain(action.finalization.evidenceScope) || !exact(action.finalization.evidenceScope, ['kind']) || action.finalization.evidenceScope.kind !== 'candidate' || !['approve', 'reject'].includes(action.finalization.decision))
119
+ return undefined;
120
+ return action;
121
+ }
122
+ return undefined;
123
+ }
124
+ export function reduceRewriteLifecycle(current, rawAction, context) {
125
+ let action;
126
+ let transitionFingerprint;
127
+ try {
128
+ action = normalizeAction(rawAction);
129
+ if (!action)
130
+ return fail('invalid_action');
131
+ transitionFingerprint = hash('hyv:lifecycle-transition:v1', action);
132
+ }
133
+ catch {
134
+ return fail('invalid_action');
135
+ }
136
+ if (current.transitionFingerprint === transitionFingerprint)
137
+ return { ok: true, artifact: current };
138
+ if (action.parentArtifactFingerprint === current.parentArtifactFingerprint)
139
+ return fail('conflicting_replay');
140
+ if (action.parentArtifactFingerprint !== current.artifactFingerprint)
141
+ return fail('stale_parent');
142
+ if (current.status === 'approved' || current.status === 'needs_escalation')
143
+ return fail('terminal_state');
144
+ if (action.type === 'semantic_submission') {
145
+ if (current.status !== 'needs_semantic_review')
146
+ return fail('out_of_order_transition');
147
+ const required = current.semanticPolicy === 'normal' ? 1 : 3;
148
+ if (action.verdicts.length !== required)
149
+ return fail('invalid_verdict_count');
150
+ const ids = action.verdicts.map((verdict) => verdict.evaluatorId);
151
+ if (new Set(ids).size !== ids.length)
152
+ return fail('duplicate_evaluator');
153
+ const authorized = current.semanticPolicy === 'normal' ? context.authorizedSemanticEvaluatorIds.normal : context.authorizedSemanticEvaluatorIds.highAssurance;
154
+ if (current.semanticPolicy === 'high_assurance' && new Set(authorized).size < 3)
155
+ return fail('invalid_verdict_count');
156
+ if (ids.some((id) => !authorized.includes(id)))
157
+ return fail('evaluator_not_authorized');
158
+ for (const verdict of action.verdicts) {
159
+ if (action.taskFingerprint !== current.semanticTaskFingerprint || verdict.taskFingerprint !== action.taskFingerprint)
160
+ return fail('task_fingerprint_mismatch');
161
+ if (!same(verdict.binding, current.binding))
162
+ return fail('invalid_binding');
163
+ if (verdict.judgmentType !== 'semantic' || hash('hyv:semantic-evidence-scope:v1', verdict.evidenceScope) !== current.semanticEvidenceScopeFingerprint)
164
+ return fail('evidence_scope_mismatch');
165
+ if ((verdict.approved && verdict.violations.length) || (!verdict.approved && !verdict.violations.length))
166
+ return fail('contradictory_verdict');
167
+ }
168
+ const verdictFingerprints = action.verdicts.map((verdict) => hash('hyv:semantic-verdict:v1', verdict));
169
+ const approvals = action.verdicts.filter((verdict) => verdict.approved).length;
170
+ const status = approvals === required ? 'ready_for_human_review' : 'needs_escalation';
171
+ const reason = status === 'needs_escalation' ? (approvals === 0 ? 'semantic_rejection' : 'semantic_disagreement') : undefined;
172
+ return { ok: true, artifact: artifact({ version: '1', status, parentArtifactFingerprint: current.artifactFingerprint, transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy, semanticTaskFingerprint: current.semanticTaskFingerprint, semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint, verdictFingerprints, ...(reason ? { reason } : {}) }) };
173
+ }
174
+ if (current.status !== 'ready_for_human_review')
175
+ return fail('out_of_order_transition');
176
+ const finalization = action.finalization;
177
+ if (finalization.parentArtifactFingerprint !== current.artifactFingerprint || !same(finalization.binding, current.binding) || finalization.judgmentType !== 'human_finalization' || finalization.evidenceScope.kind !== 'candidate')
178
+ return fail('invalid_binding');
179
+ if (!context.authorizedHumanFinalizerIds.includes(finalization.evaluatorId))
180
+ return fail('human_finalizer_not_authorized');
181
+ if (finalization.decision === 'reject')
182
+ return { ok: true, artifact: artifact({ version: '1', status: 'needs_escalation', parentArtifactFingerprint: current.artifactFingerprint, transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy, semanticTaskFingerprint: current.semanticTaskFingerprint, semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint, verdictFingerprints: current.verdictFingerprints, reason: 'human_rejection' }) };
183
+ if (!finalization.capability)
184
+ return fail('capability_required');
185
+ const capability = verifyApprovalCapability(finalization.capability, context.trustStore, { now: context.now, expectedSubjectArtifactFingerprint: current.artifactFingerprint, binding: current.binding, expectedPurpose: 'hyv.final-approval' });
186
+ if (!capability.ok)
187
+ return fail('capability_invalid');
188
+ return { ok: true, artifact: artifact({ version: '1', status: 'approved', parentArtifactFingerprint: current.artifactFingerprint, transitionFingerprint, binding: current.binding, semanticPolicy: current.semanticPolicy, semanticTaskFingerprint: current.semanticTaskFingerprint, semanticEvidenceScopeFingerprint: current.semanticEvidenceScopeFingerprint, verdictFingerprints: current.verdictFingerprints, capabilityFingerprint: capability.capabilityFingerprint }) };
189
+ }
@@ -1,17 +1,101 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { createHash, generateKeyPairSync, sign } from 'node:crypto';
2
3
  import test from 'node:test';
3
- import { parseSemanticVerdict, reviewSemanticVerdicts } from './semantic-review.js';
4
- test('accepts only three independent clean semantic verdicts', () => {
5
- const result = reviewSemanticVerdicts(['deepseek', 'kimi', 'sonnet'].map((evaluatorId) => parseSemanticVerdict(evaluatorId, { approved: true, violations: [] })));
6
- assert.equal(result.status, 'accepted');
7
- });
8
- test('escalates disagreement and action drift', () => {
9
- const disagreement = reviewSemanticVerdicts([
10
- parseSemanticVerdict('deepseek', { approved: true, violations: [] }),
11
- parseSemanticVerdict('kimi', { approved: false, violations: ['action_change'] }),
12
- parseSemanticVerdict('sonnet', { approved: false, violations: ['action_change'] }),
13
- ]);
14
- assert.equal(disagreement.reason, 'evaluator_disagreement');
15
- const rejected = reviewSemanticVerdicts(['deepseek', 'kimi', 'sonnet'].map((evaluatorId) => parseSemanticVerdict(evaluatorId, { approved: false, violations: ['action_change'] })));
16
- assert.equal(rejected.reason, 'semantic_violation');
4
+ import { canonicalJson, canonicalJsonBytes } from './canonical-json.js';
5
+ import { createInitialLifecycleArtifact, parseSemanticVerdict, prepareSemanticReviewTask, reduceRewriteLifecycle, reviewSemanticVerdicts } from './semantic-review.js';
6
+ const deterministicBase = { version: '1', verificationKind: 'standard', passed: true, analysisVersion: '2', rulesetVersion: '3.2.0', preservationMetricVersion: 'legacy-set-v1', preservationScore: 100, sourceHash: '4'.repeat(64), candidateHash: '5'.repeat(64), profileId: 'founder.primary', profileRevisionDigest: '6'.repeat(64), regressionKeys: [] };
7
+ const deterministicFingerprint = createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(deterministicBase)}`).digest('hex');
8
+ const binding = { rewriteTaskFingerprint: '1'.repeat(64), rewriteResponseFingerprint: '2'.repeat(64), deterministicArtifactFingerprint: deterministicFingerprint, sourceHash: deterministicBase.sourceHash, candidateHash: deterministicBase.candidateHash, profileId: deterministicBase.profileId, profileRevisionDigest: deterministicBase.profileRevisionDigest, rulesetVersion: deterministicBase.rulesetVersion, schemaVersion: '1' };
9
+ const receipt = { version: '1', taskFingerprint: binding.rewriteTaskFingerprint, responseFingerprint: binding.rewriteResponseFingerprint, adapterIds: [], replacementSentenceIds: [3, 1] };
10
+ function task(policy = 'normal') { return prepareSemanticReviewTask(binding, policy, receipt, ['action_change']); }
11
+ function deterministic(passed = true) { const base = { ...deterministicBase, passed }; return { ...base, artifactFingerprint: createHash('sha256').update(`hyv:deterministic-verification:v1\0${canonicalJson(base)}`).digest('hex') }; }
12
+ const emptyTrust = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [] };
13
+ const context = { now: 150, trustStore: emptyTrust, authorizedSemanticEvaluatorIds: { normal: ['one'], highAssurance: ['one', 'two', 'three'] }, authorizedHumanFinalizerIds: ['human-1'] };
14
+ test('preserves the version 3.2 semantic aggregation API', () => {
15
+ const verdicts = ['one', 'two', 'three'].map((id) => parseSemanticVerdict(id, { approved: true, violations: [] }));
16
+ assert.equal(reviewSemanticVerdicts(verdicts).status, 'accepted');
17
+ });
18
+ test('normal review clears with one clean authorized verdict and rejection escalates', () => {
19
+ const reviewTask = task();
20
+ const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
21
+ const clean = parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] });
22
+ const cleared = reduceRewriteLifecycle(initial, { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [clean] }, context);
23
+ assert.equal(cleared.ok && cleared.artifact.status, 'ready_for_human_review');
24
+ const rejected = parseSemanticVerdict(reviewTask, 'one', { approved: false, violations: ['action_change'] });
25
+ const escalated = reduceRewriteLifecycle(initial, { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [rejected] }, context);
26
+ assert.equal(escalated.ok && escalated.artifact.status, 'needs_escalation');
27
+ });
28
+ test('high assurance is permutation-stable and requires three configured evaluators', () => {
29
+ const reviewTask = task('high_assurance');
30
+ const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
31
+ const verdicts = ['three', 'one', 'two'].map((id) => parseSemanticVerdict(reviewTask, id, { approved: true, violations: [] }));
32
+ const action = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts };
33
+ const left = reduceRewriteLifecycle(initial, action, context);
34
+ const right = reduceRewriteLifecycle(initial, { ...action, verdicts: [...verdicts].reverse() }, context);
35
+ assert.deepEqual(left, right);
36
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...action, verdicts: verdicts.slice(0, 2) }, context), { ok: false, error: 'invalid_verdict_count' });
37
+ });
38
+ test('enforces replay, parent order, terminal states, bindings, and evaluator authority', () => {
39
+ const reviewTask = task();
40
+ const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
41
+ const action = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] })] };
42
+ const next = reduceRewriteLifecycle(initial, action, context);
43
+ assert.equal(next.ok, true);
44
+ if (!next.ok)
45
+ return;
46
+ assert.strictEqual(reduceRewriteLifecycle(next.artifact, action, context).ok, true);
47
+ assert.deepEqual(reduceRewriteLifecycle(next.artifact, { ...action, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: false, violations: ['action_change'] })] }, context), { ok: false, error: 'conflicting_replay' });
48
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...action, parentArtifactFingerprint: '9'.repeat(64) }, context), { ok: false, error: 'stale_parent' });
49
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...action, verdicts: [parseSemanticVerdict(reviewTask, 'unknown', { approved: true, violations: [] })] }, context), { ok: false, error: 'evaluator_not_authorized' });
50
+ const terminal = reduceRewriteLifecycle(createInitialLifecycleArtifact(reviewTask, deterministic()), { ...action, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: false, violations: ['action_change'] })] }, context);
51
+ if (terminal.ok)
52
+ assert.deepEqual(reduceRewriteLifecycle(terminal.artifact, { ...action, parentArtifactFingerprint: terminal.artifact.artifactFingerprint }, context), { ok: false, error: 'terminal_state' });
53
+ });
54
+ test('authorized human rejection escalates; approval requires a valid external capability', () => {
55
+ const reviewTask = task();
56
+ const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
57
+ const semantic = reduceRewriteLifecycle(initial, { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] })] }, context);
58
+ assert.equal(semantic.ok, true);
59
+ if (!semantic.ok)
60
+ return;
61
+ const approvalBase = { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: { kind: 'candidate' }, decision: 'approve' };
62
+ assert.deepEqual(reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: approvalBase }, context), { ok: false, error: 'capability_required' });
63
+ assert.deepEqual(reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: { ...approvalBase, capability: { payload: 'bad', signature: 'bad' } } }, context), { ok: false, error: 'capability_invalid' });
64
+ const rejection = reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: { kind: 'candidate' }, decision: 'reject' } }, context);
65
+ assert.equal(rejection.ok && rejection.artifact.status, 'needs_escalation');
66
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519');
67
+ const trustStore = { version: '1', audience: '@holdyourvoice/hyv', maxCapabilityLifetimeSeconds: 300, keys: [{ issuer: 'host', keyId: 'k1', publicKeySpki: publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'), status: 'active' }] };
68
+ const claims = { version: '1', purpose: 'hyv.final-approval', issuer: 'host', audience: '@holdyourvoice/hyv', subjectArtifactFingerprint: semantic.artifact.artifactFingerprint, sourceHash: binding.sourceHash, candidateHash: binding.candidateHash, profileId: binding.profileId, profileRevisionDigest: binding.profileRevisionDigest, keyId: 'k1', issuedAt: 100, notBefore: 100, expiresAt: 200, nonce: 'nonce' };
69
+ const payload = canonicalJsonBytes(claims);
70
+ const capability = { payload: payload.toString('base64url'), signature: sign(null, payload, privateKey).toString('base64url') };
71
+ const approved = reduceRewriteLifecycle(semantic.artifact, { version: '1', type: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, finalization: { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: semantic.artifact.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: { kind: 'candidate' }, decision: 'approve', capability } }, { ...context, trustStore });
72
+ assert.equal(approved.ok && approved.artifact.status, 'approved');
73
+ assert.doesNotMatch(JSON.stringify(approved), /nonce|payload|signature/);
74
+ });
75
+ test('failed deterministic verification cannot create an initial lifecycle artifact', () => {
76
+ assert.throws(() => createInitialLifecycleArtifact(task(), deterministic(false)), /passed deterministic verification/);
77
+ const reviewTask = task();
78
+ assert.throws(() => createInitialLifecycleArtifact({ ...reviewTask, evidenceScope: { sentenceIds: [99] } }, deterministic()), /passed deterministic verification/);
79
+ });
80
+ test('rejects oversized or deeply nested actions as invalid without throwing', () => {
81
+ const reviewTask = task();
82
+ const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
83
+ const verdict = parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] });
84
+ const oversized = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: Array.from({ length: 1000 }, () => verdict) };
85
+ assert.deepEqual(reduceRewriteLifecycle(initial, oversized, context), { ok: false, error: 'invalid_action' });
86
+ let nested = 'x';
87
+ for (let index = 0; index < 70; index += 1)
88
+ nested = [nested];
89
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...oversized, verdicts: [{ ...verdict, binding: nested }] }, context), { ok: false, error: 'invalid_action' });
90
+ const malformedFinalization = { version: '1', type: 'human_finalization', parentArtifactFingerprint: initial.artifactFingerprint, finalization: { version: '1', judgmentType: 'human_finalization', parentArtifactFingerprint: initial.artifactFingerprint, binding, evaluatorId: 'human-1', evidenceScope: null, decision: 'reject' } };
91
+ assert.deepEqual(reduceRewriteLifecycle(initial, malformedFinalization, context), { ok: false, error: 'invalid_action' });
92
+ });
93
+ test('semantic submissions reject changed task, binding, and evidence scope', () => {
94
+ const reviewTask = task();
95
+ const initial = createInitialLifecycleArtifact(reviewTask, deterministic());
96
+ const verdict = parseSemanticVerdict(reviewTask, 'one', { approved: true, violations: [] });
97
+ const base = { version: '1', type: 'semantic_submission', parentArtifactFingerprint: initial.artifactFingerprint, taskFingerprint: reviewTask.taskFingerprint, verdicts: [verdict] };
98
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...base, taskFingerprint: '9'.repeat(64) }, context), { ok: false, error: 'task_fingerprint_mismatch' });
99
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...base, verdicts: [{ ...verdict, binding: { ...binding, candidateHash: '9'.repeat(64) } }] }, context), { ok: false, error: 'invalid_binding' });
100
+ assert.deepEqual(reduceRewriteLifecycle(initial, { ...base, verdicts: [{ ...verdict, evidenceScope: { sentenceIds: [99] } }] }, context), { ok: false, error: 'evidence_scope_mismatch' });
17
101
  });
@@ -0,0 +1,39 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import test from 'node:test';
7
+ test('synthetic Stage 1 packet executes outside the repository and remains blocked', () => {
8
+ const parent = mkdtempSync(join(tmpdir(), 'hyv-stage1-dry-run-test-'));
9
+ const outputRoot = join(parent, 'packet');
10
+ try {
11
+ const output = JSON.parse(execFileSync(process.execPath, ['scripts/run-stage1-dry-run.mjs', '--out', outputRoot], { encoding: 'utf8' }));
12
+ assert.equal(output.decision, 'BLOCKED');
13
+ assert.equal(output.promotable, false);
14
+ assert.deepEqual(output.blockers, ['human_writer_evidence_deferred', 'synthetic_fixture_evidence', 'locked_human_evidence_required']);
15
+ const report = JSON.parse(readFileSync(join(outputRoot, 'report.json'), 'utf8'));
16
+ assert.equal(report.decision, 'BLOCKED');
17
+ assert.equal(report.promotable, false);
18
+ assert.match(report.reportDigest, /^[a-f0-9]{64}$/);
19
+ }
20
+ finally {
21
+ rmSync(parent, { recursive: true, force: true });
22
+ }
23
+ });
24
+ test('synthetic Stage 1 packet refuses repository output', () => {
25
+ assert.throws(() => execFileSync(process.execPath, ['scripts/run-stage1-dry-run.mjs', '--out', join(process.cwd(), 'stage1-output')], { stdio: 'pipe' }), /Dry-run output must stay outside the repository/);
26
+ });
27
+ test('synthetic Stage 1 packet refuses a symlinked ancestor into the repository', () => {
28
+ const parent = mkdtempSync(join(tmpdir(), 'hyv-stage1-symlink-test-'));
29
+ const repositoryTarget = join(process.cwd(), 'stage1-symlink-target');
30
+ mkdirSync(repositoryTarget);
31
+ symlinkSync(repositoryTarget, join(parent, 'redirect'));
32
+ try {
33
+ assert.throws(() => execFileSync(process.execPath, ['scripts/run-stage1-dry-run.mjs', '--out', join(parent, 'redirect', 'packet')], { stdio: 'pipe' }), /Dry-run output must stay outside the repository/);
34
+ }
35
+ finally {
36
+ rmSync(parent, { recursive: true, force: true });
37
+ rmSync(repositoryTarget, { recursive: true, force: true });
38
+ }
39
+ });