@holdyourvoice/hyv 3.1.1 → 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.
- package/Readme.md +76 -17
- package/dist/ai-editor-rules.js +151 -0
- package/dist/ai-editor.js +104 -8
- package/dist/ai-editor.test.js +135 -22
- package/dist/approval-capability.js +111 -0
- package/dist/approval-capability.test.js +52 -0
- package/dist/approval-context.js +54 -0
- package/dist/approval-context.test.js +38 -0
- package/dist/benchmark.js +232 -0
- package/dist/benchmark.test.js +328 -0
- package/dist/canonical-json.js +123 -0
- package/dist/canonical-json.test.js +24 -0
- package/dist/cli.js +359 -21
- package/dist/cli.test.js +275 -7
- package/dist/copy-spec.js +35 -8
- package/dist/editorial-packs.js +25 -1
- package/dist/editorial-packs.test.js +45 -0
- package/dist/hygiene.js +91 -0
- package/dist/hygiene.test.js +73 -0
- package/dist/judgment-task.js +171 -0
- package/dist/judgment-task.test.js +162 -0
- package/dist/learning.js +240 -100
- package/dist/learning.test.js +203 -3
- package/dist/lifecycle-adapter.js +75 -0
- package/dist/lifecycle-adapter.test.js +56 -0
- package/dist/mcp-tools.js +110 -9
- package/dist/mcp-tools.test.js +188 -10
- package/dist/mcp.js +228 -9
- package/dist/mcp.test.js +248 -12
- package/dist/pipeline.js +81 -15
- package/dist/pipeline.test.js +94 -2
- package/dist/preservation.js +89 -0
- package/dist/preservation.test.js +22 -0
- package/dist/profile.js +87 -0
- package/dist/profile.test.js +114 -0
- package/dist/rebuild-task.js +226 -0
- package/dist/rebuild-task.test.js +179 -0
- package/dist/release-audit.test.js +144 -2
- package/dist/rewrite-task.js +136 -16
- package/dist/rewrite-task.test.js +72 -4
- package/dist/rule-reconciliation.test.js +50 -0
- package/dist/semantic-review.js +176 -7
- package/dist/semantic-review.test.js +98 -14
- package/dist/stage1-dry-run.test.js +39 -0
- package/dist/stage1-evaluation.js +579 -0
- package/dist/stage1-evaluation.test.js +184 -0
- package/dist/stage1-human-packet.test.js +102 -0
- package/dist/stage1-schema-contract.test.js +95 -0
- package/dist/stage2-human-packet.test.js +81 -0
- package/dist/version.js +1 -0
- package/dist/voice-dna.js +53 -1
- package/dist/voice-dna.test.js +79 -1
- package/package.json +2 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const MAX_DEPTH = 64;
|
|
2
|
+
const MAX_NODES = 10_000;
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
5
|
+
}
|
|
6
|
+
function validUnicode(value) {
|
|
7
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
8
|
+
const code = value.charCodeAt(index);
|
|
9
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
10
|
+
const next = value.charCodeAt(index + 1);
|
|
11
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
12
|
+
return false;
|
|
13
|
+
index += 1;
|
|
14
|
+
}
|
|
15
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
function serialize(value, depth, budget) {
|
|
21
|
+
budget.nodes += 1;
|
|
22
|
+
if (depth > MAX_DEPTH || budget.nodes > MAX_NODES)
|
|
23
|
+
throw new Error('Value is not valid canonical JSON: complexity limit exceeded.');
|
|
24
|
+
if (value === null || typeof value === 'boolean')
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
if (!validUnicode(value))
|
|
28
|
+
throw new Error('Value is not valid canonical JSON: lone surrogate.');
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'number') {
|
|
32
|
+
if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value)) || Object.is(value, -0))
|
|
33
|
+
throw new Error('Value is not valid canonical JSON: unsafe number.');
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return `[${value.map((item) => serialize(item, depth + 1, budget)).join(',')}]`;
|
|
38
|
+
if (isPlainObject(value)) {
|
|
39
|
+
const entries = Object.keys(value).sort().map((key) => {
|
|
40
|
+
if (!validUnicode(key))
|
|
41
|
+
throw new Error('Value is not valid canonical JSON: lone surrogate key.');
|
|
42
|
+
return `${JSON.stringify(key)}:${serialize(value[key], depth + 1, budget)}`;
|
|
43
|
+
});
|
|
44
|
+
return `{${entries.join(',')}}`;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Value is not valid canonical JSON: unsupported value.');
|
|
47
|
+
}
|
|
48
|
+
export function canonicalJson(value) { return serialize(value, 0, { nodes: 0 }); }
|
|
49
|
+
export function canonicalJsonBytes(value) {
|
|
50
|
+
return Buffer.from(canonicalJson(value), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
function hasDuplicateKeys(source) {
|
|
53
|
+
const stack = [];
|
|
54
|
+
let index = 0;
|
|
55
|
+
let expectingKey = false;
|
|
56
|
+
while (index < source.length) {
|
|
57
|
+
const char = source[index];
|
|
58
|
+
if (char === '{') {
|
|
59
|
+
stack.push(new Set());
|
|
60
|
+
expectingKey = true;
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (char === '[') {
|
|
65
|
+
stack.push(null);
|
|
66
|
+
expectingKey = false;
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (char === '}' || char === ']') {
|
|
71
|
+
stack.pop();
|
|
72
|
+
expectingKey = false;
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === ',') {
|
|
77
|
+
expectingKey = stack.at(-1) instanceof Set;
|
|
78
|
+
index += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === '"') {
|
|
82
|
+
let end = index + 1;
|
|
83
|
+
while (end < source.length) {
|
|
84
|
+
if (source[end] === '\\') {
|
|
85
|
+
end += 2;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (source[end] === '"')
|
|
89
|
+
break;
|
|
90
|
+
end += 1;
|
|
91
|
+
}
|
|
92
|
+
if (expectingKey && source[end + 1] === ':') {
|
|
93
|
+
const key = JSON.parse(source.slice(index, end + 1));
|
|
94
|
+
const keys = stack.at(-1);
|
|
95
|
+
if (keys.has(key))
|
|
96
|
+
return true;
|
|
97
|
+
keys.add(key);
|
|
98
|
+
expectingKey = false;
|
|
99
|
+
}
|
|
100
|
+
index = end + 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
export function parseCanonicalJson(bytes) {
|
|
108
|
+
const source = Buffer.from(bytes).toString('utf8');
|
|
109
|
+
if (!Buffer.from(source, 'utf8').equals(Buffer.from(bytes)) || source.includes('\ufffd'))
|
|
110
|
+
throw new Error('Payload is not canonical JSON.');
|
|
111
|
+
if (hasDuplicateKeys(source))
|
|
112
|
+
throw new Error('Payload contains a duplicate JSON key.');
|
|
113
|
+
let value;
|
|
114
|
+
try {
|
|
115
|
+
value = JSON.parse(source);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new Error('Payload is not canonical JSON.');
|
|
119
|
+
}
|
|
120
|
+
if (canonicalJson(value) !== source)
|
|
121
|
+
throw new Error('Payload is not canonical JSON.');
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { canonicalJson, canonicalJsonBytes, parseCanonicalJson } from './canonical-json.js';
|
|
4
|
+
test('canonicalizes nested objects, arrays, escapes, unicode, and numbers', () => {
|
|
5
|
+
const value = { z: [3, { b: '\u20ac', a: '\n' }], a: 1e-7 };
|
|
6
|
+
assert.equal(canonicalJson(value), '{"a":1e-7,"z":[3,{"a":"\\n","b":"€"}]}');
|
|
7
|
+
assert.deepEqual(canonicalJsonBytes(value), Buffer.from(canonicalJson(value), 'utf8'));
|
|
8
|
+
});
|
|
9
|
+
test('rejects duplicate keys, lone surrogates, unsafe integers, negative zero, and unsupported values', () => {
|
|
10
|
+
assert.throws(() => parseCanonicalJson(Buffer.from('{"a":1,"a":1}')), /duplicate/i);
|
|
11
|
+
for (const value of ['\ud800', Number.MAX_SAFE_INTEGER + 1, -0, undefined]) {
|
|
12
|
+
assert.throws(() => canonicalJson(value), /canonical/i);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
test('requires exact canonical payload bytes', () => {
|
|
16
|
+
assert.deepEqual(parseCanonicalJson(Buffer.from('{"a":1,"b":2}')), { a: 1, b: 2 });
|
|
17
|
+
assert.throws(() => parseCanonicalJson(Buffer.from('{ "a": 1, "b": 2 }')), /canonical/i);
|
|
18
|
+
});
|
|
19
|
+
test('rejects inputs beyond the canonical complexity budget', () => {
|
|
20
|
+
let value = 1;
|
|
21
|
+
for (let index = 0; index < 70; index += 1)
|
|
22
|
+
value = [value];
|
|
23
|
+
assert.throws(() => canonicalJson(value), /complexity limit/);
|
|
24
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -1,20 +1,146 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
2
|
+
import { closeSync, constants, fstatSync, linkSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
4
|
+
import { RULESET_VERSION, serializedRules } from './ai-editor.js';
|
|
4
5
|
import { parseCopySpec } from './copy-spec.js';
|
|
5
6
|
import { analyzeBatch, parseWritingBrief } from './editorial-packs.js';
|
|
6
|
-
import {
|
|
7
|
+
import { clearLearning, composeLearning, inspectLearning, migrateLearningV2ToV3, profileFingerprint, ratifyLearningEvent, recordLearningInstruction, supersedeLearningEvent } from './learning.js';
|
|
8
|
+
import { cleanHygiene, finalOutputCheck, inspectHygiene } from './hygiene.js';
|
|
7
9
|
import { analyze, rewritePrompt, verify, verifyWithCopySpec } from './pipeline.js';
|
|
8
10
|
import { parseProfile } from './profile.js';
|
|
9
11
|
import { evaluateRewriteResponse, parseRewriteTask, prepareRewriteTask } from './rewrite-task.js';
|
|
12
|
+
import { parseJudgmentEnvelope, preparePostCandidateJudgment, preparePreEditJudgment, reducePostCandidate, reducePreEdit } from './judgment-task.js';
|
|
13
|
+
import { evaluateRebuildResponse, parseRebuildTask, prepareRebuildTask } from './rebuild-task.js';
|
|
14
|
+
import { canonicalJson, parseCanonicalJson } from './canonical-json.js';
|
|
15
|
+
import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.js';
|
|
10
16
|
import { buildProfile } from './voice-dna.js';
|
|
11
|
-
|
|
17
|
+
import { loadApprovalContext } from './approval-context.js';
|
|
18
|
+
const usage = 'Commands: profile, analyze, hygiene, final-check, batch-analyze, rewrite-prompt, prepare-rewrite, apply-rewrite, prepare-judgment, reduce-judgment, prepare-rebuild, apply-rebuild, verify, verify-spec, lifecycle, learning, patterns, mcp';
|
|
19
|
+
const MAX_JSON_BYTES = 1024 * 1024;
|
|
12
20
|
function input(path) {
|
|
13
21
|
return path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
|
|
14
22
|
}
|
|
23
|
+
function parseBoundedJson(text) {
|
|
24
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_JSON_BYTES)
|
|
25
|
+
throw new Error('JSON input exceeds the byte limit.');
|
|
26
|
+
const value = JSON.parse(text);
|
|
27
|
+
canonicalJson(value);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function readBoundedDescriptor(descriptor) {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
while (size <= MAX_JSON_BYTES) {
|
|
34
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, MAX_JSON_BYTES + 1 - size));
|
|
35
|
+
const count = readSync(descriptor, chunk, 0, chunk.length, null);
|
|
36
|
+
if (!count)
|
|
37
|
+
break;
|
|
38
|
+
chunks.push(chunk.subarray(0, count));
|
|
39
|
+
size += count;
|
|
40
|
+
}
|
|
41
|
+
if (size > MAX_JSON_BYTES)
|
|
42
|
+
throw new Error('JSON input exceeds the byte limit.');
|
|
43
|
+
return Buffer.concat(chunks, size).toString('utf8');
|
|
44
|
+
}
|
|
45
|
+
function readJson(path) {
|
|
46
|
+
if (path === '-')
|
|
47
|
+
return parseBoundedJson(readBoundedDescriptor(0));
|
|
48
|
+
let descriptor;
|
|
49
|
+
try {
|
|
50
|
+
descriptor = openSync(path, constants.O_RDONLY);
|
|
51
|
+
return parseBoundedJson(readBoundedDescriptor(descriptor));
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (descriptor !== undefined)
|
|
55
|
+
closeSync(descriptor);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function capabilityArguments(args) {
|
|
59
|
+
const values = [];
|
|
60
|
+
let source;
|
|
61
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
62
|
+
if (args[index] === '--capability-stdin') {
|
|
63
|
+
if (source)
|
|
64
|
+
throw new Error('Choose one capability source.');
|
|
65
|
+
source = { kind: 'stdin' };
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (args[index] === '--capability-file') {
|
|
69
|
+
const path = args[index + 1];
|
|
70
|
+
if (source || !path || path.startsWith('--capability-'))
|
|
71
|
+
throw new Error('Choose one capability source.');
|
|
72
|
+
source = { kind: 'file', path };
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
values.push(args[index]);
|
|
77
|
+
}
|
|
78
|
+
if (!source)
|
|
79
|
+
return { values };
|
|
80
|
+
if (source.kind === 'stdin' && values.includes('-'))
|
|
81
|
+
throw new Error('Capability stdin cannot be combined with another stdin input.');
|
|
82
|
+
let raw;
|
|
83
|
+
if (source.kind === 'stdin')
|
|
84
|
+
raw = readBoundedDescriptor(0);
|
|
85
|
+
else {
|
|
86
|
+
let descriptor;
|
|
87
|
+
try {
|
|
88
|
+
descriptor = openSync(source.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
89
|
+
const before = fstatSync(descriptor);
|
|
90
|
+
if (!before.isFile() || before.uid !== process.geteuid?.() || (before.mode & 0o077) !== 0 || before.nlink !== 1 || before.size > MAX_JSON_BYTES)
|
|
91
|
+
throw new Error('Capability file is unavailable or unsafe.');
|
|
92
|
+
raw = readBoundedDescriptor(descriptor);
|
|
93
|
+
const after = fstatSync(descriptor);
|
|
94
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
|
|
95
|
+
throw new Error('Capability file is unavailable or unsafe.');
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error('Capability file is unavailable or unsafe.');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (descriptor !== undefined)
|
|
102
|
+
closeSync(descriptor);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Buffer.byteLength(raw, 'utf8') > MAX_JSON_BYTES)
|
|
106
|
+
throw new Error('JSON input exceeds the byte limit.');
|
|
107
|
+
return { values, capability: parseCanonicalJson(Buffer.from(raw, 'utf8')) };
|
|
108
|
+
}
|
|
15
109
|
function readProfile(path) {
|
|
16
110
|
return parseProfile(JSON.parse(input(path)));
|
|
17
111
|
}
|
|
112
|
+
function requireProfileV3(profile) {
|
|
113
|
+
if (profile.version !== '3')
|
|
114
|
+
throw new Error('This learning operation requires a Profile v3.');
|
|
115
|
+
return profile;
|
|
116
|
+
}
|
|
117
|
+
function learningArguments(args) {
|
|
118
|
+
const values = [];
|
|
119
|
+
const options = {};
|
|
120
|
+
for (const argument of args) {
|
|
121
|
+
if (!argument.startsWith('--')) {
|
|
122
|
+
values.push(argument);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const [name, ...parts] = argument.slice(2).split('=');
|
|
126
|
+
const value = parts.join('=').trim();
|
|
127
|
+
if (!value)
|
|
128
|
+
throw new Error(`Learning option --${name} requires a value.`);
|
|
129
|
+
if (name === 'mutation-id' && value.length <= 200)
|
|
130
|
+
options.mutationId = value;
|
|
131
|
+
else if (name === 'authority' && ['founder', 'team', 'system'].includes(value))
|
|
132
|
+
options.authority = value;
|
|
133
|
+
else if (name === 'provenance' && value.length <= 500)
|
|
134
|
+
options.provenance = value;
|
|
135
|
+
else if (name === 'weight' && Number.isFinite(Number(value)) && Number(value) > 0)
|
|
136
|
+
options.weight = Number(value);
|
|
137
|
+
else if (name === 'compatibility' && ['same-or-newer', 'exact'].includes(value))
|
|
138
|
+
options.compatibility = value;
|
|
139
|
+
else
|
|
140
|
+
throw new Error(`Invalid learning option: --${name}=${value}`);
|
|
141
|
+
}
|
|
142
|
+
return { values, options };
|
|
143
|
+
}
|
|
18
144
|
function readBrief(path) {
|
|
19
145
|
return path ? parseWritingBrief(JSON.parse(input(path))) : undefined;
|
|
20
146
|
}
|
|
@@ -51,6 +177,7 @@ function prepareContext(paths) {
|
|
|
51
177
|
function json(value) {
|
|
52
178
|
console.log(JSON.stringify(value, null, 2));
|
|
53
179
|
}
|
|
180
|
+
function canonical(value) { process.stdout.write(`${canonicalJson(value)}\n`); }
|
|
54
181
|
function profileArguments(args) {
|
|
55
182
|
const [output, ...rest] = args;
|
|
56
183
|
const samples = [];
|
|
@@ -70,6 +197,58 @@ function profileArguments(args) {
|
|
|
70
197
|
throw new Error('Usage: hyv profile profile.json sample-a.md sample-b.md [sample-c.md] [--avoid=phrase]');
|
|
71
198
|
return { output, samples, avoid };
|
|
72
199
|
}
|
|
200
|
+
function cleanedPath(path) {
|
|
201
|
+
const extension = extname(path);
|
|
202
|
+
const stem = extension ? path.slice(0, -extension.length) : path;
|
|
203
|
+
return `${stem}.cleaned${extension}`;
|
|
204
|
+
}
|
|
205
|
+
function hygieneArguments(args) {
|
|
206
|
+
const [path, ...options] = args;
|
|
207
|
+
if (!path)
|
|
208
|
+
throw new Error('Usage: hyv hygiene draft.md [--fix] [--output=cleaned.md]');
|
|
209
|
+
let fix = false;
|
|
210
|
+
let output;
|
|
211
|
+
for (const option of options) {
|
|
212
|
+
if (option === '--fix')
|
|
213
|
+
fix = true;
|
|
214
|
+
else if (option.startsWith('--output='))
|
|
215
|
+
output = option.slice('--output='.length).trim();
|
|
216
|
+
else
|
|
217
|
+
throw new Error('Usage: hyv hygiene draft.md [--fix] [--output=cleaned.md]');
|
|
218
|
+
}
|
|
219
|
+
if (output !== undefined && (!output || !fix))
|
|
220
|
+
throw new Error('--output requires --fix and a non-empty path.');
|
|
221
|
+
return { path, fix, ...(output ? { output } : {}) };
|
|
222
|
+
}
|
|
223
|
+
function writeNewFileAtomically(path, text) {
|
|
224
|
+
const temporaryDirectory = mkdtempSync(join(dirname(resolve(path)), '.hyv-hygiene-'));
|
|
225
|
+
const temporaryPath = join(temporaryDirectory, 'cleaned');
|
|
226
|
+
let primaryError;
|
|
227
|
+
try {
|
|
228
|
+
writeFileSync(temporaryPath, text, 'utf8');
|
|
229
|
+
try {
|
|
230
|
+
linkSync(temporaryPath, path);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const code = error.code;
|
|
234
|
+
if (!['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV'].includes(code ?? ''))
|
|
235
|
+
throw error;
|
|
236
|
+
throw new Error(`Atomic hygiene output is not supported by this filesystem: ${path}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
primaryError = error.code === 'EEXIST' ? new Error(`Hygiene output already exists: ${path}`) : error;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (!primaryError)
|
|
247
|
+
console.error(`Warning: output was published, but temporary-file cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
}
|
|
249
|
+
if (primaryError)
|
|
250
|
+
throw primaryError;
|
|
251
|
+
}
|
|
73
252
|
export async function runCli(args) {
|
|
74
253
|
const [command, ...rest] = args;
|
|
75
254
|
if (command === 'profile') {
|
|
@@ -84,6 +263,37 @@ export async function runCli(args) {
|
|
|
84
263
|
json(analyze(input(draft), readProfile(profilePath), readBrief(briefPath)));
|
|
85
264
|
return 0;
|
|
86
265
|
}
|
|
266
|
+
if (command === 'hygiene') {
|
|
267
|
+
const { path, fix, output } = hygieneArguments(rest);
|
|
268
|
+
if (fix && path === '-')
|
|
269
|
+
throw new Error('hyv hygiene --fix requires a file path so the original can be preserved.');
|
|
270
|
+
const text = input(path);
|
|
271
|
+
if (!fix) {
|
|
272
|
+
json(inspectHygiene(text));
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const outputPath = output ?? cleanedPath(path);
|
|
276
|
+
if (resolve(outputPath) === resolve(path))
|
|
277
|
+
throw new Error('Hygiene output must differ from the input path.');
|
|
278
|
+
const result = cleanHygiene(text);
|
|
279
|
+
writeNewFileAtomically(outputPath, result.cleaned);
|
|
280
|
+
json({ ...result.report, changed: result.changed, changes: result.changes, outputPath });
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (command === 'final-check') {
|
|
284
|
+
const [path, ...options] = rest;
|
|
285
|
+
if (!path || options.length)
|
|
286
|
+
throw new Error('Usage: hyv final-check <path|->');
|
|
287
|
+
const result = finalOutputCheck(input(path));
|
|
288
|
+
if (!result.accepted) {
|
|
289
|
+
console.error(JSON.stringify(result, null, 2));
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
if (result.changed)
|
|
293
|
+
console.error(JSON.stringify({ changed: true, changes: result.changes }, null, 2));
|
|
294
|
+
process.stdout.write(result.output);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
87
297
|
if (command === 'batch-analyze') {
|
|
88
298
|
if (rest.length < 2)
|
|
89
299
|
throw new Error('Usage: hyv batch-analyze draft-a.md draft-b.md [draft-c.md]');
|
|
@@ -116,6 +326,50 @@ export async function runCli(args) {
|
|
|
116
326
|
json(result);
|
|
117
327
|
return result.status === 'accepted' ? 0 : 2;
|
|
118
328
|
}
|
|
329
|
+
if (command === 'prepare-judgment') {
|
|
330
|
+
const [stage, kind, draft, profilePath, output, candidatePath] = rest;
|
|
331
|
+
if (!stage || !kind || !draft || !profilePath || !output)
|
|
332
|
+
throw new Error('Usage: hyv prepare-judgment pre-edit|post-candidate kind draft.md profile.json task.json [candidate.md]');
|
|
333
|
+
if (stage === 'post-candidate' && !candidatePath)
|
|
334
|
+
throw new Error('Usage: hyv prepare-judgment post-candidate kind draft.md profile.json task.json candidate.md');
|
|
335
|
+
const profile = readProfile(profilePath);
|
|
336
|
+
const task = stage === 'pre-edit'
|
|
337
|
+
? preparePreEditJudgment(input(draft), profile, kind)
|
|
338
|
+
: preparePostCandidateJudgment(input(draft), input(candidatePath ?? ''), profile, kind);
|
|
339
|
+
writeFileSync(output, `${JSON.stringify(task, null, 2)}\n`);
|
|
340
|
+
json({ version: task.version, stage: task.stage, judgmentType: task.judgmentType, taskFingerprint: task.taskFingerprint });
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
if (command === 'reduce-judgment') {
|
|
344
|
+
if (rest.length < 3)
|
|
345
|
+
throw new Error('Usage: hyv reduce-judgment envelope.json envelope.json [envelope.json...]');
|
|
346
|
+
const envelopes = rest.map((path) => parseJudgmentEnvelope(JSON.parse(input(path))));
|
|
347
|
+
const stage = envelopes[0]?.stage;
|
|
348
|
+
json(stage === 'pre-edit' ? reducePreEdit(envelopes) : reducePostCandidate(envelopes));
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
if (command === 'prepare-rebuild') {
|
|
352
|
+
const { values, capability } = capabilityArguments(rest);
|
|
353
|
+
const [draft, profilePath, reductionPath, specPath, output, briefPath] = values;
|
|
354
|
+
if (!draft || !profilePath || !reductionPath || !specPath || !output || !capability) {
|
|
355
|
+
throw new Error('Usage: hyv prepare-rebuild draft.md profile.json reduction.json copy-spec.json task.json [writing-brief.json] (--capability-stdin|--capability-file path)');
|
|
356
|
+
}
|
|
357
|
+
const context = loadApprovalContext();
|
|
358
|
+
const task = prepareRebuildTask(input(draft), readProfile(profilePath), readJson(reductionPath), parseCopySpec(JSON.parse(input(specPath))), capability, context.trustStore, context.now, briefPath ? parseWritingBrief(JSON.parse(input(briefPath))) : undefined);
|
|
359
|
+
writeFileSync(output, `${JSON.stringify(task, null, 2)}\n`);
|
|
360
|
+
json({ version: task.version, fingerprint: task.fingerprint, recommendationFingerprint: task.recommendationFingerprint, authorizationFingerprint: task.authorizationFingerprint });
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
if (command === 'apply-rebuild') {
|
|
364
|
+
const { values, capability } = capabilityArguments(rest);
|
|
365
|
+
const [taskPath, responsePath, profilePath, ...extra] = values;
|
|
366
|
+
if (!taskPath || !responsePath || !profilePath || extra.length || !capability)
|
|
367
|
+
throw new Error('Usage: hyv apply-rebuild task.json response.json profile.json (--capability-stdin|--capability-file path)');
|
|
368
|
+
const context = loadApprovalContext();
|
|
369
|
+
const result = evaluateRebuildResponse(parseRebuildTask(JSON.parse(input(taskPath))), input(responsePath), readProfile(profilePath), capability, context.trustStore, context.now);
|
|
370
|
+
json(result);
|
|
371
|
+
return result.status === 'accepted' ? 0 : 2;
|
|
372
|
+
}
|
|
119
373
|
if (command === 'verify') {
|
|
120
374
|
const [original, candidate, profilePath, briefPath] = rest;
|
|
121
375
|
if (!original || !candidate || !profilePath)
|
|
@@ -124,9 +378,6 @@ export async function runCli(args) {
|
|
|
124
378
|
const originalText = input(original);
|
|
125
379
|
const candidateText = input(candidate);
|
|
126
380
|
const result = verify(originalText, candidateText, profile, readBrief(briefPath));
|
|
127
|
-
const learning = recordVerifiedCandidate(profile, result, candidateText);
|
|
128
|
-
if (learning === 'write_failed')
|
|
129
|
-
console.error('Warning: verification passed, but local learning could not be saved.');
|
|
130
381
|
json(result);
|
|
131
382
|
return result.passed ? 0 : 2;
|
|
132
383
|
}
|
|
@@ -137,38 +388,125 @@ export async function runCli(args) {
|
|
|
137
388
|
const profile = readProfile(profilePath);
|
|
138
389
|
const candidateText = input(candidate);
|
|
139
390
|
const result = verifyWithCopySpec(input(original), candidateText, profile, parseCopySpec(JSON.parse(input(specPath))), readBrief(briefPath));
|
|
140
|
-
if (result.passed) {
|
|
141
|
-
const learning = recordVerifiedCandidate(profile, result, candidateText);
|
|
142
|
-
if (learning === 'write_failed')
|
|
143
|
-
console.error('Warning: verification passed, but local learning could not be saved.');
|
|
144
|
-
}
|
|
145
391
|
json(result);
|
|
146
392
|
return result.passed ? 0 : 2;
|
|
147
393
|
}
|
|
394
|
+
if (command === 'lifecycle') {
|
|
395
|
+
const [action, ...raw] = rest;
|
|
396
|
+
if (action === 'prepare-semantic') {
|
|
397
|
+
const [deterministicPath, bindingPath, receiptPath, policy, violationsPath, output, ...extra] = raw;
|
|
398
|
+
if (!deterministicPath || !bindingPath || !receiptPath || !policy || !violationsPath || !output || extra.length || !['normal', 'high_assurance'].includes(policy))
|
|
399
|
+
throw new Error('Usage: hyv lifecycle prepare-semantic deterministic.json binding.json receipt.json <normal|high_assurance> violations.json output.json');
|
|
400
|
+
if (policy === 'high_assurance')
|
|
401
|
+
throw new Error('High-assurance semantic review requires a trusted embedding.');
|
|
402
|
+
const result = prepareLifecycle(readJson(deterministicPath), readJson(bindingPath), readJson(receiptPath), policy, readJson(violationsPath));
|
|
403
|
+
const serialized = canonicalJson(result);
|
|
404
|
+
writeFileSync(output, `${serialized}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
405
|
+
process.stdout.write(`${serialized}\n`);
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
if (action === 'submit-verdict') {
|
|
409
|
+
const [artifactPath, taskPath, evaluatorId, verdictPath, ...extra] = raw;
|
|
410
|
+
if (!artifactPath || !taskPath || !evaluatorId || !verdictPath || extra.length)
|
|
411
|
+
throw new Error('Usage: hyv lifecycle submit-verdict artifact.json task.json evaluator-id verdict.json');
|
|
412
|
+
const artifact = readJson(artifactPath);
|
|
413
|
+
const task = readJson(taskPath);
|
|
414
|
+
if (task.policy !== 'normal')
|
|
415
|
+
throw new Error('High-assurance semantic review requires a trusted embedding.');
|
|
416
|
+
const result = submitSemanticVerdict(artifact, task, evaluatorId, readJson(verdictPath), loadApprovalContext());
|
|
417
|
+
canonical(result.ok ? result.artifact : { error: result.error });
|
|
418
|
+
return result.ok && result.artifact.status === 'ready_for_human_review' ? 0 : 2;
|
|
419
|
+
}
|
|
420
|
+
if (action === 'inspect') {
|
|
421
|
+
const [artifactPath, ...extra] = raw;
|
|
422
|
+
if (!artifactPath || extra.length)
|
|
423
|
+
throw new Error('Usage: hyv lifecycle inspect artifact.json');
|
|
424
|
+
canonical(inspectLifecycle(readJson(artifactPath)));
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
if (action === 'validate-final-approval' || action === 'finalize') {
|
|
428
|
+
const { values, capability } = capabilityArguments(raw);
|
|
429
|
+
if (action === 'validate-final-approval') {
|
|
430
|
+
const [artifactPath, ...extra] = values;
|
|
431
|
+
if (!artifactPath || extra.length || !capability)
|
|
432
|
+
throw new Error('Usage: hyv lifecycle validate-final-approval artifact.json (--capability-stdin|--capability-file path)');
|
|
433
|
+
const result = validateFinalApproval(readJson(artifactPath), capability, loadApprovalContext());
|
|
434
|
+
canonical(result);
|
|
435
|
+
return result.ok ? 0 : 2;
|
|
436
|
+
}
|
|
437
|
+
const [artifactPath, decisionPath, ...extra] = values;
|
|
438
|
+
if (!artifactPath || !decisionPath || extra.length)
|
|
439
|
+
throw new Error('Usage: hyv lifecycle finalize artifact.json decision.json [--capability-stdin|--capability-file path]');
|
|
440
|
+
const decision = readJson(decisionPath);
|
|
441
|
+
if (decision.decision === 'approve' && !capability)
|
|
442
|
+
throw new Error('Approval requires a capability.');
|
|
443
|
+
if (decision.decision === 'reject' && capability)
|
|
444
|
+
throw new Error('Rejection does not accept a capability.');
|
|
445
|
+
const result = finalizeLifecycle(readJson(artifactPath), decision, loadApprovalContext(), capability);
|
|
446
|
+
canonical(result.ok ? result.artifact : { error: result.error });
|
|
447
|
+
return result.ok && result.artifact.status === 'approved' ? 0 : 2;
|
|
448
|
+
}
|
|
449
|
+
throw new Error('Usage: hyv lifecycle <prepare-semantic|submit-verdict|inspect|validate-final-approval|finalize> ...');
|
|
450
|
+
}
|
|
148
451
|
if (command === 'learning') {
|
|
149
|
-
const [action,
|
|
452
|
+
const [action, ...raw] = rest;
|
|
453
|
+
if (action === 'record-approved') {
|
|
454
|
+
const { values, capability } = capabilityArguments(raw);
|
|
455
|
+
const [readyPath, approvedPath, originalPath, candidatePath, profilePath, decisionPath, ...contextPaths] = values;
|
|
456
|
+
if (!readyPath || !approvedPath || !originalPath || !candidatePath || !profilePath || !decisionPath || !capability)
|
|
457
|
+
throw new Error('Usage: hyv learning record-approved ready.json approved.json original.md candidate.md profile.json decision.json [copy-spec.json] [writing-brief.json] (--capability-stdin|--capability-file path)');
|
|
458
|
+
const context = prepareContext(contextPaths);
|
|
459
|
+
const status = recordApprovedLearning({ ready: readJson(readyPath), approved: readJson(approvedPath), decision: readJson(decisionPath), capability, source: input(originalPath), candidate: input(candidatePath), profile: readProfile(profilePath), context: loadApprovalContext(), copySpec: context.copySpec, writingBrief: context.writingBrief });
|
|
460
|
+
canonical({ status });
|
|
461
|
+
return status === 'write_failed' ? 2 : 0;
|
|
462
|
+
}
|
|
463
|
+
const { values, options } = learningArguments(raw);
|
|
464
|
+
const [profilePath, ...operands] = values;
|
|
150
465
|
if (!action || !profilePath)
|
|
151
|
-
throw new Error('Usage: hyv learning <show|add|clear> profile.json [
|
|
466
|
+
throw new Error('Usage: hyv learning <show|inspect|add|record|ratify|supersede|migrate|clear> profile.json [value] [options]');
|
|
152
467
|
const profile = readProfile(profilePath);
|
|
153
468
|
if (action === 'show') {
|
|
154
|
-
json({ profile: profileFingerprint(profile), preferences: composeLearning(profile) });
|
|
469
|
+
json({ profile: profileFingerprint(profile), preferences: composeLearning(profile, options) });
|
|
470
|
+
return 0;
|
|
471
|
+
}
|
|
472
|
+
if (action === 'inspect') {
|
|
473
|
+
if (Object.keys(options).length)
|
|
474
|
+
throw new Error('Usage: hyv learning inspect profile.json');
|
|
475
|
+
json(inspectLearning(profile));
|
|
155
476
|
return 0;
|
|
156
477
|
}
|
|
157
|
-
if (action === 'add') {
|
|
158
|
-
const text =
|
|
478
|
+
if (action === 'add' || action === 'record') {
|
|
479
|
+
const text = operands.join(' ').trim();
|
|
159
480
|
if (!text)
|
|
160
|
-
throw new Error('Usage: hyv learning
|
|
161
|
-
|
|
481
|
+
throw new Error('Usage: hyv learning record profile.json "instruction" [options]');
|
|
482
|
+
const result = recordLearningInstruction(profile, text, options);
|
|
483
|
+
json(action === 'add' ? { added: result.status === 'recorded' } : result);
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
if (action === 'ratify' || action === 'supersede') {
|
|
487
|
+
const [eventId, ...extra] = operands;
|
|
488
|
+
if (!eventId || extra.length)
|
|
489
|
+
throw new Error(`Usage: hyv learning ${action} profile.json event-id [options]`);
|
|
490
|
+
json(action === 'ratify' ? ratifyLearningEvent(requireProfileV3(profile), eventId, options) : supersedeLearningEvent(requireProfileV3(profile), eventId, options));
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
if (action === 'migrate') {
|
|
494
|
+
const [targetPath, ...extra] = operands;
|
|
495
|
+
if (!targetPath || extra.length || profile.version !== '2')
|
|
496
|
+
throw new Error('Usage: hyv learning migrate source-v2.json target-v3.json [options]');
|
|
497
|
+
json(migrateLearningV2ToV3(profile, requireProfileV3(readProfile(targetPath)), options));
|
|
162
498
|
return 0;
|
|
163
499
|
}
|
|
164
500
|
if (action === 'clear') {
|
|
501
|
+
if (operands.length || Object.keys(options).length)
|
|
502
|
+
throw new Error('Usage: hyv learning clear profile.json');
|
|
165
503
|
json({ cleared: clearLearning(profile) });
|
|
166
504
|
return 0;
|
|
167
505
|
}
|
|
168
|
-
throw new Error('Usage: hyv learning <show|add|clear> profile.json [
|
|
506
|
+
throw new Error('Usage: hyv learning <show|inspect|add|record|ratify|supersede|migrate|clear> profile.json [value] [options]');
|
|
169
507
|
}
|
|
170
508
|
if (command === 'patterns') {
|
|
171
|
-
json({ version: RULESET_VERSION, rules:
|
|
509
|
+
json({ version: RULESET_VERSION, rules: serializedRules() });
|
|
172
510
|
return 0;
|
|
173
511
|
}
|
|
174
512
|
if (command === 'mcp') {
|