@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.
- package/Readme.md +23 -10
- package/dist/ai-editor-rules.js +5 -2
- package/dist/ai-editor.js +52 -9
- package/dist/ai-editor.test.js +62 -10
- 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 +272 -19
- package/dist/cli.test.js +205 -8
- package/dist/hygiene.js +6 -0
- package/dist/hygiene.test.js +7 -1
- 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 +101 -7
- package/dist/mcp-tools.test.js +156 -6
- package/dist/mcp.js +213 -6
- package/dist/mcp.test.js +210 -11
- package/dist/pipeline.js +78 -14
- package/dist/pipeline.test.js +36 -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 +111 -2
- package/dist/rewrite-task.js +136 -16
- package/dist/rewrite-task.test.js +62 -7
- 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 -1
- 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,22 +1,146 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { linkSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { closeSync, constants, fstatSync, linkSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
4
4
|
import { RULESET_VERSION, serializedRules } from './ai-editor.js';
|
|
5
5
|
import { parseCopySpec } from './copy-spec.js';
|
|
6
6
|
import { analyzeBatch, parseWritingBrief } from './editorial-packs.js';
|
|
7
|
-
import {
|
|
7
|
+
import { clearLearning, composeLearning, inspectLearning, migrateLearningV2ToV3, profileFingerprint, ratifyLearningEvent, recordLearningInstruction, supersedeLearningEvent } from './learning.js';
|
|
8
8
|
import { cleanHygiene, finalOutputCheck, inspectHygiene } from './hygiene.js';
|
|
9
9
|
import { analyze, rewritePrompt, verify, verifyWithCopySpec } from './pipeline.js';
|
|
10
10
|
import { parseProfile } from './profile.js';
|
|
11
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';
|
|
12
16
|
import { buildProfile } from './voice-dna.js';
|
|
13
|
-
|
|
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;
|
|
14
20
|
function input(path) {
|
|
15
21
|
return path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
|
|
16
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
|
+
}
|
|
17
109
|
function readProfile(path) {
|
|
18
110
|
return parseProfile(JSON.parse(input(path)));
|
|
19
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
|
+
}
|
|
20
144
|
function readBrief(path) {
|
|
21
145
|
return path ? parseWritingBrief(JSON.parse(input(path))) : undefined;
|
|
22
146
|
}
|
|
@@ -53,6 +177,7 @@ function prepareContext(paths) {
|
|
|
53
177
|
function json(value) {
|
|
54
178
|
console.log(JSON.stringify(value, null, 2));
|
|
55
179
|
}
|
|
180
|
+
function canonical(value) { process.stdout.write(`${canonicalJson(value)}\n`); }
|
|
56
181
|
function profileArguments(args) {
|
|
57
182
|
const [output, ...rest] = args;
|
|
58
183
|
const samples = [];
|
|
@@ -201,6 +326,50 @@ export async function runCli(args) {
|
|
|
201
326
|
json(result);
|
|
202
327
|
return result.status === 'accepted' ? 0 : 2;
|
|
203
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
|
+
}
|
|
204
373
|
if (command === 'verify') {
|
|
205
374
|
const [original, candidate, profilePath, briefPath] = rest;
|
|
206
375
|
if (!original || !candidate || !profilePath)
|
|
@@ -209,9 +378,6 @@ export async function runCli(args) {
|
|
|
209
378
|
const originalText = input(original);
|
|
210
379
|
const candidateText = input(candidate);
|
|
211
380
|
const result = verify(originalText, candidateText, profile, readBrief(briefPath));
|
|
212
|
-
const learning = recordVerifiedCandidate(profile, result, candidateText);
|
|
213
|
-
if (learning === 'write_failed')
|
|
214
|
-
console.error('Warning: verification passed, but local learning could not be saved.');
|
|
215
381
|
json(result);
|
|
216
382
|
return result.passed ? 0 : 2;
|
|
217
383
|
}
|
|
@@ -222,35 +388,122 @@ export async function runCli(args) {
|
|
|
222
388
|
const profile = readProfile(profilePath);
|
|
223
389
|
const candidateText = input(candidate);
|
|
224
390
|
const result = verifyWithCopySpec(input(original), candidateText, profile, parseCopySpec(JSON.parse(input(specPath))), readBrief(briefPath));
|
|
225
|
-
if (result.passed) {
|
|
226
|
-
const learning = recordVerifiedCandidate(profile, result, candidateText);
|
|
227
|
-
if (learning === 'write_failed')
|
|
228
|
-
console.error('Warning: verification passed, but local learning could not be saved.');
|
|
229
|
-
}
|
|
230
391
|
json(result);
|
|
231
392
|
return result.passed ? 0 : 2;
|
|
232
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
|
+
}
|
|
233
451
|
if (command === 'learning') {
|
|
234
|
-
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;
|
|
235
465
|
if (!action || !profilePath)
|
|
236
|
-
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]');
|
|
237
467
|
const profile = readProfile(profilePath);
|
|
238
468
|
if (action === 'show') {
|
|
239
|
-
json({ profile: profileFingerprint(profile), preferences: composeLearning(profile) });
|
|
469
|
+
json({ profile: profileFingerprint(profile), preferences: composeLearning(profile, options) });
|
|
240
470
|
return 0;
|
|
241
471
|
}
|
|
242
|
-
if (action === '
|
|
243
|
-
|
|
472
|
+
if (action === 'inspect') {
|
|
473
|
+
if (Object.keys(options).length)
|
|
474
|
+
throw new Error('Usage: hyv learning inspect profile.json');
|
|
475
|
+
json(inspectLearning(profile));
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
if (action === 'add' || action === 'record') {
|
|
479
|
+
const text = operands.join(' ').trim();
|
|
244
480
|
if (!text)
|
|
245
|
-
throw new Error('Usage: hyv learning
|
|
246
|
-
|
|
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));
|
|
247
498
|
return 0;
|
|
248
499
|
}
|
|
249
500
|
if (action === 'clear') {
|
|
501
|
+
if (operands.length || Object.keys(options).length)
|
|
502
|
+
throw new Error('Usage: hyv learning clear profile.json');
|
|
250
503
|
json({ cleared: clearLearning(profile) });
|
|
251
504
|
return 0;
|
|
252
505
|
}
|
|
253
|
-
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]');
|
|
254
507
|
}
|
|
255
508
|
if (command === 'patterns') {
|
|
256
509
|
json({ version: RULESET_VERSION, rules: serializedRules() });
|