@aiwg/cli 2026.9.4 → 2026.9.6
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/THIRD_PARTY_NOTICES.md +16 -0
- package/agentic/code/providers/capability-matrix.yaml +45 -3
- package/agentic/code/providers/deepseek-harness/README.md +8 -0
- package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
- package/agentic/code/providers/model-capabilities.v1.json +11 -0
- package/agentic/code/providers/model-catalog.v1.json +8 -0
- package/bin/aiwg.mjs +1 -0
- package/dist/src/api/index.d.ts +14 -0
- package/dist/src/api/index.js +14 -0
- package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
- package/dist/src/artifacts/index-files.js +17 -2
- package/dist/src/artifacts/repair.js +55 -6
- package/dist/src/catalog/cli.js +21 -7
- package/dist/src/catalog/cli.mjs +22 -7
- package/dist/src/cli/agent-spawn.js +10 -1
- package/dist/src/cli/handlers/artifacts.js +22 -3
- package/dist/src/cli/handlers/help.js +3 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/models.js +2 -2
- package/dist/src/cli/handlers/output-mode.js +1 -1
- package/dist/src/cli/handlers/runtime-info.js +1 -1
- package/dist/src/cli/handlers/sessions.js +55 -15
- package/dist/src/cli/handlers/steward.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +5 -0
- package/dist/src/cli/handlers/writer-profile.js +110 -0
- package/dist/src/cli/handlers/writing.js +122 -0
- package/dist/src/cli/router.js +5 -1
- package/dist/src/config/project-artifacts-runtime.mjs +33 -1
- package/dist/src/config/project-artifacts.js +1 -1
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
- package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
- package/dist/src/dataset/fortemi-live-qualification.js +20 -21
- package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
- package/dist/src/dataset/fortemi-run-receipt.js +74 -0
- package/dist/src/dataset/index.d.ts +2 -0
- package/dist/src/dataset/index.js +2 -0
- package/dist/src/extensions/commands/definitions.js +24 -0
- package/dist/src/extensions/manifest.js +3 -0
- package/dist/src/mcp/server.mjs +2 -0
- package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
- package/dist/src/models/model-capabilities.v1.json +11 -0
- package/dist/src/models/model-catalog.v1.json +8 -0
- package/dist/src/models/provider-policy.js +1 -1
- package/dist/src/network-analysis/analyzer.js +667 -0
- package/dist/src/network-analysis/citations.js +107 -0
- package/dist/src/network-analysis/forensics.js +132 -0
- package/dist/src/network-analysis/governance.js +216 -0
- package/dist/src/network-analysis/index.js +10 -0
- package/dist/src/network-analysis/probe.js +405 -0
- package/dist/src/network-analysis/recipes.js +88 -0
- package/dist/src/network-analysis/research.js +181 -0
- package/dist/src/network-analysis/termshark.js +252 -0
- package/dist/src/network-analysis/verification.js +171 -0
- package/dist/src/output-modes/registry.js +37 -6
- package/dist/src/output-modes/runtime.js +164 -28
- package/dist/src/providers/capability-matrix.yaml +45 -3
- package/dist/src/providers/provider-definitions.js +49 -0
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/providers/transformation-receipt.js +3 -2
- package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/contracts.js +2 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/workspace-discovery.js +5 -1
- package/dist/src/skills/deployer.js +6 -6
- package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
- package/dist/src/writing/channel-packs.js +13 -0
- package/dist/src/writing/contextual-diagnostics.js +142 -0
- package/dist/src/writing/example-generator.js +7 -6
- package/dist/src/writing/exemplar-selection.js +186 -0
- package/dist/src/writing/fidelity.js +61 -0
- package/dist/src/writing/validation-engine.js +32 -15
- package/dist/src/writing/voice-evaluation.js +301 -0
- package/dist/src/writing/voice-revision.js +201 -0
- package/dist/src/writing/writer-migration.js +216 -0
- package/dist/src/writing/writer-profile-legacy.js +145 -0
- package/dist/src/writing/writer-profile-store.js +117 -0
- package/dist/src/writing/writer-profile.js +222 -0
- package/dist/src/writing/writing-brief.js +166 -0
- package/dist/src/writing/writing-channels.js +63 -0
- package/dist/src/writing/writing-consumer.js +39 -0
- package/dist/src/writing/writing-receipt.js +266 -0
- package/package.json +2 -1
- package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
- package/tools/agents/deploy-agents.mjs +7 -3
- package/tools/agents/providers/antigravity.mjs +1 -1
- package/tools/agents/providers/base.mjs +3 -2
- package/tools/agents/providers/deepseek-harness.mjs +66 -0
- package/tools/agents/providers/hermes.mjs +1 -1
- package/tools/agents/providers/openhuman.mjs +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Writing Validation Engine
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* and
|
|
4
|
+
* Contextual writing review with a deprecated heuristic scoring adapter.
|
|
5
|
+
* Pattern matches and legacy scores do not establish human or machine authorship.
|
|
6
6
|
*
|
|
7
7
|
* @implements @.aiwg/requirements/use-cases/UC-001-validate-ai-generated-content.md
|
|
8
8
|
* @architecture @.aiwg/architecture/software-architecture-doc.md - Section 5.1 WritingValidator
|
|
@@ -16,10 +16,15 @@ import { readFile } from 'fs/promises';
|
|
|
16
16
|
import { existsSync } from 'fs';
|
|
17
17
|
import { ValidationRuleLoader } from './validation-rules.js';
|
|
18
18
|
import { loadScoringConfig, getScoringConfig } from './scoring-config-loader.js';
|
|
19
|
+
import { diagnoseWriting } from './contextual-diagnostics.js';
|
|
19
20
|
/**
|
|
20
21
|
* Core Writing Validation Engine
|
|
21
22
|
*/
|
|
22
23
|
export class WritingValidationEngine {
|
|
24
|
+
/** Contextual editorial review without an authorship score or publication gate. */
|
|
25
|
+
diagnose(content, options) {
|
|
26
|
+
return diagnoseWriting(content, options);
|
|
27
|
+
}
|
|
23
28
|
ruleLoader;
|
|
24
29
|
ruleSet = null;
|
|
25
30
|
initialized = false;
|
|
@@ -68,9 +73,9 @@ export class WritingValidationEngine {
|
|
|
68
73
|
issues.push({
|
|
69
74
|
type: 'missing_authenticity',
|
|
70
75
|
severity: 'info',
|
|
71
|
-
message: '
|
|
76
|
+
message: 'Legacy heuristic found few specificity features; this says nothing about authorship',
|
|
72
77
|
location: { start: 0, end: 0, line: 1, column: 1 },
|
|
73
|
-
suggestion: `
|
|
78
|
+
suggestion: `Only if supported by supplied facts and author intent, consider: ${authenticityAnalysis.missingMarkers.join(', ')}`,
|
|
74
79
|
context: authenticityAnalysis.missingMarkers.slice(0, 3).join('; ')
|
|
75
80
|
});
|
|
76
81
|
}
|
|
@@ -85,6 +90,8 @@ export class WritingValidationEngine {
|
|
|
85
90
|
issues,
|
|
86
91
|
summary,
|
|
87
92
|
suggestions,
|
|
93
|
+
scoreSemantics: 'deprecated-uncalibrated-heuristic',
|
|
94
|
+
contextualDiagnostics: this.diagnose(content),
|
|
88
95
|
humanMarkers: authenticityAnalysis.humanMarkers,
|
|
89
96
|
aiTells: authenticityAnalysis.aiTells
|
|
90
97
|
};
|
|
@@ -392,7 +399,7 @@ export class WritingValidationEngine {
|
|
|
392
399
|
}
|
|
393
400
|
// Missing authenticity
|
|
394
401
|
if (authenticityAnalysis.missingMarkers.length > 0) {
|
|
395
|
-
suggestions.push(`
|
|
402
|
+
suggestions.push(`Legacy authenticity heuristic: add details only when supported by supplied facts and author intent (${authenticityAnalysis.missingMarkers.slice(0, 2).join(', ')}); never invent experiences or numbers`);
|
|
396
403
|
}
|
|
397
404
|
// Specific improvements
|
|
398
405
|
const hasTransitionIssues = issues.some(i => i.message.includes('transition'));
|
|
@@ -406,7 +413,7 @@ export class WritingValidationEngine {
|
|
|
406
413
|
// If score is low, provide general guidance
|
|
407
414
|
const score = this.calculateOverallScore(summary, authenticityAnalysis);
|
|
408
415
|
if (score < config.thresholds.lowScoreWarning) {
|
|
409
|
-
suggestions.push('Consider adding
|
|
416
|
+
suggestions.push('Consider adding supplied metrics, problem acknowledgments and technical details where relevant; never invent them to raise a legacy heuristic score');
|
|
410
417
|
}
|
|
411
418
|
return suggestions;
|
|
412
419
|
}
|
|
@@ -428,7 +435,7 @@ export class WritingValidationEngine {
|
|
|
428
435
|
adjustments.scoreModifier += 10;
|
|
429
436
|
}
|
|
430
437
|
if (!/\b\d+(%|ms|MB|GB)\b/.test(content)) {
|
|
431
|
-
adjustments.suggestions.push('Technical context: Include
|
|
438
|
+
adjustments.suggestions.push('Technical context: Include metrics and performance numbers only when supplied and relevant');
|
|
432
439
|
}
|
|
433
440
|
break;
|
|
434
441
|
case 'executive':
|
|
@@ -436,7 +443,7 @@ export class WritingValidationEngine {
|
|
|
436
443
|
const hedgeCount = (content.match(/\b(may|might|could|perhaps)\b/gi) || []).length;
|
|
437
444
|
if (hedgeCount > 3) {
|
|
438
445
|
adjustments.scoreModifier -= 10;
|
|
439
|
-
adjustments.suggestions.push('Executive context:
|
|
446
|
+
adjustments.suggestions.push('Executive context: Review unnecessary hedging while preserving uncertainty and evidence strength');
|
|
440
447
|
}
|
|
441
448
|
break;
|
|
442
449
|
case 'casual':
|
|
@@ -451,9 +458,17 @@ export class WritingValidationEngine {
|
|
|
451
458
|
generateTextReport(result) {
|
|
452
459
|
const lines = [];
|
|
453
460
|
lines.push('=== Writing Validation Report ===\n');
|
|
454
|
-
lines.push(
|
|
455
|
-
lines.push(`
|
|
456
|
-
lines.push(`
|
|
461
|
+
lines.push('Legacy scores below are deprecated, uncalibrated heuristics; they do not identify authorship or determine publication readiness.');
|
|
462
|
+
lines.push(`Overall Score (legacy heuristic): ${result.score}/100`);
|
|
463
|
+
lines.push(`Specificity features (legacy authenticityScore): ${result.summary.authenticityScore}/100`);
|
|
464
|
+
lines.push(`Pattern matches (legacy aiPatternScore): ${result.summary.aiPatternScore}/100\n`);
|
|
465
|
+
if (result.contextualDiagnostics) {
|
|
466
|
+
lines.push('=== Contextual Editorial Review ===');
|
|
467
|
+
for (const d of result.contextualDiagnostics.diagnostics)
|
|
468
|
+
lines.push(`[${d.resolution}] ${d.start}:${d.end} ${d.ruleId}: ${d.explanation}${d.reason ? ` Retained: ${d.reason}` : ''}`);
|
|
469
|
+
lines.push(...result.contextualDiagnostics.notices);
|
|
470
|
+
lines.push('Zero highlights is not a publication gate.\n');
|
|
471
|
+
}
|
|
457
472
|
lines.push(`Total Issues: ${result.summary.totalIssues}`);
|
|
458
473
|
lines.push(` Critical: ${result.summary.criticalCount}`);
|
|
459
474
|
lines.push(` Warnings: ${result.summary.warningCount}`);
|
|
@@ -484,12 +499,12 @@ export class WritingValidationEngine {
|
|
|
484
499
|
lines.push('');
|
|
485
500
|
}
|
|
486
501
|
if (result.humanMarkers.length > 0) {
|
|
487
|
-
lines.push('===
|
|
502
|
+
lines.push('=== Legacy Specificity Features ===');
|
|
488
503
|
result.humanMarkers.forEach(m => lines.push(` ✓ ${m}`));
|
|
489
504
|
lines.push('');
|
|
490
505
|
}
|
|
491
506
|
if (result.aiTells.length > 0) {
|
|
492
|
-
lines.push('===
|
|
507
|
+
lines.push('=== Legacy Pattern Matches ===');
|
|
493
508
|
result.aiTells.forEach(t => lines.push(` ✗ ${t}`));
|
|
494
509
|
lines.push('');
|
|
495
510
|
}
|
|
@@ -505,6 +520,7 @@ export class WritingValidationEngine {
|
|
|
505
520
|
}
|
|
506
521
|
const lines = [];
|
|
507
522
|
lines.push('=== Batch Validation Report ===\n');
|
|
523
|
+
lines.push('Deprecated heuristic threshold comparisons only; these are not authorship or publication judgments.\n');
|
|
508
524
|
lines.push(`Total Files: ${results.size}\n`);
|
|
509
525
|
let totalScore = 0;
|
|
510
526
|
let passed = 0;
|
|
@@ -555,15 +571,16 @@ export class WritingValidationEngine {
|
|
|
555
571
|
</head>
|
|
556
572
|
<body>
|
|
557
573
|
<h1>Writing Validation Report</h1>
|
|
574
|
+
<p>Deprecated, uncalibrated legacy heuristics. These scores do not identify authorship or determine publication readiness. Zero highlights is not a publication gate.</p>
|
|
558
575
|
<div class="score">${result.score}/100</div>
|
|
559
576
|
|
|
560
577
|
<div class="summary">
|
|
561
578
|
<div class="metric">
|
|
562
|
-
<div>
|
|
579
|
+
<div>Legacy specificity heuristic (authenticityScore)</div>
|
|
563
580
|
<div class="metric-value">${result.summary.authenticityScore}/100</div>
|
|
564
581
|
</div>
|
|
565
582
|
<div class="metric">
|
|
566
|
-
<div>
|
|
583
|
+
<div>Legacy pattern heuristic (aiPatternScore)</div>
|
|
567
584
|
<div class="metric-value">${result.summary.aiPatternScore}/100</div>
|
|
568
585
|
</div>
|
|
569
586
|
<div class="metric">
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const id = z.string().min(1).max(120);
|
|
4
|
+
const nonempty = z.string().min(1);
|
|
5
|
+
const sha = z.string().regex(/^[a-f0-9]{64}$/);
|
|
6
|
+
export const evaluationHash = (text) => createHash('sha256').update(text).digest('hex');
|
|
7
|
+
export const EVALUATION_WORKFLOWS = ['unassisted', 'minimal-editing', 'notes-plus-author-edits', 'notes-plus-exemplars-plus-author-edits'];
|
|
8
|
+
export const EVALUATION_CONDITIONS = Array.from({ length: 8 }, (_, n) => ({
|
|
9
|
+
id: `r${(n >> 2) & 1}v${(n >> 1) & 1}e${n & 1}`,
|
|
10
|
+
rules: Boolean(n & 4), voiceDescription: Boolean(n & 2), exemplars: Boolean(n & 1),
|
|
11
|
+
}));
|
|
12
|
+
export const EVALUATION_DIMENSIONS = ['factual-fidelity', 'author-authenticity', 'reader-suitability', 'effort', 'within-author-preservation', 'between-author-diversity'];
|
|
13
|
+
const model = z.object({ id, family: nonempty, provider: nonempty, snapshot: nonempty, decoding: z.record(z.string(), z.union([z.string(), z.number().finite(), z.boolean(), z.null()])), promptSha256: sha }).strict();
|
|
14
|
+
const document = z.object({ id, authorId: id, topic: nonempty, split: z.enum(['development', 'enrollment', 'heldout']), text: nonempty, sha256: sha }).strict();
|
|
15
|
+
export const evaluationManifestSchema = z.object({
|
|
16
|
+
schemaVersion: z.literal(1), id,
|
|
17
|
+
developmentAuthorIds: z.array(id), finalAuthorIds: z.array(id), documents: z.array(document),
|
|
18
|
+
duplicatePolicy: z.object({ metric: z.literal('normalized-word-trigram-jaccard-v1'), threshold: z.number().gt(0).lte(1), justification: nonempty }).strict(),
|
|
19
|
+
controls: z.array(z.object({ kind: z.enum(['same-author-different-topic', 'different-author-same-topic']), leftDocumentId: id, rightDocumentId: id }).strict()),
|
|
20
|
+
models: z.array(model), extractionModelIds: z.array(id),
|
|
21
|
+
judges: z.array(z.object({ id, modelId: id, role: z.literal('reader').default('reader'), independentCalibrationSha256: sha, selfPreferenceAuditSha256: sha }).strict()),
|
|
22
|
+
humanJudges: z.array(z.object({ id, role: z.enum(['author', 'reader']), authorId: id.optional() }).strict()).default([]),
|
|
23
|
+
voluntaryStrata: z.array(z.object({ authorId: id, suppliedByAuthor: z.literal(true), language: nonempty.optional(), proficiency: nonempty.optional() }).strict()),
|
|
24
|
+
}).strict();
|
|
25
|
+
const unique = (items) => new Set(items).size === items.length;
|
|
26
|
+
const words = (text) => text.normalize('NFKC').toLowerCase().match(/[\p{L}\p{M}\p{N}]+/gu) ?? [];
|
|
27
|
+
function shingles(text) {
|
|
28
|
+
const w = words(text);
|
|
29
|
+
const size = Math.min(3, w.length);
|
|
30
|
+
return new Set(Array.from({ length: Math.max(0, w.length - size + 1) }, (_, i) => w.slice(i, i + size).join(' ')));
|
|
31
|
+
}
|
|
32
|
+
function nearDuplicate(left, right, threshold) {
|
|
33
|
+
if (words(left).join(' ') === words(right).join(' '))
|
|
34
|
+
return true;
|
|
35
|
+
const a = shingles(left);
|
|
36
|
+
const b = shingles(right);
|
|
37
|
+
const intersection = [...a].filter(v => b.has(v)).length;
|
|
38
|
+
return intersection / (a.size + b.size - intersection) >= threshold;
|
|
39
|
+
}
|
|
40
|
+
/** Reject contaminated manifests before fitting. Checks supplied texts, not undisclosed data or semantic paraphrases. */
|
|
41
|
+
export function parseEvaluationManifest(value) {
|
|
42
|
+
const m = evaluationManifestSchema.parse(value);
|
|
43
|
+
for (const ids of [m.developmentAuthorIds, m.finalAuthorIds, m.documents.map(d => d.id), m.models.map(v => v.id), [...m.judges, ...m.humanJudges].map(v => v.id), m.extractionModelIds, m.voluntaryStrata.map(v => v.authorId)])
|
|
44
|
+
if (!unique(ids))
|
|
45
|
+
throw new Error('Duplicate evaluation identifiers');
|
|
46
|
+
if (m.developmentAuthorIds.some(a => m.finalAuthorIds.includes(a)))
|
|
47
|
+
throw new Error('Development and final authors overlap');
|
|
48
|
+
for (const d of m.documents) {
|
|
49
|
+
if (evaluationHash(d.text) !== d.sha256)
|
|
50
|
+
throw new Error('Document integrity mismatch');
|
|
51
|
+
if (!(d.split === 'development' ? m.developmentAuthorIds : m.finalAuthorIds).includes(d.authorId))
|
|
52
|
+
throw new Error('Document violates author partition');
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < m.documents.length; i++)
|
|
55
|
+
for (let j = i + 1; j < m.documents.length; j++) {
|
|
56
|
+
if (nearDuplicate(m.documents[i].text, m.documents[j].text, m.duplicatePolicy.threshold))
|
|
57
|
+
throw new Error('Duplicate or near-duplicate documents must be excluded before fitting');
|
|
58
|
+
}
|
|
59
|
+
const documents = new Map(m.documents.map(d => [d.id, d]));
|
|
60
|
+
for (const c of m.controls) {
|
|
61
|
+
const a = documents.get(c.leftDocumentId);
|
|
62
|
+
const b = documents.get(c.rightDocumentId);
|
|
63
|
+
if (!a || !b || a.id === b.id)
|
|
64
|
+
throw new Error('Control references missing or identical documents');
|
|
65
|
+
if (c.kind === 'same-author-different-topic' ? a.authorId !== b.authorId || a.topic === b.topic : a.authorId === b.authorId || a.topic !== b.topic)
|
|
66
|
+
throw new Error('Control does not match declared author/topic relationship');
|
|
67
|
+
}
|
|
68
|
+
const models = new Map(m.models.map(v => [v.id, v]));
|
|
69
|
+
if (m.extractionModelIds.some(v => !models.has(v)))
|
|
70
|
+
throw new Error('Unknown extraction model');
|
|
71
|
+
const extractionFamilies = new Set(m.extractionModelIds.map(v => models.get(v).family));
|
|
72
|
+
for (const judge of m.judges) {
|
|
73
|
+
const selected = models.get(judge.modelId);
|
|
74
|
+
if (!selected || extractionFamilies.has(selected.family) || m.extractionModelIds.some(id => { const extraction = models.get(id); return extraction.provider === selected.provider && extraction.snapshot === selected.snapshot; }))
|
|
75
|
+
throw new Error('Judge must be independent of extraction model family');
|
|
76
|
+
}
|
|
77
|
+
if (m.humanJudges.some(j => j.role === 'author' && (!j.authorId || !m.finalAuthorIds.includes(j.authorId))))
|
|
78
|
+
throw new Error('Author judges require a known final author');
|
|
79
|
+
if (m.voluntaryStrata.some(v => ![...m.developmentAuthorIds, ...m.finalAuthorIds].includes(v.authorId)))
|
|
80
|
+
throw new Error('Unknown voluntary stratum author');
|
|
81
|
+
return m;
|
|
82
|
+
}
|
|
83
|
+
const preregistrationSchema = z.object({
|
|
84
|
+
schemaVersion: z.literal(1), id, manifestSha256: sha,
|
|
85
|
+
pilot: z.object({ artifactSha256: sha, completedAt: z.string().datetime(), authorIds: z.array(id).min(1), findings: nonempty }).strict(),
|
|
86
|
+
registeredAt: z.string().datetime(), finalDataNotAccessed: z.literal(true),
|
|
87
|
+
plannedAuthors: z.number().int().min(2), sampleSizeJustification: nonempty,
|
|
88
|
+
thresholds: z.array(z.object({ dimension: z.enum(EVALUATION_DIMENSIONS), value: z.number().finite(), direction: z.enum(['at-least', 'at-most']), justification: nonempty }).strict()).min(1),
|
|
89
|
+
primaryOutcomes: z.array(z.enum(EVALUATION_DIMENSIONS)).min(1), analysisPlan: nonempty,
|
|
90
|
+
selectorPoliciesSha256: sha, budget: z.object({ limit: z.number().int().positive(), tokenizerId: nonempty, tokenizerVersion: nonempty }).strict(),
|
|
91
|
+
}).strict();
|
|
92
|
+
/** Records caller declarations; hashes/timestamps do not attest that a pilot occurred or final data stayed unseen. */
|
|
93
|
+
export function preregisterEvaluation(manifest, value) {
|
|
94
|
+
const m = parseEvaluationManifest(manifest);
|
|
95
|
+
const p = preregistrationSchema.parse(value);
|
|
96
|
+
if (p.manifestSha256 !== evaluationHash(JSON.stringify(m)))
|
|
97
|
+
throw new Error('Preregistration does not bind this manifest');
|
|
98
|
+
if (Date.parse(p.registeredAt) <= Date.parse(p.pilot.completedAt))
|
|
99
|
+
throw new Error('Preregister after pilot completion');
|
|
100
|
+
if (!unique(p.pilot.authorIds) || p.pilot.authorIds.some(a => !m.developmentAuthorIds.includes(a)))
|
|
101
|
+
throw new Error('Pilot authors must come from development partition');
|
|
102
|
+
if (!unique(p.primaryOutcomes) || !unique(p.thresholds.map(t => t.dimension)))
|
|
103
|
+
throw new Error('Duplicate preregistered outcomes');
|
|
104
|
+
return p;
|
|
105
|
+
}
|
|
106
|
+
const stimulusSchema = z.object({ id, taskId: id, authorId: id, heldoutDocumentId: id, text: nonempty, sha256: sha, budgetUsed: z.number().int().nonnegative(), tokenizerId: nonempty, tokenizerVersion: nonempty, conditionId: z.enum(['r0v0e0', 'r0v0e1', 'r0v1e0', 'r0v1e1', 'r1v0e0', 'r1v0e1', 'r1v1e0', 'r1v1e1']), workflow: z.enum(EVALUATION_WORKFLOWS), modelId: id.nullable(), genre: nonempty, lengthBand: nonempty, editingStrength: nonempty }).strict();
|
|
107
|
+
/** Latin-square rotations plus reversed blocks. Balance is exact only for complete 2N-judge blocks per role. */
|
|
108
|
+
export function createBlindEvaluationPackets(stimuliInput, judges, seed) {
|
|
109
|
+
const stimuli = z.array(stimulusSchema).min(2).parse(stimuliInput);
|
|
110
|
+
if (stimuli.some(s => evaluationHash(s.text) !== s.sha256))
|
|
111
|
+
throw new Error('Stimulus integrity mismatch');
|
|
112
|
+
z.array(z.object({ id, role: z.enum(['author', 'reader']) }).strict()).min(1).parse(judges);
|
|
113
|
+
if (!seed || !unique(stimuli.map(s => s.id)) || !unique(judges.map(j => j.id)))
|
|
114
|
+
throw new Error('Seed and unique stimulus/judge IDs required');
|
|
115
|
+
if (new Set(stimuli.map(s => `${s.authorId}\0${s.taskId}\0${s.heldoutDocumentId}`)).size !== 1)
|
|
116
|
+
throw new Error('Blind block must compare one author and task');
|
|
117
|
+
const shuffled = [...stimuli].sort((a, b) => evaluationHash(`${seed}:stimulus:${a.id}`).localeCompare(evaluationHash(`${seed}:stimulus:${b.id}`)));
|
|
118
|
+
const packets = [];
|
|
119
|
+
const privateKeyMap = [];
|
|
120
|
+
for (const role of ['author', 'reader']) {
|
|
121
|
+
const group = judges.filter(j => j.role === role).sort((a, b) => evaluationHash(`${seed}:judge:${a.id}`).localeCompare(evaluationHash(`${seed}:judge:${b.id}`)));
|
|
122
|
+
group.forEach((judge, index) => {
|
|
123
|
+
const base = Math.floor(index / stimuli.length) % 2 ? [...shuffled].reverse() : shuffled;
|
|
124
|
+
const offset = index % stimuli.length;
|
|
125
|
+
const ordered = [...base.slice(offset), ...base.slice(0, offset)];
|
|
126
|
+
const packetId = evaluationHash(`${seed}:${role}:${judge.id}:${JSON.stringify(stimuli)}`).slice(0, 24);
|
|
127
|
+
const items = ordered.map((stimulus, i) => ({ label: `option-${i + 1}`, stimulus }));
|
|
128
|
+
packets.push({ packetId, role, items: items.map(i => ({ label: i.label, text: i.stimulus.text })) });
|
|
129
|
+
privateKeyMap.push({ packetId, judgeId: judge.id, role, items });
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { packets, privateKeyMap, balance: 'Rotations/reversal by role; complete 2N blocks balance positions and reversal, incomplete blocks may be imbalanced. Text itself may reveal authorship or condition.' };
|
|
133
|
+
}
|
|
134
|
+
function validateManifestStimuli(m, input) {
|
|
135
|
+
const stimuli = z.array(stimulusSchema).parse(input);
|
|
136
|
+
const models = new Map(m.models.map(v => [v.id, v]));
|
|
137
|
+
if (!unique(stimuli.map(s => s.id)))
|
|
138
|
+
throw new Error('Duplicate stimulus IDs');
|
|
139
|
+
if (stimuli.some(s => !m.finalAuthorIds.includes(s.authorId) || s.modelId !== null && !models.has(s.modelId)))
|
|
140
|
+
throw new Error('Unknown final author or model');
|
|
141
|
+
for (const s of stimuli) {
|
|
142
|
+
const task = m.documents.find(d => d.id === s.heldoutDocumentId);
|
|
143
|
+
if (!task || task.split !== 'heldout' || task.authorId !== s.authorId || evaluationHash(s.text) !== s.sha256)
|
|
144
|
+
throw new Error('Stimulus must bind a heldout author document and output hash');
|
|
145
|
+
if (s.workflow === 'unassisted' && (s.modelId !== null || s.conditionId !== 'r0v0e0') || s.workflow !== 'unassisted' && s.modelId === null)
|
|
146
|
+
throw new Error('Workflow/model labels conflict');
|
|
147
|
+
if (s.workflow === 'notes-plus-exemplars-plus-author-edits' && !s.conditionId.endsWith('e1') || s.workflow === 'notes-plus-author-edits' && s.conditionId.endsWith('e1'))
|
|
148
|
+
throw new Error('Workflow/exemplar labels conflict');
|
|
149
|
+
}
|
|
150
|
+
return stimuli;
|
|
151
|
+
}
|
|
152
|
+
function ingestRatings(ratings, context) {
|
|
153
|
+
const parsed = z.array(z.object({ stimulusId: id, authorId: id, judgeId: id, role: z.enum(['author', 'reader']), dimension: z.enum(EVALUATION_DIMENSIONS), value: z.number().finite().nullable(), missingReason: nonempty.optional(), tie: z.boolean() }).strict()).parse(ratings);
|
|
154
|
+
if (!unique(parsed.map(r => JSON.stringify([r.stimulusId, r.judgeId, r.role, r.dimension]))))
|
|
155
|
+
throw new Error('Duplicate rating observations');
|
|
156
|
+
if (parsed.some(r => r.value === null && (!r.missingReason || r.tie) || r.value !== null && r.missingReason !== undefined))
|
|
157
|
+
throw new Error('Missing ratings require a reason and cannot also be a tie');
|
|
158
|
+
if (!context)
|
|
159
|
+
return parsed;
|
|
160
|
+
const m = parseEvaluationManifest(context.manifest);
|
|
161
|
+
const stimuli = validateManifestStimuli(m, context.stimuli);
|
|
162
|
+
const judges = new Map([...m.judges, ...m.humanJudges].map(j => [j.id, j]));
|
|
163
|
+
const key = (r) => JSON.stringify([r.stimulusId, r.judgeId, r.role, r.dimension]);
|
|
164
|
+
const validate = (r) => {
|
|
165
|
+
const stimulus = stimuli.find(s => s.id === r.stimulusId);
|
|
166
|
+
const judge = judges.get(r.judgeId);
|
|
167
|
+
if (!stimulus || !judge || judge.role !== r.role || 'authorId' in judge && judge.role === 'author' && judge.authorId !== stimulus.authorId)
|
|
168
|
+
throw new Error('Unknown or incompatible rating judge, role or stimulus');
|
|
169
|
+
return stimulus;
|
|
170
|
+
};
|
|
171
|
+
for (const r of parsed)
|
|
172
|
+
if (validate(r).authorId !== r.authorId)
|
|
173
|
+
throw new Error('Rating/stimulus author mismatch');
|
|
174
|
+
if (context.assignments !== undefined) {
|
|
175
|
+
const assignments = z.array(z.object({ stimulusId: id, judgeId: id, role: z.enum(['author', 'reader']), dimension: z.enum(EVALUATION_DIMENSIONS) }).strict()).parse(context.assignments);
|
|
176
|
+
if (!unique(assignments.map(key)))
|
|
177
|
+
throw new Error('Duplicate rating assignments');
|
|
178
|
+
for (const assignment of assignments)
|
|
179
|
+
validate(assignment);
|
|
180
|
+
const expected = new Set(assignments.map(key));
|
|
181
|
+
if (parsed.some(r => !expected.has(key(r))))
|
|
182
|
+
throw new Error('Unassigned rating observation');
|
|
183
|
+
const received = new Set(parsed.map(key));
|
|
184
|
+
for (const assignment of assignments)
|
|
185
|
+
if (!received.has(key(assignment)))
|
|
186
|
+
parsed.push({ ...assignment, authorId: validate(assignment).authorId, value: null, missingReason: 'assigned-rating-not-returned', tie: false });
|
|
187
|
+
}
|
|
188
|
+
const kinds = new Set(parsed.map(r => m.judges.some(j => j.id === r.judgeId) ? 'model' : 'human'));
|
|
189
|
+
if (kinds.size > 1)
|
|
190
|
+
throw new Error('Human outcomes and model diagnostics require separate aggregation calls');
|
|
191
|
+
return parsed;
|
|
192
|
+
}
|
|
193
|
+
export function summarizeEvaluationRatings(ratings, context) {
|
|
194
|
+
const parsed = ingestRatings(ratings, context);
|
|
195
|
+
const judgeKind = !context || !parsed.length ? 'unknown' : context.manifest.judges.some(j => j.id === parsed[0].judgeId) ? 'model' : 'human';
|
|
196
|
+
return ['author', 'reader'].flatMap(role => EVALUATION_DIMENSIONS.map(dimension => {
|
|
197
|
+
const group = parsed.filter(r => r.role === role && r.dimension === dimension);
|
|
198
|
+
const observed = group.filter(r => r.value !== null);
|
|
199
|
+
return { role, dimension, judgeKind, missingnessCompleteness: context?.assignments === undefined ? 'unknown' : 'declared-assignments', observed: observed.length, missing: group.length - observed.length, ties: observed.filter(r => r.tie).length, mean: observed.length ? observed.reduce((sum, r) => sum + r.value, 0) / observed.length : null };
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
function random(seed) {
|
|
203
|
+
let state = Number.parseInt(evaluationHash(seed).slice(0, 8), 16);
|
|
204
|
+
return () => { state = (Math.imul(1664525, state) + 1013904223) >>> 0; return state / 0x100000000; };
|
|
205
|
+
}
|
|
206
|
+
/** Equal-weight author means, then author-cluster percentile bootstrap; no independent-text pseudo-replication. */
|
|
207
|
+
export function bootstrapAuthorMeans(rows, options) {
|
|
208
|
+
z.array(z.object({ authorId: id, value: z.number().finite().nullable() }).strict()).parse(rows);
|
|
209
|
+
if (!options.seed || !Number.isSafeInteger(options.iterations) || options.iterations < 2 || !Number.isFinite(options.confidence) || options.confidence <= 0 || options.confidence >= 1)
|
|
210
|
+
throw new Error('Invalid bootstrap options');
|
|
211
|
+
const authors = [...new Set(rows.map(r => r.authorId))].sort();
|
|
212
|
+
const means = authors.map(authorId => rows.filter(r => r.authorId === authorId && r.value !== null).map(r => r.value)).filter(v => v.length).map(v => v.reduce((a, b) => a + b, 0) / v.length);
|
|
213
|
+
const estimate = means.length ? means.reduce((a, b) => a + b, 0) / means.length : null;
|
|
214
|
+
if (means.length < 2)
|
|
215
|
+
return { estimate, interval: null, authors: means.length, excludedAuthors: authors.length - means.length, method: 'author-cluster-percentile' };
|
|
216
|
+
const rng = random(options.seed);
|
|
217
|
+
const draws = [];
|
|
218
|
+
for (let i = 0; i < options.iterations; i++) {
|
|
219
|
+
let sum = 0;
|
|
220
|
+
for (let j = 0; j < means.length; j++)
|
|
221
|
+
sum += means[Math.floor(rng() * means.length)];
|
|
222
|
+
draws.push(sum / means.length);
|
|
223
|
+
}
|
|
224
|
+
draws.sort((a, b) => a - b);
|
|
225
|
+
const tail = (1 - options.confidence) / 2;
|
|
226
|
+
return { estimate, interval: [draws[Math.floor(tail * (draws.length - 1))], draws[Math.ceil((1 - tail) * (draws.length - 1))]], authors: means.length, excludedAuthors: authors.length - means.length, method: 'author-cluster-percentile' };
|
|
227
|
+
}
|
|
228
|
+
/** Outcomes must already share a preregistered decision rule; never compare arbitrary raw scales. */
|
|
229
|
+
export function reportMetricDisagreement(rows, calibration) {
|
|
230
|
+
z.object({ humanReferenceSha256: sha, independentOfFinalData: z.literal(true), decisionRule: nonempty }).strict().parse(calibration);
|
|
231
|
+
const outcome = z.enum(['pass', 'fail', 'tie']).nullable();
|
|
232
|
+
z.array(z.object({ itemId: id, human: outcome, metric: outcome }).strict()).parse(rows);
|
|
233
|
+
if (!unique(rows.map(r => r.itemId)))
|
|
234
|
+
throw new Error('Duplicate metric comparison IDs');
|
|
235
|
+
const paired = rows.filter(r => r.human !== null && r.metric !== null);
|
|
236
|
+
const disagreements = paired.filter(r => r.human !== r.metric);
|
|
237
|
+
return { paired: paired.length, missing: rows.length - paired.length, ties: paired.filter(r => r.human === 'tie' || r.metric === 'tie').length, disagreements: disagreements.map(r => r.itemId), disagreementRate: paired.length ? disagreements.length / paired.length : null, calibration: { ...calibration }, authorshipCertification: false };
|
|
238
|
+
}
|
|
239
|
+
/** Qualification is a design completeness check, not a quality verdict or evidence that humans participated. */
|
|
240
|
+
export function evaluationDesignReadiness(manifest, preregistration, stimuliInput) {
|
|
241
|
+
const m = parseEvaluationManifest(manifest);
|
|
242
|
+
const p = preregisterEvaluation(m, preregistration);
|
|
243
|
+
const stimuli = validateManifestStimuli(m, stimuliInput);
|
|
244
|
+
const gaps = [];
|
|
245
|
+
const models = new Map(m.models.map(v => [v.id, v]));
|
|
246
|
+
for (const s of stimuli)
|
|
247
|
+
if (s.budgetUsed > p.budget.limit || s.tokenizerId !== p.budget.tokenizerId || s.tokenizerVersion !== p.budget.tokenizerVersion)
|
|
248
|
+
throw new Error('Stimulus violates frozen budget/tokenizer');
|
|
249
|
+
if (new Set(stimuli.filter(s => s.modelId !== null).map(s => models.get(s.modelId).family)).size < 2)
|
|
250
|
+
gaps.push('at-least-two-model-families');
|
|
251
|
+
if (m.finalAuthorIds.length < p.plannedAuthors)
|
|
252
|
+
gaps.push('preregistered-author-count');
|
|
253
|
+
for (const author of m.finalAuthorIds)
|
|
254
|
+
for (const split of ['enrollment', 'heldout'])
|
|
255
|
+
if (!m.documents.some(d => d.authorId === author && d.split === split))
|
|
256
|
+
gaps.push(`document:${author}:${split}`);
|
|
257
|
+
for (const kind of ['same-author-different-topic', 'different-author-same-topic'])
|
|
258
|
+
if (!m.controls.some(c => c.kind === kind))
|
|
259
|
+
gaps.push(`control:${kind}`);
|
|
260
|
+
const families = [...new Set(stimuli.filter(s => s.modelId !== null).map(s => models.get(s.modelId).family))];
|
|
261
|
+
for (const task of m.documents.filter(d => d.split === 'heldout'))
|
|
262
|
+
for (const family of families) {
|
|
263
|
+
const available = stimuli.filter(s => s.heldoutDocumentId === task.id && s.modelId !== null && models.get(s.modelId).family === family);
|
|
264
|
+
for (const c of EVALUATION_CONDITIONS)
|
|
265
|
+
if (!available.some(s => s.conditionId === c.id && s.workflow.startsWith('notes-plus')))
|
|
266
|
+
gaps.push(`condition:${task.id}:${family}:${c.id}`);
|
|
267
|
+
}
|
|
268
|
+
for (const workflow of EVALUATION_WORKFLOWS)
|
|
269
|
+
if (!stimuli.some(s => s.workflow === workflow))
|
|
270
|
+
gaps.push(`workflow:${workflow}`);
|
|
271
|
+
return { designComplete: gaps.length === 0, gaps, humanEvaluation: 'not-established', qualityVerdict: 'not-established' };
|
|
272
|
+
}
|
|
273
|
+
/** Descriptive subgroup estimates only; sparse cells return no interval. No inferred demographic strata. */
|
|
274
|
+
export function summarizeEvaluationByStratum(manifest, stimuli, ratings, options) {
|
|
275
|
+
const m = parseEvaluationManifest(manifest);
|
|
276
|
+
const parsed = validateManifestStimuli(m, stimuli);
|
|
277
|
+
const ingested = ingestRatings(ratings, { manifest: m, stimuli: parsed, assignments: options.assignments });
|
|
278
|
+
const judgeKind = !ingested.length ? 'unknown' : m.judges.some(j => j.id === ingested[0].judgeId) ? 'model' : 'human';
|
|
279
|
+
const groups = new Map();
|
|
280
|
+
for (const r of ingested) {
|
|
281
|
+
const s = parsed.find(v => v.id === r.stimulusId);
|
|
282
|
+
if (!s || s.authorId !== r.authorId)
|
|
283
|
+
throw new Error('Rating/stimulus author mismatch');
|
|
284
|
+
const voluntary = m.voluntaryStrata.find(v => v.authorId === r.authorId);
|
|
285
|
+
const family = s.modelId === null ? 'unassisted' : m.models.find(v => v.id === s.modelId)?.family;
|
|
286
|
+
if (!family)
|
|
287
|
+
throw new Error('Unknown model family');
|
|
288
|
+
const stratum = { condition: s.conditionId, workflow: s.workflow, genre: s.genre, length: s.lengthBand, editingStrength: s.editingStrength, language: voluntary?.language ?? 'not-supplied', proficiency: voluntary?.proficiency ?? 'not-supplied', modelFamily: family };
|
|
289
|
+
for (const [axis, value] of Object.entries(stratum)) {
|
|
290
|
+
const key = JSON.stringify([axis, value, r.role, r.dimension]);
|
|
291
|
+
const group = groups.get(key) ?? [];
|
|
292
|
+
group.push({ authorId: r.authorId, value: r.value });
|
|
293
|
+
groups.set(key, group);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, rows]) => {
|
|
297
|
+
const [axis, value, role, dimension] = JSON.parse(key);
|
|
298
|
+
return { axis, value, role, dimension, judgeKind, missingnessCompleteness: options.assignments === undefined ? 'unknown' : 'declared-assignments', missing: rows.filter(r => r.value === null).length, ...bootstrapAuthorMeans(rows, { ...options, seed: `${options.seed}:${key}` }) };
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=voice-evaluation.js.map
|