@holdyourvoice/hyv 3.4.4 → 3.5.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 +7 -0
- package/dist/agents/load.test.js +1 -1
- package/dist/ai-editor-rules.js +34 -0
- package/dist/ai-editor.js +120 -9
- package/dist/ai-editor.test.js +99 -8
- package/dist/ai-shadow-fixtures.js +7 -0
- package/dist/ai-shadow-generator.js +17 -0
- package/dist/backtest.js +16 -0
- package/dist/backtest.test.js +20 -0
- package/dist/cli.js +225 -8
- package/dist/cli.test.js +102 -5
- package/dist/editorial-packs.js +1 -0
- package/dist/hold-your-voice.mcpb +0 -0
- package/dist/local-eval.js +98 -0
- package/dist/local-eval.test.js +20 -0
- package/dist/mcp-tools.js +22 -2
- package/dist/mcp-tools.test.js +33 -1
- package/dist/mcp.js +35 -4
- package/dist/mcp.test.js +2 -2
- package/dist/pipeline.js +5 -4
- package/dist/pipeline.test.js +12 -0
- package/dist/profile-compose.js +97 -0
- package/dist/profile-compose.test.js +32 -0
- package/dist/profile-score.js +79 -0
- package/dist/profile-score.test.js +22 -0
- package/dist/profile-watch.js +34 -0
- package/dist/profile-watch.test.js +23 -0
- package/dist/profile.js +33 -2
- package/dist/profile.test.js +27 -0
- package/dist/rebuild-task.test.js +1 -1
- package/dist/rule-allowances.js +27 -0
- package/dist/rule-allowances.test.js +17 -0
- package/dist/sample-ingest.js +94 -0
- package/dist/sample-ingest.test.js +52 -0
- package/dist/strict-quality.js +64 -0
- package/dist/strict-quality.test.js +62 -0
- package/dist/version.js +1 -1
- package/dist/voice-dna.js +33 -1
- package/dist/voice-dna.test.js +12 -1
- package/dist/writing-examples.js +83 -0
- package/dist/writing-examples.test.js +35 -0
- package/package.json +56 -11
- package/skills/hyv-analyze/SKILL.md +3 -1
- package/skills/hyv-analyze/agent.json +2 -1
- package/skills/hyv-backtest/SKILL.md +14 -0
- package/skills/hyv-backtest/agent.json +16 -0
- package/skills/hyv-backtest/agents/openai.yaml +4 -0
- package/skills/hyv-evaluate-local/SKILL.md +14 -0
- package/skills/hyv-evaluate-local/agent.json +16 -0
- package/skills/hyv-evaluate-local/agents/openai.yaml +4 -0
- package/skills/hyv-final-check/SKILL.md +2 -0
- package/skills/hyv-find-writing-examples/SKILL.md +10 -0
- package/skills/hyv-find-writing-examples/agent.json +16 -0
- package/skills/hyv-find-writing-examples/agents/openai.yaml +4 -0
- package/skills/hyv-ingest/SKILL.md +31 -0
- package/skills/hyv-ingest/agent.json +28 -0
- package/skills/hyv-ingest/agents/openai.yaml +4 -0
- package/skills/hyv-patterns/SKILL.md +2 -0
- package/skills/hyv-profile/SKILL.md +2 -0
- package/skills/hyv-score/SKILL.md +26 -0
- package/skills/hyv-score/agent.json +28 -0
- package/skills/hyv-score/agents/openai.yaml +4 -0
- package/skills/hyv-strict-check/SKILL.md +26 -0
- package/skills/hyv-strict-check/agent.json +33 -0
- package/skills/hyv-strict-check/agents/openai.yaml +4 -0
- package/skills/hyv-verify/SKILL.md +1 -1
- package/skills/hyv-verify/agent.json +1 -0
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { closeSync, constants, fstatSync, linkSync, mkdtempSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, extname, join, resolve } from 'node:path';
|
|
2
|
+
import { closeSync, constants, fstatSync, linkSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, extname, isAbsolute, 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';
|
|
@@ -15,16 +15,24 @@ import { evaluateRebuildResponse, parseRebuildTask, prepareRebuildTask, writerRe
|
|
|
15
15
|
import { canonicalJson, parseCanonicalJson } from './canonical-json.js';
|
|
16
16
|
import { MAX_JSON_BYTES } from './internal.js';
|
|
17
17
|
import { finalizeLifecycle, inspectLifecycle, prepareLifecycle, recordApprovedLearning, submitSemanticVerdict, validateFinalApproval } from './lifecycle-adapter.js';
|
|
18
|
-
import { buildProfile } from './voice-dna.js';
|
|
18
|
+
import { buildProfile, buildProfileV3 } from './voice-dna.js';
|
|
19
19
|
import { loadApprovalContext } from './approval-context.js';
|
|
20
20
|
import { formatFactLintReport, lintFacts } from './fact-linter.js';
|
|
21
21
|
import { lintLogic } from './logic-linter.js';
|
|
22
22
|
import { loadAll, validateAll, validateId, sortedIds, describe, emitJson, emitPrompt } from './agents/index.js';
|
|
23
23
|
import { inspectDeliveryIntegrity, parseDeliveryIntegrityPolicy } from './delivery-integrity.js';
|
|
24
24
|
import { assessProfileReadiness } from './profile-quality.js';
|
|
25
|
+
import { evaluateStrictQuality } from './strict-quality.js';
|
|
25
26
|
import { normalizeFinding, parseSurfacePolicy } from './disposition.js';
|
|
26
27
|
import { composeTeamProfile, parseTeamProfileBundle } from './team-profile.js';
|
|
27
|
-
|
|
28
|
+
import { composeProfiles, parseProfileRatio } from './profile-compose.js';
|
|
29
|
+
import { scoreHeldoutProfile } from './profile-score.js';
|
|
30
|
+
import { ingestGmailSentMbox, ingestTelegramDesktopJson } from './sample-ingest.js';
|
|
31
|
+
import { evaluateIsolatedBacktest } from './backtest.js';
|
|
32
|
+
import { watchProfileSamples } from './profile-watch.js';
|
|
33
|
+
import { evaluateLocalComposite } from './local-eval.js';
|
|
34
|
+
import { findWritingExamples } from './writing-examples.js';
|
|
35
|
+
const usage = 'Commands: agent, profile, team-profile, analyze, score, backtest, evaluate-local, ingest, strict-check, hygiene, inspect-hidden-text, apply-hidden-text-policy, final-check, delivery-check, fact-lint, logic-lint, batch-analyze, rewrite-prompt, prepare-rewrite, apply-rewrite, prepare-judgment, reduce-judgment, prepare-rebuild, rebuild-writer-request, apply-rebuild, verify, verify-spec, lifecycle, learning, patterns, dispositions, mcp';
|
|
28
36
|
function input(path) {
|
|
29
37
|
return path === '-' ? readFileSync(0, 'utf8') : readFileSync(path, 'utf8');
|
|
30
38
|
}
|
|
@@ -382,17 +390,206 @@ function runAgent(args) {
|
|
|
382
390
|
}
|
|
383
391
|
throw new Error('Usage: hyv agent <list|validate|describe|emit> ...');
|
|
384
392
|
}
|
|
393
|
+
async function runProfileWatch(args) {
|
|
394
|
+
const [output, ...rest] = args;
|
|
395
|
+
const samples = [];
|
|
396
|
+
let id = '';
|
|
397
|
+
let channel;
|
|
398
|
+
let debounceMs = 500;
|
|
399
|
+
for (const value of rest) {
|
|
400
|
+
if (value.startsWith('--id='))
|
|
401
|
+
id = value.slice('--id='.length);
|
|
402
|
+
else if (value.startsWith('--channel='))
|
|
403
|
+
channel = value.slice('--channel='.length);
|
|
404
|
+
else if (value.startsWith('--debounce-ms='))
|
|
405
|
+
debounceMs = Number(value.slice('--debounce-ms='.length));
|
|
406
|
+
else
|
|
407
|
+
samples.push(value);
|
|
408
|
+
}
|
|
409
|
+
if (!output || !isAbsolute(output) || !id || !channel || samples.length < 2)
|
|
410
|
+
throw new Error('Usage: hyv profile watch /absolute/profile.json --id=writer.channel --channel=email sample-a.md sample-b.md [--debounce-ms=500]');
|
|
411
|
+
outputOutsideGitCheckout(dirname(output));
|
|
412
|
+
let initial = true;
|
|
413
|
+
const rebuild = () => {
|
|
414
|
+
const profile = buildProfileV3(samples.map(input), id, channel);
|
|
415
|
+
writeFileSync(output, JSON.stringify(profile, null, 2) + '\n', { encoding: 'utf8', flag: initial ? 'wx' : 'w', mode: 0o600 });
|
|
416
|
+
initial = false;
|
|
417
|
+
json({ version: '1', status: 'rebuilt', sampleCount: samples.length, profileId: profile.id, revisionDigest: profile.revisionDigest });
|
|
418
|
+
};
|
|
419
|
+
rebuild();
|
|
420
|
+
const handle = watchProfileSamples({ samples, debounceMs, rebuild });
|
|
421
|
+
await new Promise((resolve) => process.once('SIGINT', resolve));
|
|
422
|
+
handle.close();
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
385
425
|
function runProfile(args) {
|
|
426
|
+
if (args[0] === 'watch')
|
|
427
|
+
return runProfileWatch(args.slice(1));
|
|
386
428
|
if (args[0] === 'assess') {
|
|
387
429
|
if (args.length < 3)
|
|
388
430
|
throw new Error('Usage: hyv profile assess sample-a.md sample-b.md [sample-c.md]');
|
|
389
431
|
json(assessProfileReadiness(args.slice(1).map(input)));
|
|
390
432
|
return 0;
|
|
391
433
|
}
|
|
434
|
+
if (args[0] === 'compose') {
|
|
435
|
+
const rest = args.slice(1);
|
|
436
|
+
const ratioIndex = rest.findIndex((value) => value === '--ratio' || value.startsWith('--ratio='));
|
|
437
|
+
if (ratioIndex < 0)
|
|
438
|
+
throw new Error('Usage: hyv profile compose --ratio 70:30 profile-a.json profile-b.json [profile-c.json]');
|
|
439
|
+
const ratio = rest[ratioIndex] === '--ratio' ? rest[ratioIndex + 1] : rest[ratioIndex].slice('--ratio='.length);
|
|
440
|
+
const profilePaths = rest.filter((_, index) => index !== ratioIndex && index !== ratioIndex + Number(rest[ratioIndex] === '--ratio'));
|
|
441
|
+
if (!ratio || profilePaths.length < 2)
|
|
442
|
+
throw new Error('Usage: hyv profile compose --ratio 70:30 profile-a.json profile-b.json [profile-c.json]');
|
|
443
|
+
const profiles = profilePaths.map(readProfile);
|
|
444
|
+
if (profiles.some((profile) => profile.version !== '3'))
|
|
445
|
+
throw new Error('Profile composition requires Profile v3 inputs.');
|
|
446
|
+
json(composeProfiles(profiles, parseProfileRatio(ratio, profiles.length)));
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
if (args[0] === 'v3') {
|
|
450
|
+
const [output, ...rest] = args.slice(1);
|
|
451
|
+
const samples = [];
|
|
452
|
+
const avoid = [];
|
|
453
|
+
let id = '';
|
|
454
|
+
let channel;
|
|
455
|
+
let tone;
|
|
456
|
+
for (const argument of rest) {
|
|
457
|
+
if (argument.startsWith('--id='))
|
|
458
|
+
id = argument.slice('--id='.length);
|
|
459
|
+
else if (argument.startsWith('--channel='))
|
|
460
|
+
channel = argument.slice('--channel='.length);
|
|
461
|
+
else if (argument.startsWith('--avoid='))
|
|
462
|
+
avoid.push(argument.slice('--avoid='.length));
|
|
463
|
+
else if (argument.startsWith('--tone=')) {
|
|
464
|
+
const values = argument.slice('--tone='.length).split(',').map(Number);
|
|
465
|
+
if (values.length !== 5 || values.some((value) => !Number.isFinite(value) || value < 0 || value > 1))
|
|
466
|
+
throw new Error('Tone must use five 0–1 comma-separated values: formality,confidence,warmth,energy,complexity.');
|
|
467
|
+
tone = { formality: values[0], confidence: values[1], warmth: values[2], energy: values[3], complexity: values[4] };
|
|
468
|
+
}
|
|
469
|
+
else
|
|
470
|
+
samples.push(argument);
|
|
471
|
+
}
|
|
472
|
+
if (!output || !id || !channel || samples.length < 2)
|
|
473
|
+
throw new Error('Usage: hyv profile v3 profile.json --id=writer.channel --channel=email sample-a.md sample-b.md [--tone=0,0,0,0,0] [--avoid=phrase]');
|
|
474
|
+
writeJson(output, buildProfileV3(samples.map(input), id, channel, avoid, tone));
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
392
477
|
const { output, samples, avoid } = profileArguments(args);
|
|
393
478
|
writeJson(output, buildProfile(samples.map(input), avoid));
|
|
394
479
|
return 0;
|
|
395
480
|
}
|
|
481
|
+
function runScore(args) {
|
|
482
|
+
const [draftPath, profilePath, ...rest] = args;
|
|
483
|
+
if (!draftPath || !profilePath)
|
|
484
|
+
throw new Error('Usage: hyv score draft.md profile.json heldout-a.md heldout-b.md heldout-c.md [--channel=channel]');
|
|
485
|
+
const samplePaths = [];
|
|
486
|
+
let channel;
|
|
487
|
+
for (const value of rest) {
|
|
488
|
+
if (value.startsWith('--channel='))
|
|
489
|
+
channel = value.slice('--channel='.length);
|
|
490
|
+
else
|
|
491
|
+
samplePaths.push(value);
|
|
492
|
+
}
|
|
493
|
+
if (samplePaths.length < 3)
|
|
494
|
+
throw new Error('Usage: hyv score draft.md profile.json heldout-a.md heldout-b.md heldout-c.md [--channel=channel]');
|
|
495
|
+
json(scoreHeldoutProfile(input(draftPath), readProfile(profilePath), samplePaths.map(input), channel));
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
function runBacktest(args) {
|
|
499
|
+
const [contextPath, targetPath, candidatePath, profilePath, ...heldoutPaths] = args;
|
|
500
|
+
if (!contextPath || !targetPath || !candidatePath || !profilePath || heldoutPaths.length < 3)
|
|
501
|
+
throw new Error('Usage: hyv backtest context.md heldout-target.md candidate.md profile.json heldout-a.md heldout-b.md heldout-c.md');
|
|
502
|
+
json(evaluateIsolatedBacktest(input(contextPath), input(targetPath), input(candidatePath), readProfile(profilePath), heldoutPaths.map(input)));
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
function readEvalParagraphs(path, label) {
|
|
506
|
+
const value = readJson(path);
|
|
507
|
+
if (!Array.isArray(value) || !value.every((item) => item && typeof item === 'object' && typeof item.paragraph_id === 'string' && typeof item.text === 'string'))
|
|
508
|
+
throw new Error(`${label} must be a JSON array of { paragraph_id, text } values.`);
|
|
509
|
+
return value.map((item) => ({ paragraphId: item.paragraph_id, text: item.text }));
|
|
510
|
+
}
|
|
511
|
+
function runEvaluateLocal(args) {
|
|
512
|
+
const [inputPath, candidatePath, userPath, aiShadowPath] = args;
|
|
513
|
+
if (!inputPath || !candidatePath || !userPath || !aiShadowPath || args.length !== 4)
|
|
514
|
+
throw new Error('Usage: hyv evaluate-local input.md candidate.md user-paragraphs.json ai-shadow-paragraphs.json');
|
|
515
|
+
json(evaluateLocalComposite(input(inputPath), input(candidatePath), readEvalParagraphs(userPath, 'User paragraphs'), readEvalParagraphs(aiShadowPath, 'AI-shadow paragraphs')));
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
function outputOutsideGitCheckout(path) {
|
|
519
|
+
if (!isAbsolute(path))
|
|
520
|
+
throw new Error('Sample ingest output must be an absolute path outside a Git checkout.');
|
|
521
|
+
let current;
|
|
522
|
+
try {
|
|
523
|
+
const stats = lstatSync(path);
|
|
524
|
+
if (!stats.isDirectory() || stats.isSymbolicLink())
|
|
525
|
+
throw new Error('Sample ingest output directory must be an existing non-symlink directory outside a Git checkout.');
|
|
526
|
+
current = realpathSync(path);
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (error instanceof Error && error.message.includes('Sample ingest output directory'))
|
|
530
|
+
throw error;
|
|
531
|
+
throw new Error('Sample ingest output directory must already exist and remain outside a Git checkout.');
|
|
532
|
+
}
|
|
533
|
+
for (;;) {
|
|
534
|
+
try {
|
|
535
|
+
lstatSync(join(current, '.git'));
|
|
536
|
+
throw new Error('Sample ingest output must be outside a Git checkout.');
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof Error && error.message === 'Sample ingest output must be outside a Git checkout.')
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
const parent = dirname(current);
|
|
543
|
+
if (parent === current)
|
|
544
|
+
return;
|
|
545
|
+
current = parent;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function runIngest(args) {
|
|
549
|
+
const [sourceType, sourcePath, ...options] = args;
|
|
550
|
+
let owner = '';
|
|
551
|
+
let output = '';
|
|
552
|
+
const blockedWords = [];
|
|
553
|
+
for (const option of options) {
|
|
554
|
+
if (option.startsWith('--owner='))
|
|
555
|
+
owner = option.slice('--owner='.length);
|
|
556
|
+
else if (option.startsWith('--output='))
|
|
557
|
+
output = option.slice('--output='.length);
|
|
558
|
+
else if (option.startsWith('--blocked='))
|
|
559
|
+
blockedWords.push(option.slice('--blocked='.length));
|
|
560
|
+
else
|
|
561
|
+
throw new Error('Usage: hyv ingest <gmail-sent-mbox|telegram-desktop-json> export --owner=owner --output=/absolute/safe-directory [--blocked=word]');
|
|
562
|
+
}
|
|
563
|
+
if (!sourcePath || !owner || !output || !['gmail-sent-mbox', 'telegram-desktop-json'].includes(sourceType ?? ''))
|
|
564
|
+
throw new Error('Usage: hyv ingest <gmail-sent-mbox|telegram-desktop-json> export --owner=owner --output=/absolute/safe-directory [--blocked=word]');
|
|
565
|
+
outputOutsideGitCheckout(output);
|
|
566
|
+
const result = sourceType === 'gmail-sent-mbox'
|
|
567
|
+
? ingestGmailSentMbox(input(sourcePath), owner, blockedWords)
|
|
568
|
+
: ingestTelegramDesktopJson(input(sourcePath), owner, blockedWords);
|
|
569
|
+
const outputDirectory = realpathSync(output);
|
|
570
|
+
const samplesPath = join(outputDirectory, 'samples.jsonl');
|
|
571
|
+
const receiptPath = join(outputDirectory, 'receipt.json');
|
|
572
|
+
try {
|
|
573
|
+
writeFileSync(samplesPath, result.samples.map((sample) => JSON.stringify({ text: sample })).join('\n') + (result.samples.length ? '\n' : ''), { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
if (error.code === 'ENOENT')
|
|
577
|
+
throw new Error('Sample ingest output directory must already exist and remain outside a Git checkout.');
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
writeFileSync(receiptPath, JSON.stringify(result.receipt, null, 2) + '\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
try {
|
|
585
|
+
rmSync(samplesPath, { force: true });
|
|
586
|
+
}
|
|
587
|
+
catch { }
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
json(result.receipt);
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
396
593
|
function runTeamProfile(args) {
|
|
397
594
|
const [action, bundlePath, authorPath, ...brandPaths] = args;
|
|
398
595
|
if (action === 'validate' && bundlePath && !authorPath) {
|
|
@@ -423,6 +620,14 @@ function runAnalyze(args) {
|
|
|
423
620
|
json(analyze(input(draft), readProfile(profilePath), readBrief(briefPath)));
|
|
424
621
|
return 0;
|
|
425
622
|
}
|
|
623
|
+
function runStrictCheck(args) {
|
|
624
|
+
const [draft, profilePath, ...samplePaths] = args;
|
|
625
|
+
if (!draft || !profilePath || samplePaths.length < 2)
|
|
626
|
+
throw new Error('Usage: hyv strict-check draft.md profile-v3.json sample-a.md sample-b.md [sample-c.md ...]');
|
|
627
|
+
const report = evaluateStrictQuality(input(draft), readProfile(profilePath), samplePaths.map(input));
|
|
628
|
+
json(report);
|
|
629
|
+
return report.disposition === 'strict-ready' ? 0 : 2;
|
|
630
|
+
}
|
|
426
631
|
function runHygiene(args) {
|
|
427
632
|
const { path, fix, output } = hygieneArguments(args);
|
|
428
633
|
if (fix && path === '-')
|
|
@@ -527,11 +732,18 @@ function runBatchAnalyze(args) {
|
|
|
527
732
|
return 0;
|
|
528
733
|
}
|
|
529
734
|
function runRewritePrompt(args) {
|
|
530
|
-
const [draft, profilePath,
|
|
531
|
-
|
|
532
|
-
|
|
735
|
+
const [draft, profilePath, ...rest] = args;
|
|
736
|
+
const exampleOption = rest.find((value) => value.startsWith('--examples-json='));
|
|
737
|
+
const briefPaths = rest.filter((value) => !value.startsWith('--'));
|
|
738
|
+
const briefPath = briefPaths[0];
|
|
739
|
+
if (!draft || !profilePath || briefPaths.length > 1 || rest.some((value) => value.startsWith('--') && !value.startsWith('--examples-json=')))
|
|
740
|
+
throw new Error('Usage: hyv rewrite-prompt draft.md profile.json [writing-brief.json] [--examples-json=local-examples.json]');
|
|
533
741
|
const profile = readProfile(profilePath);
|
|
534
|
-
|
|
742
|
+
const examplesValue = exampleOption ? readJson(exampleOption.slice('--examples-json='.length)) : undefined;
|
|
743
|
+
if (examplesValue !== undefined && (!Array.isArray(examplesValue) || !examplesValue.every((item) => item && typeof item === 'object' && typeof item.basename === 'string' && typeof item.text === 'string')))
|
|
744
|
+
throw new Error('Local examples must be a JSON array of { basename, text } values.');
|
|
745
|
+
const draftText = input(draft);
|
|
746
|
+
console.log(rewritePrompt(draftText, profile, composeLearning(profile), readBrief(briefPath), examplesValue ? findWritingExamples(draftText, examplesValue) : []));
|
|
535
747
|
return 0;
|
|
536
748
|
}
|
|
537
749
|
function runPrepareRewrite(args) {
|
|
@@ -773,6 +985,11 @@ const commandHandlers = {
|
|
|
773
985
|
profile: runProfile,
|
|
774
986
|
'team-profile': runTeamProfile,
|
|
775
987
|
analyze: runAnalyze,
|
|
988
|
+
score: runScore,
|
|
989
|
+
backtest: runBacktest,
|
|
990
|
+
'evaluate-local': runEvaluateLocal,
|
|
991
|
+
ingest: runIngest,
|
|
992
|
+
'strict-check': runStrictCheck,
|
|
776
993
|
hygiene: runHygiene,
|
|
777
994
|
'inspect-hidden-text': runInspectHiddenText,
|
|
778
995
|
'apply-hidden-text-policy': runApplyHiddenTextPolicy,
|
package/dist/cli.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
@@ -45,13 +45,32 @@ test('creates an explicit local avoid list and exposes the ruleset', () => {
|
|
|
45
45
|
rmSync(directory, { recursive: true, force: true });
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
+
test('builds a channel-specific Profile v3 from local samples', () => {
|
|
49
|
+
const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-profile-v3-cli-'));
|
|
50
|
+
try {
|
|
51
|
+
const first = join(directory, 'first.md');
|
|
52
|
+
const second = join(directory, 'second.md');
|
|
53
|
+
const profile = join(directory, 'profile.json');
|
|
54
|
+
writeFileSync(first, 'I write directly about the release. The owner checks the evidence.');
|
|
55
|
+
writeFileSync(second, 'I keep the mechanism visible. The next step stays clear.');
|
|
56
|
+
const result = run(['profile', 'v3', profile, '--id=founder.email', '--channel=email', '--tone=0.4,0.7,0.6,0.3,0.5', first, second]);
|
|
57
|
+
assert.equal(result.status, 0, result.stderr);
|
|
58
|
+
const parsed = JSON.parse(readFileSync(profile, 'utf8'));
|
|
59
|
+
assert.equal(parsed.version, '3');
|
|
60
|
+
assert.equal(parsed.channel, 'email');
|
|
61
|
+
assert.equal(parsed.tone.warmth, 0.6);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
rmSync(directory, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
48
67
|
test('publishes the same normalized reconciled catalog and version through CLI and MCP', () => {
|
|
49
68
|
const result = run(['patterns']);
|
|
50
69
|
assert.equal(result.status, 0, result.stderr);
|
|
51
70
|
const cliCatalog = JSON.parse(result.stdout);
|
|
52
71
|
const mcpCatalog = patternsForMcp();
|
|
53
|
-
assert.equal(cliCatalog.version, '3.
|
|
54
|
-
assert.equal(cliCatalog.rules.length,
|
|
72
|
+
assert.equal(cliCatalog.version, '3.5.0-local.3');
|
|
73
|
+
assert.equal(cliCatalog.rules.length, 182);
|
|
55
74
|
assert.deepEqual(cliCatalog, mcpCatalog);
|
|
56
75
|
});
|
|
57
76
|
test('runs contextual analysis and batch analysis without changing the profile contract', () => {
|
|
@@ -82,6 +101,78 @@ test('runs contextual analysis and batch analysis without changing the profile c
|
|
|
82
101
|
rmSync(directory, { recursive: true, force: true });
|
|
83
102
|
}
|
|
84
103
|
});
|
|
104
|
+
test('scores a candidate only against explicit held-out local samples', () => {
|
|
105
|
+
const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-score-cli-'));
|
|
106
|
+
try {
|
|
107
|
+
const samples = Array.from({ length: 3 }, (_, index) => join(directory, 'sample-' + index + '.md'));
|
|
108
|
+
const profile = join(directory, 'profile.json');
|
|
109
|
+
const draft = join(directory, 'draft.md');
|
|
110
|
+
const text = [
|
|
111
|
+
'I write a direct note about the launch. The owner checks the evidence before we ship. The next step stays clear and small. The report remains useful.',
|
|
112
|
+
'I name the trade-off before I make the decision. We keep the mechanism visible for the person doing the work. The release has one owner. The report stays useful.',
|
|
113
|
+
'I start from evidence in the issue. Then I explain the constraint and choose a concrete next step. The team checks the result. The report stays useful.',
|
|
114
|
+
];
|
|
115
|
+
for (const [index, sample] of samples.entries())
|
|
116
|
+
writeFileSync(sample, text[index]);
|
|
117
|
+
writeFileSync(draft, 'I name the evidence, explain the trade-off, and choose a next step. The owner checks the work before release. The report stays useful for the team.');
|
|
118
|
+
assert.equal(run(['profile', profile, ...samples]).status, 0);
|
|
119
|
+
const result = run(['score', draft, profile, ...samples]);
|
|
120
|
+
assert.equal(result.status, 0, result.stderr);
|
|
121
|
+
const score = JSON.parse(result.stdout);
|
|
122
|
+
assert.equal(score.version, '1');
|
|
123
|
+
assert.equal(score.selfSimilarity.ceiling, 100);
|
|
124
|
+
assert.equal(Object.keys(score.components).length, 13);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
rmSync(directory, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test('ingests an explicit Gmail export only to an absolute directory outside a Git checkout', () => {
|
|
131
|
+
const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-ingest-cli-'));
|
|
132
|
+
try {
|
|
133
|
+
const source = join(directory, 'sent.mbox');
|
|
134
|
+
writeFileSync(source, [
|
|
135
|
+
'From sender@example.com Mon Jan 1 00:00:00 2026',
|
|
136
|
+
'From: Owner <owner@example.com>',
|
|
137
|
+
'',
|
|
138
|
+
'Reach owner@example.com before we ship.',
|
|
139
|
+
].join('\n'));
|
|
140
|
+
const result = run(['ingest', 'gmail-sent-mbox', source, '--owner=Owner <owner@example.com>', '--output=' + directory]);
|
|
141
|
+
assert.equal(result.status, 0, result.stderr);
|
|
142
|
+
const receipt = JSON.parse(result.stdout);
|
|
143
|
+
assert.equal(receipt.samplesAccepted, 1);
|
|
144
|
+
assert.equal(JSON.stringify(receipt).includes('owner@example.com'), false);
|
|
145
|
+
assert.equal(readFileSync(join(directory, 'samples.jsonl'), 'utf8').includes('REDACTED:EMAIL'), true);
|
|
146
|
+
assert.equal(run(['ingest', 'gmail-sent-mbox', source, '--owner=Owner <owner@example.com>', '--output=' + process.cwd()]).status, 1);
|
|
147
|
+
const linkedOutput = join(directory, 'linked-output');
|
|
148
|
+
symlinkSync(process.cwd(), linkedOutput);
|
|
149
|
+
const linked = run(['ingest', 'gmail-sent-mbox', source, '--owner=Owner <owner@example.com>', '--output=' + linkedOutput]);
|
|
150
|
+
assert.equal(linked.status, 1);
|
|
151
|
+
assert.match(linked.stderr, /non-symlink directory/);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
rmSync(directory, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
test('exposes strict quality as an opt-in v3-only CLI gate', () => {
|
|
158
|
+
const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-strict-cli-'));
|
|
159
|
+
try {
|
|
160
|
+
const profile = join(directory, 'profile.json');
|
|
161
|
+
const draft = join(directory, 'draft.md');
|
|
162
|
+
const samples = Array.from({ length: 5 }, (_, index) => join(directory, `sample-${index}.md`));
|
|
163
|
+
for (const [index, sample] of samples.entries())
|
|
164
|
+
writeFileSync(sample, `i write sample ${index}. `.repeat(300));
|
|
165
|
+
writeFileSync(profile, JSON.stringify({ version: '2', sampleCount: 2, metrics: { sentenceLength: 5, sentenceVariation: 1, sentenceStructure: [], rhythm: 1, paragraphLength: 1, openingMoves: [], vocabulary: [], lexicalDensity: 0.5, pointOfView: 'mixed', punctuation: { '!': 0, '?': 0, ';': 0, ':': 0, '—': 0 }, caseStyle: 'mixed', questionRate: 0, transitions: [] }, avoid: [] }));
|
|
166
|
+
writeFileSync(draft, 'The launch starts Tuesday.');
|
|
167
|
+
const result = run(['strict-check', draft, profile, ...samples]);
|
|
168
|
+
assert.equal(result.status, 2, result.stderr);
|
|
169
|
+
assert.equal(JSON.parse(result.stdout).disposition, 'blocked');
|
|
170
|
+
assert.equal(JSON.parse(result.stdout).findings[0].id, 'strict.profile.version');
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
rmSync(directory, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
85
176
|
test('inspects and conservatively fixes Unicode hygiene without overwriting either file', () => {
|
|
86
177
|
const directory = mkdtempSync(join(tmpdir(), 'holdyourvoice-cli-'));
|
|
87
178
|
try {
|
|
@@ -560,12 +651,18 @@ test('agent list enumerates every command as one portable package', () => {
|
|
|
560
651
|
const result = run(['agent', 'list']);
|
|
561
652
|
assert.equal(result.status, 0, result.stderr);
|
|
562
653
|
const entries = JSON.parse(result.stdout);
|
|
563
|
-
assert.equal(entries.length,
|
|
654
|
+
assert.equal(entries.length, 29);
|
|
564
655
|
const ids = entries.map((entry) => entry.id);
|
|
565
656
|
assert.equal(new Set(ids).size, ids.length);
|
|
566
|
-
for (const id of ['hyv-profile', 'hyv-analyze', 'hyv-verify', 'hyv-mcp', 'hyv-patterns']) {
|
|
657
|
+
for (const id of ['hyv-profile', 'hyv-score', 'hyv-ingest', 'hyv-backtest', 'hyv-evaluate-local', 'hyv-find-writing-examples', 'hyv-analyze', 'hyv-verify', 'hyv-mcp', 'hyv-patterns', 'hyv-strict-check']) {
|
|
567
658
|
assert.ok(ids.includes(id), `missing ${id}`);
|
|
568
659
|
}
|
|
660
|
+
const strict = JSON.parse(run(['agent', 'describe', 'hyv-strict-check']).stdout).agent;
|
|
661
|
+
const analyze = JSON.parse(run(['agent', 'describe', 'hyv-analyze']).stdout).agent;
|
|
662
|
+
const verify = JSON.parse(run(['agent', 'describe', 'hyv-verify']).stdout).agent;
|
|
663
|
+
assert.deepEqual(strict.handoff_to, ['hyv-final-check']);
|
|
664
|
+
assert.ok(analyze.handoff_to.includes('hyv-strict-check'));
|
|
665
|
+
assert.ok(verify.handoff_to.includes('hyv-strict-check'));
|
|
569
666
|
for (const entry of entries) {
|
|
570
667
|
assert.ok(entry.role);
|
|
571
668
|
assert.ok(entry.workflow_phase);
|
package/dist/editorial-packs.js
CHANGED
|
@@ -47,6 +47,7 @@ export function parseWritingBrief(value) {
|
|
|
47
47
|
const brief = value;
|
|
48
48
|
if (brief.version !== '1' || !isText(brief.audience, 500) || !isText(brief.intent, 500) || !formats.includes(brief.format)
|
|
49
49
|
|| (brief.readerKnowsAuthor !== undefined && typeof brief.readerKnowsAuthor !== 'boolean')
|
|
50
|
+
|| (brief.personality !== undefined && !isText(brief.personality, 500))
|
|
50
51
|
|| (brief.vocabulary !== undefined && !isTerms(brief.vocabulary))
|
|
51
52
|
|| (brief.prohibitedTerms !== undefined && !isTerms(brief.prohibitedTerms))
|
|
52
53
|
|| (brief.title !== undefined && !isText(brief.title, 500))
|
|
Binary file
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { analyzeAiEditor } from './ai-editor.js';
|
|
2
|
+
import { profileMetrics } from './voice-dna.js';
|
|
3
|
+
import { words } from './text.js';
|
|
4
|
+
const STOP = new Set(['the', 'and', 'that', 'with', 'this', 'from', 'your', 'have', 'were', 'they', 'will', 'into', 'about']);
|
|
5
|
+
function terms(text) { return words(text.toLowerCase()).filter((word) => word.length > 2 && !STOP.has(word)); }
|
|
6
|
+
function f1(left, right) {
|
|
7
|
+
const a = new Set(terms(left));
|
|
8
|
+
const b = new Set(terms(right));
|
|
9
|
+
const intersection = [...a].filter((word) => b.has(word)).length;
|
|
10
|
+
return a.size + b.size === 0 ? 1 : Number((2 * intersection / (a.size + b.size)).toFixed(3));
|
|
11
|
+
}
|
|
12
|
+
function vector(text) {
|
|
13
|
+
const m = profileMetrics(text);
|
|
14
|
+
const punctuation = Object.values(m.punctuation).reduce((sum, value) => sum + value, 0);
|
|
15
|
+
return [m.sentenceLength, m.sentenceVariation, m.rhythm, m.paragraphLength, m.lexicalDensity, m.questionRate, punctuation, m.openingMoves.length, m.transitions.length];
|
|
16
|
+
}
|
|
17
|
+
function cosine(left, right) {
|
|
18
|
+
const dot = left.reduce((sum, value, index) => sum + value * right[index], 0);
|
|
19
|
+
const magnitude = Math.sqrt(left.reduce((sum, value) => sum + value * value, 0) * right.reduce((sum, value) => sum + value * value, 0));
|
|
20
|
+
return Number((magnitude ? dot / magnitude : 0).toFixed(3));
|
|
21
|
+
}
|
|
22
|
+
function groupedParagraphs(values, label) {
|
|
23
|
+
if (values.length < 2 || values.some((value) => !value.paragraphId || !value.text.trim()) || new Set(values.map((value) => value.paragraphId)).size < 2)
|
|
24
|
+
throw new Error(`${label} needs at least two paragraph IDs with text.`);
|
|
25
|
+
}
|
|
26
|
+
function tfIdf(documents) {
|
|
27
|
+
const documentTerms = documents.map((document) => terms(document));
|
|
28
|
+
const df = new Map();
|
|
29
|
+
for (const document of documentTerms)
|
|
30
|
+
for (const term of new Set(document))
|
|
31
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
32
|
+
const vocabulary = [...df.keys()].sort();
|
|
33
|
+
const idf = new Map([...df].map(([term, frequency]) => [term, Math.log((documents.length + 1) / (frequency + 1)) + 1]));
|
|
34
|
+
const toVector = (document) => {
|
|
35
|
+
const termsInDocument = terms(document);
|
|
36
|
+
const count = new Map();
|
|
37
|
+
for (const term of termsInDocument)
|
|
38
|
+
count.set(term, (count.get(term) ?? 0) + 1);
|
|
39
|
+
const vector = {};
|
|
40
|
+
for (const [term, occurrences] of count)
|
|
41
|
+
if (idf.has(term))
|
|
42
|
+
vector[term] = (occurrences / Math.max(1, termsInDocument.length)) * idf.get(term);
|
|
43
|
+
return vector;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
vocabulary,
|
|
47
|
+
idf,
|
|
48
|
+
vectors: documents.map(toVector),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function vectorWithIdf(document, idf) {
|
|
52
|
+
const documentTerms = terms(document);
|
|
53
|
+
const count = new Map();
|
|
54
|
+
for (const term of documentTerms)
|
|
55
|
+
count.set(term, (count.get(term) ?? 0) + 1);
|
|
56
|
+
const vector = {};
|
|
57
|
+
for (const [term, occurrences] of count)
|
|
58
|
+
if (idf.has(term))
|
|
59
|
+
vector[term] = (occurrences / Math.max(1, documentTerms.length)) * idf.get(term);
|
|
60
|
+
return vector;
|
|
61
|
+
}
|
|
62
|
+
function sigmoid(value) { return value >= 0 ? 1 / (1 + Math.exp(-value)) : Math.exp(value) / (1 + Math.exp(value)); }
|
|
63
|
+
/** Deterministic, train-only logistic regression over sparse TF-IDF vectors. */
|
|
64
|
+
function localAuthorshipProbability(candidate, user, shadow) {
|
|
65
|
+
const training = [...user.map((item) => ({ text: item.text, label: 1 })), ...shadow.map((item) => ({ text: item.text, label: 0 }))];
|
|
66
|
+
const transformed = tfIdf(training.map((item) => item.text));
|
|
67
|
+
const weights = {};
|
|
68
|
+
let bias = 0;
|
|
69
|
+
for (let epoch = 0; epoch < 80; epoch += 1) {
|
|
70
|
+
for (let index = 0; index < training.length; index += 1) {
|
|
71
|
+
const vector = transformed.vectors[index];
|
|
72
|
+
const error = training[index].label - sigmoid(bias + Object.entries(vector).reduce((sum, [term, value]) => sum + (weights[term] ?? 0) * value, 0));
|
|
73
|
+
bias += 0.12 * error;
|
|
74
|
+
for (const [term, value] of Object.entries(vector))
|
|
75
|
+
weights[term] = (weights[term] ?? 0) + 0.12 * error * value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const candidateVector = vectorWithIdf(candidate, transformed.idf);
|
|
79
|
+
const score = bias + Object.entries(candidateVector).reduce((sum, [term, value]) => sum + (weights[term] ?? 0) * value, 0);
|
|
80
|
+
return Number(sigmoid(score).toFixed(3));
|
|
81
|
+
}
|
|
82
|
+
/** Optional local evaluation. Groups whole paragraph IDs before train/test to prevent variant leakage. */
|
|
83
|
+
export function evaluateLocalComposite(input, candidate, user, aiShadow) {
|
|
84
|
+
groupedParagraphs(user, 'User paragraphs');
|
|
85
|
+
groupedParagraphs(aiShadow, 'AI-shadow paragraphs');
|
|
86
|
+
const ids = [...new Set([...user, ...aiShadow].map((value) => value.paragraphId))].sort();
|
|
87
|
+
const testIds = ids.filter((id, index) => index % 3 === 0);
|
|
88
|
+
const trainIds = ids.filter((id) => !testIds.includes(id));
|
|
89
|
+
const trainUser = user.filter((value) => trainIds.includes(value.paragraphId));
|
|
90
|
+
const trainShadow = aiShadow.filter((value) => trainIds.includes(value.paragraphId));
|
|
91
|
+
const author = trainUser.length && trainShadow.length ? {
|
|
92
|
+
candidateUserProbability: localAuthorshipProbability(candidate, trainUser, trainShadow),
|
|
93
|
+
trainExamples: trainUser.length + trainShadow.length,
|
|
94
|
+
} : { disposition: 'abstain', reason: 'The paragraph-grouped split leaves no train examples for one class.' };
|
|
95
|
+
const inputFindings = analyzeAiEditor(input).findings.length;
|
|
96
|
+
const candidateFindings = analyzeAiEditor(candidate).findings.length;
|
|
97
|
+
return { version: '1', split: { trainParagraphIds: trainIds, testParagraphIds: testIds }, authorshipTfIdfLogReg: author, contentF1: f1(input, candidate), aiTellReduction: { inputFindings, candidateFindings, reduction: inputFindings ? Number(((inputFindings - candidateFindings) / inputFindings).toFixed(3)) : 0 }, stylometricCosine9: cosine(vector(input), vector(candidate)) };
|
|
98
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { evaluateLocalComposite } from './local-eval.js';
|
|
4
|
+
test('groups paragraph IDs before optional local composite evaluation', () => {
|
|
5
|
+
const user = [{ paragraphId: 'u1', text: 'I check the source and explain the mechanism.' }, { paragraphId: 'u2', text: 'The owner names one next step for the release.' }, { paragraphId: 'u3', text: 'I keep the evidence visible for the operator.' }];
|
|
6
|
+
const shadow = [{ paragraphId: 's1', text: 'This transformative framework unlocks synergy.' }, { paragraphId: 's2', text: 'The holistic ecosystem creates value.' }, { paragraphId: 's3', text: 'This is a game-changer for every stakeholder.' }];
|
|
7
|
+
const report = evaluateLocalComposite('We leverage a holistic framework.', 'We check the source and name the next step.', user, shadow);
|
|
8
|
+
assert.equal(report.version, '1');
|
|
9
|
+
assert.equal(report.split.trainParagraphIds.some((id) => report.split.testParagraphIds.includes(id)), false);
|
|
10
|
+
assert.ok(report.contentF1 >= 0 && report.contentF1 <= 1);
|
|
11
|
+
assert.ok(report.stylometricCosine9 >= 0 && report.stylometricCosine9 <= 1);
|
|
12
|
+
assert.equal(report.aiTellReduction.candidateFindings < report.aiTellReduction.inputFindings, true);
|
|
13
|
+
});
|
|
14
|
+
test('keeps every paragraph variant on one side of the local evaluation split', () => {
|
|
15
|
+
const user = [{ paragraphId: 'u1', text: 'I inspect the source.' }, { paragraphId: 'u1', text: 'I inspect the source before shipping.' }, { paragraphId: 'u2', text: 'I name the mechanism.' }];
|
|
16
|
+
const shadow = [{ paragraphId: 's1', text: 'This holistic framework transforms outcomes.' }, { paragraphId: 's1', text: 'This transformative framework unlocks outcomes.' }, { paragraphId: 's2', text: 'The ecosystem creates value.' }];
|
|
17
|
+
const report = evaluateLocalComposite('The source is visible.', 'I inspect the source before shipping.', user, shadow);
|
|
18
|
+
assert.equal(report.split.trainParagraphIds.includes('u1'), report.split.testParagraphIds.includes('u1') === false);
|
|
19
|
+
assert.equal(report.split.trainParagraphIds.includes('s1'), report.split.testParagraphIds.includes('s1') === false);
|
|
20
|
+
});
|
package/dist/mcp-tools.js
CHANGED
|
@@ -16,6 +16,11 @@ import { MAX_JSON_BYTES } from './internal.js';
|
|
|
16
16
|
import { lintFacts } from './fact-linter.js';
|
|
17
17
|
import { inspectDeliveryIntegrity, parseDeliveryIntegrityPolicy } from './delivery-integrity.js';
|
|
18
18
|
import { assessProfileReadiness } from './profile-quality.js';
|
|
19
|
+
import { evaluateStrictQuality } from './strict-quality.js';
|
|
20
|
+
import { scoreHeldoutProfile } from './profile-score.js';
|
|
21
|
+
import { findWritingExamples } from './writing-examples.js';
|
|
22
|
+
import { evaluateIsolatedBacktest } from './backtest.js';
|
|
23
|
+
import { evaluateLocalComposite } from './local-eval.js';
|
|
19
24
|
function profileFromJson(profileJson) {
|
|
20
25
|
try {
|
|
21
26
|
return parseProfile(JSON.parse(profileJson));
|
|
@@ -54,15 +59,30 @@ export function buildProfileForMcp(samples, avoid = []) {
|
|
|
54
59
|
export function analyzeForMcp(draft, profileJson, writingBriefJson) {
|
|
55
60
|
return analyze(draft, profileFromJson(profileJson), writingBriefFromJson(writingBriefJson));
|
|
56
61
|
}
|
|
62
|
+
export function strictCheckForMcp(draft, profileJson, samples, writingBriefJson) {
|
|
63
|
+
return evaluateStrictQuality(draft, profileFromJson(profileJson), samples, writingBriefFromJson(writingBriefJson));
|
|
64
|
+
}
|
|
65
|
+
export function scoreHeldoutForMcp(draft, profileJson, samples, channel) {
|
|
66
|
+
return scoreHeldoutProfile(draft, profileFromJson(profileJson), samples, channel);
|
|
67
|
+
}
|
|
68
|
+
export function backtestForMcp(context, target, candidate, profileJson, samples) {
|
|
69
|
+
return evaluateIsolatedBacktest(context, target, candidate, profileFromJson(profileJson), samples);
|
|
70
|
+
}
|
|
71
|
+
export function evaluateLocalForMcp(input, candidate, user, aiShadow) {
|
|
72
|
+
return evaluateLocalComposite(input, candidate, user, aiShadow);
|
|
73
|
+
}
|
|
57
74
|
export function inspectHygieneForMcp(draft) {
|
|
58
75
|
return inspectHygiene(draft);
|
|
59
76
|
}
|
|
60
77
|
export function finalOutputCheckForMcp(text) {
|
|
61
78
|
return finalOutputCheck(text);
|
|
62
79
|
}
|
|
63
|
-
export function
|
|
80
|
+
export function findWritingExamplesForMcp(query, samples) {
|
|
81
|
+
return findWritingExamples(query, samples);
|
|
82
|
+
}
|
|
83
|
+
export function rewritePromptForMcp(draft, profileJson, options = {}, writingBriefJson, samples) {
|
|
64
84
|
const profile = profileFromJson(profileJson);
|
|
65
|
-
return { prompt: rewritePrompt(draft, profile, composeLearning(profile, options), writingBriefFromJson(writingBriefJson)) };
|
|
85
|
+
return { prompt: rewritePrompt(draft, profile, composeLearning(profile, options), writingBriefFromJson(writingBriefJson), samples ? findWritingExamples(draft, samples) : []) };
|
|
66
86
|
}
|
|
67
87
|
export function prepareRewriteForMcp(draft, profileJson, copySpecJson, writingBriefJson) {
|
|
68
88
|
return prepareRewriteTask(draft, profileFromJson(profileJson), copySpecJson ? copySpecFromJson(copySpecJson) : undefined, writingBriefFromJson(writingBriefJson));
|