@holdyourvoice/hyv 3.6.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/Readme.md +93 -154
  2. package/dist/ai-editor-rules.js +1 -0
  3. package/dist/ai-editor.js +23 -33
  4. package/dist/analysis.js +33 -0
  5. package/dist/cli/agents.js +88 -0
  6. package/dist/cli/checks.js +226 -0
  7. package/dist/cli/io.js +141 -0
  8. package/dist/cli/lifecycle.js +162 -0
  9. package/dist/cli/profiles.js +246 -0
  10. package/dist/cli/rewriting.js +111 -0
  11. package/dist/cli.js +5 -974
  12. package/dist/copy-spec.js +6 -6
  13. package/dist/editorial-packs.js +4 -3
  14. package/dist/fact-linter.js +45 -27
  15. package/dist/hidden-text.js +6 -6
  16. package/dist/hold-your-voice.mcpb +0 -0
  17. package/dist/internal.js +8 -0
  18. package/dist/judgment-task.js +10 -14
  19. package/dist/learning.js +77 -71
  20. package/dist/lifecycle-adapter.js +2 -5
  21. package/dist/local-eval.js +16 -31
  22. package/dist/mcp-server.js +272 -0
  23. package/dist/mcp-tools.js +10 -19
  24. package/dist/mcp.js +2 -267
  25. package/dist/pipeline.js +18 -105
  26. package/dist/profile-compose.js +18 -11
  27. package/dist/profile-score.js +6 -6
  28. package/dist/profile.js +17 -19
  29. package/dist/provenance-status.js +1 -3
  30. package/dist/rebuild-task.js +18 -52
  31. package/dist/recomposition.js +18 -13
  32. package/dist/rewrite-prompt.js +82 -0
  33. package/dist/rewrite-response.js +27 -0
  34. package/dist/rewrite-task.js +73 -98
  35. package/dist/rule-allowances.js +6 -10
  36. package/dist/semantic-review.js +71 -47
  37. package/dist/stage1-evaluation.js +29 -14
  38. package/dist/strict-quality.js +10 -18
  39. package/dist/version.js +1 -1
  40. package/dist/voice-dna.js +34 -21
  41. package/dist/writing-examples.js +2 -7
  42. package/package.json +6 -3
  43. package/skills/hyv-prepare-judgment/SKILL.md +4 -0
  44. package/dist/agents/catalog.test.js +0 -60
  45. package/dist/agents/emit.test.js +0 -64
  46. package/dist/agents/load.test.js +0 -149
  47. package/dist/ai-editor.test.js +0 -265
  48. package/dist/approval-capability.test.js +0 -52
  49. package/dist/approval-context.test.js +0 -38
  50. package/dist/backtest.test.js +0 -20
  51. package/dist/benchmark.test.js +0 -328
  52. package/dist/canonical-json.test.js +0 -24
  53. package/dist/cli.test.js +0 -731
  54. package/dist/editorial-packs.test.js +0 -94
  55. package/dist/fact-linter.test.js +0 -85
  56. package/dist/hidden-text.test.js +0 -26
  57. package/dist/hygiene.test.js +0 -83
  58. package/dist/judgment-task.test.js +0 -162
  59. package/dist/learning.test.js +0 -325
  60. package/dist/lifecycle-adapter.test.js +0 -56
  61. package/dist/local-eval.test.js +0 -20
  62. package/dist/logic-linter-corpus.test.js +0 -22
  63. package/dist/logic-linter.test.js +0 -39
  64. package/dist/mcp-tools.test.js +0 -286
  65. package/dist/mcp.test.js +0 -312
  66. package/dist/mirror-refs.test.js +0 -63
  67. package/dist/pipeline.test.js +0 -247
  68. package/dist/preservation.test.js +0 -22
  69. package/dist/production-gates.test.js +0 -34
  70. package/dist/profile-compose.test.js +0 -32
  71. package/dist/profile-score.test.js +0 -22
  72. package/dist/profile-watch.test.js +0 -23
  73. package/dist/profile.test.js +0 -141
  74. package/dist/provenance-status.test.js +0 -22
  75. package/dist/rebuild-task.test.js +0 -206
  76. package/dist/recomposition.test.js +0 -34
  77. package/dist/release-audit.test.js +0 -292
  78. package/dist/rewrite-task.test.js +0 -166
  79. package/dist/rule-allowances.test.js +0 -17
  80. package/dist/rule-reconciliation.test.js +0 -50
  81. package/dist/sample-ingest.test.js +0 -52
  82. package/dist/semantic-review.test.js +0 -101
  83. package/dist/stage1-dry-run.test.js +0 -39
  84. package/dist/stage1-evaluation.test.js +0 -184
  85. package/dist/stage1-human-packet.test.js +0 -102
  86. package/dist/stage1-schema-contract.test.js +0 -95
  87. package/dist/stage2-human-packet.test.js +0 -81
  88. package/dist/strict-quality.test.js +0 -62
  89. package/dist/text-provenance.feature.test.js +0 -45
  90. package/dist/text.test.js +0 -16
  91. package/dist/voice-dna.test.js +0 -121
  92. package/dist/writing-examples.test.js +0 -35
@@ -0,0 +1,226 @@
1
+ import { linkSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, extname, join, resolve } from 'node:path';
3
+ import { RULESET_VERSION, serializedRules } from '../ai-editor.js';
4
+ import { parseCopySpec } from '../copy-spec.js';
5
+ import { analyzeBatch } from '../editorial-packs.js';
6
+ import { cleanHygiene, finalOutputCheck, inspectHygiene } from '../hygiene.js';
7
+ import { applyHiddenTextPolicy, inspectHiddenText, parseHiddenTextPolicy } from '../hidden-text.js';
8
+ import { analyze, verify, verifyWithCopySpec } from '../pipeline.js';
9
+ import { formatFactLintReport, lintFacts } from '../fact-linter.js';
10
+ import { lintLogic } from '../logic-linter.js';
11
+ import { inspectDeliveryIntegrity, parseDeliveryIntegrityPolicy } from '../delivery-integrity.js';
12
+ import { evaluateStrictQuality } from '../strict-quality.js';
13
+ import { normalizeFinding, parseSurfacePolicy } from '../disposition.js';
14
+ import { input, readJson, readProfile, readBrief, json } from './io.js';
15
+ function cleanedPath(path) {
16
+ const extension = extname(path);
17
+ const stem = extension ? path.slice(0, -extension.length) : path;
18
+ return `${stem}.cleaned${extension}`;
19
+ }
20
+ function hygieneArguments(args) {
21
+ const [path, ...options] = args;
22
+ if (!path)
23
+ throw new Error('Usage: hyv hygiene draft.md [--fix] [--output=cleaned.md]');
24
+ let fix = false;
25
+ let output;
26
+ for (const option of options) {
27
+ if (option === '--fix')
28
+ fix = true;
29
+ else if (option.startsWith('--output='))
30
+ output = option.slice('--output='.length).trim();
31
+ else
32
+ throw new Error('Usage: hyv hygiene draft.md [--fix] [--output=cleaned.md]');
33
+ }
34
+ if (output !== undefined && (!output || !fix))
35
+ throw new Error('--output requires --fix and a non-empty path.');
36
+ return { path, fix, ...(output ? { output } : {}) };
37
+ }
38
+ function writeNewFileAtomically(path, text) {
39
+ const temporaryDirectory = mkdtempSync(join(dirname(resolve(path)), '.hyv-hygiene-'));
40
+ const temporaryPath = join(temporaryDirectory, 'cleaned');
41
+ let primaryError;
42
+ try {
43
+ writeFileSync(temporaryPath, text, 'utf8');
44
+ try {
45
+ linkSync(temporaryPath, path);
46
+ }
47
+ catch (error) {
48
+ const code = error.code;
49
+ if (!['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV'].includes(code ?? ''))
50
+ throw error;
51
+ throw new Error(`Atomic hygiene output is not supported by this filesystem: ${path}`);
52
+ }
53
+ }
54
+ catch (error) {
55
+ primaryError = error.code === 'EEXIST' ? new Error(`Hygiene output already exists: ${path}`) : error;
56
+ }
57
+ try {
58
+ rmSync(temporaryDirectory, { recursive: true, force: true });
59
+ }
60
+ catch (error) {
61
+ if (!primaryError)
62
+ console.error(`Warning: output was published, but temporary-file cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
63
+ }
64
+ if (primaryError)
65
+ throw primaryError;
66
+ }
67
+ export function runDeliveryCheck(args) {
68
+ const [path, policyPath, ...extra] = args;
69
+ if (!path || extra.length)
70
+ throw new Error('Usage: hyv delivery-check <path|-> [policy.json]');
71
+ const report = inspectDeliveryIntegrity(input(path), policyPath ? parseDeliveryIntegrityPolicy(readJson(policyPath)) : undefined, process.cwd());
72
+ json(report);
73
+ return report.passed ? 0 : 2;
74
+ }
75
+ export function runAnalyze(args) {
76
+ const [draft, profilePath, briefPath] = args;
77
+ if (!draft || !profilePath)
78
+ throw new Error('Usage: hyv analyze draft.md profile.json [writing-brief.json]');
79
+ json(analyze(input(draft), readProfile(profilePath), readBrief(briefPath)));
80
+ return 0;
81
+ }
82
+ export function runStrictCheck(args) {
83
+ const [draft, profilePath, ...samplePaths] = args;
84
+ if (!draft || !profilePath || samplePaths.length < 2)
85
+ throw new Error('Usage: hyv strict-check draft.md profile-v3.json sample-a.md sample-b.md [sample-c.md ...]');
86
+ const report = evaluateStrictQuality(input(draft), readProfile(profilePath), samplePaths.map(input));
87
+ json(report);
88
+ return report.disposition === 'strict-ready' ? 0 : 2;
89
+ }
90
+ export function runHygiene(args) {
91
+ const { path, fix, output } = hygieneArguments(args);
92
+ if (fix && path === '-')
93
+ throw new Error('hyv hygiene --fix requires a file path so the original can be preserved.');
94
+ const text = input(path);
95
+ if (!fix) {
96
+ json(inspectHygiene(text));
97
+ return 0;
98
+ }
99
+ const outputPath = output ?? cleanedPath(path);
100
+ if (resolve(outputPath) === resolve(path))
101
+ throw new Error('Hygiene output must differ from the input path.');
102
+ const result = cleanHygiene(text);
103
+ writeNewFileAtomically(outputPath, result.cleaned);
104
+ json({ ...result.report, changed: result.changed, changes: result.changes, outputPath });
105
+ return 0;
106
+ }
107
+ export function runInspectHiddenText(args) {
108
+ const [path, policyPath, ...extra] = args;
109
+ if (!path || extra.length)
110
+ throw new Error('Usage: hyv inspect-hidden-text draft.md [policy.json]');
111
+ json(inspectHiddenText(input(path), policyPath ? parseHiddenTextPolicy(readJson(policyPath)) : undefined));
112
+ return 0;
113
+ }
114
+ export function runApplyHiddenTextPolicy(args) {
115
+ const [path, policyPath, output, ...extra] = args;
116
+ if (!path || !policyPath || !output || extra.length)
117
+ throw new Error('Usage: hyv apply-hidden-text-policy draft.md policy.json output.md');
118
+ if (path === '-' || resolve(path) === resolve(output))
119
+ throw new Error('Hidden-text output must differ from the input path.');
120
+ const result = applyHiddenTextPolicy(input(path), parseHiddenTextPolicy(readJson(policyPath)));
121
+ writeNewFileAtomically(output, result.output);
122
+ json({ ...result, outputPath: output });
123
+ return 0;
124
+ }
125
+ export function runFinalCheck(args) {
126
+ const [path, ...options] = args;
127
+ if (!path || options.length)
128
+ throw new Error('Usage: hyv final-check <path|->');
129
+ const result = finalOutputCheck(input(path));
130
+ if (!result.accepted) {
131
+ console.error(JSON.stringify(result, null, 2));
132
+ return 2;
133
+ }
134
+ if (result.changed)
135
+ console.error(JSON.stringify({ changed: true, changes: result.changes }, null, 2));
136
+ process.stdout.write(result.output);
137
+ return 0;
138
+ }
139
+ export function runFactLint(args) {
140
+ const [draftPath, ...options] = args;
141
+ const sources = [];
142
+ let metadata;
143
+ let strict = false;
144
+ let human = false;
145
+ if (!draftPath)
146
+ throw new Error('Usage: hyv fact-lint <draft|-> --source=id:path [--source=id:path] [--metadata=metadata.json] [--strict] [--human]');
147
+ for (const option of options) {
148
+ if (option === '--strict') {
149
+ strict = true;
150
+ continue;
151
+ }
152
+ if (option === '--human') {
153
+ human = true;
154
+ continue;
155
+ }
156
+ if (option.startsWith('--source=')) {
157
+ const value = option.slice('--source='.length);
158
+ const separator = value.indexOf(':');
159
+ const id = value.slice(0, separator).trim();
160
+ const path = value.slice(separator + 1);
161
+ if (separator < 1 || !id || !path)
162
+ throw new Error('Sources must use --source=id:path.');
163
+ sources.push({ id, text: input(path) });
164
+ continue;
165
+ }
166
+ if (option.startsWith('--metadata=')) {
167
+ metadata = JSON.parse(input(option.slice('--metadata='.length)));
168
+ continue;
169
+ }
170
+ throw new Error('Usage: hyv fact-lint <draft|-> --source=id:path [--source=id:path] [--metadata=metadata.json] [--strict] [--human]');
171
+ }
172
+ const report = lintFacts({ sources, draft: input(draftPath), metadata });
173
+ if (human)
174
+ console.log(formatFactLintReport(report));
175
+ else
176
+ json(report);
177
+ return strict && report.findings.some((item) => item.severity === 'error') ? 2 : 0;
178
+ }
179
+ export function runLogicLint(args) {
180
+ const [draftPath, briefPath, ...extra] = args;
181
+ if (!draftPath || extra.length)
182
+ throw new Error('Usage: hyv logic-lint <draft|-> [writing-brief.json]');
183
+ const report = lintLogic(input(draftPath), readBrief(briefPath));
184
+ json(report);
185
+ return report.passed ? 0 : 2;
186
+ }
187
+ export function runBatchAnalyze(args) {
188
+ if (args.length < 2)
189
+ throw new Error('Usage: hyv batch-analyze draft-a.md draft-b.md [draft-c.md]');
190
+ json(analyzeBatch(args.map(input)));
191
+ return 0;
192
+ }
193
+ export function runVerify(args) {
194
+ const [original, candidate, profilePath, briefPath] = args;
195
+ if (!original || !candidate || !profilePath)
196
+ throw new Error('Usage: hyv verify original.md candidate.md profile.json [writing-brief.json]');
197
+ const profile = readProfile(profilePath);
198
+ const originalText = input(original);
199
+ const candidateText = input(candidate);
200
+ const result = verify(originalText, candidateText, profile, readBrief(briefPath));
201
+ json(result);
202
+ return result.passed ? 0 : 2;
203
+ }
204
+ export function runVerifySpec(args) {
205
+ const [original, candidate, profilePath, specPath, briefPath] = args;
206
+ if (!original || !candidate || !profilePath || !specPath)
207
+ throw new Error('Usage: hyv verify-spec original.md candidate.md profile.json copy-spec.json [writing-brief.json]');
208
+ const profile = readProfile(profilePath);
209
+ const candidateText = input(candidate);
210
+ const result = verifyWithCopySpec(input(original), candidateText, profile, parseCopySpec(JSON.parse(input(specPath))), readBrief(briefPath));
211
+ json(result);
212
+ return result.passed ? 0 : 2;
213
+ }
214
+ export function runPatterns() {
215
+ json({ version: RULESET_VERSION, rules: serializedRules() });
216
+ return 0;
217
+ }
218
+ export function runDispositions(args) {
219
+ const [draft, profilePath, briefPath, surfacePolicyPath] = args;
220
+ if (!draft || !profilePath)
221
+ throw new Error('Usage: hyv dispositions draft.md profile.json [writing-brief.json]');
222
+ const report = analyze(input(draft), readProfile(profilePath), readBrief(briefPath));
223
+ const policy = surfacePolicyPath ? parseSurfacePolicy(readJson(surfacePolicyPath)) : undefined;
224
+ json({ version: '1', findings: [report.voiceDna, report.aiEditor, report.editorial].flatMap((engine) => engine?.findings.map((finding) => normalizeFinding(finding, policy)) ?? []) });
225
+ return 0;
226
+ }
package/dist/cli/io.js ADDED
@@ -0,0 +1,141 @@
1
+ import { closeSync, constants, fstatSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs';
2
+ import { parseCopySpec } from '../copy-spec.js';
3
+ import { parseWritingBrief } from '../editorial-packs.js';
4
+ import { parseProfile } from '../profile.js';
5
+ import { canonicalJson, parseCanonicalJson } from '../canonical-json.js';
6
+ import { MAX_JSON_BYTES } from '../internal.js';
7
+ export function input(path) {
8
+ return path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
9
+ }
10
+ function parseBoundedJson(text) {
11
+ if (Buffer.byteLength(text, 'utf8') > MAX_JSON_BYTES)
12
+ throw new Error('JSON input exceeds the byte limit.');
13
+ const value = JSON.parse(text);
14
+ canonicalJson(value);
15
+ return value;
16
+ }
17
+ function readBoundedDescriptor(descriptor) {
18
+ const chunks = [];
19
+ let size = 0;
20
+ while (size <= MAX_JSON_BYTES) {
21
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, MAX_JSON_BYTES + 1 - size));
22
+ const count = readSync(descriptor, chunk, 0, chunk.length, null);
23
+ if (!count)
24
+ break;
25
+ chunks.push(chunk.subarray(0, count));
26
+ size += count;
27
+ }
28
+ if (size > MAX_JSON_BYTES)
29
+ throw new Error('JSON input exceeds the byte limit.');
30
+ return Buffer.concat(chunks, size).toString('utf8');
31
+ }
32
+ function withDescriptor(path, flags, read, openError) {
33
+ let descriptor;
34
+ try {
35
+ descriptor = openSync(path, flags);
36
+ }
37
+ catch (error) {
38
+ throw openError ? new Error(openError) : error;
39
+ }
40
+ try {
41
+ return read(descriptor);
42
+ }
43
+ finally {
44
+ closeSync(descriptor);
45
+ }
46
+ }
47
+ export function readJson(path) {
48
+ return path === '-'
49
+ ? parseBoundedJson(readBoundedDescriptor(0))
50
+ : withDescriptor(path, constants.O_RDONLY, (descriptor) => parseBoundedJson(readBoundedDescriptor(descriptor)));
51
+ }
52
+ function readCapabilityDescriptor(descriptor) {
53
+ try {
54
+ const before = fstatSync(descriptor);
55
+ if (!before.isFile() || before.uid !== process.geteuid?.() || (before.mode & 0o077) !== 0 || before.nlink !== 1 || before.size > MAX_JSON_BYTES) {
56
+ throw new Error('Capability file is unavailable or unsafe.');
57
+ }
58
+ const raw = readBoundedDescriptor(descriptor);
59
+ const after = fstatSync(descriptor);
60
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
61
+ throw new Error('Capability file is unavailable or unsafe.');
62
+ }
63
+ return raw;
64
+ }
65
+ catch {
66
+ throw new Error('Capability file is unavailable or unsafe.');
67
+ }
68
+ }
69
+ export function capabilityArguments(args) {
70
+ const values = [];
71
+ let source;
72
+ for (let index = 0; index < args.length; index += 1) {
73
+ const argument = args[index];
74
+ if (argument === '--capability-stdin') {
75
+ if (source)
76
+ throw new Error('Choose one capability source.');
77
+ source = { kind: 'stdin' };
78
+ }
79
+ else if (argument === '--capability-file') {
80
+ const path = args[++index];
81
+ if (source || !path || path.startsWith('--capability-'))
82
+ throw new Error('Choose one capability source.');
83
+ source = { kind: 'file', path };
84
+ }
85
+ else {
86
+ values.push(argument);
87
+ }
88
+ }
89
+ if (!source)
90
+ return { values };
91
+ if (source.kind === 'stdin' && values.includes('-'))
92
+ throw new Error('Capability stdin cannot be combined with another stdin input.');
93
+ const raw = source.kind === 'stdin'
94
+ ? readBoundedDescriptor(0)
95
+ : withDescriptor(source.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0), readCapabilityDescriptor, 'Capability file is unavailable or unsafe.');
96
+ if (Buffer.byteLength(raw, 'utf8') > MAX_JSON_BYTES)
97
+ throw new Error('JSON input exceeds the byte limit.');
98
+ return { values, capability: parseCanonicalJson(Buffer.from(raw, 'utf8')) };
99
+ }
100
+ export function readProfile(path) {
101
+ return parseProfile(JSON.parse(input(path)));
102
+ }
103
+ export function readBrief(path) {
104
+ return path ? parseWritingBrief(JSON.parse(input(path))) : undefined;
105
+ }
106
+ function tryParse(parse, value) {
107
+ try {
108
+ return parse(value);
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ }
114
+ export function prepareContext(paths) {
115
+ let copySpec;
116
+ let writingBrief;
117
+ for (const path of paths) {
118
+ const value = JSON.parse(input(path));
119
+ const parsedCopySpec = tryParse(parseCopySpec, value);
120
+ if (parsedCopySpec) {
121
+ if (copySpec)
122
+ throw new Error('Prepare-rewrite accepts at most one CopySpec.');
123
+ copySpec = parsedCopySpec;
124
+ continue;
125
+ }
126
+ const parsedBrief = tryParse(parseWritingBrief, value);
127
+ if (!parsedBrief)
128
+ throw new Error(`Expected a valid CopySpec or WritingBrief at ${path}.`);
129
+ if (writingBrief)
130
+ throw new Error('Prepare-rewrite accepts at most one WritingBrief.');
131
+ writingBrief = parsedBrief;
132
+ }
133
+ return { copySpec, writingBrief };
134
+ }
135
+ export function json(value) {
136
+ console.log(JSON.stringify(value, null, 2));
137
+ }
138
+ export function canonical(value) { process.stdout.write(`${canonicalJson(value)}\n`); }
139
+ export function writeJson(path, value) {
140
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
141
+ }
@@ -0,0 +1,162 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { clearLearning, composeLearning, inspectLearning, migrateLearningV2ToV3, profileFingerprint, ratifyLearningEvent, recordLearningInstruction, supersedeLearningEvent } from '../learning.js';
3
+ import { canonicalJson } from '../canonical-json.js';
4
+ import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from '../lifecycle-adapter.js';
5
+ import { loadApprovalContext } from '../approval-context.js';
6
+ import { input, readJson, capabilityArguments, readProfile, prepareContext, json, canonical } from './io.js';
7
+ function requireProfileV3(profile) {
8
+ if (profile.version !== '3')
9
+ throw new Error('This learning operation requires a Profile v3.');
10
+ return profile;
11
+ }
12
+ function learningArguments(args) {
13
+ const values = [];
14
+ const options = {};
15
+ for (const argument of args) {
16
+ if (!argument.startsWith('--')) {
17
+ values.push(argument);
18
+ continue;
19
+ }
20
+ const [name, ...parts] = argument.slice(2).split('=');
21
+ const value = parts.join('=').trim();
22
+ if (!value)
23
+ throw new Error(`Learning option --${name} requires a value.`);
24
+ if (name === 'mutation-id' && value.length <= 200)
25
+ options.mutationId = value;
26
+ else if (name === 'authority' && ['founder', 'team', 'system'].includes(value))
27
+ options.authority = value;
28
+ else if (name === 'provenance' && value.length <= 500)
29
+ options.provenance = value;
30
+ else if (name === 'weight' && Number.isFinite(Number(value)) && Number(value) > 0)
31
+ options.weight = Number(value);
32
+ else if (name === 'compatibility' && ['same-or-newer', 'exact'].includes(value))
33
+ options.compatibility = value;
34
+ else
35
+ throw new Error(`Invalid learning option: --${name}=${value}`);
36
+ }
37
+ return { values, options };
38
+ }
39
+ export function runLifecycle(args) {
40
+ const [action, ...raw] = args;
41
+ if (action === 'prepare-semantic')
42
+ return prepareSemanticLifecycle(raw);
43
+ if (action === 'submit-verdict')
44
+ return submitLifecycleVerdict(raw);
45
+ if (action === 'inspect')
46
+ return inspectLifecycleArtifact(raw);
47
+ if (action === 'validate-final-approval' || action === 'finalize')
48
+ return finishLifecycle(action, raw);
49
+ throw new Error('Usage: hyv lifecycle <prepare-semantic|submit-verdict|inspect|validate-final-approval|finalize> ...');
50
+ }
51
+ function prepareSemanticLifecycle(args) {
52
+ const [deterministicPath, bindingPath, receiptPath, policy, violationsPath, output, ...extra] = args;
53
+ if (!deterministicPath || !bindingPath || !receiptPath || !policy || !violationsPath || !output || extra.length || !['normal', 'high_assurance'].includes(policy))
54
+ throw new Error('Usage: hyv lifecycle prepare-semantic deterministic.json binding.json receipt.json <normal|high_assurance> violations.json output.json');
55
+ if (policy === 'high_assurance')
56
+ throw new Error('High-assurance semantic review requires a trusted embedding.');
57
+ const result = prepareLifecycle(readJson(deterministicPath), readJson(bindingPath), readJson(receiptPath), policy, readJson(violationsPath));
58
+ const serialized = canonicalJson(result);
59
+ writeFileSync(output, `${serialized}\n`, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
60
+ process.stdout.write(`${serialized}\n`);
61
+ return 0;
62
+ }
63
+ function submitLifecycleVerdict(args) {
64
+ const [artifactPath, taskPath, evaluatorId, verdictPath, ...extra] = args;
65
+ if (!artifactPath || !taskPath || !evaluatorId || !verdictPath || extra.length)
66
+ throw new Error('Usage: hyv lifecycle submit-verdict artifact.json task.json evaluator-id verdict.json');
67
+ const artifact = readJson(artifactPath);
68
+ const task = readJson(taskPath);
69
+ if (task.policy !== 'normal')
70
+ throw new Error('High-assurance semantic review requires a trusted embedding.');
71
+ const result = submitSemanticVerdict(artifact, task, evaluatorId, readJson(verdictPath), loadApprovalContext());
72
+ canonical(result.ok ? result.artifact : { error: result.error });
73
+ return result.ok && result.artifact.status === 'ready_for_human_review' ? 0 : 2;
74
+ }
75
+ function inspectLifecycleArtifact(args) {
76
+ const [artifactPath, ...extra] = args;
77
+ if (!artifactPath || extra.length)
78
+ throw new Error('Usage: hyv lifecycle inspect artifact.json');
79
+ canonical(inspectLifecycle(readJson(artifactPath)));
80
+ return 0;
81
+ }
82
+ function finishLifecycle(action, args) {
83
+ const { values, capability } = capabilityArguments(args);
84
+ if (action === 'validate-final-approval') {
85
+ const [artifactPath, ...extra] = values;
86
+ if (!artifactPath || extra.length || !capability)
87
+ throw new Error('Usage: hyv lifecycle validate-final-approval artifact.json (--capability-stdin|--capability-file path)');
88
+ const result = validateFinalApproval(readJson(artifactPath), capability, loadApprovalContext());
89
+ canonical(result);
90
+ return result.ok ? 0 : 2;
91
+ }
92
+ const [artifactPath, decisionPath, ...extra] = values;
93
+ if (!artifactPath || !decisionPath || extra.length)
94
+ throw new Error('Usage: hyv lifecycle finalize artifact.json decision.json [--capability-stdin|--capability-file path]');
95
+ const decision = readJson(decisionPath);
96
+ if (decision.decision === 'approve' && !capability)
97
+ throw new Error('Approval requires a capability.');
98
+ if (decision.decision === 'reject' && capability)
99
+ throw new Error('Rejection does not accept a capability.');
100
+ const result = finalizeLifecycle(readJson(artifactPath), decision, loadApprovalContext(), capability);
101
+ canonical(result.ok ? result.artifact : { error: result.error });
102
+ return result.ok && result.artifact.status === 'approved' ? 0 : 2;
103
+ }
104
+ export function runLearning(args) {
105
+ const [action, ...raw] = args;
106
+ if (action === 'record-approved')
107
+ return runRecordApprovedLearning(raw);
108
+ const { values, options } = learningArguments(raw);
109
+ const [profilePath, ...operands] = values;
110
+ if (!action || !profilePath)
111
+ throw new Error('Usage: hyv learning <show|inspect|add|record|ratify|supersede|migrate|clear> profile.json [value] [options]');
112
+ const profile = readProfile(profilePath);
113
+ if (action === 'show') {
114
+ json({ profile: profileFingerprint(profile), preferences: composeLearning(profile, options) });
115
+ return 0;
116
+ }
117
+ if (action === 'inspect') {
118
+ if (Object.keys(options).length)
119
+ throw new Error('Usage: hyv learning inspect profile.json');
120
+ json(inspectLearning(profile));
121
+ return 0;
122
+ }
123
+ if (action === 'add' || action === 'record') {
124
+ const text = operands.join(' ').trim();
125
+ if (!text)
126
+ throw new Error('Usage: hyv learning record profile.json "instruction" [options]');
127
+ const result = recordLearningInstruction(profile, text, options);
128
+ json(action === 'add' ? { added: result.status === 'recorded' } : result);
129
+ return 0;
130
+ }
131
+ if (action === 'ratify' || action === 'supersede') {
132
+ const [eventId, ...extra] = operands;
133
+ if (!eventId || extra.length)
134
+ throw new Error(`Usage: hyv learning ${action} profile.json event-id [options]`);
135
+ json(action === 'ratify' ? ratifyLearningEvent(requireProfileV3(profile), eventId, options) : supersedeLearningEvent(requireProfileV3(profile), eventId, options));
136
+ return 0;
137
+ }
138
+ if (action === 'migrate') {
139
+ const [targetPath, ...extra] = operands;
140
+ if (!targetPath || extra.length || profile.version !== '2')
141
+ throw new Error('Usage: hyv learning migrate source-v2.json target-v3.json [options]');
142
+ json(migrateLearningV2ToV3(profile, requireProfileV3(readProfile(targetPath)), options));
143
+ return 0;
144
+ }
145
+ if (action === 'clear') {
146
+ if (operands.length || Object.keys(options).length)
147
+ throw new Error('Usage: hyv learning clear profile.json');
148
+ json({ cleared: clearLearning(profile) });
149
+ return 0;
150
+ }
151
+ throw new Error('Usage: hyv learning <show|inspect|add|record|ratify|supersede|migrate|clear> profile.json [value] [options]');
152
+ }
153
+ function runRecordApprovedLearning(args) {
154
+ const { values, capability } = capabilityArguments(args);
155
+ const [readyPath, approvedPath, originalPath, candidatePath, profilePath, decisionPath, ...contextPaths] = values;
156
+ if (!readyPath || !approvedPath || !originalPath || !candidatePath || !profilePath || !decisionPath || !capability)
157
+ 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)');
158
+ const context = prepareContext(contextPaths);
159
+ 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 });
160
+ canonical({ status });
161
+ return status === 'write_failed' ? 2 : 0;
162
+ }